blob: f747987f01fa659c894933891ff5396b5d898aa4 [file] [log] [blame]
// Copyright 2017 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/renderer/declarative_content_hooks_delegate.h"
#include "base/bind.h"
#include "base/stl_util.h"
#include "extensions/common/api/declarative/declarative_constants.h"
#include "extensions/renderer/bindings/api_type_reference_map.h"
#include "extensions/renderer/bindings/argument_spec.h"
#include "gin/arguments.h"
#include "gin/converter.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_selector.h"
namespace extensions {
namespace {
void RunDeclarativeContentHooksDelegateHandlerCallback(
const v8::FunctionCallbackInfo<v8::Value>& info) {
CHECK(info.Data()->IsExternal());
v8::Local<v8::External> external = info.Data().As<v8::External>();
auto* callback =
static_cast<DeclarativeContentHooksDelegate::HandlerCallback*>(
external->Value());
callback->Run(info);
}
// Copies the 'own' properties from src -> dst.
bool V8Assign(v8::Local<v8::Context> context,
v8::Local<v8::Object> src,
v8::Local<v8::Object> dst) {
v8::Local<v8::Array> own_property_names;
if (!src->GetOwnPropertyNames(context).ToLocal(&own_property_names))
return false;
uint32_t length = own_property_names->Length();
for (uint32_t i = 0; i < length; ++i) {
v8::Local<v8::Value> key;
if (!own_property_names->Get(context, i).ToLocal(&key))
return false;
DCHECK(key->IsString() || key->IsUint32());
v8::Local<v8::Value> prop_value;
if (!src->Get(context, key).ToLocal(&prop_value))
return false;
v8::Maybe<bool> success =
key->IsString()
? dst->CreateDataProperty(context, key.As<v8::String>(), prop_value)
: dst->CreateDataProperty(context, key.As<v8::Uint32>()->Value(),
prop_value);
if (!success.IsJust() || !success.FromJust())
return false;
}
return true;
}
// Canonicalizes any css selectors specified in a page state matcher, returning
// true on success.
bool CanonicalizeCssSelectors(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
std::string* error) {
v8::Isolate* isolate = context->GetIsolate();
v8::Local<v8::String> key =
gin::StringToSymbol(isolate, declarative_content_constants::kCss);
v8::Maybe<bool> has_css = object->HasOwnProperty(context, key);
// Note: don't bother populating |error| if script threw an exception.
if (!has_css.IsJust())
return false;
if (!has_css.FromJust())
return true;
v8::Local<v8::Value> css;
if (!object->Get(context, key).ToLocal(&css))
return false;
if (css->IsUndefined())
return true;
if (!css->IsArray())
return false;
v8::Local<v8::Array> css_array = css.As<v8::Array>();
uint32_t length = css_array->Length();
for (uint32_t i = 0; i < length; ++i) {
v8::Local<v8::Value> val;
if (!css_array->Get(context, i).ToLocal(&val) || !val->IsString())
return false;
v8::String::Utf8Value selector(isolate, val.As<v8::String>());
// Note: See the TODO in css_natives_handler.cc.
std::string parsed =
blink::CanonicalizeSelector(
blink::WebString::FromUTF8(*selector, selector.length()),
blink::kWebSelectorTypeCompound)
.Utf8();
if (parsed.empty()) {
*error =
"Invalid CSS selector: " + std::string(*selector, selector.length());
return false;
}
v8::Maybe<bool> set_result =
css_array->Set(context, i, gin::StringToSymbol(isolate, parsed));
if (!set_result.IsJust() || !set_result.FromJust())
return false;
}
return true;
}
// Validates the source object against the expected spec, and copies over values
// to |this_object|. Returns true on success.
bool Validate(const ArgumentSpec* spec,
const APITypeReferenceMap& type_refs,
v8::Local<v8::Context> context,
v8::Local<v8::Object> this_object,
v8::Local<v8::Object> source_object,
const std::string& type_name,
std::string* error) {
if (!source_object.IsEmpty() &&
!V8Assign(context, source_object, this_object)) {
return false;
}
v8::Isolate* isolate = context->GetIsolate();
v8::Maybe<bool> set_result = this_object->CreateDataProperty(
context,
gin::StringToSymbol(isolate,
declarative_content_constants::kInstanceType),
gin::StringToSymbol(isolate, type_name));
if (!set_result.IsJust() || !set_result.FromJust()) {
return false;
}
if (!spec->ParseArgument(context, this_object, type_refs, nullptr, nullptr,
error)) {
return false;
}
if (type_name == declarative_content_constants::kPageStateMatcherType &&
!CanonicalizeCssSelectors(context, this_object, error)) {
return false;
}
return true;
}
} // namespace
DeclarativeContentHooksDelegate::DeclarativeContentHooksDelegate() {}
DeclarativeContentHooksDelegate::~DeclarativeContentHooksDelegate() {}
void DeclarativeContentHooksDelegate::InitializeTemplate(
v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> object_template,
const APITypeReferenceMap& type_refs) {
// Add constructors for the API types.
// TODO(devlin): We'll need to extract out common logic here and share it with
// declarativeWebRequest.
struct {
const char* full_name;
const char* exposed_name;
} kTypes[] = {
{declarative_content_constants::kPageStateMatcherType,
"PageStateMatcher"},
{declarative_content_constants::kShowAction, "ShowAction"},
{declarative_content_constants::kShowAction, "ShowPageAction"},
{declarative_content_constants::kSetIcon, "SetIcon"},
{declarative_content_constants::kRequestContentScript,
"RequestContentScript"},
};
callbacks_.reserve(base::size(kTypes));
for (const auto& type : kTypes) {
const ArgumentSpec* spec = type_refs.GetSpec(type.full_name);
DCHECK(spec);
// This object should outlive any calls to the function, so this
// base::Unretained and the callback itself are safe. Similarly, the same
// bindings system owns all these objects, so the spec and type refs should
// also be safe.
callbacks_.push_back(std::make_unique<HandlerCallback>(
base::Bind(&DeclarativeContentHooksDelegate::HandleCall,
base::Unretained(this), spec, &type_refs, type.full_name)));
object_template->Set(
gin::StringToSymbol(isolate, type.exposed_name),
v8::FunctionTemplate::New(
isolate, &RunDeclarativeContentHooksDelegateHandlerCallback,
v8::External::New(isolate, callbacks_.back().get())));
}
}
void DeclarativeContentHooksDelegate::HandleCall(
const ArgumentSpec* spec,
const APITypeReferenceMap* type_refs,
const std::string& type_name,
const v8::FunctionCallbackInfo<v8::Value>& info) {
gin::Arguments arguments(info);
v8::Isolate* isolate = arguments.isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// TODO(devlin): It would be pretty nice to be able to throw an error if
// Arguments::IsConstructCall() is false. That would ensure that the caller
// used `new declarativeContent.Foo()`, which is a) the documented approach
// and b) allows us (more) confidence that the |this| object we receive is
// an unmodified instance. But we don't know how many extensions enforcing
// that may break, and it's also incompatible with SetIcon().
v8::Local<v8::Object> this_object = info.This();
if (this_object.IsEmpty()) {
// Crazy script (e.g. declarativeContent.Foo.apply(null, args);).
NOTREACHED();
return;
}
// TODO(devlin): Find a way to use APISignature here? It's a little awkward
// because of undocumented expected properties like instanceType and not
// requiring an argument at all. We may need a better way of expressing these
// in the JSON schema.
if (arguments.Length() > 1) {
arguments.ThrowTypeError("Invalid invocation.");
return;
}
v8::Local<v8::Object> properties;
if (arguments.Length() == 1 && !arguments.GetNext(&properties)) {
arguments.ThrowTypeError("Invalid invocation.");
return;
}
std::string error;
bool success = false;
{
v8::TryCatch try_catch(isolate);
success = Validate(spec, *type_refs, context, this_object, properties,
type_name, &error);
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}
}
if (!success)
arguments.ThrowTypeError("Invalid invocation: " + error);
}
} // namespace extensions