blob: 6b2bb3e4e1e7ed58643c4ba9496f067e2acdc88b [file] [log] [blame]
// Copyright (c) 2006-2008 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/browser/extensions/extension.h"
#include "base/file_path.h"
#include "base/logging.h"
#include "base/string_util.h"
#include "net/base/net_util.h"
#include "chrome/browser/extensions/extension_error_reporter.h"
#include "chrome/common/extensions/user_script.h"
#include "chrome/common/resource_bundle.h"
#include "chrome/common/url_constants.h"
const char Extension::kManifestFilename[] = "manifest.json";
const wchar_t* Extension::kContentScriptsKey = L"content_scripts";
const wchar_t* Extension::kCssKey = L"css";
const wchar_t* Extension::kDescriptionKey = L"description";
const wchar_t* Extension::kIdKey = L"id";
const wchar_t* Extension::kJsKey = L"js";
const wchar_t* Extension::kMatchesKey = L"matches";
const wchar_t* Extension::kNameKey = L"name";
const wchar_t* Extension::kPermissionsKey = L"permissions";
const wchar_t* Extension::kPluginsDirKey = L"plugins_dir";
const wchar_t* Extension::kBackgroundKey = L"background";
const wchar_t* Extension::kRunAtKey = L"run_at";
const wchar_t* Extension::kThemeKey = L"theme";
const wchar_t* Extension::kToolstripsKey = L"toolstrips";
const wchar_t* Extension::kVersionKey = L"version";
const wchar_t* Extension::kZipHashKey = L"zip_hash";
const char* Extension::kRunAtDocumentStartValue = "document_start";
const char* Extension::kRunAtDocumentEndValue = "document_end";
// Extension-related error messages. Some of these are simple patterns, where a
// '*' is replaced at runtime with a specific value. This is used instead of
// printf because we want to unit test them and scanf is hard to make
// cross-platform.
const char* Extension::kInvalidContentScriptError =
"Invalid value for 'content_scripts[*]'.";
const char* Extension::kInvalidContentScriptsListError =
"Invalid value for 'content_scripts'.";
const char* Extension::kInvalidCssError =
"Invalid value for 'content_scripts[*].css[*]'.";
const char* Extension::kInvalidCssListError =
"Required value 'content_scripts[*].css is invalid.";
const char* Extension::kInvalidDescriptionError =
"Invalid value for 'description'.";
const char* Extension::kInvalidIdError =
"Required value 'id' is missing or invalid.";
const char* Extension::kInvalidJsError =
"Invalid value for 'content_scripts[*].js[*]'.";
const char* Extension::kInvalidJsListError =
"Required value 'content_scripts[*].js is invalid.";
const char* Extension::kInvalidManifestError =
"Manifest is missing or invalid.";
const char* Extension::kInvalidMatchCountError =
"Invalid value for 'content_scripts[*].matches. There must be at least one "
"match specified.";
const char* Extension::kInvalidMatchError =
"Invalid value for 'content_scripts[*].matches[*]'.";
const char* Extension::kInvalidMatchesError =
"Required value 'content_scripts[*].matches' is missing or invalid.";
const char* Extension::kInvalidNameError =
"Required value 'name' is missing or invalid.";
const char* Extension::kInvalidPermissionsError =
"Required value 'permissions' is missing or invalid.";
const char* Extension::kInvalidPermissionCountWarning =
"Warning, 'permissions' key found, but array is empty.";
const char* Extension::kInvalidPermissionError =
"Invalid value for 'permissions[*]'.";
const char* Extension::kInvalidPermissionSchemeError =
"Invalid scheme for 'permissions[*]'. Only 'http' and 'https' are "
"allowed.";
const char* Extension::kInvalidPluginsDirError =
"Invalid value for 'plugins_dir'.";
const char* Extension::kInvalidBackgroundError =
"Invalid value for 'background'.";
const char* Extension::kInvalidRunAtError =
"Invalid value for 'content_scripts[*].run_at'.";
const char* Extension::kInvalidToolstripError =
"Invalid value for 'toolstrips[*]'";
const char* Extension::kInvalidToolstripsError =
"Invalid value for 'toolstrips'.";
const char* Extension::kInvalidVersionError =
"Required value 'version' is missing or invalid.";
const char* Extension::kInvalidZipHashError =
"Required key 'zip_hash' is missing or invalid.";
const char* Extension::kMissingFileError =
"At least one js or css file is required for 'content_scripts[*]'.";
const size_t Extension::kIdSize = 20; // SHA1 (160 bits) == 20 bytes
Extension::Extension(const Extension& rhs)
: path_(rhs.path_),
extension_url_(rhs.extension_url_),
id_(rhs.id_),
version_(new Version(*rhs.version_)),
name_(rhs.name_),
description_(rhs.description_),
content_scripts_(rhs.content_scripts_),
plugins_dir_(rhs.plugins_dir_),
zip_hash_(rhs.zip_hash_),
theme_paths_(rhs.theme_paths_) {
}
const std::string Extension::VersionString() const {
return version_->GetString();
}
// static
GURL Extension::GetResourceURL(const GURL& extension_url,
const std::string& relative_path) {
DCHECK(extension_url.SchemeIs(chrome::kExtensionScheme));
DCHECK(extension_url.path() == "/");
GURL ret_val = GURL(extension_url.spec() + relative_path);
DCHECK(StartsWithASCII(ret_val.spec(), extension_url.spec(), false));
return ret_val;
}
FilePath Extension::GetThemeResourcePath(const int resource_id) {
std::wstring id = IntToWString(resource_id);
std::string path = theme_paths_[id];
if (path.size())
return path_.AppendASCII(path.c_str());
return FilePath();
}
// static
FilePath Extension::GetResourcePath(const FilePath& extension_path,
const std::string& relative_path) {
// Build up a file:// URL and convert that back to a FilePath. This avoids
// URL encoding and path separator issues.
// Convert the extension's root to a file:// URL.
GURL extension_url = net::FilePathToFileURL(extension_path);
if (!extension_url.is_valid())
return FilePath();
// Append the requested path.
GURL::Replacements replacements;
std::string new_path(extension_url.path());
new_path += "/";
new_path += relative_path;
replacements.SetPathStr(new_path);
GURL file_url = extension_url.ReplaceComponents(replacements);
if (!file_url.is_valid())
return FilePath();
// Convert the result back to a FilePath.
FilePath ret_val;
if (!net::FileURLToFilePath(file_url, &ret_val))
return FilePath();
// Double-check that the path we ended up with is actually inside the
// extension root. We can do this with a simple prefix match because:
// a) We control the prefix on both sides, and they should match.
// b) GURL normalizes things like "../" and "//" before it gets to us.
if (ret_val.value().find(extension_path.value() +
FilePath::kSeparators[0]) != 0)
return FilePath();
return ret_val;
}
// Creates an error messages from a pattern.
static std::string FormatErrorMessage(const std::string& format,
const std::string s1) {
std::string ret_val = format;
ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1);
return ret_val;
}
static std::string FormatErrorMessage(const std::string& format,
const std::string s1,
const std::string s2) {
std::string ret_val = format;
ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1);
ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s2);
return ret_val;
}
Extension::Extension(const FilePath& path) {
DCHECK(path.IsAbsolute());
#if defined(OS_WIN)
// Normalize any drive letter to upper-case. We do this for consistency with
// net_utils::FilePathToFileURL(), which does the same thing, to make string
// comparisons simpler.
std::wstring path_str = path.value();
if (path_str.size() >= 2 && path_str[0] >= L'a' && path_str[0] <= L'z' &&
path_str[1] == ':')
path_str[0] += ('A' - 'a');
path_ = FilePath(path_str);
#else
path_ = path;
#endif
}
// Helper method that loads a UserScript object from a dictionary in the
// content_script list of the manifest.
bool Extension::LoadUserScriptHelper(const DictionaryValue* content_script,
int definition_index, std::string* error,
UserScript* result) {
// run_at
if (content_script->HasKey(kRunAtKey)) {
std::string run_location;
if (!content_script->GetString(kRunAtKey, &run_location)) {
*error = FormatErrorMessage(kInvalidRunAtError,
IntToString(definition_index));
return false;
}
if (run_location == kRunAtDocumentStartValue) {
result->set_run_location(UserScript::DOCUMENT_START);
} else if (run_location == kRunAtDocumentEndValue) {
result->set_run_location(UserScript::DOCUMENT_END);
} else {
*error = FormatErrorMessage(kInvalidRunAtError,
IntToString(definition_index));
return false;
}
}
// matches
ListValue* matches = NULL;
if (!content_script->GetList(kMatchesKey, &matches)) {
*error = FormatErrorMessage(kInvalidMatchesError,
IntToString(definition_index));
return false;
}
if (matches->GetSize() == 0) {
*error = FormatErrorMessage(kInvalidMatchCountError,
IntToString(definition_index));
return false;
}
for (size_t j = 0; j < matches->GetSize(); ++j) {
std::string match_str;
if (!matches->GetString(j, &match_str)) {
*error = FormatErrorMessage(kInvalidMatchError,
IntToString(definition_index),
IntToString(j));
return false;
}
URLPattern pattern;
if (!pattern.Parse(match_str)) {
*error = FormatErrorMessage(kInvalidMatchError,
IntToString(definition_index),
IntToString(j));
return false;
}
result->add_url_pattern(pattern);
}
// js and css keys
ListValue* js = NULL;
if (content_script->HasKey(kJsKey) &&
!content_script->GetList(kJsKey, &js)) {
*error = FormatErrorMessage(kInvalidJsListError,
IntToString(definition_index));
return false;
}
ListValue* css = NULL;
if (content_script->HasKey(kCssKey) &&
!content_script->GetList(kCssKey, &css)) {
*error = FormatErrorMessage(kInvalidCssListError,
IntToString(definition_index));
return false;
}
// The manifest needs to have at least one js or css user script definition.
if (((js ? js->GetSize() : 0) + (css ? css->GetSize() : 0)) == 0) {
*error = FormatErrorMessage(kMissingFileError,
IntToString(definition_index));
return false;
}
if (js) {
for (size_t script_index = 0; script_index < js->GetSize();
++script_index) {
Value* value;
std::wstring relative;
if (!js->Get(script_index, &value) || !value->GetAsString(&relative)) {
*error = FormatErrorMessage(kInvalidJsError,
IntToString(definition_index),
IntToString(script_index));
return false;
}
// TODO(georged): Make GetResourceURL accept wstring too
GURL url = GetResourceURL(WideToUTF8(relative));
FilePath path = GetResourcePath(WideToUTF8(relative));
result->js_scripts().push_back(UserScript::File(path, url));
}
}
if (css) {
for (size_t script_index = 0; script_index < css->GetSize();
++script_index) {
Value* value;
std::wstring relative;
if (!css->Get(script_index, &value) || !value->GetAsString(&relative)) {
*error = FormatErrorMessage(kInvalidCssError,
IntToString(definition_index),
IntToString(script_index));
return false;
}
// TODO(georged): Make GetResourceURL accept wstring too
GURL url = GetResourceURL(WideToUTF8(relative));
FilePath path = GetResourcePath(WideToUTF8(relative));
result->css_scripts().push_back(UserScript::File(path, url));
}
}
return true;
}
bool Extension::InitFromValue(const DictionaryValue& source, bool require_id,
std::string* error) {
// Initialize id.
if (source.HasKey(kIdKey)) {
if (!source.GetString(kIdKey, &id_)) {
*error = kInvalidIdError;
return false;
}
// Normalize the string to lowercase, so it can be used as an URL component
// (where GURL will lowercase it).
StringToLowerASCII(&id_);
// Verify that the id is legal. The id is a hex string of the SHA-1 hash of
// the public key.
std::vector<uint8> id_bytes;
if (!HexStringToBytes(id_, &id_bytes) || id_bytes.size() != kIdSize) {
*error = kInvalidIdError;
return false;
}
} else if (require_id) {
*error = kInvalidIdError;
return false;
} else {
// Generate a random ID
static int counter = 0;
id_ = StringPrintf("%x", counter);
++counter;
// pad the string out to 40 chars with zeroes.
id_.insert(0, 40 - id_.length(), '0');
}
// Initialize the URL.
extension_url_ = GURL(std::string(chrome::kExtensionScheme) +
chrome::kStandardSchemeSeparator + id_ + "/");
// Initialize version.
std::string version_str;
if (!source.GetString(kVersionKey, &version_str)) {
*error = kInvalidVersionError;
return false;
}
version_.reset(Version::GetVersionFromString(version_str));
if (!version_.get()) {
*error = kInvalidVersionError;
return false;
}
// Initialize name.
if (!source.GetString(kNameKey, &name_)) {
*error = kInvalidNameError;
return false;
}
// Initialize description (optional).
if (source.HasKey(kDescriptionKey)) {
if (!source.GetString(kDescriptionKey, &description_)) {
*error = kInvalidDescriptionError;
return false;
}
}
// Initialize zip hash (only present in zip)
// There's no need to verify it at this point. If it's in a bogus format
// it won't pass the hash verify step.
if (source.HasKey(kZipHashKey)) {
if (!source.GetString(kZipHashKey, &zip_hash_)) {
*error = kInvalidZipHashError;
return false;
}
}
// Initialize plugins dir (optional).
if (source.HasKey(kPluginsDirKey)) {
std::string plugins_dir;
if (!source.GetString(kPluginsDirKey, &plugins_dir)) {
*error = kInvalidPluginsDirError;
return false;
}
plugins_dir_ = path_.AppendASCII(plugins_dir);
}
// Initialize background url (optional).
if (source.HasKey(kBackgroundKey)) {
std::string background_str;
if (!source.GetString(kBackgroundKey, &background_str)) {
*error = kInvalidBackgroundError;
return false;
}
background_url_ = GetResourceURL(background_str);
}
// Initialize toolstrips (optional).
if (source.HasKey(kToolstripsKey)) {
ListValue* list_value;
if (!source.GetList(kToolstripsKey, &list_value)) {
*error = kInvalidToolstripsError;
return false;
}
for (size_t i = 0; i < list_value->GetSize(); ++i) {
std::string toolstrip;
if (!list_value->GetString(i, &toolstrip)) {
*error = FormatErrorMessage(kInvalidToolstripError, IntToString(i));
return false;
}
toolstrips_.push_back(toolstrip);
}
}
if (source.HasKey(kThemeKey)) {
DictionaryValue* dict_value;
if (source.GetDictionary(kThemeKey, &dict_value)) {
DictionaryValue::key_iterator iter = dict_value->begin_keys();
while (iter != dict_value->end_keys()) {
std::string val;
if (dict_value->GetString(*iter, &val)) {
std::wstring id = *iter;
theme_paths_[id] = val;
}
++iter;
}
ResourceBundle::GetSharedInstance().SetThemeExtension(*this);
}
}
// Initialize content scripts (optional).
if (source.HasKey(kContentScriptsKey)) {
ListValue* list_value;
if (!source.GetList(kContentScriptsKey, &list_value)) {
*error = kInvalidContentScriptsListError;
return false;
}
for (size_t i = 0; i < list_value->GetSize(); ++i) {
DictionaryValue* content_script;
if (!list_value->GetDictionary(i, &content_script)) {
*error = FormatErrorMessage(kInvalidContentScriptError,
IntToString(i));
return false;
}
UserScript script;
if (!LoadUserScriptHelper(content_script, i, error, &script))
return false; // Failed to parse script context definition
script.set_extension_id(id());
content_scripts_.push_back(script);
}
}
// Initialize the permissions (optional).
if (source.HasKey(kPermissionsKey)) {
ListValue* hosts = NULL;
if (!source.GetList(kPermissionsKey, &hosts)) {
*error = FormatErrorMessage(kInvalidPermissionsError, "");
return false;
}
if (hosts->GetSize() == 0) {
ExtensionErrorReporter::GetInstance()->ReportError(
kInvalidPermissionCountWarning, false);
}
for (size_t i = 0; i < hosts->GetSize(); ++i) {
std::string host_str;
if (!hosts->GetString(i, &host_str)) {
*error = FormatErrorMessage(kInvalidPermissionError,
IntToString(i));
return false;
}
URLPattern pattern;
if (!pattern.Parse(host_str)) {
*error = FormatErrorMessage(kInvalidPermissionError,
IntToString(i));
return false;
}
// Only accept http/https persmissions at the moment.
if ((pattern.scheme() != chrome::kHttpScheme) &&
(pattern.scheme() != chrome::kHttpsScheme)) {
*error = FormatErrorMessage(kInvalidPermissionSchemeError,
IntToString(i));
return false;
}
permissions_.push_back(pattern);
}
}
return true;
}