blob: 4ead4638978de6139e59144606a8e86305cba254 [file] [log] [blame]
// Copyright 2014 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/common/features/simple_feature.h"
#include <algorithm>
#include <map>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/macros.h"
#include "base/sha1.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "components/crx_file/id_util.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/features/feature_provider.h"
#include "extensions/common/features/feature_util.h"
#include "extensions/common/switches.h"
using crx_file::id_util::HashedIdInHex;
namespace extensions {
namespace {
// A singleton copy of the --whitelisted-extension-id so that we don't need to
// copy it from the CommandLine each time.
std::string* g_whitelisted_extension_id = NULL;
Feature::Availability IsAvailableToManifestForBind(
const std::string& extension_id,
Manifest::Type type,
Manifest::Location location,
int manifest_version,
Feature::Platform platform,
const Feature* feature) {
return feature->IsAvailableToManifest(
extension_id, type, location, manifest_version, platform);
}
Feature::Availability IsAvailableToContextForBind(const Extension* extension,
Feature::Context context,
const GURL& url,
Feature::Platform platform,
const Feature* feature) {
return feature->IsAvailableToContext(extension, context, url, platform);
}
// TODO(aa): Can we replace all this manual parsing with JSON schema stuff?
void ParseVector(const base::Value* value,
std::vector<std::string>* vector) {
const base::ListValue* list_value = NULL;
if (!value->GetAsList(&list_value))
return;
vector->clear();
size_t list_size = list_value->GetSize();
vector->reserve(list_size);
for (size_t i = 0; i < list_size; ++i) {
std::string str_val;
CHECK(list_value->GetString(i, &str_val));
vector->push_back(str_val);
}
std::sort(vector->begin(), vector->end());
}
template<typename T>
void ParseEnum(const std::string& string_value,
T* enum_value,
const std::map<std::string, T>& mapping) {
const auto& iter = mapping.find(string_value);
if (iter == mapping.end())
CRASH_WITH_MINIDUMP("Enum value not found: " + string_value);
*enum_value = iter->second;
}
template<typename T>
void ParseEnum(const base::DictionaryValue* value,
const std::string& property,
T* enum_value,
const std::map<std::string, T>& mapping) {
std::string string_value;
if (!value->GetString(property, &string_value))
return;
ParseEnum(string_value, enum_value, mapping);
}
template<typename T>
void ParseEnumVector(const base::Value* value,
std::vector<T>* enum_vector,
const std::map<std::string, T>& mapping) {
enum_vector->clear();
std::string property_string;
if (value->GetAsString(&property_string)) {
if (property_string == "all") {
enum_vector->reserve(mapping.size());
for (const auto& it : mapping)
enum_vector->push_back(it.second);
}
std::sort(enum_vector->begin(), enum_vector->end());
return;
}
std::vector<std::string> string_vector;
ParseVector(value, &string_vector);
enum_vector->reserve(string_vector.size());
for (const auto& str : string_vector) {
T enum_value = static_cast<T>(0);
ParseEnum(str, &enum_value, mapping);
enum_vector->push_back(enum_value);
}
std::sort(enum_vector->begin(), enum_vector->end());
}
void ParseURLPatterns(const base::DictionaryValue* value,
const std::string& key,
URLPatternSet* set) {
const base::ListValue* matches = NULL;
if (value->GetList(key, &matches)) {
set->ClearPatterns();
for (size_t i = 0; i < matches->GetSize(); ++i) {
std::string pattern;
CHECK(matches->GetString(i, &pattern));
set->AddPattern(URLPattern(URLPattern::SCHEME_ALL, pattern));
}
}
}
// Gets a human-readable name for the given extension type, suitable for giving
// to developers in an error message.
std::string GetDisplayName(Manifest::Type type) {
switch (type) {
case Manifest::TYPE_UNKNOWN:
return "unknown";
case Manifest::TYPE_EXTENSION:
return "extension";
case Manifest::TYPE_HOSTED_APP:
return "hosted app";
case Manifest::TYPE_LEGACY_PACKAGED_APP:
return "legacy packaged app";
case Manifest::TYPE_PLATFORM_APP:
return "packaged app";
case Manifest::TYPE_THEME:
return "theme";
case Manifest::TYPE_USER_SCRIPT:
return "user script";
case Manifest::TYPE_SHARED_MODULE:
return "shared module";
case Manifest::NUM_LOAD_TYPES:
NOTREACHED();
}
NOTREACHED();
return "";
}
// Gets a human-readable name for the given context type, suitable for giving
// to developers in an error message.
std::string GetDisplayName(Feature::Context context) {
switch (context) {
case Feature::UNSPECIFIED_CONTEXT:
return "unknown";
case Feature::BLESSED_EXTENSION_CONTEXT:
// "privileged" is vague but hopefully the developer will understand that
// means background or app window.
return "privileged page";
case Feature::UNBLESSED_EXTENSION_CONTEXT:
// "iframe" is a bit of a lie/oversimplification, but that's the most
// common unblessed context.
return "extension iframe";
case Feature::CONTENT_SCRIPT_CONTEXT:
return "content script";
case Feature::WEB_PAGE_CONTEXT:
return "web page";
case Feature::BLESSED_WEB_PAGE_CONTEXT:
return "hosted app";
case Feature::WEBUI_CONTEXT:
return "webui";
case Feature::SERVICE_WORKER_CONTEXT:
return "service worker";
}
NOTREACHED();
return "";
}
// Gets a human-readable list of the display names (pluralized, comma separated
// with the "and" in the correct place) for each of |enum_types|.
template <typename EnumType>
std::string ListDisplayNames(const std::vector<EnumType>& enum_types) {
std::string display_name_list;
for (size_t i = 0; i < enum_types.size(); ++i) {
// Pluralize type name.
display_name_list += GetDisplayName(enum_types[i]) + "s";
// Comma-separate entries, with an Oxford comma if there is more than 2
// total entries.
if (enum_types.size() > 2) {
if (i < enum_types.size() - 2)
display_name_list += ", ";
else if (i == enum_types.size() - 2)
display_name_list += ", and ";
} else if (enum_types.size() == 2 && i == 0) {
display_name_list += " and ";
}
}
return display_name_list;
}
bool IsCommandLineSwitchEnabled(const std::string& switch_name) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switch_name + "=1"))
return true;
if (command_line->HasSwitch(std::string("enable-") + switch_name))
return true;
return false;
}
bool IsWhitelistedForTest(const std::string& extension_id) {
// TODO(jackhou): Delete the commandline whitelisting mechanism.
// Since it is only used it tests, ideally it should not be set via the
// commandline. At the moment the commandline is used as a mechanism to pass
// the id to the renderer process.
if (!g_whitelisted_extension_id) {
g_whitelisted_extension_id = new std::string(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kWhitelistedExtensionID));
}
return !g_whitelisted_extension_id->empty() &&
*g_whitelisted_extension_id == extension_id;
}
} // namespace
SimpleFeature::ScopedWhitelistForTest::ScopedWhitelistForTest(
const std::string& id)
: previous_id_(g_whitelisted_extension_id) {
g_whitelisted_extension_id = new std::string(id);
}
SimpleFeature::ScopedWhitelistForTest::~ScopedWhitelistForTest() {
delete g_whitelisted_extension_id;
g_whitelisted_extension_id = previous_id_;
}
struct SimpleFeature::Mappings {
Mappings() {
extension_types["extension"] = Manifest::TYPE_EXTENSION;
extension_types["theme"] = Manifest::TYPE_THEME;
extension_types["legacy_packaged_app"] = Manifest::TYPE_LEGACY_PACKAGED_APP;
extension_types["hosted_app"] = Manifest::TYPE_HOSTED_APP;
extension_types["platform_app"] = Manifest::TYPE_PLATFORM_APP;
extension_types["shared_module"] = Manifest::TYPE_SHARED_MODULE;
contexts["blessed_extension"] = Feature::BLESSED_EXTENSION_CONTEXT;
contexts["unblessed_extension"] = Feature::UNBLESSED_EXTENSION_CONTEXT;
contexts["content_script"] = Feature::CONTENT_SCRIPT_CONTEXT;
contexts["web_page"] = Feature::WEB_PAGE_CONTEXT;
contexts["blessed_web_page"] = Feature::BLESSED_WEB_PAGE_CONTEXT;
contexts["webui"] = Feature::WEBUI_CONTEXT;
locations["component"] = SimpleFeature::COMPONENT_LOCATION;
locations["external_component"] =
SimpleFeature::EXTERNAL_COMPONENT_LOCATION;
locations["policy"] = SimpleFeature::POLICY_LOCATION;
platforms["chromeos"] = Feature::CHROMEOS_PLATFORM;
platforms["linux"] = Feature::LINUX_PLATFORM;
platforms["mac"] = Feature::MACOSX_PLATFORM;
platforms["win"] = Feature::WIN_PLATFORM;
}
std::map<std::string, Manifest::Type> extension_types;
std::map<std::string, Feature::Context> contexts;
std::map<std::string, SimpleFeature::Location> locations;
std::map<std::string, Feature::Platform> platforms;
};
SimpleFeature::SimpleFeature()
: location_(UNSPECIFIED_LOCATION),
min_manifest_version_(0),
max_manifest_version_(0),
component_extensions_auto_granted_(true) {}
SimpleFeature::~SimpleFeature() {}
bool SimpleFeature::HasDependencies() const {
return !dependencies_.empty();
}
void SimpleFeature::AddFilter(scoped_ptr<SimpleFeatureFilter> filter) {
filters_.push_back(filter.Pass());
}
std::string SimpleFeature::Parse(const base::DictionaryValue* dictionary) {
static base::LazyInstance<SimpleFeature::Mappings> mappings =
LAZY_INSTANCE_INITIALIZER;
no_parent_ = false;
for (base::DictionaryValue::Iterator it(*dictionary);
!it.IsAtEnd();
it.Advance()) {
std::string key = it.key();
const base::Value* value = &it.value();
if (key == "matches") {
ParseURLPatterns(dictionary, "matches", &matches_);
} else if (key == "blacklist") {
ParseVector(value, &blacklist_);
} else if (key == "whitelist") {
ParseVector(value, &whitelist_);
} else if (key == "dependencies") {
ParseVector(value, &dependencies_);
} else if (key == "extension_types") {
ParseEnumVector<Manifest::Type>(value, &extension_types_,
mappings.Get().extension_types);
} else if (key == "contexts") {
ParseEnumVector<Context>(value, &contexts_,
mappings.Get().contexts);
} else if (key == "location") {
ParseEnum<Location>(dictionary, "location", &location_,
mappings.Get().locations);
} else if (key == "platforms") {
ParseEnumVector<Platform>(value, &platforms_,
mappings.Get().platforms);
} else if (key == "min_manifest_version") {
dictionary->GetInteger("min_manifest_version", &min_manifest_version_);
} else if (key == "max_manifest_version") {
dictionary->GetInteger("max_manifest_version", &max_manifest_version_);
} else if (key == "noparent") {
dictionary->GetBoolean("noparent", &no_parent_);
} else if (key == "component_extensions_auto_granted") {
dictionary->GetBoolean("component_extensions_auto_granted",
&component_extensions_auto_granted_);
} else if (key == "command_line_switch") {
dictionary->GetString("command_line_switch", &command_line_switch_);
}
}
// NOTE: ideally we'd sanity check that "matches" can be specified if and
// only if there's a "web_page" or "webui" context, but without
// (Simple)Features being aware of their own heirarchy this is impossible.
//
// For example, we might have feature "foo" available to "web_page" context
// and "matches" google.com/*. Then a sub-feature "foo.bar" might override
// "matches" to be chromium.org/*. That sub-feature doesn't need to specify
// "web_page" context because it's inherited, but we don't know that here.
std::string result;
for (const auto& filter : filters_) {
result = filter->Parse(dictionary);
if (!result.empty())
break;
}
return result;
}
Feature::Availability SimpleFeature::IsAvailableToManifest(
const std::string& extension_id,
Manifest::Type type,
Manifest::Location location,
int manifest_version,
Platform platform) const {
// Check extension type first to avoid granting platform app permissions
// to component extensions.
// HACK(kalman): user script -> extension. Solve this in a more generic way
// when we compile feature files.
Manifest::Type type_to_check = (type == Manifest::TYPE_USER_SCRIPT) ?
Manifest::TYPE_EXTENSION : type;
if (!extension_types_.empty() &&
!ContainsValue(extension_types_, type_to_check)) {
return CreateAvailability(INVALID_TYPE, type);
}
if (IsIdInBlacklist(extension_id))
return CreateAvailability(FOUND_IN_BLACKLIST, type);
// TODO(benwells): don't grant all component extensions.
// See http://crbug.com/370375 for more details.
// Component extensions can access any feature.
// NOTE: Deliberately does not match EXTERNAL_COMPONENT.
if (component_extensions_auto_granted_ && location == Manifest::COMPONENT)
return CreateAvailability(IS_AVAILABLE, type);
if (!whitelist_.empty() && !IsIdInWhitelist(extension_id) &&
!IsWhitelistedForTest(extension_id)) {
return CreateAvailability(NOT_FOUND_IN_WHITELIST, type);
}
if (!MatchesManifestLocation(location))
return CreateAvailability(INVALID_LOCATION, type);
if (!platforms_.empty() && !ContainsValue(platforms_, platform))
return CreateAvailability(INVALID_PLATFORM, type);
if (min_manifest_version_ != 0 && manifest_version < min_manifest_version_)
return CreateAvailability(INVALID_MIN_MANIFEST_VERSION, type);
if (max_manifest_version_ != 0 && manifest_version > max_manifest_version_)
return CreateAvailability(INVALID_MAX_MANIFEST_VERSION, type);
if (!command_line_switch_.empty() &&
!IsCommandLineSwitchEnabled(command_line_switch_)) {
return CreateAvailability(MISSING_COMMAND_LINE_SWITCH, type);
}
for (const auto& filter : filters_) {
Availability availability = filter->IsAvailableToManifest(
extension_id, type, location, manifest_version, platform);
if (!availability.is_available())
return availability;
}
return CheckDependencies(base::Bind(&IsAvailableToManifestForBind,
extension_id,
type,
location,
manifest_version,
platform));
}
Feature::Availability SimpleFeature::IsAvailableToContext(
const Extension* extension,
SimpleFeature::Context context,
const GURL& url,
SimpleFeature::Platform platform) const {
if (extension) {
Availability result = IsAvailableToManifest(extension->id(),
extension->GetType(),
extension->location(),
extension->manifest_version(),
platform);
if (!result.is_available())
return result;
}
if (!contexts_.empty() && !ContainsValue(contexts_, context))
return CreateAvailability(INVALID_CONTEXT, context);
// TODO(kalman): Consider checking |matches_| regardless of context type.
// Fewer surprises, and if the feature configuration wants to isolate
// "matches" from say "blessed_extension" then they can use complex features.
if ((context == WEB_PAGE_CONTEXT || context == WEBUI_CONTEXT) &&
!matches_.MatchesURL(url)) {
return CreateAvailability(INVALID_URL, url);
}
for (const auto& filter : filters_) {
Availability availability =
filter->IsAvailableToContext(extension, context, url, platform);
if (!availability.is_available())
return availability;
}
// TODO(kalman): Assert that if the context was a webpage or WebUI context
// then at some point a "matches" restriction was checked.
return CheckDependencies(base::Bind(
&IsAvailableToContextForBind, extension, context, url, platform));
}
std::string SimpleFeature::GetAvailabilityMessage(
AvailabilityResult result,
Manifest::Type type,
const GURL& url,
Context context) const {
switch (result) {
case IS_AVAILABLE:
return std::string();
case NOT_FOUND_IN_WHITELIST:
case FOUND_IN_BLACKLIST:
return base::StringPrintf(
"'%s' is not allowed for specified extension ID.",
name().c_str());
case INVALID_URL:
return base::StringPrintf("'%s' is not allowed on %s.",
name().c_str(), url.spec().c_str());
case INVALID_TYPE:
return base::StringPrintf(
"'%s' is only allowed for %s, but this is a %s.",
name().c_str(),
ListDisplayNames(std::vector<Manifest::Type>(
extension_types_.begin(), extension_types_.end())).c_str(),
GetDisplayName(type).c_str());
case INVALID_CONTEXT:
return base::StringPrintf(
"'%s' is only allowed to run in %s, but this is a %s",
name().c_str(),
ListDisplayNames(std::vector<Context>(
contexts_.begin(), contexts_.end())).c_str(),
GetDisplayName(context).c_str());
case INVALID_LOCATION:
return base::StringPrintf(
"'%s' is not allowed for specified install location.",
name().c_str());
case INVALID_PLATFORM:
return base::StringPrintf(
"'%s' is not allowed for specified platform.",
name().c_str());
case INVALID_MIN_MANIFEST_VERSION:
return base::StringPrintf(
"'%s' requires manifest version of at least %d.",
name().c_str(),
min_manifest_version_);
case INVALID_MAX_MANIFEST_VERSION:
return base::StringPrintf(
"'%s' requires manifest version of %d or lower.",
name().c_str(),
max_manifest_version_);
case NOT_PRESENT:
return base::StringPrintf(
"'%s' requires a different Feature that is not present.",
name().c_str());
case UNSUPPORTED_CHANNEL:
return base::StringPrintf(
"'%s' is unsupported in this version of the platform.",
name().c_str());
case MISSING_COMMAND_LINE_SWITCH:
return base::StringPrintf(
"'%s' requires the '%s' command line switch to be enabled.",
name().c_str(), command_line_switch_.c_str());
}
NOTREACHED();
return std::string();
}
Feature::Availability SimpleFeature::CreateAvailability(
AvailabilityResult result) const {
return Availability(
result, GetAvailabilityMessage(result, Manifest::TYPE_UNKNOWN, GURL(),
UNSPECIFIED_CONTEXT));
}
Feature::Availability SimpleFeature::CreateAvailability(
AvailabilityResult result, Manifest::Type type) const {
return Availability(result, GetAvailabilityMessage(result, type, GURL(),
UNSPECIFIED_CONTEXT));
}
Feature::Availability SimpleFeature::CreateAvailability(
AvailabilityResult result,
const GURL& url) const {
return Availability(
result, GetAvailabilityMessage(result, Manifest::TYPE_UNKNOWN, url,
UNSPECIFIED_CONTEXT));
}
Feature::Availability SimpleFeature::CreateAvailability(
AvailabilityResult result,
Context context) const {
return Availability(
result, GetAvailabilityMessage(result, Manifest::TYPE_UNKNOWN, GURL(),
context));
}
bool SimpleFeature::IsInternal() const {
return false;
}
bool SimpleFeature::IsIdInBlacklist(const std::string& extension_id) const {
return IsIdInList(extension_id, blacklist_);
}
bool SimpleFeature::IsIdInWhitelist(const std::string& extension_id) const {
return IsIdInList(extension_id, whitelist_);
}
// static
bool SimpleFeature::IsIdInArray(const std::string& extension_id,
const char* const array[],
size_t array_length) {
if (!IsValidExtensionId(extension_id))
return false;
const char* const* start = array;
const char* const* end = array + array_length;
return ((std::find(start, end, extension_id) != end) ||
(std::find(start, end, HashedIdInHex(extension_id)) != end));
}
// static
bool SimpleFeature::IsIdInList(const std::string& extension_id,
const std::vector<std::string>& list) {
if (!IsValidExtensionId(extension_id))
return false;
return (ContainsValue(list, extension_id) ||
ContainsValue(list, HashedIdInHex(extension_id)));
}
bool SimpleFeature::MatchesManifestLocation(
Manifest::Location manifest_location) const {
switch (location_) {
case SimpleFeature::UNSPECIFIED_LOCATION:
return true;
case SimpleFeature::COMPONENT_LOCATION:
return manifest_location == Manifest::COMPONENT;
case SimpleFeature::EXTERNAL_COMPONENT_LOCATION:
return manifest_location == Manifest::EXTERNAL_COMPONENT;
case SimpleFeature::POLICY_LOCATION:
return manifest_location == Manifest::EXTERNAL_POLICY ||
manifest_location == Manifest::EXTERNAL_POLICY_DOWNLOAD;
}
NOTREACHED();
return false;
}
Feature::Availability SimpleFeature::CheckDependencies(
const base::Callback<Availability(const Feature*)>& checker) const {
for (const auto& dep_name : dependencies_) {
Feature* dependency =
ExtensionAPI::GetSharedInstance()->GetFeatureDependency(dep_name);
if (!dependency)
return CreateAvailability(NOT_PRESENT);
Availability dependency_availability = checker.Run(dependency);
if (!dependency_availability.is_available())
return dependency_availability;
}
return CreateAvailability(IS_AVAILABLE);
}
// static
bool SimpleFeature::IsValidExtensionId(const std::string& extension_id) {
// Belt-and-suspenders philosophy here. We should be pretty confident by this
// point that we've validated the extension ID format, but in case something
// slips through, we avoid a class of attack where creative ID manipulation
// leads to hash collisions.
// 128 bits / 4 = 32 mpdecimal characters
return (extension_id.length() == 32);
}
} // namespace extensions