diff options
Diffstat (limited to 'chromium/extensions/renderer')
266 files changed, 35649 insertions, 0 deletions
diff --git a/chromium/extensions/renderer/BUILD.gn b/chromium/extensions/renderer/BUILD.gn new file mode 100644 index 00000000000..1b8f7d0efbe --- /dev/null +++ b/chromium/extensions/renderer/BUILD.gn @@ -0,0 +1,44 @@ +# 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. + +import("//build/config/features.gni") +import("//extensions/extensions.gni") + +assert(enable_extensions) + +# GYP version: extensions/extensions.gyp:extensions_renderer +source_set("renderer") { + sources = rebase_path(extensions_gypi_values.extensions_renderer_sources, + ".", + "//extensions") + + configs += [ + "//build/config:precompiled_headers", + + # TODO(jschuh): crbug.com/167187 fix size_t to int truncations. + "//build/config/compiler:no_size_t_to_int_warning", + ] + + deps = [ + "//chrome:resources", + "//components/guest_view/renderer", + "//content:resources", + "//extensions:extensions_resources", + "//gin", + "//mojo/edk/js", + "//mojo/public/js", + "//skia", + "//third_party/WebKit/public:blink", + ] + + if (enable_wifi_display) { + wifi_display_sources = rebase_path( + extensions_gypi_values.extensions_renderer_sources_wifi_display, + ".", + "//extensions") + sources += wifi_display_sources + + deps += [ "//third_party/wds:libwds" ] + } +} diff --git a/chromium/extensions/renderer/DEPS b/chromium/extensions/renderer/DEPS new file mode 100644 index 00000000000..6d1446295a6 --- /dev/null +++ b/chromium/extensions/renderer/DEPS @@ -0,0 +1,25 @@ +include_rules = [ + "+components/translate/core/language_detection", + "+content/public/child", + "+content/public/renderer", + + "+gin", + "+mojo/edk/js", + + "+third_party/skia/include/core", + "+third_party/cld_2/src/public/compact_lang_det.h", + "+third_party/cld_2/src/public/encodings.h", + + "+third_party/WebKit/public/platform", + "+third_party/WebKit/public/web", + + "+third_party/wds/src/libwds/public", + + "+tools/json_schema_compiler", + + "-v8", + "+v8/include", + + "+storage/common/fileapi", +] + diff --git a/chromium/extensions/renderer/OWNERS b/chromium/extensions/renderer/OWNERS new file mode 100644 index 00000000000..899a192f3bf --- /dev/null +++ b/chromium/extensions/renderer/OWNERS @@ -0,0 +1,2 @@ +# For V8 related changes, please consider also adding +jochen@chromium.org diff --git a/chromium/extensions/renderer/activity_log_converter_strategy.cc b/chromium/extensions/renderer/activity_log_converter_strategy.cc new file mode 100644 index 00000000000..f76f9558e84 --- /dev/null +++ b/chromium/extensions/renderer/activity_log_converter_strategy.cc @@ -0,0 +1,83 @@ +// 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/renderer/activity_log_converter_strategy.h" + +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "v8/include/v8.h" + +namespace extensions { + +namespace { + +// Summarize a V8 value. This performs a shallow conversion in all cases, and +// returns only a string with a description of the value (e.g., +// "[HTMLElement]"). +scoped_ptr<base::Value> SummarizeV8Value(v8::Isolate* isolate, + v8::Local<v8::Object> object) { + v8::TryCatch try_catch(isolate); + v8::Isolate::DisallowJavascriptExecutionScope scope( + isolate, v8::Isolate::DisallowJavascriptExecutionScope::THROW_ON_FAILURE); + v8::Local<v8::String> name = v8::String::NewFromUtf8(isolate, "["); + if (object->IsFunction()) { + name = + v8::String::Concat(name, v8::String::NewFromUtf8(isolate, "Function")); + v8::Local<v8::Value> fname = + v8::Local<v8::Function>::Cast(object)->GetName(); + if (fname->IsString() && v8::Local<v8::String>::Cast(fname)->Length()) { + name = v8::String::Concat(name, v8::String::NewFromUtf8(isolate, " ")); + name = v8::String::Concat(name, v8::Local<v8::String>::Cast(fname)); + name = v8::String::Concat(name, v8::String::NewFromUtf8(isolate, "()")); + } + } else { + name = v8::String::Concat(name, object->GetConstructorName()); + } + name = v8::String::Concat(name, v8::String::NewFromUtf8(isolate, "]")); + + if (try_catch.HasCaught()) { + return scoped_ptr<base::Value>( + new base::StringValue("[JS Execution Exception]")); + } + + return scoped_ptr<base::Value>( + new base::StringValue(std::string(*v8::String::Utf8Value(name)))); +} + +} // namespace + +ActivityLogConverterStrategy::ActivityLogConverterStrategy() {} + +ActivityLogConverterStrategy::~ActivityLogConverterStrategy() {} + +bool ActivityLogConverterStrategy::FromV8Object( + v8::Local<v8::Object> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const { + return FromV8Internal(value, out, isolate, callback); +} + +bool ActivityLogConverterStrategy::FromV8Array( + v8::Local<v8::Array> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const { + return FromV8Internal(value, out, isolate, callback); +} + +bool ActivityLogConverterStrategy::FromV8Internal( + v8::Local<v8::Object> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const { + scoped_ptr<base::Value> parsed_value; + parsed_value = SummarizeV8Value(isolate, value); + *out = parsed_value.release(); + + return true; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/activity_log_converter_strategy.h b/chromium/extensions/renderer/activity_log_converter_strategy.h new file mode 100644 index 00000000000..fd7a1d2babf --- /dev/null +++ b/chromium/extensions/renderer/activity_log_converter_strategy.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_ACTIVITY_LOG_CONVERTER_STRATEGY_H_ +#define EXTENSIONS_RENDERER_ACTIVITY_LOG_CONVERTER_STRATEGY_H_ + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "content/public/child/v8_value_converter.h" +#include "v8/include/v8.h" + +namespace extensions { + +// This class is used by activity logger and extends behavior of +// V8ValueConverter. It overwrites conversion of V8 arrays and objects. When +// converting arrays and objects, we must not invoke any JS code which may +// result in triggering activity logger. In such case, the log entries will be +// generated due to V8 object conversion rather than extension activity. +class ActivityLogConverterStrategy + : public content::V8ValueConverter::Strategy { + public: + typedef content::V8ValueConverter::Strategy::FromV8ValueCallback + FromV8ValueCallback; + + ActivityLogConverterStrategy(); + ~ActivityLogConverterStrategy() override; + + // content::V8ValueConverter::Strategy implementation. + bool FromV8Object(v8::Local<v8::Object> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const override; + bool FromV8Array(v8::Local<v8::Array> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const override; + + private: + bool FromV8Internal(v8::Local<v8::Object> value, + base::Value** out, + v8::Isolate* isolate, + const FromV8ValueCallback& callback) const; + + DISALLOW_COPY_AND_ASSIGN(ActivityLogConverterStrategy); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_ACTIVITY_LOG_CONVERTER_STRATEGY_H_ diff --git a/chromium/extensions/renderer/activity_log_converter_strategy_unittest.cc b/chromium/extensions/renderer/activity_log_converter_strategy_unittest.cc new file mode 100644 index 00000000000..fbb9597d661 --- /dev/null +++ b/chromium/extensions/renderer/activity_log_converter_strategy_unittest.cc @@ -0,0 +1,172 @@ +// 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 "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "extensions/renderer/activity_log_converter_strategy.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "v8/include/v8.h" + +using content::V8ValueConverter; + +namespace extensions { + +class ActivityLogConverterStrategyTest : public testing::Test { + public: + ActivityLogConverterStrategyTest() + : isolate_(v8::Isolate::GetCurrent()), + handle_scope_(isolate_), + context_(isolate_, v8::Context::New(isolate_)), + context_scope_(context()) {} + + protected: + void SetUp() override { + converter_.reset(V8ValueConverter::create()); + strategy_.reset(new ActivityLogConverterStrategy()); + converter_->SetFunctionAllowed(true); + converter_->SetStrategy(strategy_.get()); + } + + testing::AssertionResult VerifyNull(v8::Local<v8::Value> v8_value) { + scoped_ptr<base::Value> value( + converter_->FromV8Value(v8_value, context())); + if (value->IsType(base::Value::TYPE_NULL)) + return testing::AssertionSuccess(); + return testing::AssertionFailure(); + } + + testing::AssertionResult VerifyBoolean(v8::Local<v8::Value> v8_value, + bool expected) { + bool out; + scoped_ptr<base::Value> value( + converter_->FromV8Value(v8_value, context())); + if (value->IsType(base::Value::TYPE_BOOLEAN) + && value->GetAsBoolean(&out) + && out == expected) + return testing::AssertionSuccess(); + return testing::AssertionFailure(); + } + + testing::AssertionResult VerifyInteger(v8::Local<v8::Value> v8_value, + int expected) { + int out; + scoped_ptr<base::Value> value( + converter_->FromV8Value(v8_value, context())); + if (value->IsType(base::Value::TYPE_INTEGER) + && value->GetAsInteger(&out) + && out == expected) + return testing::AssertionSuccess(); + return testing::AssertionFailure(); + } + + testing::AssertionResult VerifyDouble(v8::Local<v8::Value> v8_value, + double expected) { + double out; + scoped_ptr<base::Value> value( + converter_->FromV8Value(v8_value, context())); + if (value->IsType(base::Value::TYPE_DOUBLE) + && value->GetAsDouble(&out) + && out == expected) + return testing::AssertionSuccess(); + return testing::AssertionFailure(); + } + + testing::AssertionResult VerifyString(v8::Local<v8::Value> v8_value, + const std::string& expected) { + std::string out; + scoped_ptr<base::Value> value( + converter_->FromV8Value(v8_value, context())); + if (value->IsType(base::Value::TYPE_STRING) + && value->GetAsString(&out) + && out == expected) + return testing::AssertionSuccess(); + return testing::AssertionFailure(); + } + + v8::Local<v8::Context> context() const { + return v8::Local<v8::Context>::New(isolate_, context_); + } + + v8::Isolate* isolate_; + v8::HandleScope handle_scope_; + v8::Global<v8::Context> context_; + v8::Context::Scope context_scope_; + scoped_ptr<V8ValueConverter> converter_; + scoped_ptr<ActivityLogConverterStrategy> strategy_; +}; + +TEST_F(ActivityLogConverterStrategyTest, ConversionTest) { + const char* source = "(function() {" + "function foo() {}" + "return {" + "null: null," + "true: true," + "false: false," + "positive_int: 42," + "negative_int: -42," + "zero: 0," + "double: 88.8," + "big_integral_double: 9007199254740992.0," // 2.0^53 + "string: \"foobar\"," + "empty_string: \"\"," + "dictionary: {" + "foo: \"bar\"," + "hot: \"dog\"," + "}," + "empty_dictionary: {}," + "list: [ \"monkey\", \"balls\" ]," + "empty_list: []," + "function: (0, function() {})," // ensure function is anonymous + "named_function: foo" + "};" + "})();"; + + v8::MicrotasksScope microtasks( + isolate_, v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::Local<v8::Script> script( + v8::Script::Compile(v8::String::NewFromUtf8(isolate_, source))); + v8::Local<v8::Object> v8_object = script->Run().As<v8::Object>(); + + EXPECT_TRUE(VerifyString(v8_object, "[Object]")); + EXPECT_TRUE( + VerifyNull(v8_object->Get(v8::String::NewFromUtf8(isolate_, "null")))); + EXPECT_TRUE(VerifyBoolean( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "true")), true)); + EXPECT_TRUE(VerifyBoolean( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "false")), false)); + EXPECT_TRUE(VerifyInteger( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "positive_int")), 42)); + EXPECT_TRUE(VerifyInteger( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "negative_int")), -42)); + EXPECT_TRUE(VerifyInteger( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "zero")), 0)); + EXPECT_TRUE(VerifyDouble( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "double")), 88.8)); + EXPECT_TRUE(VerifyDouble( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "big_integral_double")), + 9007199254740992.0)); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "string")), "foobar")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "empty_string")), "")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "dictionary")), + "[Object]")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "empty_dictionary")), + "[Object]")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "list")), "[Array]")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "empty_list")), + "[Array]")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "function")), + "[Function]")); + EXPECT_TRUE(VerifyString( + v8_object->Get(v8::String::NewFromUtf8(isolate_, "named_function")), + "[Function foo()]")); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/automation/automation_api_helper.cc b/chromium/extensions/renderer/api/automation/automation_api_helper.cc new file mode 100644 index 00000000000..ab10682f6db --- /dev/null +++ b/chromium/extensions/renderer/api/automation/automation_api_helper.cc @@ -0,0 +1,90 @@ +// 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/renderer/api/automation/automation_api_helper.h" + +#include "content/public/renderer/render_view.h" +#include "extensions/common/extension_messages.h" +#include "third_party/WebKit/public/web/WebAXObject.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebElement.h" +#include "third_party/WebKit/public/web/WebExceptionCode.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebNode.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace extensions { + +AutomationApiHelper::AutomationApiHelper(content::RenderView* render_view) + : content::RenderViewObserver(render_view) { +} + +AutomationApiHelper::~AutomationApiHelper() { +} + +bool AutomationApiHelper::OnMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(AutomationApiHelper, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_AutomationQuerySelector, OnQuerySelector) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void AutomationApiHelper::OnQuerySelector(int request_id, + int acc_obj_id, + const base::string16& selector) { + ExtensionHostMsg_AutomationQuerySelector_Error error; + if (!render_view() || !render_view()->GetWebView() || + !render_view()->GetWebView()->mainFrame()) { + error.value = ExtensionHostMsg_AutomationQuerySelector_Error::kNoMainFrame; + Send(new ExtensionHostMsg_AutomationQuerySelector_Result( + routing_id(), request_id, error, 0)); + return; + } + blink::WebDocument document = + render_view()->GetWebView()->mainFrame()->document(); + if (document.isNull()) { + error.value = + ExtensionHostMsg_AutomationQuerySelector_Error::kNoDocument; + Send(new ExtensionHostMsg_AutomationQuerySelector_Result( + routing_id(), request_id, error, 0)); + return; + } + blink::WebNode start_node = document; + if (acc_obj_id > 0) { + blink::WebAXObject start_acc_obj = + document.accessibilityObjectFromID(acc_obj_id); + if (start_acc_obj.isNull()) { + error.value = + ExtensionHostMsg_AutomationQuerySelector_Error::kNodeDestroyed; + Send(new ExtensionHostMsg_AutomationQuerySelector_Result( + routing_id(), request_id, error, 0)); + return; + } + + start_node = start_acc_obj.node(); + while (start_node.isNull()) { + start_acc_obj = start_acc_obj.parentObject(); + start_node = start_acc_obj.node(); + } + } + blink::WebString web_selector(selector); + blink::WebExceptionCode ec = 0; + blink::WebElement result_element = start_node.querySelector(web_selector, ec); + int result_acc_obj_id = 0; + if (!ec && !result_element.isNull()) { + blink::WebAXObject result_acc_obj = result_element.accessibilityObject(); + if (!result_acc_obj.isDetached()) { + while (result_acc_obj.accessibilityIsIgnored()) + result_acc_obj = result_acc_obj.parentObject(); + + result_acc_obj_id = result_acc_obj.axID(); + } + } + Send(new ExtensionHostMsg_AutomationQuerySelector_Result( + routing_id(), request_id, error, result_acc_obj_id)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/automation/automation_api_helper.h b/chromium/extensions/renderer/api/automation/automation_api_helper.h new file mode 100644 index 00000000000..32aa2ef0dff --- /dev/null +++ b/chromium/extensions/renderer/api/automation/automation_api_helper.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_ +#define EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_ + +#include "base/macros.h" +#include "base/strings/string16.h" +#include "content/public/renderer/render_view_observer.h" + +namespace extensions { + +// Renderer-side implementation for chrome.automation API (for the few pieces +// which aren't built in to the existing accessibility system). +class AutomationApiHelper : public content::RenderViewObserver { + public: + explicit AutomationApiHelper(content::RenderView* render_view); + ~AutomationApiHelper() override; + + private: + // RenderViewObserver implementation. + bool OnMessageReceived(const IPC::Message& message) override; + + void OnQuerySelector(int acc_obj_id, + int request_id, + const base::string16& selector); + + DISALLOW_COPY_AND_ASSIGN(AutomationApiHelper); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_ diff --git a/chromium/extensions/renderer/api/display_source/OWNERS b/chromium/extensions/renderer/api/display_source/OWNERS new file mode 100644 index 00000000000..f7e4f6f8751 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/OWNERS @@ -0,0 +1,2 @@ +alexander.shalamov@intel.com +mikhail.pozdnyakov@intel.com diff --git a/chromium/extensions/renderer/api/display_source/display_source_session.cc b/chromium/extensions/renderer/api/display_source/display_source_session.cc new file mode 100644 index 00000000000..20b14420dd9 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/display_source_session.cc @@ -0,0 +1,43 @@ +// Copyright 2015 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/api/display_source/display_source_session.h" + +#if defined(ENABLE_WIFI_DISPLAY) +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_session.h" +#endif + +namespace extensions { + +DisplaySourceSessionParams::DisplaySourceSessionParams() + : auth_method(api::display_source::AUTHENTICATION_METHOD_NONE) { +} + +DisplaySourceSessionParams::~DisplaySourceSessionParams() = default; + +DisplaySourceSession::DisplaySourceSession() + : state_(Idle) { +} + +DisplaySourceSession::~DisplaySourceSession() = default; + +void DisplaySourceSession::SetNotificationCallbacks( + const base::Closure& terminated_callback, + const ErrorCallback& error_callback) { + DCHECK(terminated_callback_.is_null()); + DCHECK(error_callback_.is_null()); + + terminated_callback_ = terminated_callback; + error_callback_ = error_callback; +} + +scoped_ptr<DisplaySourceSession> DisplaySourceSessionFactory::CreateSession( + const DisplaySourceSessionParams& params) { +#if defined(ENABLE_WIFI_DISPLAY) + return scoped_ptr<DisplaySourceSession>(new WiFiDisplaySession(params)); +#endif + return nullptr; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/display_source_session.h b/chromium/extensions/renderer/api/display_source/display_source_session.h new file mode 100644 index 00000000000..922126d994b --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/display_source_session.h @@ -0,0 +1,116 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_DISPLAY_SOURCE_SESSION_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_DISPLAY_SOURCE_SESSION_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "extensions/common/api/display_source.h" +#include "third_party/WebKit/public/web/WebDOMMediaStreamTrack.h" + +namespace content { +class RenderFrame; +} + +namespace extensions { + +using DisplaySourceAuthInfo = api::display_source::AuthenticationInfo; +using DisplaySourceAuthMethod = api::display_source::AuthenticationMethod; +using DisplaySourceErrorType = api::display_source::ErrorType; + +// This class represents a generic display source session interface. +class DisplaySourceSession { + public: + using CompletionCallback = + base::Callback<void(bool success, const std::string& error_description)>; + using ErrorCallback = + base::Callback<void(DisplaySourceErrorType error_type, + const std::string& error_description)>; + + // State flow is ether: + // 'Idle' -> 'Establishing' -> 'Established' -> 'Terminating' -> 'Idle' + // (terminated by Terminate() call) + // or + // 'Idle' -> 'Establishing' -> 'Established' -> 'Idle' + // (terminated from sink device or due to an error) + enum State { + Idle, + Establishing, + Established, + Terminating + }; + + virtual ~DisplaySourceSession(); + + // Starts the session. + // The session state should be set to 'Establishing' immediately after this + // method is called. + // |callback| : Called with 'success' flag set to 'true' if the session is + // successfully started (state should be set to 'Established') + // + // Called with 'success' flag set to 'false' if the session + // has failed to start (state should be set back to 'Idle'). + // The 'error_description' argument contains description of + // an error that had caused the call falure. + virtual void Start(const CompletionCallback& callback) = 0; + + // Terminates the session. + // The session state should be set to 'Terminating' immediately after this + // method is called. + // |callback| : Called with 'success' flag set to 'true' if the session is + // successfully terminated (state should be set to 'Idle') + // + // Called with 'success' flag set to 'false' if the session + // could not terminate (state should not be modified). + // The 'error_description' argument contains description of + // an error that had caused the call falure. + virtual void Terminate(const CompletionCallback& callback) = 0; + + State state() const { return state_; } + + // Sets the callbacks invoked to inform about the session's state changes. + // It is required to set the callbacks before the session is started. + // |terminated_callback| : Called when session was terminated (state + // should be set to 'Idle') + // |error_callback| : Called if a fatal error has occured and the session + // will be terminated soon for emergency reasons. + void SetNotificationCallbacks(const base::Closure& terminated_callback, + const ErrorCallback& error_callback); + + protected: + DisplaySourceSession(); + + State state_; + base::Closure terminated_callback_; + ErrorCallback error_callback_; + + private: + DISALLOW_COPY_AND_ASSIGN(DisplaySourceSession); +}; + +struct DisplaySourceSessionParams { + DisplaySourceSessionParams(); + ~DisplaySourceSessionParams(); + + int sink_id; + blink::WebMediaStreamTrack video_track; + blink::WebMediaStreamTrack audio_track; + DisplaySourceAuthMethod auth_method; + std::string auth_data; + content::RenderFrame* render_frame; +}; + +class DisplaySourceSessionFactory { + public: + static scoped_ptr<DisplaySourceSession> CreateSession( + const DisplaySourceSessionParams& params); + private: + DISALLOW_COPY_AND_ASSIGN(DisplaySourceSessionFactory); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_DISPLAY_SOURCE_SESSION_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.cc new file mode 100644 index 00000000000..d17ca85056b --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.cc @@ -0,0 +1,166 @@ +// Copyright 2016 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 "base/logging.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h" + +#include <cstring> + +namespace extensions { + +WiFiDisplayElementaryStreamDescriptor::WiFiDisplayElementaryStreamDescriptor( + const WiFiDisplayElementaryStreamDescriptor& other) { + if (!other.empty()) { + data_.reset(new uint8_t[other.size()]); + std::memcpy(data(), other.data(), other.size()); + } +} + +WiFiDisplayElementaryStreamDescriptor::WiFiDisplayElementaryStreamDescriptor( + WiFiDisplayElementaryStreamDescriptor&&) = default; + +WiFiDisplayElementaryStreamDescriptor::WiFiDisplayElementaryStreamDescriptor( + DescriptorTag tag, + uint8_t length) + : data_(new uint8_t[kHeaderSize + length]) { + uint8_t* p = data(); + *p++ = tag; + *p++ = length; + DCHECK_EQ(private_data(), p); +} + +WiFiDisplayElementaryStreamDescriptor:: + ~WiFiDisplayElementaryStreamDescriptor() {} + +WiFiDisplayElementaryStreamDescriptor& WiFiDisplayElementaryStreamDescriptor:: +operator=(WiFiDisplayElementaryStreamDescriptor&&) = default; + +const uint8_t* WiFiDisplayElementaryStreamDescriptor::data() const { + return data_.get(); +} + +uint8_t* WiFiDisplayElementaryStreamDescriptor::data() { + return data_.get(); +} + +size_t WiFiDisplayElementaryStreamDescriptor::size() const { + if (empty()) + return 0u; + return kHeaderSize + length(); +} + +WiFiDisplayElementaryStreamDescriptor +WiFiDisplayElementaryStreamDescriptor::AVCTimingAndHRD::Create() { + WiFiDisplayElementaryStreamDescriptor descriptor( + DESCRIPTOR_TAG_AVC_TIMING_AND_HRD, 2u); + uint8_t* p = descriptor.private_data(); + *p++ = (false << 7) | // hrd_management_valid_flag + (0x3Fu << 1) | // reserved (all six bits on) + (false << 0); // picture_and_timing_info_present + // No picture nor timing info bits. + *p++ = (false << 7) | // fixed_frame_rate_flag + (false << 6) | // temporal_poc_flag + (false << 5) | // picture_to_display_conversion_flag + (0x1Fu << 0); // reserved (all five bits on) + DCHECK_EQ(descriptor.end(), p); + return descriptor; +} + +WiFiDisplayElementaryStreamDescriptor +WiFiDisplayElementaryStreamDescriptor::AVCVideo::Create( + uint8_t profile_idc, + bool constraint_set0_flag, + bool constraint_set1_flag, + bool constraint_set2_flag, + uint8_t avc_compatible_flags, + uint8_t level_idc, + bool avc_still_present) { + const bool avc_24_hour_picture_flag = false; + WiFiDisplayElementaryStreamDescriptor descriptor(DESCRIPTOR_TAG_AVC_VIDEO, + 4u); + uint8_t* p = descriptor.private_data(); + *p++ = profile_idc; + *p++ = (constraint_set0_flag << 7) | (constraint_set1_flag << 6) | + (constraint_set2_flag << 5) | (avc_compatible_flags << 0); + *p++ = level_idc; + *p++ = (avc_still_present << 7) | (avc_24_hour_picture_flag << 6) | + (0x3Fu << 0); // Reserved (all 6 bits on) + DCHECK_EQ(descriptor.end(), p); + return descriptor; +} + +namespace { +struct LPCMAudioStreamByte0 { + enum : uint8_t { + kBitsPerSampleShift = 3u, + kBitsPerSampleMask = ((1u << 2) - 1u) << kBitsPerSampleShift, + kEmphasisFlagShift = 0u, + kEmphasisFlagMask = 1u << kEmphasisFlagShift, + kReservedOnBitsShift = 1u, + kReservedOnBitsMask = ((1u << 2) - 1u) << kReservedOnBitsShift, + kSamplingFrequencyShift = 5u, + kSamplingFrequencyMask = ((1u << 3) - 1u) << kSamplingFrequencyShift, + }; +}; + +struct LPCMAudioStreamByte1 { + enum : uint8_t { + kNumberOfChannelsShift = 5u, + kNumberOfChannelsMask = ((1u << 3) - 1u) << kNumberOfChannelsShift, + kReservedOnBitsShift = 0u, + kReservedOnBitsMask = ((1u << 4) - 1u) << kReservedOnBitsShift, + // The bit not listed above having a shift 4u is a reserved off bit. + }; +}; +} // namespace + +WiFiDisplayElementaryStreamDescriptor +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::Create( + SamplingFrequency sampling_frequency, + BitsPerSample bits_per_sample, + bool emphasis_flag, + NumberOfChannels number_of_channels) { + WiFiDisplayElementaryStreamDescriptor descriptor( + DESCRIPTOR_TAG_LPCM_AUDIO_STREAM, 2u); + uint8_t* p = descriptor.private_data(); + *p++ = (sampling_frequency << LPCMAudioStreamByte0::kSamplingFrequencyShift) | + (bits_per_sample << LPCMAudioStreamByte0::kBitsPerSampleShift) | + LPCMAudioStreamByte0::kReservedOnBitsMask | + (emphasis_flag << LPCMAudioStreamByte0::kEmphasisFlagShift); + *p++ = (number_of_channels << LPCMAudioStreamByte1::kNumberOfChannelsShift) | + LPCMAudioStreamByte1::kReservedOnBitsMask; + DCHECK_EQ(descriptor.end(), p); + return descriptor; +} + +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::BitsPerSample +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::bits_per_sample() + const { + return static_cast<BitsPerSample>( + (private_data()[0] & LPCMAudioStreamByte0::kBitsPerSampleMask) >> + LPCMAudioStreamByte0::kBitsPerSampleShift); +} + +bool WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::emphasis_flag() + const { + return (private_data()[0] & LPCMAudioStreamByte0::kEmphasisFlagMask) != 0u; +} + +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::NumberOfChannels +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::number_of_channels() + const { + return static_cast<NumberOfChannels>( + (private_data()[1] & LPCMAudioStreamByte1::kNumberOfChannelsMask) >> + LPCMAudioStreamByte1::kNumberOfChannelsShift); +} + +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::SamplingFrequency +WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream::sampling_frequency() + const { + return static_cast<SamplingFrequency>( + (private_data()[0] & LPCMAudioStreamByte0::kSamplingFrequencyMask) >> + LPCMAudioStreamByte0::kSamplingFrequencyShift); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h new file mode 100644 index 00000000000..c149af851f8 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h @@ -0,0 +1,122 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_DESCRIPTOR_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_DESCRIPTOR_H_ + +#include <stdint.h> +#include <type_traits> + +#include "base/memory/scoped_ptr.h" + +namespace extensions { + +// WiFi Display elementary stream descriptors are used for passing descriptive +// information about elementary streams to WiFiDisplayTransportStreamPacketizer +// which then packetizes that information so that it can be passed to a remote +// sink. +class WiFiDisplayElementaryStreamDescriptor { + public: + enum { kHeaderSize = 2u }; + + enum DescriptorTag : uint8_t { + DESCRIPTOR_TAG_AVC_VIDEO = 0x28u, + DESCRIPTOR_TAG_AVC_TIMING_AND_HRD = 0x2A, + DESCRIPTOR_TAG_LPCM_AUDIO_STREAM = 0x83u, + }; + + // Make Google Test treat this class as a container. + using const_iterator = const uint8_t*; + using iterator = const uint8_t*; + + WiFiDisplayElementaryStreamDescriptor( + const WiFiDisplayElementaryStreamDescriptor&); + WiFiDisplayElementaryStreamDescriptor( + WiFiDisplayElementaryStreamDescriptor&&); + ~WiFiDisplayElementaryStreamDescriptor(); + + WiFiDisplayElementaryStreamDescriptor& operator=( + WiFiDisplayElementaryStreamDescriptor&&); + + const uint8_t* begin() const { return data(); } + const uint8_t* data() const; + bool empty() const { return !data_; } + const uint8_t* end() const { return data() + size(); } + size_t size() const; + + DescriptorTag tag() const { return static_cast<DescriptorTag>(data()[0]); } + uint8_t length() const { return data()[1]; } + + // AVC (Advanced Video Coding) timing and HRD (Hypothetical Reference + // Decoder) descriptor provides timing and HRD parameters for a video stream. + struct AVCTimingAndHRD { + static WiFiDisplayElementaryStreamDescriptor Create(); + }; + + // AVC (Advanced Video Coding) video descriptor provides basic coding + // parameters for a video stream. + struct AVCVideo { + static WiFiDisplayElementaryStreamDescriptor Create( + uint8_t profile_idc, + bool constraint_set0_flag, + bool constraint_set1_flag, + bool constraint_set2_flag, + uint8_t avc_compatible_flags, + uint8_t level_idc, + bool avc_still_present); + }; + + class LPCMAudioStream; + + protected: + WiFiDisplayElementaryStreamDescriptor(DescriptorTag tag, uint8_t length); + + uint8_t* data(); + const uint8_t* private_data() const { return data() + kHeaderSize; } + uint8_t* private_data() { return data() + kHeaderSize; } + + private: + scoped_ptr<uint8_t[]> data_; +}; + +// LPCM (Linear pulse-code modulation) audio stream descriptor provides basic +// coding parameters for a private WiFi Display LPCM audio stream. +class WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream + : public WiFiDisplayElementaryStreamDescriptor { + public: + enum { kTag = DESCRIPTOR_TAG_LPCM_AUDIO_STREAM }; + enum BitsPerSample : uint8_t { BITS_PER_SAMPLE_16 = 0u }; + enum NumberOfChannels : uint8_t { + NUMBER_OF_CHANNELS_DUAL_MONO = 0u, + NUMBER_OF_CHANNELS_STEREO = 1u + }; + enum SamplingFrequency : uint8_t { + SAMPLING_FREQUENCY_44_1K = 1u, + SAMPLING_FREQUENCY_48K = 2u + }; + + static WiFiDisplayElementaryStreamDescriptor Create( + SamplingFrequency sampling_frequency, + BitsPerSample bits_per_sample, + bool emphasis_flag, + NumberOfChannels number_of_channels); + + BitsPerSample bits_per_sample() const; + bool emphasis_flag() const; + NumberOfChannels number_of_channels() const; + SamplingFrequency sampling_frequency() const; +}; + +// Subclasses of WiFiDisplayElementaryStreamDescriptor MUST NOT define new +// member variables but only new non-virtual member functions which parse +// the inherited data. This allows WiFiDisplayElementaryStreamDescriptor +// pointers to be cast to subclass pointers. +static_assert( + std::is_standard_layout< + WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream>::value, + "Forbidden memory layout for an elementary stream descriptor"); + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_DESCRIPTOR_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor_unittest.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor_unittest.cc new file mode 100644 index 00000000000..4e049db9ba7 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor_unittest.cc @@ -0,0 +1,151 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h" + +#include <algorithm> +#include <vector> + +#include "testing/gtest/include/gtest/gtest.h" + +using LPCMAudioStreamDescriptor = + extensions::WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream; + +namespace extensions { + +namespace { + +// Copy constructors cannot always be tested by calling them directly as +// a compiler is allowed to optimize copy constructor calls away in certain +// cases (that is called return value optimization). Therefore, this helper +// function is needed to really create a copy of an object. +template <typename T> +T Copy(const T& t) { + return t; +} + +class Data : public std::vector<uint8_t> { + public: + template <size_t N> + explicit Data(const char (&str)[N]) { + EXPECT_EQ('\0', str[N - 1]); + insert(end(), str, str + N - 1); + } + + bool operator==(const WiFiDisplayElementaryStreamDescriptor& rhs) const { + return size() == rhs.size() && std::equal(begin(), end(), rhs.begin()); + } +}; + +TEST(WiFiDisplayElementaryStreamDescriptorTest, AVCTimingAndHRDDescriptor) { + using AVCTimingAndHRDDescriptor = + WiFiDisplayElementaryStreamDescriptor::AVCTimingAndHRD; + EXPECT_EQ(Data("\x2A\x02\x7E\x1F"), AVCTimingAndHRDDescriptor::Create()); + EXPECT_EQ(Data("\x2A\x02\x7E\x1F"), + Copy(AVCTimingAndHRDDescriptor::Create())); +} + +TEST(WiFiDisplayElementaryStreamDescriptorTest, AVCVideoDescriptor) { + using AVCVideoDescriptor = WiFiDisplayElementaryStreamDescriptor::AVCVideo; + EXPECT_EQ( + Data("\x28\x04\x00\x00\x00\x3F"), + AVCVideoDescriptor::Create(0x0u, false, false, false, 0x0u, 0x0u, false)); + EXPECT_EQ(Data("\x28\x04\x00\x00\x00\x3F"), + Copy(AVCVideoDescriptor::Create(0x0u, false, false, false, 0x0u, + 0x0u, false))); + EXPECT_EQ(Data("\x28\x04\xFF\x00\x00\x3F"), + AVCVideoDescriptor::Create(0xFFu, false, false, false, 0x0u, 0x0u, + false)); + EXPECT_EQ( + Data("\x28\x04\x00\x80\x00\x3F"), + AVCVideoDescriptor::Create(0x0u, true, false, false, 0x0u, 0x0u, false)); + EXPECT_EQ( + Data("\x28\x04\x00\x40\x00\x3F"), + AVCVideoDescriptor::Create(0x0u, false, true, false, 0x0u, 0x0u, false)); + EXPECT_EQ( + Data("\x28\x04\x00\x20\x00\x3F"), + AVCVideoDescriptor::Create(0x0u, false, false, true, 0x0u, 0x0u, false)); + EXPECT_EQ(Data("\x28\x04\x00\x1F\x00\x3F"), + AVCVideoDescriptor::Create(0x0u, false, false, false, 0x1Fu, 0x0u, + false)); + EXPECT_EQ(Data("\x28\x04\x00\x00\xFF\x3F"), + AVCVideoDescriptor::Create(0x0u, false, false, false, 0x0u, 0xFFu, + false)); + EXPECT_EQ( + Data("\x28\x04\x00\x00\x00\xBF"), + AVCVideoDescriptor::Create(0x0u, false, false, false, 0x0u, 0x0u, true)); +} + +class LPCMAudioStreamDescriptorTest + : public testing::TestWithParam< + testing::tuple<LPCMAudioStreamDescriptor::SamplingFrequency, + LPCMAudioStreamDescriptor::BitsPerSample, + bool, + LPCMAudioStreamDescriptor::NumberOfChannels, + Data>> { + protected: + LPCMAudioStreamDescriptorTest() + : sampling_frequency_(testing::get<0>(GetParam())), + bits_per_sample_(testing::get<1>(GetParam())), + emphasis_flag_(testing::get<2>(GetParam())), + number_of_channels_(testing::get<3>(GetParam())), + expected_data_(testing::get<4>(GetParam())), + descriptor_(LPCMAudioStreamDescriptor::Create(sampling_frequency_, + bits_per_sample_, + emphasis_flag_, + number_of_channels_)) {} + + const LPCMAudioStreamDescriptor::SamplingFrequency sampling_frequency_; + const LPCMAudioStreamDescriptor::BitsPerSample bits_per_sample_; + const bool emphasis_flag_; + const LPCMAudioStreamDescriptor::NumberOfChannels number_of_channels_; + const Data expected_data_; + const WiFiDisplayElementaryStreamDescriptor descriptor_; +}; + +TEST_P(LPCMAudioStreamDescriptorTest, Create) { + EXPECT_EQ(expected_data_, descriptor_); + EXPECT_EQ(expected_data_, Copy(descriptor_)); +} + +TEST_P(LPCMAudioStreamDescriptorTest, Accessors) { + ASSERT_EQ(LPCMAudioStreamDescriptor::kTag, descriptor_.tag()); + const LPCMAudioStreamDescriptor& descriptor = + *static_cast<const LPCMAudioStreamDescriptor*>(&descriptor_); + EXPECT_EQ(sampling_frequency_, descriptor.sampling_frequency()); + EXPECT_EQ(bits_per_sample_, descriptor.bits_per_sample()); + EXPECT_EQ(emphasis_flag_, descriptor.emphasis_flag()); + EXPECT_EQ(number_of_channels_, descriptor.number_of_channels()); +} + +INSTANTIATE_TEST_CASE_P( + WiFiDisplayElementaryStreamDescriptorTests, + LPCMAudioStreamDescriptorTest, + testing::Values(testing::make_tuple( + LPCMAudioStreamDescriptor::SAMPLING_FREQUENCY_44_1K, + LPCMAudioStreamDescriptor::BITS_PER_SAMPLE_16, + false, + LPCMAudioStreamDescriptor::NUMBER_OF_CHANNELS_DUAL_MONO, + Data("\x83\x02\x26\x0F")), + testing::make_tuple( + LPCMAudioStreamDescriptor::SAMPLING_FREQUENCY_48K, + LPCMAudioStreamDescriptor::BITS_PER_SAMPLE_16, + false, + LPCMAudioStreamDescriptor::NUMBER_OF_CHANNELS_DUAL_MONO, + Data("\x83\x02\x46\x0F")), + testing::make_tuple( + LPCMAudioStreamDescriptor::SAMPLING_FREQUENCY_44_1K, + LPCMAudioStreamDescriptor::BITS_PER_SAMPLE_16, + true, + LPCMAudioStreamDescriptor::NUMBER_OF_CHANNELS_DUAL_MONO, + Data("\x83\x02\x27\x0F")), + testing::make_tuple( + LPCMAudioStreamDescriptor::SAMPLING_FREQUENCY_44_1K, + LPCMAudioStreamDescriptor::BITS_PER_SAMPLE_16, + false, + LPCMAudioStreamDescriptor::NUMBER_OF_CHANNELS_STEREO, + Data("\x83\x02\x26\x2F")))); + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.cc new file mode 100644 index 00000000000..2c84885db87 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.cc @@ -0,0 +1,46 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_elementary_stream_info.h" + +#include <utility> + +namespace extensions { + +WiFiDisplayElementaryStreamInfo::WiFiDisplayElementaryStreamInfo( + ElementaryStreamType type) + : type_(type) {} + +WiFiDisplayElementaryStreamInfo::WiFiDisplayElementaryStreamInfo( + ElementaryStreamType type, + DescriptorVector descriptors) + : descriptors_(std::move(descriptors)), type_(type) {} + +WiFiDisplayElementaryStreamInfo::WiFiDisplayElementaryStreamInfo( + const WiFiDisplayElementaryStreamInfo&) = default; + +WiFiDisplayElementaryStreamInfo::WiFiDisplayElementaryStreamInfo( + WiFiDisplayElementaryStreamInfo&&) = default; + +WiFiDisplayElementaryStreamInfo::~WiFiDisplayElementaryStreamInfo() {} + +WiFiDisplayElementaryStreamInfo& WiFiDisplayElementaryStreamInfo::operator=( + WiFiDisplayElementaryStreamInfo&&) = default; + +void WiFiDisplayElementaryStreamInfo::AddDescriptor( + WiFiDisplayElementaryStreamDescriptor descriptor) { + descriptors_.emplace_back(std::move(descriptor)); +} + +const WiFiDisplayElementaryStreamDescriptor* +WiFiDisplayElementaryStreamInfo::FindDescriptor( + DescriptorTag descriptor_tag) const { + for (const auto& descriptor : descriptors()) { + if (descriptor.tag() == descriptor_tag) + return &descriptor; + } + return nullptr; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h new file mode 100644 index 00000000000..a0e63a82b01 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h @@ -0,0 +1,58 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_INFO_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_INFO_H_ + +#include <stdint.h> +#include <vector> + +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h" + +namespace extensions { + +// WiFi Display elementary stream info is a container for elementary stream +// information and is used for passing that information to a WiFi Display +// transport stream packetizer. +class WiFiDisplayElementaryStreamInfo { + public: + using DescriptorVector = std::vector<WiFiDisplayElementaryStreamDescriptor>; + using DescriptorTag = WiFiDisplayElementaryStreamDescriptor::DescriptorTag; + + enum ElementaryStreamType : uint8_t { + AUDIO_AAC = 0x0Fu, + AUDIO_AC3 = 0x81u, + AUDIO_LPCM = 0x83u, + VIDEO_H264 = 0x1Bu, + }; + + explicit WiFiDisplayElementaryStreamInfo(ElementaryStreamType type); + WiFiDisplayElementaryStreamInfo(ElementaryStreamType type, + DescriptorVector descriptors); + WiFiDisplayElementaryStreamInfo(const WiFiDisplayElementaryStreamInfo&); + WiFiDisplayElementaryStreamInfo(WiFiDisplayElementaryStreamInfo&&); + ~WiFiDisplayElementaryStreamInfo(); + + WiFiDisplayElementaryStreamInfo& operator=(WiFiDisplayElementaryStreamInfo&&); + + const DescriptorVector& descriptors() const { return descriptors_; } + ElementaryStreamType type() const { return type_; } + + void AddDescriptor(WiFiDisplayElementaryStreamDescriptor descriptor); + const WiFiDisplayElementaryStreamDescriptor* FindDescriptor( + DescriptorTag descriptor_tag) const; + template <typename Descriptor> + const Descriptor* FindDescriptor() const { + return static_cast<const Descriptor*>( + FindDescriptor(static_cast<DescriptorTag>(Descriptor::kTag))); + } + + private: + DescriptorVector descriptors_; + ElementaryStreamType type_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_INFO_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.cc new file mode 100644 index 00000000000..39a6e3fcaf8 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.cc @@ -0,0 +1,188 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h" + +#include <cstring> + +#include "base/logging.h" + +namespace extensions { +namespace { + +// Code and parameters related to the Packetized Elementary Stream (PES) +// specification. +namespace pes { + +const size_t kOptionalHeaderBaseSize = 3u; +const size_t kPacketHeaderBaseSize = 6u; +const size_t kTimeStampSize = 5u; +const size_t kPacketHeaderMaxSize = + kPacketHeaderBaseSize + kOptionalHeaderBaseSize + 2u * kTimeStampSize; + +const size_t kUnitDataAlignment = 4u; + +size_t FillInTimeStamp(uint8_t* dst, + uint8_t pts_dts_indicator, + const base::TimeTicks& ts) { + // Convert to the number of 90 kHz ticks since some epoch. + // Always round up so that the number of ticks is never smaller than + // the number of program clock reference base ticks (which is not rounded + // because program clock reference is encoded with higher precision). + const uint64_t us = + static_cast<uint64_t>((ts - base::TimeTicks()).InMicroseconds()); + const uint64_t n = (us * 90u + 999u) / 1000u; + + // Expand PTS DTS indicator and a 33 bit time stamp to 40 bits: + // * 4 PTS DTS indicator bits, 3 time stamp bits, 1 on bit + // * 15 time stamp bits, 1 on bit + // * 15 time stamp bits, 1 on bit + size_t i = 0u; + dst[i++] = (pts_dts_indicator << 4) | (((n >> 30) & 0x7u) << 1) | (0x1u << 0); + dst[i++] = (n >> 22) & 0xFFu; + dst[i++] = (((n >> 15) & 0x7Fu) << 1) | (0x1u << 0); + dst[i++] = (n >> 7) & 0xFFu; + dst[i++] = (((n >> 0) & 0x7Fu) << 1) | (0x1u << 0); + DCHECK_EQ(i, kTimeStampSize); + return i; +} + +size_t FillInOptionalHeader(uint8_t* dst, + const base::TimeTicks& pts, + const base::TimeTicks& dts, + size_t unit_header_size) { + size_t i = 0u; + dst[i++] = (0x2u << 6) | // Marker bits (0b10) + (0x0u << 4) | // Scrambling control (0b00 for not) + (0x0u << 3) | // Priority + (0x0u << 2) | // Data alignment indicator (0b0 for not) + (0x0u << 1) | // Copyright (0b0 for not) + (0x0u << 0); // Original (0b0 for copy) + const uint8_t pts_dts_indicator = + !pts.is_null() ? (!dts.is_null() ? 0x3u : 0x2u) : 0x0u; + dst[i++] = (pts_dts_indicator << 6) | // PTS DTS indicator + (0x0u << 5) | // ESCR flag + (0x0u << 4) | // ES rate flag + (0x0u << 3) | // DSM trick mode flag + (0x0u << 2) | // Additional copy info flag + (0x0u << 1) | // CRC flag + (0x0u << 0); // Extension flag + const size_t header_length_index = i++; + const size_t optional_header_base_end_index = i; + DCHECK_EQ(i, kOptionalHeaderBaseSize); + + // Optional fields: + // PTS and DTS. + if (!pts.is_null()) { + i += FillInTimeStamp(&dst[i], pts_dts_indicator, pts); + if (!dts.is_null()) + i += FillInTimeStamp(&dst[i], 0x1u, dts); + } + + // Stuffing bytes (for unit data alignment). + const size_t remainder = + (kPacketHeaderBaseSize + i + unit_header_size) % kUnitDataAlignment; + if (remainder) { + const size_t n = kUnitDataAlignment - remainder; + std::memset(&dst[i], 0xFF, n); + i += n; + } + + dst[header_length_index] = i - optional_header_base_end_index; + return i; +} + +size_t FillInPacketHeader(uint8_t* dst, + uint8_t stream_id, + const base::TimeTicks& pts, + const base::TimeTicks& dts, + size_t unit_header_size, + size_t unit_size) { + // Reserve space for packet header base. + size_t i = kPacketHeaderBaseSize; + const size_t header_base_end_index = i; + + // Fill in optional header. + i += FillInOptionalHeader(&dst[i], pts, dts, unit_header_size); + + // Compute packet length. + size_t packet_length = + (i - header_base_end_index) + unit_header_size + unit_size; + if (packet_length >> 16) { + // The packet length is too large to be represented. That should only + // happen for video frames for which the packet length is not mandatory + // but may be set to 0, too. + DCHECK_GE(static_cast<unsigned>(stream_id), + WiFiDisplayElementaryStreamPacketizer::kFirstVideoStreamId); + DCHECK_LE(static_cast<unsigned>(stream_id), + WiFiDisplayElementaryStreamPacketizer::kLastVideoStreamId); + packet_length = 0u; + } + + // Fill in packet header base. + size_t j = 0u; + dst[j++] = 0x00u; // Packet start code prefix (0x000001 in three bytes). + dst[j++] = 0x00u; // + dst[j++] = 0x01u; // + dst[j++] = stream_id; + dst[j++] = packet_length >> 8; + dst[j++] = packet_length & 0xFFu; + DCHECK_EQ(j, kPacketHeaderBaseSize); + + return i; +} + +} // namespace pes + +} // namespace + +WiFiDisplayElementaryStreamPacket::WiFiDisplayElementaryStreamPacket( + const HeaderBuffer& header_data, + size_t header_size, + const uint8_t* unit_header_data, + size_t unit_header_size, + const uint8_t* unit_data, + size_t unit_size) + : header_(header_buffer_, header_size), + unit_header_(unit_header_data, unit_header_size), + unit_(unit_data, unit_size) { + // Copy the actual header data bytes from the |header_data| argument to + // the |header_buffer_| member buffer used in the member initialization list. + std::memcpy(header_buffer_, header_data, header_.size()); +} + +WiFiDisplayElementaryStreamPacket::WiFiDisplayElementaryStreamPacket( + WiFiDisplayElementaryStreamPacket&& other) + : header_(header_buffer_, other.header().size()), + unit_header_(other.unit_header().data(), other.unit_header().size()), + unit_(other.unit().data(), other.unit().size()) { + // Copy the actual header data bytes from |other.header().data()| to + // the |header_buffer_| member buffer used in the member initialization list. + std::memcpy(header_buffer_, other.header().data(), header_.size()); +} + +// static +WiFiDisplayElementaryStreamPacket +WiFiDisplayElementaryStreamPacketizer::EncodeElementaryStreamUnit( + uint8_t stream_id, + const uint8_t* unit_header_data, + size_t unit_header_size, + const uint8_t* unit_data, + size_t unit_size, + const base::TimeTicks& pts, + const base::TimeTicks& dts) { + if (!unit_header_data) { + DCHECK_EQ(0u, unit_header_size); + unit_header_data = unit_data; + } + + uint8_t header_data[pes::kPacketHeaderMaxSize]; + size_t header_size = pes::FillInPacketHeader(header_data, stream_id, pts, dts, + unit_header_size, unit_size); + return WiFiDisplayElementaryStreamPacket(header_data, header_size, + unit_header_data, unit_header_size, + unit_data, unit_size); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h new file mode 100644 index 00000000000..4201a58287d --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h @@ -0,0 +1,72 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_PACKETIZER_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_PACKETIZER_H_ + +#include "base/time/time.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_stream_packet_part.h" + +namespace extensions { + +// WiFi Display elementary stream packet represents a Packetized Elementary +// Stream (PES) packet containing WiFi Display elementary stream unit data. +class WiFiDisplayElementaryStreamPacket { + public: + using HeaderBuffer = uint8_t[19]; + + WiFiDisplayElementaryStreamPacket(const HeaderBuffer& header_data, + size_t header_size, + const uint8_t* unit_header_data, + size_t unit_header_size, + const uint8_t* unit_data, + size_t unit_size); + // WiFiDisplayElementaryStreamPacketizer::EncodeElementaryStreamUnit returns + // WiFiDisplayElementaryStreamPacket so WiFiDisplayElementaryStreamPacket + // must be move constructible (as it is not copy constructible). + // A compiler should however use return value optimization and elide each + // call to this move constructor. + WiFiDisplayElementaryStreamPacket(WiFiDisplayElementaryStreamPacket&& other); + + const WiFiDisplayStreamPacketPart& header() const { return header_; } + const WiFiDisplayStreamPacketPart& unit_header() const { + return unit_header_; + } + const WiFiDisplayStreamPacketPart& unit() const { return unit_; } + + private: + HeaderBuffer header_buffer_; + WiFiDisplayStreamPacketPart header_; + WiFiDisplayStreamPacketPart unit_header_; + WiFiDisplayStreamPacketPart unit_; + + DISALLOW_COPY_AND_ASSIGN(WiFiDisplayElementaryStreamPacket); +}; + +// The WiFi Display elementary stream packetizer packetizes unit buffers to +// Packetized Elementary Stream (PES) packets. +// It is used internally by a WiFi Display transport stream packetizer. +class WiFiDisplayElementaryStreamPacketizer { + public: + enum : uint8_t { + kPrivateStream1Id = 0xBDu, + kFirstAudioStreamId = 0xC0u, + kLastAudioStreamId = 0xDFu, + kFirstVideoStreamId = 0xE0u, + kLastVideoStreamId = 0xEFu, + }; + + static WiFiDisplayElementaryStreamPacket EncodeElementaryStreamUnit( + uint8_t stream_id, + const uint8_t* unit_header_data, + size_t unit_header_size, + const uint8_t* unit_data, + size_t unit_size, + const base::TimeTicks& pts, + const base::TimeTicks& dts); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_ELEMENTARY_STREAM_PACKETIZER_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.cc new file mode 100644 index 00000000000..f77571d58a2 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.cc @@ -0,0 +1,247 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_media_manager.h" + +#include "base/logging.h" +#include "base/rand_util.h" +#include "content/public/renderer/media_stream_utils.h" + +namespace extensions { + +namespace { + +const char kErrorNoVideoFormatData[] = + "Failed to get video format data from the given MediaStreamTrack object"; +const char kErrorSinkCannotPlayVideo[] = + "The sink cannot play video from the given MediaStreamTrack object"; +const char kErrorSinkCannotPlayAudio[] = + "The sink cannot play audio from the given MediaStreamTrack object"; +} // namespace + +WiFiDisplayMediaManager::WiFiDisplayMediaManager( + const blink::WebMediaStreamTrack& video_track, + const blink::WebMediaStreamTrack& audio_track, + const ErrorCallback& error_callback) + : video_track_(video_track), + audio_track_(audio_track), + error_callback_(error_callback) { + DCHECK(!video_track.isNull() || !audio_track.isNull()); + DCHECK(!error_callback_.is_null()); +} + +WiFiDisplayMediaManager::~WiFiDisplayMediaManager() { +} + +void WiFiDisplayMediaManager::Play() { + NOTIMPLEMENTED(); +} + +void WiFiDisplayMediaManager::Teardown() { + NOTIMPLEMENTED(); +} + +void WiFiDisplayMediaManager::Pause() { + NOTIMPLEMENTED(); +} + +bool WiFiDisplayMediaManager::IsPaused() const { + NOTIMPLEMENTED(); + return true; +} + +wds::SessionType WiFiDisplayMediaManager::GetSessionType() const { + uint16_t session_type = 0; + if (!video_track_.isNull()) + session_type |= wds::VideoSession; + + if (!audio_track_.isNull()) + session_type |= wds::AudioSession; + + return static_cast<wds::SessionType>(session_type); +} + +void WiFiDisplayMediaManager::SetSinkRtpPorts(int port1, int port2) { + sink_rtp_ports_ = std::pair<int, int>(port1, port2); +} + +std::pair<int, int> WiFiDisplayMediaManager::GetSinkRtpPorts() const { + return sink_rtp_ports_; +} + +int WiFiDisplayMediaManager::GetLocalRtpPort() const { + NOTIMPLEMENTED(); + return 0; +} + +namespace { +struct VideoFormat { + wds::RateAndResolution rr; + int width; + int height; + int frame_rate; +}; + +const VideoFormat cea_table[] = { + {wds::CEA640x480p60, 640, 480, 60}, + {wds::CEA720x480p60, 720, 480, 60}, + {wds::CEA720x576p50, 720, 576, 50}, + {wds::CEA1280x720p30, 1280, 720, 30}, + {wds::CEA1280x720p60, 1280, 720, 60}, + {wds::CEA1920x1080p30, 1920, 1080, 30}, + {wds::CEA1920x1080p60, 1920, 1080, 60}, + {wds::CEA1280x720p25, 1280, 720, 25}, + {wds::CEA1280x720p50, 1280, 720, 50}, + {wds::CEA1920x1080p25, 1920, 1080, 25}, + {wds::CEA1920x1080p50, 1920, 1080, 50}, + {wds::CEA1280x720p24, 1280, 720, 24}, + {wds::CEA1920x1080p24, 1920, 1080, 24} +}; + +const VideoFormat vesa_table[] = { + {wds::VESA800x600p30, 800, 600, 30}, + {wds::VESA800x600p60, 800, 600, 60}, + {wds::VESA1024x768p30, 1024, 768, 30}, + {wds::VESA1024x768p60, 1024, 768, 60}, + {wds::VESA1152x864p30, 1152, 864, 30}, + {wds::VESA1152x864p60, 1152, 864, 60}, + {wds::VESA1280x768p30, 1280, 768, 30}, + {wds::VESA1280x768p60, 1280, 768, 60}, + {wds::VESA1280x800p30, 1280, 800, 30}, + {wds::VESA1280x800p60, 1280, 800, 60}, + {wds::VESA1360x768p30, 1360, 768, 30}, + {wds::VESA1360x768p60, 1360, 768, 60}, + {wds::VESA1366x768p30, 1366, 768, 30}, + {wds::VESA1366x768p60, 1366, 768, 60}, + {wds::VESA1280x1024p30, 1280, 1024, 30}, + {wds::VESA1280x1024p60, 1280, 1024, 60}, + {wds::VESA1400x1050p30, 1400, 1050, 30}, + {wds::VESA1400x1050p60, 1400, 1050, 60}, + {wds::VESA1440x900p30, 1440, 900, 30}, + {wds::VESA1440x900p60, 1440, 900, 60}, + {wds::VESA1600x900p30, 1600, 900, 30}, + {wds::VESA1600x900p60, 1600, 900, 60}, + {wds::VESA1600x1200p30, 1600, 1200, 30}, + {wds::VESA1600x1200p60, 1600, 1200, 60}, + {wds::VESA1680x1024p30, 1680, 1024, 30}, + {wds::VESA1680x1024p60, 1680, 1024, 60}, + {wds::VESA1680x1050p30, 1680, 1050, 30}, + {wds::VESA1680x1050p60, 1680, 1050, 60}, + {wds::VESA1920x1200p30, 1920, 1200, 30} +}; + +const VideoFormat hh_table[] = { + {wds::HH800x480p30, 800, 480, 30}, + {wds::HH800x480p60, 800, 480, 60}, + {wds::HH854x480p30, 854, 480, 30}, + {wds::HH854x480p60, 854, 480, 60}, + {wds::HH864x480p30, 864, 480, 30}, + {wds::HH864x480p60, 864, 480, 60}, + {wds::HH640x360p30, 640, 360, 30}, + {wds::HH640x360p60, 640, 360, 60}, + {wds::HH960x540p30, 960, 540, 30}, + {wds::HH960x540p60, 960, 540, 60}, + {wds::HH848x480p30, 848, 480, 30}, + {wds::HH848x480p60, 848, 480, 60} +}; + +template <wds::ResolutionType type, unsigned N> +bool FindRateResolution(const media::VideoCaptureFormat* format, + const wds::RateAndResolutionsBitmap& bitmap, + const VideoFormat (&table)[N], + wds::H264VideoFormat* result /*out*/) { + for (unsigned i = 0; i < N; ++i) { + if (bitmap.test(table[i].rr)) { + if (format->frame_size.width() == table[i].width && + format->frame_size.height() == table[i].height && + format->frame_rate == table[i].frame_rate) { + result->rate_resolution = table[i].rr; + result->type = type; + return true; + } + } + } + return false; +} + +bool FindOptimalFormat( + const media::VideoCaptureFormat* capture_format, + const std::vector<wds::H264VideoCodec>& sink_supported_codecs, + wds::H264VideoFormat* result /*out*/) { + DCHECK(result); + for (const wds::H264VideoCodec& codec : sink_supported_codecs) { + bool found = + FindRateResolution<wds::CEA>( + capture_format, codec.cea_rr, cea_table, result) || + FindRateResolution<wds::VESA>( + capture_format, codec.vesa_rr, vesa_table, result) || + FindRateResolution<wds::HH>( + capture_format, codec.hh_rr, hh_table, result); + if (found) { + result->profile = codec.profile; + result->level = codec.level; + return true; + } + } + return false; +} + +} // namespace + +wds::H264VideoFormat WiFiDisplayMediaManager::GetOptimalVideoFormat() const { + return optimal_video_format_; +} + +void WiFiDisplayMediaManager::SendIDRPicture() { + NOTIMPLEMENTED(); +} + +std::string WiFiDisplayMediaManager::GetSessionId() const { + return base::RandBytesAsString(8); +} + +bool WiFiDisplayMediaManager::InitOptimalVideoFormat( + const wds::NativeVideoFormat& sink_native_format, + const std::vector<wds::H264VideoCodec>& sink_supported_codecs) { + const media::VideoCaptureFormat* capture_format = + content::GetCurrentVideoTrackFormat(video_track_); + if (!capture_format) { + error_callback_.Run(kErrorNoVideoFormatData); + return false; + } + + if (!FindOptimalFormat( + capture_format, sink_supported_codecs, &optimal_video_format_)) { + error_callback_.Run(kErrorSinkCannotPlayVideo); + return false; + } + + return true; +} + +bool WiFiDisplayMediaManager::InitOptimalAudioFormat( + const std::vector<wds::AudioCodec>& sink_codecs) { + for (const wds::AudioCodec& codec : sink_codecs) { + // MediaStreamTrack contains LPCM audio. + if (codec.format == wds::LPCM) { + optimal_audio_codec_ = codec; + // Picking a single mode. + wds::AudioModes optimal_mode; + if (codec.modes.test(wds::LPCM_44_1K_16B_2CH)) + optimal_mode.set(wds::LPCM_44_1K_16B_2CH); + else + optimal_mode.set(wds::LPCM_48K_16B_2CH); + optimal_audio_codec_.modes = optimal_mode; + return true; + } + } + error_callback_.Run(kErrorSinkCannotPlayAudio); + return false; +} + +wds::AudioCodec WiFiDisplayMediaManager::GetOptimalAudioFormat() const { + return optimal_audio_codec_; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.h new file mode 100644 index 00000000000..277771cb2bc --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.h @@ -0,0 +1,69 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_BROWSER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_MANAGER_H_ +#define EXTENSIONS_BROWSER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_MANAGER_H_ + +#include <string> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/macros.h" +#include "third_party/WebKit/public/web/WebDOMMediaStreamTrack.h" +#include "third_party/wds/src/libwds/public/media_manager.h" + +namespace extensions { + +class WiFiDisplayMediaManager : public wds::SourceMediaManager { + public: + using ErrorCallback = base::Callback<void(const std::string&)>; + + WiFiDisplayMediaManager( + const blink::WebMediaStreamTrack& video_track, + const blink::WebMediaStreamTrack& audio_track, + const ErrorCallback& error_callback); + + ~WiFiDisplayMediaManager() override; + + private: + // wds::SourceMediaManager overrides. + void Play() override; + + void Pause() override; + void Teardown() override; + bool IsPaused() const override; + wds::SessionType GetSessionType() const override; + void SetSinkRtpPorts(int port1, int port2) override; + std::pair<int, int> GetSinkRtpPorts() const override; + int GetLocalRtpPort() const override; + + bool InitOptimalVideoFormat( + const wds::NativeVideoFormat& sink_native_format, + const std::vector<wds::H264VideoCodec>& sink_supported_codecs) override; + wds::H264VideoFormat GetOptimalVideoFormat() const override; + bool InitOptimalAudioFormat( + const std::vector<wds::AudioCodec>& sink_supported_codecs) override; + wds::AudioCodec GetOptimalAudioFormat() const override; + + void SendIDRPicture() override; + + std::string GetSessionId() const override; + + private: + blink::WebMediaStreamTrack video_track_; + blink::WebMediaStreamTrack audio_track_; + + std::pair<int, int> sink_rtp_ports_; + wds::H264VideoFormat optimal_video_format_; + wds::AudioCodec optimal_audio_codec_; + + ErrorCallback error_callback_; + + DISALLOW_COPY_AND_ASSIGN(WiFiDisplayMediaManager); +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_MANAGER_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.cc new file mode 100644 index 00000000000..f8f9cf23b5d --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.cc @@ -0,0 +1,110 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_media_packetizer.h" + +#include <utility> + +#include "base/logging.h" +#include "base/rand_util.h" +#include "crypto/random.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h" + +namespace extensions { +namespace { +const size_t kMaxTransportStreamPacketCount = 7u; +const uint8_t kProtocolPayloadTypeMP2T = 33u; +const uint8_t kProtocolVersion = 2u; +} // namespace + +WiFiDisplayMediaDatagramPacket::WiFiDisplayMediaDatagramPacket() = default; + +WiFiDisplayMediaDatagramPacket::WiFiDisplayMediaDatagramPacket( + WiFiDisplayMediaDatagramPacket&&) = default; + +WiFiDisplayMediaPacketizer::WiFiDisplayMediaPacketizer( + const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos, + const PacketizedCallback& on_packetized) + : WiFiDisplayTransportStreamPacketizer(delay_for_unit_time_stamps, + std::move(stream_infos)), + on_packetized_media_datagram_packet_(on_packetized) { + // Sequence numbers are mainly used for detecting lossed packets within one + // RTP session. The initial value SHOULD be random (unpredictable) to make + // known-plaintext attacks on encryption more difficult, in case the packets + // flow through a translator that encrypts them. + crypto::RandBytes(&sequence_number_, sizeof(sequence_number_)); + base::RandBytes(&synchronization_source_identifier_, + sizeof(synchronization_source_identifier_)); +} + +WiFiDisplayMediaPacketizer::~WiFiDisplayMediaPacketizer() {} + +bool WiFiDisplayMediaPacketizer::OnPacketizedTransportStreamPacket( + const WiFiDisplayTransportStreamPacket& transport_stream_packet, + bool flush) { + DCHECK(CalledOnValidThread()); + + if (media_datagram_packet_.empty()) { + // Convert time to the number of 90 kHz ticks since some epoch. + const uint64_t us = static_cast<uint64_t>( + (base::TimeTicks::Now() - base::TimeTicks()).InMicroseconds()); + const uint64_t time_stamp = (us * 90u + 500u) / 1000u; + const uint8_t header_without_identifiers[] = { + (kProtocolVersion << 6 | // Version (2 bits) + 0x0u << 5 | // Padding (no) + 0x0u << 4 | // Extension (no) + 0u << 0), // CSRC count (4 bits) + (0x0u << 7 | // Marker (no) + kProtocolPayloadTypeMP2T << 0), // Payload type (7 bits) + sequence_number_ >> 8, // Sequence number (16 bits) + sequence_number_ & 0xFFu, // + (time_stamp >> 24) & 0xFFu, // Time stamp (32 bits) + (time_stamp >> 16) & 0xFFu, // + (time_stamp >> 8) & 0xFFu, // + time_stamp & 0xFFu}; // + ++sequence_number_; + media_datagram_packet_.reserve( + sizeof(header_without_identifiers) + + sizeof(synchronization_source_identifier_) + + kMaxTransportStreamPacketCount * + WiFiDisplayTransportStreamPacket::kPacketSize); + media_datagram_packet_.insert(media_datagram_packet_.end(), + std::begin(header_without_identifiers), + std::end(header_without_identifiers)); + media_datagram_packet_.insert( + media_datagram_packet_.end(), + std::begin(synchronization_source_identifier_), + std::end(synchronization_source_identifier_)); + DCHECK_EQ(0u, media_datagram_packet_.size() / + WiFiDisplayTransportStreamPacket::kPacketSize); + } + + // Append header and payload data. + media_datagram_packet_.insert(media_datagram_packet_.end(), + transport_stream_packet.header().begin(), + transport_stream_packet.header().end()); + media_datagram_packet_.insert(media_datagram_packet_.end(), + transport_stream_packet.payload().begin(), + transport_stream_packet.payload().end()); + media_datagram_packet_.insert(media_datagram_packet_.end(), + transport_stream_packet.filler().size(), + transport_stream_packet.filler().value()); + + // Combine multiple transport stream packets into one datagram packet + // by delaying delegation whenever possible. + if (!flush) { + const size_t transport_stream_packet_count = + media_datagram_packet_.size() / + WiFiDisplayTransportStreamPacket::kPacketSize; + if (transport_stream_packet_count < kMaxTransportStreamPacketCount) + return true; + } + + WiFiDisplayMediaDatagramPacket packet; + packet.swap(media_datagram_packet_); + return on_packetized_media_datagram_packet_.Run(std::move(packet)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.h new file mode 100644 index 00000000000..23c07e08119 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.h @@ -0,0 +1,61 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_PACKETIZER_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_PACKETIZER_H_ + +#include <vector> + +#include "base/callback.h" +#include "base/move.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h" + +namespace extensions { + +// This class represents an RTP datagram packet containing MPEG Transport +// Stream (MPEG-TS) packets as a payload. +class WiFiDisplayMediaDatagramPacket : public std::vector<uint8_t> { + public: + WiFiDisplayMediaDatagramPacket(); + WiFiDisplayMediaDatagramPacket(WiFiDisplayMediaDatagramPacket&&); + + private: + DISALLOW_COPY_AND_ASSIGN_WITH_MOVE_FOR_BIND(WiFiDisplayMediaDatagramPacket); +}; + +// The WiFi Display media packetizer packetizes unit buffers to media datagram +// packets containing MPEG Transport Stream (MPEG-TS) packets containing either +// meta information or Packetized Elementary Stream (PES) packets containing +// unit data. +// +// Whenever a media datagram packet is fully created and thus ready for further +// processing, a callback is called. +class WiFiDisplayMediaPacketizer : public WiFiDisplayTransportStreamPacketizer { + public: + using PacketizedCallback = + base::Callback<bool(WiFiDisplayMediaDatagramPacket)>; + + WiFiDisplayMediaPacketizer( + const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos, + const PacketizedCallback& on_packetized); + ~WiFiDisplayMediaPacketizer() override; + + protected: + bool OnPacketizedTransportStreamPacket( + const WiFiDisplayTransportStreamPacket& transport_stream_packet, + bool flush) override; + + private: + using SourceIdentifier = uint8_t[4]; + + WiFiDisplayMediaDatagramPacket media_datagram_packet_; + PacketizedCallback on_packetized_media_datagram_packet_; + uint16_t sequence_number_; + SourceIdentifier synchronization_source_identifier_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_MEDIA_PACKETIZER_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer_unittest.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer_unittest.cc new file mode 100644 index 00000000000..5253a1baa76 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer_unittest.cc @@ -0,0 +1,905 @@ +// Copyright 2016 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 <algorithm> +#include <array> +#include <list> + +#include "base/big_endian.h" +#include "base/bind.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_media_packetizer.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h" +#include "testing/gtest/include/gtest/gtest.h" + +using PacketPart = extensions::WiFiDisplayStreamPacketPart; + +namespace extensions { + +std::ostream& operator<<(std::ostream& os, const PacketPart& part) { + const auto flags = os.flags(); + os << "{" << std::hex << std::noshowbase; + for (const auto& item : part) { + if (&item != &*part.begin()) + os << ", "; + os << "0x" << static_cast<unsigned>(item); + } + os.setf(flags, std::ios::basefield | std::ios::showbase); + return os << "}"; +} + +bool operator==(const PacketPart& a, const PacketPart& b) { + if (a.size() != b.size()) + return false; + return std::equal(a.begin(), a.end(), b.begin()); +} + +namespace { + +namespace pes { +const unsigned kDtsFlag = 0x0040u; +const unsigned kMarkerFlag = 0x8000u; +const unsigned kPtsFlag = 0x0080u; +const size_t kUnitDataAlignment = sizeof(uint32_t); +} + +namespace rtp { +const unsigned kVersionMask = 0xC000u; +const unsigned kVersion2 = 0x8000u; +const unsigned kPaddingFlag = 0x2000u; +const unsigned kExtensionFlag = 0x1000u; +const unsigned kContributingSourceCountMask = 0x0F00u; +const unsigned kMarkerFlag = 0x0010u; +const unsigned kPayloadTypeMask = 0x007Fu; +const unsigned kPayloadTypeMP2T = 0x0021u; +} // namespace rtp + +namespace ts { +const uint64_t kTimeStampMask = (static_cast<uint64_t>(1u) << 33) - 1u; +const uint64_t kTimeStampSecond = 90000u; // 90 kHz +const uint64_t kProgramClockReferenceSecond = + 300u * kTimeStampSecond; // 27 MHz + +// Packet header: +const size_t kPacketHeaderSize = 4u; +const unsigned kSyncByte = 0x47u; +const uint32_t kSyncByteMask = 0xFF000000u; +const uint32_t kTransportErrorIndicator = 0x00800000u; +const uint32_t kPayloadUnitStartIndicator = 0x00400000u; +const uint32_t kTransportPriority = 0x00200000u; +const uint32_t kScramblingControlMask = 0x000000C0u; +const uint32_t kAdaptationFieldFlag = 0x00000020u; +const uint32_t kPayloadFlag = 0x00000010u; + +// Adaptation field: +const unsigned kRandomAccessFlag = 0x40u; +const unsigned kPcrFlag = 0x10u; +} // namespace ts + +namespace widi { +const unsigned kProgramAssociationTablePacketId = 0x0000u; +const unsigned kProgramMapTablePacketId = 0x0100u; +const unsigned kProgramClockReferencePacketId = 0x1000u; +const unsigned kVideoStreamPacketId = 0x1011u; +const unsigned kFirstAudioStreamPacketId = 0x1100u; +const size_t kMaxTransportStreamPacketCountPerDatagramPacket = 7u; +} // namespace widi + +template <typename PacketContainer> +class PacketCollector { + public: + PacketContainer FetchPackets() { + PacketContainer container; + container.swap(packets_); + return container; + } + + protected: + PacketContainer packets_; +}; + +class FakeMediaPacketizer + : public WiFiDisplayMediaPacketizer, + public PacketCollector<std::vector<std::vector<uint8_t>>> { + public: + FakeMediaPacketizer(const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos) + : WiFiDisplayMediaPacketizer( + delay_for_unit_time_stamps, + std::move(stream_infos), + base::Bind(&FakeMediaPacketizer::OnPacketizedMediaDatagramPacket, + base::Unretained(this))) {} + + // Extend the interface in order to allow to bypass packetization of units to + // Packetized Elementary Stream (PES) packets and further to Transport Stream + // (TS) packets and to test only packetization of TS packets to media + // datagram packets. + bool EncodeTransportStreamPacket( + const WiFiDisplayTransportStreamPacket& transport_stream_packet, + bool flush) { + return OnPacketizedTransportStreamPacket(transport_stream_packet, flush); + } + + private: + bool OnPacketizedMediaDatagramPacket( + WiFiDisplayMediaDatagramPacket media_datagram_packet) { + packets_.emplace_back(std::move(media_datagram_packet)); + return true; + } +}; + +class FakeTransportStreamPacketizer + : public WiFiDisplayTransportStreamPacketizer, + public PacketCollector<std::list<WiFiDisplayTransportStreamPacket>> { + public: + FakeTransportStreamPacketizer( + const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos) + : WiFiDisplayTransportStreamPacketizer(delay_for_unit_time_stamps, + std::move(stream_infos)) {} + + using WiFiDisplayTransportStreamPacketizer::NormalizeUnitTimeStamps; + + protected: + bool OnPacketizedTransportStreamPacket( + const WiFiDisplayTransportStreamPacket& transport_stream_packet, + bool flush) override { + // Make a copy of header bytes as they are in stack. + headers_.emplace_back(transport_stream_packet.header().begin(), + transport_stream_packet.header().end()); + const auto& header = headers_.back(); + if (transport_stream_packet.payload().empty()) { + packets_.emplace_back(header.data(), header.size()); + } else { + packets_.emplace_back(header.data(), header.size(), + transport_stream_packet.payload().begin()); + } + EXPECT_EQ(transport_stream_packet.header().size(), + packets_.back().header().size()); + EXPECT_EQ(transport_stream_packet.payload().size(), + packets_.back().payload().size()); + EXPECT_EQ(transport_stream_packet.filler().size(), + packets_.back().filler().size()); + return true; + } + + private: + std::vector<std::vector<uint8_t>> headers_; +}; + +struct ProgramClockReference { + enum { kInvalidBase = ~static_cast<uint64_t>(0u) }; + uint64_t base; + uint16_t extension; +}; + +ProgramClockReference ParseProgramClockReference(const uint8_t pcr_bytes[6]) { + const uint8_t reserved_pcr_bits = pcr_bytes[4] & 0x7Eu; + EXPECT_EQ(0x7Eu, reserved_pcr_bits); + ProgramClockReference pcr; + pcr.base = pcr_bytes[0]; + pcr.base = (pcr.base << 8) | pcr_bytes[1]; + pcr.base = (pcr.base << 8) | pcr_bytes[2]; + pcr.base = (pcr.base << 8) | pcr_bytes[3]; + pcr.base = (pcr.base << 1) | ((pcr_bytes[4] & 0x80u) >> 7); + pcr.extension = pcr_bytes[4] & 0x01u; + pcr.extension = (pcr.extension << 8) | pcr_bytes[5]; + return pcr; +} + +uint64_t ParseTimeStamp(const uint8_t ts_bytes[5], uint8_t pts_dts_indicator) { + EXPECT_EQ(pts_dts_indicator, (ts_bytes[0] & 0xF0u) >> 4); + EXPECT_EQ(0x01u, ts_bytes[0] & 0x01u); + EXPECT_EQ(0x01u, ts_bytes[2] & 0x01u); + EXPECT_EQ(0x01u, ts_bytes[4] & 0x01u); + uint64_t ts = 0u; + ts = (ts_bytes[0] & 0x0Eu) >> 1; + ts = (ts << 8) | ts_bytes[1]; + ts = (ts << 7) | ((ts_bytes[2] & 0xFEu) >> 1); + ts = (ts << 8) | ts_bytes[3]; + ts = (ts << 7) | ((ts_bytes[4] & 0xFEu) >> 1); + return ts; +} + +unsigned ParseTransportStreamPacketId( + const WiFiDisplayTransportStreamPacket& packet) { + if (packet.header().size() < ts::kPacketHeaderSize) + return ~0u; + return (((packet.header().begin()[1] & 0x001Fu) << 8) | + packet.header().begin()[2]); +} + +class WiFiDisplayElementaryStreamUnitPacketizationTest + : public testing::TestWithParam< + testing::tuple<unsigned, base::TimeDelta, base::TimeDelta>> { + protected: + static base::TimeTicks SumOrNull(const base::TimeTicks& base, + const base::TimeDelta& delta) { + return delta.is_max() ? base::TimeTicks() : base + delta; + } + + WiFiDisplayElementaryStreamUnitPacketizationTest() + : unit_(testing::get<0>(GetParam())), + now_(base::TimeTicks::Now()), + dts_(SumOrNull(now_, testing::get<1>(GetParam()))), + pts_(SumOrNull(now_, testing::get<2>(GetParam()))) {} + + void CheckElementaryStreamPacketHeader( + const WiFiDisplayElementaryStreamPacket& packet, + uint8_t stream_id) { + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.header().begin()), + packet.header().size()); + uint8_t parsed_packet_start_code_prefix[3]; + EXPECT_TRUE( + header_reader.ReadBytes(parsed_packet_start_code_prefix, + sizeof(parsed_packet_start_code_prefix))); + EXPECT_EQ(0x00u, parsed_packet_start_code_prefix[0]); + EXPECT_EQ(0x00u, parsed_packet_start_code_prefix[1]); + EXPECT_EQ(0x01u, parsed_packet_start_code_prefix[2]); + uint8_t parsed_stream_id; + EXPECT_TRUE(header_reader.ReadU8(&parsed_stream_id)); + EXPECT_EQ(stream_id, parsed_stream_id); + uint16_t parsed_packet_length; + EXPECT_TRUE(header_reader.ReadU16(&parsed_packet_length)); + size_t packet_length = static_cast<size_t>(header_reader.remaining()) + + packet.unit_header().size() + packet.unit().size(); + if (packet_length >> 16) + packet_length = 0u; + EXPECT_EQ(packet_length, parsed_packet_length); + uint16_t parsed_flags; + EXPECT_TRUE(header_reader.ReadU16(&parsed_flags)); + EXPECT_EQ( + 0u, parsed_flags & ~(pes::kMarkerFlag | pes::kPtsFlag | pes::kDtsFlag)); + const bool parsed_pts_flag = (parsed_flags & pes::kPtsFlag) != 0u; + const bool parsed_dts_flag = (parsed_flags & pes::kDtsFlag) != 0u; + EXPECT_EQ(!pts_.is_null(), parsed_pts_flag); + EXPECT_EQ(!pts_.is_null() && !dts_.is_null(), parsed_dts_flag); + uint8_t parsed_header_length; + EXPECT_TRUE(header_reader.ReadU8(&parsed_header_length)); + EXPECT_EQ(header_reader.remaining(), parsed_header_length); + if (parsed_pts_flag) { + uint8_t parsed_pts_bytes[5]; + EXPECT_TRUE( + header_reader.ReadBytes(parsed_pts_bytes, sizeof(parsed_pts_bytes))); + const uint64_t parsed_pts = + ParseTimeStamp(parsed_pts_bytes, parsed_dts_flag ? 0x3u : 0x2u); + if (parsed_dts_flag) { + uint8_t parsed_dts_bytes[5]; + EXPECT_TRUE(header_reader.ReadBytes(parsed_dts_bytes, + sizeof(parsed_dts_bytes))); + const uint64_t parsed_dts = ParseTimeStamp(parsed_dts_bytes, 0x1u); + EXPECT_EQ( + static_cast<uint64_t>(90 * (pts_ - dts_).InMicroseconds() / 1000), + (parsed_pts - parsed_dts) & UINT64_C(0x1FFFFFFFF)); + } + } + while (header_reader.remaining() > 0) { + uint8_t parsed_stuffing_byte; + EXPECT_TRUE(header_reader.ReadU8(&parsed_stuffing_byte)); + EXPECT_EQ(0xFFu, parsed_stuffing_byte); + } + EXPECT_EQ(0, header_reader.remaining()); + } + + void CheckElementaryStreamPacketUnitHeader( + const WiFiDisplayElementaryStreamPacket& packet, + const uint8_t* unit_header_data, + size_t unit_header_size) { + EXPECT_EQ(unit_header_data, packet.unit_header().begin()); + EXPECT_EQ(unit_header_size, packet.unit_header().size()); + } + + void CheckElementaryStreamPacketUnit( + const WiFiDisplayElementaryStreamPacket& packet) { + EXPECT_EQ(0u, (packet.header().size() + packet.unit_header().size()) % + pes::kUnitDataAlignment); + EXPECT_EQ(unit_.data(), packet.unit().begin()); + EXPECT_EQ(unit_.size(), packet.unit().size()); + } + + void CheckTransportStreamPacketHeader( + base::BigEndianReader* header_reader, + bool expected_payload_unit_start_indicator, + unsigned expected_packet_id, + bool* adaptation_field_flag, + uint8_t expected_continuity_counter) { + uint32_t parsed_u32; + EXPECT_TRUE(header_reader->ReadU32(&parsed_u32)); + EXPECT_EQ(ts::kSyncByte << 24u, parsed_u32 & ts::kSyncByteMask); + EXPECT_EQ(0x0u, parsed_u32 & ts::kTransportErrorIndicator); + EXPECT_EQ(expected_payload_unit_start_indicator, + (parsed_u32 & ts::kPayloadUnitStartIndicator) != 0u); + EXPECT_EQ(0x0u, parsed_u32 & ts::kTransportPriority); + EXPECT_EQ(expected_packet_id, (parsed_u32 & 0x001FFF00) >> 8); + EXPECT_EQ(0x0u, parsed_u32 & ts::kScramblingControlMask); + if (!adaptation_field_flag) { + EXPECT_EQ(0x0u, parsed_u32 & ts::kAdaptationFieldFlag); + } else { + *adaptation_field_flag = (parsed_u32 & ts::kAdaptationFieldFlag) != 0u; + } + EXPECT_EQ(ts::kPayloadFlag, parsed_u32 & ts::kPayloadFlag); + EXPECT_EQ(expected_continuity_counter & 0xFu, parsed_u32 & 0x0000000Fu); + } + + void CheckTransportStreamAdaptationField( + base::BigEndianReader* header_reader, + const WiFiDisplayTransportStreamPacket& packet, + uint8_t* adaptation_field_flags) { + uint8_t parsed_adaptation_field_length; + EXPECT_TRUE(header_reader->ReadU8(&parsed_adaptation_field_length)); + if (parsed_adaptation_field_length > 0u) { + const int initial_remaining = header_reader->remaining(); + uint8_t parsed_adaptation_field_flags; + EXPECT_TRUE(header_reader->ReadU8(&parsed_adaptation_field_flags)); + if (!adaptation_field_flags) { + EXPECT_EQ(0x0u, parsed_adaptation_field_flags); + } else { + *adaptation_field_flags = parsed_adaptation_field_flags; + if (parsed_adaptation_field_flags & ts::kPcrFlag) { + uint8_t parsed_pcr_bytes[6]; + EXPECT_TRUE(header_reader->ReadBytes(parsed_pcr_bytes, + sizeof(parsed_pcr_bytes))); + parsed_pcr_ = ParseProgramClockReference(parsed_pcr_bytes); + } + } + size_t remaining_stuffing_length = + parsed_adaptation_field_length - + static_cast<size_t>(initial_remaining - header_reader->remaining()); + while (remaining_stuffing_length > 0u && header_reader->remaining() > 0) { + // Adaptation field stuffing byte in header_reader. + uint8_t parsed_stuffing_byte; + EXPECT_TRUE(header_reader->ReadU8(&parsed_stuffing_byte)); + EXPECT_EQ(0xFFu, parsed_stuffing_byte); + --remaining_stuffing_length; + } + if (packet.payload().empty()) { + // Adaptation field stuffing bytes in packet.filler(). + EXPECT_EQ(remaining_stuffing_length, packet.filler().size()); + EXPECT_EQ(0xFFu, packet.filler().value()); + } else { + EXPECT_EQ(0u, remaining_stuffing_length); + } + } + } + + void CheckTransportStreamProgramAssociationTablePacket( + const WiFiDisplayTransportStreamPacket& packet) { + static const uint8_t kProgramAssicationTable[4u + 13u] = { + // Pointer: + 0u, // Pointer field + // Table header: + 0x00u, // Table ID (PAT) + 0x80u | // Section syntax indicator (0b1 for PAT) + 0x00u | // Private bit (0b0 for PAT) + 0x30u | // Reserved bits (0b11) + 0x00u | // Section length unused bits (0b00) + 0u, // Section length (10 bits) + 13u, // + // Table syntax: + 0x00u, // Table ID extension (transport stream ID) + 0x01u, // + 0xC0u | // Reserved bits (0b11) + 0x00u | // Version (0b00000) + 0x01u, // Current indicator (0b1) + 0u, // Section number + 0u, // Last section number + // Program association table specific data: + 0x00u, // Program number + 0x01u, // + 0xE0 | // Reserved bits (0b111) + 0x01u, // Program map packet ID (13 bits) + 0x00, // + // CRC: + 0xE8u, + 0xF9u, 0x5Eu, 0x7Du}; + + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.header().begin()), + packet.header().size()); + + CheckTransportStreamPacketHeader( + &header_reader, true, widi::kProgramAssociationTablePacketId, nullptr, + continuity_.program_assication_table++); + + EXPECT_EQ(PacketPart(kProgramAssicationTable), + PacketPart(packet.header().end() - header_reader.remaining(), + static_cast<size_t>(header_reader.remaining()))); + EXPECT_TRUE(header_reader.Skip(header_reader.remaining())); + + EXPECT_EQ(0, header_reader.remaining()); + EXPECT_EQ(0u, packet.payload().size()); + } + + void CheckTransportStreamProgramMapTablePacket( + const WiFiDisplayTransportStreamPacket& packet, + const PacketPart& program_map_table) { + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.header().begin()), + packet.header().size()); + + CheckTransportStreamPacketHeader(&header_reader, true, + widi::kProgramMapTablePacketId, nullptr, + continuity_.program_map_table++); + + EXPECT_EQ(program_map_table, + PacketPart(packet.header().end() - header_reader.remaining(), + static_cast<size_t>(header_reader.remaining()))); + EXPECT_TRUE(header_reader.Skip(header_reader.remaining())); + + EXPECT_EQ(0, header_reader.remaining()); + EXPECT_EQ(0u, packet.payload().size()); + } + + void CheckTransportStreamProgramClockReferencePacket( + const WiFiDisplayTransportStreamPacket& packet) { + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.header().begin()), + packet.header().size()); + + bool parsed_adaptation_field_flag; + CheckTransportStreamPacketHeader( + &header_reader, true, widi::kProgramClockReferencePacketId, + &parsed_adaptation_field_flag, continuity_.program_clock_reference++); + EXPECT_TRUE(parsed_adaptation_field_flag); + + uint8_t parsed_adaptation_field_flags; + CheckTransportStreamAdaptationField(&header_reader, packet, + &parsed_adaptation_field_flags); + EXPECT_EQ(ts::kPcrFlag, parsed_adaptation_field_flags); + + EXPECT_EQ(0, header_reader.remaining()); + EXPECT_EQ(0u, packet.payload().size()); + } + + void CheckTransportStreamElementaryStreamPacket( + const WiFiDisplayTransportStreamPacket& packet, + const WiFiDisplayElementaryStreamPacket& elementary_stream_packet, + unsigned stream_index, + unsigned expected_packet_id, + bool expected_random_access, + const uint8_t** unit_data_pos) { + const bool first_transport_stream_packet_for_current_unit = + packet.payload().begin() == unit_.data(); + const bool last_transport_stream_packet_for_current_unit = + packet.payload().end() == unit_.data() + unit_.size(); + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.header().begin()), + packet.header().size()); + + bool parsed_adaptation_field_flag; + CheckTransportStreamPacketHeader( + &header_reader, first_transport_stream_packet_for_current_unit, + expected_packet_id, &parsed_adaptation_field_flag, + continuity_.elementary_streams[stream_index]++); + + if (first_transport_stream_packet_for_current_unit) { + // Random access can only be signified by adaptation field. + if (expected_random_access) + EXPECT_TRUE(parsed_adaptation_field_flag); + // If there is no need for padding nor for a random access indicator, + // then there is no need for an adaptation field, either. + if (!last_transport_stream_packet_for_current_unit && + !expected_random_access) { + EXPECT_FALSE(parsed_adaptation_field_flag); + } + if (parsed_adaptation_field_flag) { + uint8_t parsed_adaptation_field_flags; + CheckTransportStreamAdaptationField(&header_reader, packet, + &parsed_adaptation_field_flags); + EXPECT_EQ(expected_random_access ? ts::kRandomAccessFlag : 0u, + parsed_adaptation_field_flags); + } + + // Elementary stream header. + PacketPart parsed_elementary_stream_packet_header( + packet.header().end() - header_reader.remaining(), + std::min(elementary_stream_packet.header().size(), + static_cast<size_t>(header_reader.remaining()))); + EXPECT_EQ(elementary_stream_packet.header(), + parsed_elementary_stream_packet_header); + EXPECT_TRUE( + header_reader.Skip(parsed_elementary_stream_packet_header.size())); + + // Elementary stream unit header. + PacketPart parsed_unit_header( + packet.header().end() - header_reader.remaining(), + std::min(elementary_stream_packet.unit_header().size(), + static_cast<size_t>(header_reader.remaining()))); + EXPECT_EQ(elementary_stream_packet.unit_header(), parsed_unit_header); + EXPECT_TRUE(header_reader.Skip(parsed_unit_header.size())); + + // Time stamps. + if (parsed_elementary_stream_packet_header.size() >= 19u) { + uint64_t parsed_dts = ParseTimeStamp( + &parsed_elementary_stream_packet_header.begin()[14], 0x1u); + // Check that + // 0 <= 300 * parsed_dts - parsed_pcr_value <= + // kProgramClockReferenceSecond + // where + // parsed_pcr_value = 300 * parsed_pcr_.base + parsed_pcr_.extension + // but allow parsed_pcr_.base and parsed_dts to wrap around in 33 bits. + EXPECT_NE(ProgramClockReference::kInvalidBase, parsed_pcr_.base); + EXPECT_LE( + 300u * ((parsed_dts - parsed_pcr_.base) & ts::kTimeStampMask) - + parsed_pcr_.extension, + ts::kProgramClockReferenceSecond) + << " DTS must be not smaller than PCR!"; + } + } else { + // If there is no need for padding, then there is no need for + // an adaptation field, either. + if (!last_transport_stream_packet_for_current_unit) + EXPECT_FALSE(parsed_adaptation_field_flag); + if (parsed_adaptation_field_flag) { + CheckTransportStreamAdaptationField(&header_reader, packet, nullptr); + } + } + EXPECT_EQ(0, header_reader.remaining()); + + // Transport stream packet payload. + EXPECT_EQ(*unit_data_pos, packet.payload().begin()); + if (*unit_data_pos == packet.payload().begin()) + *unit_data_pos += packet.payload().size(); + + // Transport stream packet filler. + EXPECT_EQ(0u, packet.filler().size()); + } + + enum { kVideoOnlyUnitSize = 0x8000u }; // Not exact. Be on the safe side. + + const std::vector<uint8_t> unit_; + const base::TimeTicks now_; + const base::TimeTicks dts_; + const base::TimeTicks pts_; + + struct { + size_t program_assication_table; + size_t program_map_table; + size_t program_clock_reference; + size_t elementary_streams[3]; + } continuity_ = {0u, 0u, 0u, {0u, 0u, 0u}}; + ProgramClockReference parsed_pcr_ = {ProgramClockReference::kInvalidBase, 0u}; +}; + +TEST_P(WiFiDisplayElementaryStreamUnitPacketizationTest, + EncodeToElementaryStreamPacket) { + const size_t kMaxUnitHeaderSize = 4u; + + const uint8_t stream_id = + unit_.size() >= kVideoOnlyUnitSize + ? WiFiDisplayElementaryStreamPacketizer::kFirstVideoStreamId + : WiFiDisplayElementaryStreamPacketizer::kFirstAudioStreamId; + + WiFiDisplayElementaryStreamPacketizer packetizer; + uint8_t unit_header_data[kMaxUnitHeaderSize]; + for (size_t unit_header_size = 0u; unit_header_size <= kMaxUnitHeaderSize; + ++unit_header_size) { + WiFiDisplayElementaryStreamPacket packet = + packetizer.EncodeElementaryStreamUnit(stream_id, unit_header_data, + unit_header_size, unit_.data(), + unit_.size(), pts_, dts_); + CheckElementaryStreamPacketHeader(packet, stream_id); + CheckElementaryStreamPacketUnitHeader(packet, unit_header_data, + unit_header_size); + CheckElementaryStreamPacketUnit(packet); + } +} + +TEST_P(WiFiDisplayElementaryStreamUnitPacketizationTest, + EncodeToTransportStreamPackets) { + enum { kStreamCount = 3u }; + static const bool kBoolValues[] = {false, true}; + static const unsigned kPacketIds[kStreamCount] = { + widi::kVideoStreamPacketId, widi::kFirstAudioStreamPacketId + 0u, + widi::kFirstAudioStreamPacketId + 1u}; + static const uint8_t kProgramMapTable[4u + 42u] = { + // Pointer: + 0u, // Pointer field + // Table header: + 0x02u, // Table ID (PMT) + 0x80u | // Section syntax indicator (0b1 for PMT) + 0x00u | // Private bit (0b0 for PMT) + 0x30u | // Reserved bits (0b11) + 0x00u | // Section length unused bits (0b00) + 0u, // Section length (10 bits) + 42u, // + // Table syntax: + 0x00u, // Table ID extension (program number) + 0x01u, // + 0xC0u | // Reserved bits (0b11) + 0x00u | // Version (0b00000) + 0x01u, // Current indicator (0b1) + 0u, // Section number + 0u, // Last section number + // Program map table specific data: + 0xE0u | // Reserved bits (0b111) + 0x10u, // Program clock reference packet ID (13 bits) + 0x00u, // + 0xF0u | // Reserved bits (0b11) + 0x00u | // Program info length unused bits + 0u, // Program info length (10 bits) + 0u, // + // Elementary stream specific data: + 0x1Bu, // Stream type (H.264 in a packetized stream) + 0xE0u | // Reserved bits (0b111) + 0x10u, // Elementary packet ID (13 bits) + 0x11u, // + 0xF0u | // Reserved bits (0b1111) + 0x00u | // Elementary stream info length unused bits + 0u, // Elementary stream info length (10 bits) + 10u, // + 0x28u, // AVC video descriptor tag + 4u, // Descriptor length + 0xA5u, + 0xF5u, 0xBDu, 0xBFu, + 0x2Au, // AVC timing and HRD descriptor tag + 2u, // Descriptor length + 0x7Eu, 0x1Fu, + // Elementary stream specific data: + 0x83u, // Stream type (lossless audio in a packetized stream) + 0xE0u | // Reserved bits (0b111) + 0x11u, // Elementary packet ID (13 bits) + 0x00u, // + 0xF0u | // Reserved bits (0b1111) + 0x00u | // Elementary stream info length unused bits + 0u, // Elementary stream info length (10 bits) + 4u, // + 0x83u, // LPCM audio stream descriptor tag + 2u, // Descriptor length + 0x26u, + 0x2Fu, + // Elementary stream specific data: + 0x0Fu, // Stream type (AAC in a packetized stream) + 0xE0u | // Reserved bits (0b111) + 0x11u, // Elementary packet ID (13 bits) + 0x01u, // + 0xF0u | // Reserved bits (0b1111) + 0x00u | // Elementary stream info length unused bits + 0u, // Elementary stream info length (10 bits) + 0u, // + // CRC: + 0x4Fu, + 0x63u, 0xABu, 0x6Eu}; + static const uint8_t kStreamIds[] = { + WiFiDisplayElementaryStreamPacketizer::kFirstVideoStreamId, + WiFiDisplayElementaryStreamPacketizer::kPrivateStream1Id, + WiFiDisplayElementaryStreamPacketizer::kFirstAudioStreamId}; + + using ESDescriptor = WiFiDisplayElementaryStreamDescriptor; + std::vector<ESDescriptor> lpcm_descriptors; + lpcm_descriptors.emplace_back(ESDescriptor::LPCMAudioStream::Create( + ESDescriptor::LPCMAudioStream::SAMPLING_FREQUENCY_44_1K, + ESDescriptor::LPCMAudioStream::BITS_PER_SAMPLE_16, false, + ESDescriptor::LPCMAudioStream::NUMBER_OF_CHANNELS_STEREO)); + std::vector<ESDescriptor> video_desciptors; + video_desciptors.emplace_back(ESDescriptor::AVCVideo::Create( + 0xA5u, true, true, true, 0x15u, 0xBDu, true)); + video_desciptors.emplace_back(ESDescriptor::AVCTimingAndHRD::Create()); + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos; + stream_infos.emplace_back(WiFiDisplayElementaryStreamInfo::VIDEO_H264, + std::move(video_desciptors)); + stream_infos.emplace_back(WiFiDisplayElementaryStreamInfo::AUDIO_LPCM, + std::move(lpcm_descriptors)); + stream_infos.emplace_back(WiFiDisplayElementaryStreamInfo::AUDIO_AAC); + WiFiDisplayElementaryStreamPacketizer elementary_stream_packetizer; + FakeTransportStreamPacketizer packetizer( + base::TimeDelta::FromMilliseconds(200), std::move(stream_infos)); + + size_t packet_index = 0u; + for (unsigned stream_index = 0; stream_index < kStreamCount; ++stream_index) { + const uint8_t* unit_header_data = nullptr; + size_t unit_header_size = 0u; + if (stream_index > 0u) { // Audio stream. + if (unit_.size() >= kVideoOnlyUnitSize) + continue; + if (stream_index == 1u) { // LPCM + unit_header_data = reinterpret_cast<const uint8_t*>("\xA0\x06\x00\x09"); + unit_header_size = 4u; + } + } + for (const bool random_access : kBoolValues) { + EXPECT_TRUE(packetizer.EncodeElementaryStreamUnit( + stream_index, unit_.data(), unit_.size(), random_access, pts_, dts_, + true)); + auto normalized_pts = pts_; + auto normalized_dts = dts_; + packetizer.NormalizeUnitTimeStamps(&normalized_pts, &normalized_dts); + WiFiDisplayElementaryStreamPacket elementary_stream_packet = + elementary_stream_packetizer.EncodeElementaryStreamUnit( + kStreamIds[stream_index], unit_header_data, unit_header_size, + unit_.data(), unit_.size(), normalized_pts, normalized_dts); + + const uint8_t* unit_data_pos = unit_.data(); + for (const auto& packet : packetizer.FetchPackets()) { + switch (ParseTransportStreamPacketId(packet)) { + case widi::kProgramAssociationTablePacketId: + if (packet_index < 4u) + EXPECT_EQ(0u, packet_index); + CheckTransportStreamProgramAssociationTablePacket(packet); + break; + case widi::kProgramMapTablePacketId: + if (packet_index < 4u) + EXPECT_EQ(1u, packet_index); + CheckTransportStreamProgramMapTablePacket( + packet, PacketPart(kProgramMapTable)); + break; + case widi::kProgramClockReferencePacketId: + if (packet_index < 4u) + EXPECT_EQ(2u, packet_index); + CheckTransportStreamProgramClockReferencePacket(packet); + break; + default: + if (packet_index < 4u) + EXPECT_EQ(3u, packet_index); + CheckTransportStreamElementaryStreamPacket( + packet, elementary_stream_packet, stream_index, + kPacketIds[stream_index], random_access, &unit_data_pos); + } + ++packet_index; + } + EXPECT_EQ(unit_.data() + unit_.size(), unit_data_pos); + } + } +} + +INSTANTIATE_TEST_CASE_P( + WiFiDisplayElementaryStreamUnitPacketizationTests, + WiFiDisplayElementaryStreamUnitPacketizationTest, + testing::Combine(testing::Values(123u, 180u, 0x10000u), + testing::Values(base::TimeDelta::Max(), + base::TimeDelta::FromMicroseconds(0)), + testing::Values(base::TimeDelta::Max(), + base::TimeDelta::FromMicroseconds( + 1000 * INT64_C(0x123456789) / 90)))); + +TEST(WiFiDisplayTransportStreamPacketizationTest, EncodeToMediaDatagramPacket) { + const size_t kPacketHeaderSize = 12u; + + // Create fake units. + const size_t kUnitCount = 12u; + const size_t kUnitSize = + WiFiDisplayTransportStreamPacket::kPacketSize - 4u - 12u; + std::vector<std::array<uint8_t, kUnitSize>> units(kUnitCount); + for (auto& unit : units) + unit.fill(static_cast<uint8_t>(&unit - units.data())); + + // Create transport stream packets. + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos; + stream_infos.emplace_back(WiFiDisplayElementaryStreamInfo::VIDEO_H264); + FakeTransportStreamPacketizer transport_stream_packetizer( + base::TimeDelta::FromMilliseconds(0), std::move(stream_infos)); + for (const auto& unit : units) { + EXPECT_TRUE(transport_stream_packetizer.EncodeElementaryStreamUnit( + 0u, unit.data(), unit.size(), false, base::TimeTicks(), + base::TimeTicks(), &unit == &units.back())); + } + auto transport_stream_packets = transport_stream_packetizer.FetchPackets(); + // There should be exactly one transport stream payload packet for each unit. + // There should also be some but not too many transport stream meta + // information packets. + EXPECT_EQ(1u, transport_stream_packets.size() / kUnitCount); + + // Encode transport stream packets to datagram packets. + FakeMediaPacketizer packetizer( + base::TimeDelta::FromMilliseconds(0), + std::vector<WiFiDisplayElementaryStreamInfo>()); + for (const auto& transport_stream_packet : transport_stream_packets) { + EXPECT_TRUE(packetizer.EncodeTransportStreamPacket( + transport_stream_packet, + &transport_stream_packet == &transport_stream_packets.back())); + } + auto packets = packetizer.FetchPackets(); + + // Check datagram packets. + ProgramClockReference pcr = {ProgramClockReference::kInvalidBase, 0u}; + uint16_t sequence_number; + uint32_t synchronization_source_identifier; + auto transport_stream_packet_it = transport_stream_packets.cbegin(); + for (const auto& packet : packets) { + base::BigEndianReader header_reader( + reinterpret_cast<const char*>(packet.data()), + std::min(kPacketHeaderSize, packet.size())); + + // Packet flags. + uint16_t parsed_u16; + EXPECT_TRUE(header_reader.ReadU16(&parsed_u16)); + EXPECT_EQ(rtp::kVersion2, parsed_u16 & rtp::kVersionMask); + EXPECT_FALSE(parsed_u16 & rtp::kPaddingFlag); + EXPECT_FALSE(parsed_u16 & rtp::kExtensionFlag); + EXPECT_EQ(0u, parsed_u16 & rtp::kContributingSourceCountMask); + EXPECT_FALSE(parsed_u16 & rtp::kMarkerFlag); + EXPECT_EQ(rtp::kPayloadTypeMP2T, parsed_u16 & rtp::kPayloadTypeMask); + + // Packet sequence number. + uint16_t parsed_sequence_number; + EXPECT_TRUE(header_reader.ReadU16(&parsed_sequence_number)); + if (&packet == &packets.front()) + sequence_number = parsed_sequence_number; + EXPECT_EQ(sequence_number++, parsed_sequence_number); + + // Packet time stamp. + uint32_t parsed_time_stamp; + EXPECT_TRUE(header_reader.ReadU32(&parsed_time_stamp)); + if (pcr.base == ProgramClockReference::kInvalidBase) { + // This happens only for the first datagram packet. + EXPECT_TRUE(&packet == &packets.front()); + // Ensure that the next datagram packet reaches the else branch. + EXPECT_FALSE(&packet == &packets.back()); + } else { + // Check that + // 0 <= parsed_time_stamp - pcr.base <= kTimeStampSecond + // but allow pcr.base and parsed_time_stamp to wrap around in 32 bits. + EXPECT_LE((parsed_time_stamp - pcr.base) & 0xFFFFFFFFu, + ts::kTimeStampSecond) + << " Time stamp must not be smaller than PCR!"; + } + + // Packet synchronization source identifier. + uint32_t parsed_synchronization_source_identifier; + EXPECT_TRUE( + header_reader.ReadU32(&parsed_synchronization_source_identifier)); + if (&packet == &packets.front()) { + synchronization_source_identifier = + parsed_synchronization_source_identifier; + } + EXPECT_EQ(synchronization_source_identifier, + parsed_synchronization_source_identifier); + + EXPECT_EQ(0, header_reader.remaining()); + + // Packet payload. + size_t offset = kPacketHeaderSize; + while (offset + WiFiDisplayTransportStreamPacket::kPacketSize <= + packet.size() && + transport_stream_packet_it != transport_stream_packets.end()) { + const auto& transport_stream_packet = *transport_stream_packet_it++; + const PacketPart parsed_transport_stream_packet_header( + packet.data() + offset, transport_stream_packet.header().size()); + const PacketPart parsed_transport_stream_packet_payload( + parsed_transport_stream_packet_header.end(), + transport_stream_packet.payload().size()); + const PacketPart parsed_transport_stream_packet_filler( + parsed_transport_stream_packet_payload.end(), + transport_stream_packet.filler().size()); + offset += WiFiDisplayTransportStreamPacket::kPacketSize; + + // Check bytes. + EXPECT_EQ(transport_stream_packet.header(), + parsed_transport_stream_packet_header); + EXPECT_EQ(transport_stream_packet.payload(), + parsed_transport_stream_packet_payload); + EXPECT_EQ(transport_stream_packet.filler().size(), + std::count(parsed_transport_stream_packet_filler.begin(), + parsed_transport_stream_packet_filler.end(), + transport_stream_packet.filler().value())); + + if (ParseTransportStreamPacketId(transport_stream_packet) == + widi::kProgramClockReferencePacketId) { + pcr = ParseProgramClockReference( + &transport_stream_packet.header().begin()[6]); + } + } + EXPECT_EQ(offset, packet.size()) << " Extra packet payload bytes."; + + // Check that the payload contains a correct number of transport stream + // packets. + const size_t transport_stream_packet_count_in_datagram_packet = + packet.size() / WiFiDisplayTransportStreamPacket::kPacketSize; + if (&packet == &packets.back()) { + EXPECT_GE(transport_stream_packet_count_in_datagram_packet, 1u); + EXPECT_LE(transport_stream_packet_count_in_datagram_packet, + widi::kMaxTransportStreamPacketCountPerDatagramPacket); + } else { + EXPECT_EQ(widi::kMaxTransportStreamPacketCountPerDatagramPacket, + transport_stream_packet_count_in_datagram_packet); + } + } + EXPECT_EQ(transport_stream_packets.end(), transport_stream_packet_it); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.cc new file mode 100644 index 00000000000..f1bfe4cbea7 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.cc @@ -0,0 +1,225 @@ +// Copyright 2015 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/api/display_source/wifi_display/wifi_display_session.h" + +#include <utility> + +#include "base/logging.h" +#include "base/timer/timer.h" +#include "content/public/common/service_registry.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_media_manager.h" +#include "third_party/wds/src/libwds/public/logging.h" +#include "third_party/wds/src/libwds/public/media_manager.h" + +namespace { +const char kErrorInternal[] = "An internal error has occurred"; +const char kErrorTimeout[] = "Sink became unresponsive"; + +static void LogWDSError(const char* format, ...) { + va_list args; + va_start(args, format); + char buffer[256]; + vsnprintf(buffer, 256, format, args); + va_end(args); + DVLOG(1) << "[WDS] " << buffer; +} + +} // namespace + +namespace extensions { + +using api::display_source::ErrorType; + +WiFiDisplaySession::WiFiDisplaySession( + const DisplaySourceSessionParams& params) + : binding_(this), + params_(params), + cseq_(0), + timer_id_(0), + weak_factory_(this) { + DCHECK(params_.render_frame); + wds::LogSystem::set_error_func(&LogWDSError); + params.render_frame->GetServiceRegistry()->ConnectToRemoteService( + mojo::GetProxy(&service_)); + service_.set_connection_error_handler(base::Bind( + &WiFiDisplaySession::OnIPCConnectionError, + weak_factory_.GetWeakPtr())); + + service_->SetClient(binding_.CreateInterfacePtrAndBind()); + binding_.set_connection_error_handler(base::Bind( + &WiFiDisplaySession::OnIPCConnectionError, + weak_factory_.GetWeakPtr())); +} + +WiFiDisplaySession::~WiFiDisplaySession() { +} + +void WiFiDisplaySession::Start(const CompletionCallback& callback) { + DCHECK_EQ(DisplaySourceSession::Idle, state_); + DCHECK(!terminated_callback_.is_null()) + << "Should be set with 'SetNotificationCallbacks'"; + DCHECK(!error_callback_.is_null()) + << "Should be set with 'SetNotificationCallbacks'"; + + service_->Connect(params_.sink_id, params_.auth_method, params_.auth_data); + state_ = DisplaySourceSession::Establishing; + start_completion_callback_ = callback; +} + +void WiFiDisplaySession::Terminate(const CompletionCallback& callback) { + DCHECK_EQ(DisplaySourceSession::Established, state_); + Terminate(); + teminate_completion_callback_ = callback; +} + +void WiFiDisplaySession::OnConnected(const mojo::String& ip_address) { + DCHECK_EQ(DisplaySourceSession::Established, state_); + ip_address_ = ip_address; + media_manager_.reset( + new WiFiDisplayMediaManager( + params_.video_track, + params_.audio_track, + base::Bind( + &WiFiDisplaySession::OnMediaError, + weak_factory_.GetWeakPtr()))); + wfd_source_.reset(wds::Source::Create(this, media_manager_.get(), this)); + wfd_source_->Start(); +} + +void WiFiDisplaySession::OnConnectRequestHandled(bool success, + const mojo::String& error) { + DCHECK_EQ(DisplaySourceSession::Establishing, state_); + state_ = + success ? DisplaySourceSession::Established : DisplaySourceSession::Idle; + RunStartCallback(success, error); +} + +void WiFiDisplaySession::OnTerminated() { + DCHECK_NE(DisplaySourceSession::Idle, state_); + state_ = DisplaySourceSession::Idle; + media_manager_.reset(); + wfd_source_.reset(); + terminated_callback_.Run(); +} + +void WiFiDisplaySession::OnDisconnectRequestHandled(bool success, + const mojo::String& error) { + RunTerminateCallback(success, error); +} + +void WiFiDisplaySession::OnError(int32_t type, + const mojo::String& description) { + DCHECK(type > api::display_source::ERROR_TYPE_NONE + && type <= api::display_source::ERROR_TYPE_LAST); + DCHECK_EQ(DisplaySourceSession::Established, state_); + error_callback_.Run(static_cast<ErrorType>(type), description); +} + +void WiFiDisplaySession::OnMessage(const mojo::String& data) { + DCHECK_EQ(DisplaySourceSession::Established, state_); + DCHECK(wfd_source_); + wfd_source_->RTSPDataReceived(data); +} + +std::string WiFiDisplaySession::GetLocalIPAddress() const { + return ip_address_; +} + +int WiFiDisplaySession::GetNextCSeq(int* initial_peer_cseq) const { + return ++cseq_; +} + +void WiFiDisplaySession::SendRTSPData(const std::string& message) { + service_->SendMessage(message); +} + +unsigned WiFiDisplaySession::CreateTimer(int seconds) { + scoped_ptr<base::Timer> timer(new base::Timer(true, true)); + auto insert_ret = timers_.insert( + std::pair<int, scoped_ptr<base::Timer>>( + ++timer_id_, std::move(timer))); + DCHECK(insert_ret.second); + insert_ret.first->second->Start(FROM_HERE, + base::TimeDelta::FromSeconds(seconds), + base::Bind(&wds::Source::OnTimerEvent, + base::Unretained(wfd_source_.get()), + timer_id_)); + return static_cast<unsigned>(timer_id_); +} + +void WiFiDisplaySession::ReleaseTimer(unsigned timer_id) { + auto it = timers_.find(static_cast<int>(timer_id)); + if (it != timers_.end()) + timers_.erase(it); +} + +void WiFiDisplaySession::ErrorOccurred(wds::ErrorType error) { + DCHECK_NE(DisplaySourceSession::Idle, state_); + if (error == wds::TimeoutError) { + error_callback_.Run(api::display_source::ERROR_TYPE_TIMEOUT_ERROR, + kErrorTimeout); + } else { + error_callback_.Run(api::display_source::ERROR_TYPE_UNKNOWN_ERROR, + kErrorInternal); + } + // The session cannot continue. + Terminate(); +} + +void WiFiDisplaySession::SessionCompleted() { + DCHECK_NE(DisplaySourceSession::Idle, state_); + // The session has finished normally. + Terminate(); +} + +void WiFiDisplaySession::OnIPCConnectionError() { + // We must explicitly notify the session termination as it will never + // arrive from browser process (IPC is broken). + switch (state_) { + case DisplaySourceSession::Idle: + case DisplaySourceSession::Establishing: + RunStartCallback(false, kErrorInternal); + break; + case DisplaySourceSession::Terminating: + case DisplaySourceSession::Established: + error_callback_.Run(api::display_source::ERROR_TYPE_UNKNOWN_ERROR, + kErrorInternal); + state_ = DisplaySourceSession::Idle; + terminated_callback_.Run(); + break; + default: + NOTREACHED(); + } +} + +void WiFiDisplaySession::OnMediaError(const std::string& error) { + DCHECK_NE(DisplaySourceSession::Idle, state_); + error_callback_.Run(api::display_source::ERROR_TYPE_MEDIA_PIPELINE_ERROR, + error); + Terminate(); +} + +void WiFiDisplaySession::Terminate() { + if (state_ == DisplaySourceSession::Established) { + service_->Disconnect(); + state_ = DisplaySourceSession::Terminating; + } +} + +void WiFiDisplaySession::RunStartCallback(bool success, + const std::string& error_message) { + if (!start_completion_callback_.is_null()) + start_completion_callback_.Run(success, error_message); +} + +void WiFiDisplaySession::RunTerminateCallback( + bool success, + const std::string& error_message) { + if (!teminate_completion_callback_.is_null()) + teminate_completion_callback_.Run(success, error_message); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.h new file mode 100644 index 00000000000..bacb875dbac --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_session.h @@ -0,0 +1,95 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_SESSION_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_SESSION_H_ + +#include <map> +#include <string> + +#include "extensions/common/mojo/wifi_display_session_service.mojom.h" +#include "extensions/renderer/api/display_source/display_source_session.h" +#include "mojo/public/cpp/bindings/binding.h" +#include "third_party/wds/src/libwds/public/source.h" + +namespace base { +class Timer; +} // namespace base + +namespace extensions { + +class WiFiDisplayMediaManager; + +// This class represents a single Wi-Fi Display session. +// It manages life-cycle of the session and it is also responsible for +// exchange of session controlling (RTSP) messages with the sink. +class WiFiDisplaySession: public DisplaySourceSession, + public WiFiDisplaySessionServiceClient, + public wds::Peer::Delegate, + public wds::Peer::Observer { + public: + explicit WiFiDisplaySession( + const DisplaySourceSessionParams& params); + ~WiFiDisplaySession() override; + + private: + using DisplaySourceSession::CompletionCallback; + // DisplaySourceSession overrides. + void Start(const CompletionCallback& callback) override; + void Terminate(const CompletionCallback& callback) override; + + // WiFiDisplaySessionServiceClient overrides. + void OnConnected(const mojo::String& ip_address) override; + void OnConnectRequestHandled(bool success, + const mojo::String& error) override; + void OnTerminated() override; + void OnDisconnectRequestHandled(bool success, + const mojo::String& error) override; + void OnError(int32_t type, const mojo::String& description) override; + void OnMessage(const mojo::String& data) override; + + // wds::Peer::Delegate overrides. + unsigned CreateTimer(int seconds) override; + void ReleaseTimer(unsigned timer_id) override; + void SendRTSPData(const std::string& message) override; + std::string GetLocalIPAddress() const override; + int GetNextCSeq(int* initial_peer_cseq = nullptr) const override; + + // wds::Peer::Observer overrides. + void ErrorOccurred(wds::ErrorType error) override; + void SessionCompleted() override; + + // A connection error handler for the mojo objects used in this class. + void OnIPCConnectionError(); + + // An error handler for media pipeline error. + void OnMediaError(const std::string& error); + + void Terminate(); + + void RunStartCallback(bool success, const std::string& error = ""); + void RunTerminateCallback(bool success, const std::string& error = ""); + + private: + scoped_ptr<wds::Source> wfd_source_; + scoped_ptr<WiFiDisplayMediaManager> media_manager_; + WiFiDisplaySessionServicePtr service_; + mojo::Binding<WiFiDisplaySessionServiceClient> binding_; + std::string ip_address_; + std::map<int, scoped_ptr<base::Timer>> timers_; + + DisplaySourceSessionParams params_; + CompletionCallback start_completion_callback_; + CompletionCallback teminate_completion_callback_; + // Holds sequence number for the following RTSP request-response pair. + mutable int cseq_; + int timer_id_; + base::WeakPtrFactory<WiFiDisplaySession> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(WiFiDisplaySession); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_SESSION_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_stream_packet_part.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_stream_packet_part.h new file mode 100644 index 00000000000..152b4a7ea23 --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_stream_packet_part.h @@ -0,0 +1,43 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_STREAM_PACKET_PART_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_STREAM_PACKET_PART_H_ + +#include <stddef.h> +#include <stdint.h> + +#include "base/macros.h" + +namespace extensions { + +// WiFi Display elementary stream unit packetization consists of multiple +// packetization phases. During those phases, unit buffer bytes are not +// modified but only wrapped in packets. +// This class allows different kind of WiFi Display stream packets to refer to +// unit buffer bytes without copying them. +class WiFiDisplayStreamPacketPart { + public: + WiFiDisplayStreamPacketPart(const uint8_t* data, size_t size) + : data_(data), size_(size) {} + template <size_t N> + explicit WiFiDisplayStreamPacketPart(const uint8_t (&data)[N]) + : WiFiDisplayStreamPacketPart(data, N) {} + + const uint8_t* begin() const { return data(); } + const uint8_t* data() const { return data_; } + bool empty() const { return size() == 0u; } + const uint8_t* end() const { return data() + size(); } + size_t size() const { return size_; } + + private: + const uint8_t* const data_; + const size_t size_; + + DISALLOW_COPY_AND_ASSIGN(WiFiDisplayStreamPacketPart); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_STREAM_PACKET_BASE_H_ diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.cc b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.cc new file mode 100644 index 00000000000..eee0a52c55a --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.cc @@ -0,0 +1,727 @@ +// Copyright 2016 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/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h" + +#include <algorithm> +#include <cstring> +#include <utility> + +#include "base/logging.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_descriptor.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_info.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_elementary_stream_packetizer.h" + +namespace extensions { +namespace { + +uint32_t crc32(uint32_t crc, const uint8_t* data, size_t len) { + static const uint32_t table[256] = { + 0x00000000, 0xb71dc104, 0x6e3b8209, 0xd926430d, 0xdc760413, 0x6b6bc517, + 0xb24d861a, 0x0550471e, 0xb8ed0826, 0x0ff0c922, 0xd6d68a2f, 0x61cb4b2b, + 0x649b0c35, 0xd386cd31, 0x0aa08e3c, 0xbdbd4f38, 0x70db114c, 0xc7c6d048, + 0x1ee09345, 0xa9fd5241, 0xacad155f, 0x1bb0d45b, 0xc2969756, 0x758b5652, + 0xc836196a, 0x7f2bd86e, 0xa60d9b63, 0x11105a67, 0x14401d79, 0xa35ddc7d, + 0x7a7b9f70, 0xcd665e74, 0xe0b62398, 0x57abe29c, 0x8e8da191, 0x39906095, + 0x3cc0278b, 0x8bdde68f, 0x52fba582, 0xe5e66486, 0x585b2bbe, 0xef46eaba, + 0x3660a9b7, 0x817d68b3, 0x842d2fad, 0x3330eea9, 0xea16ada4, 0x5d0b6ca0, + 0x906d32d4, 0x2770f3d0, 0xfe56b0dd, 0x494b71d9, 0x4c1b36c7, 0xfb06f7c3, + 0x2220b4ce, 0x953d75ca, 0x28803af2, 0x9f9dfbf6, 0x46bbb8fb, 0xf1a679ff, + 0xf4f63ee1, 0x43ebffe5, 0x9acdbce8, 0x2dd07dec, 0x77708634, 0xc06d4730, + 0x194b043d, 0xae56c539, 0xab068227, 0x1c1b4323, 0xc53d002e, 0x7220c12a, + 0xcf9d8e12, 0x78804f16, 0xa1a60c1b, 0x16bbcd1f, 0x13eb8a01, 0xa4f64b05, + 0x7dd00808, 0xcacdc90c, 0x07ab9778, 0xb0b6567c, 0x69901571, 0xde8dd475, + 0xdbdd936b, 0x6cc0526f, 0xb5e61162, 0x02fbd066, 0xbf469f5e, 0x085b5e5a, + 0xd17d1d57, 0x6660dc53, 0x63309b4d, 0xd42d5a49, 0x0d0b1944, 0xba16d840, + 0x97c6a5ac, 0x20db64a8, 0xf9fd27a5, 0x4ee0e6a1, 0x4bb0a1bf, 0xfcad60bb, + 0x258b23b6, 0x9296e2b2, 0x2f2bad8a, 0x98366c8e, 0x41102f83, 0xf60dee87, + 0xf35da999, 0x4440689d, 0x9d662b90, 0x2a7bea94, 0xe71db4e0, 0x500075e4, + 0x892636e9, 0x3e3bf7ed, 0x3b6bb0f3, 0x8c7671f7, 0x555032fa, 0xe24df3fe, + 0x5ff0bcc6, 0xe8ed7dc2, 0x31cb3ecf, 0x86d6ffcb, 0x8386b8d5, 0x349b79d1, + 0xedbd3adc, 0x5aa0fbd8, 0xeee00c69, 0x59fdcd6d, 0x80db8e60, 0x37c64f64, + 0x3296087a, 0x858bc97e, 0x5cad8a73, 0xebb04b77, 0x560d044f, 0xe110c54b, + 0x38368646, 0x8f2b4742, 0x8a7b005c, 0x3d66c158, 0xe4408255, 0x535d4351, + 0x9e3b1d25, 0x2926dc21, 0xf0009f2c, 0x471d5e28, 0x424d1936, 0xf550d832, + 0x2c769b3f, 0x9b6b5a3b, 0x26d61503, 0x91cbd407, 0x48ed970a, 0xfff0560e, + 0xfaa01110, 0x4dbdd014, 0x949b9319, 0x2386521d, 0x0e562ff1, 0xb94beef5, + 0x606dadf8, 0xd7706cfc, 0xd2202be2, 0x653deae6, 0xbc1ba9eb, 0x0b0668ef, + 0xb6bb27d7, 0x01a6e6d3, 0xd880a5de, 0x6f9d64da, 0x6acd23c4, 0xddd0e2c0, + 0x04f6a1cd, 0xb3eb60c9, 0x7e8d3ebd, 0xc990ffb9, 0x10b6bcb4, 0xa7ab7db0, + 0xa2fb3aae, 0x15e6fbaa, 0xccc0b8a7, 0x7bdd79a3, 0xc660369b, 0x717df79f, + 0xa85bb492, 0x1f467596, 0x1a163288, 0xad0bf38c, 0x742db081, 0xc3307185, + 0x99908a5d, 0x2e8d4b59, 0xf7ab0854, 0x40b6c950, 0x45e68e4e, 0xf2fb4f4a, + 0x2bdd0c47, 0x9cc0cd43, 0x217d827b, 0x9660437f, 0x4f460072, 0xf85bc176, + 0xfd0b8668, 0x4a16476c, 0x93300461, 0x242dc565, 0xe94b9b11, 0x5e565a15, + 0x87701918, 0x306dd81c, 0x353d9f02, 0x82205e06, 0x5b061d0b, 0xec1bdc0f, + 0x51a69337, 0xe6bb5233, 0x3f9d113e, 0x8880d03a, 0x8dd09724, 0x3acd5620, + 0xe3eb152d, 0x54f6d429, 0x7926a9c5, 0xce3b68c1, 0x171d2bcc, 0xa000eac8, + 0xa550add6, 0x124d6cd2, 0xcb6b2fdf, 0x7c76eedb, 0xc1cba1e3, 0x76d660e7, + 0xaff023ea, 0x18ede2ee, 0x1dbda5f0, 0xaaa064f4, 0x738627f9, 0xc49be6fd, + 0x09fdb889, 0xbee0798d, 0x67c63a80, 0xd0dbfb84, 0xd58bbc9a, 0x62967d9e, + 0xbbb03e93, 0x0cadff97, 0xb110b0af, 0x060d71ab, 0xdf2b32a6, 0x6836f3a2, + 0x6d66b4bc, 0xda7b75b8, 0x035d36b5, 0xb440f7b1}; + for (; len; ++data, --len) + crc = (crc >> 8) ^ table[(crc & 0xFFu) ^ *data]; + return crc; +} + +// Code and parameters related to the Program Specific Information (PSI) +// specification. +namespace psi { + +const uint8_t kProgramAssociationTableId = 0x00u; +const uint8_t kProgramMapTableId = 0x02u; + +const uint16_t kFirstProgramNumber = 0x0001u; + +const size_t kCrcSize = 4u; +const size_t kProgramMapTableElementaryStreamEntryBaseSize = 5u; +const size_t kTableHeaderSize = 3u; + +size_t FillInTablePointer(uint8_t* dst, size_t min_size) { + size_t i = 1u; + if (i < min_size) { + std::memset(&dst[i], 0xFF, min_size - i); // Pointer filler bytes + i = min_size; + } + dst[0] = i - 1u; // Pointer field + return i; +} + +size_t FillInTableHeaderAndCrc(uint8_t* header_dst, + uint8_t* crc_dst, + uint8_t table_id) { + size_t i; + const uint8_t* const header_end = header_dst + kTableHeaderSize; + const uint8_t* const crc_end = crc_dst + kCrcSize; + const size_t section_length = static_cast<size_t>(crc_end - header_end); + DCHECK_LE(section_length, 1021u); + + // Table header. + i = 0u; + header_dst[i++] = table_id; + header_dst[i++] = + (0x1u << 7) | // Section syntax indicator (1 for PAT and PMT) + (0x0u << 6) | // Private bit (0 for PAT and PMT) + (0x3u << 4) | // Reserved bits (both bits on) + (0x0u << 2) | // Section length unused bits (both bits off) + ((section_length >> 8) & 0x03u); // Section length (10 bits) + header_dst[i++] = section_length & 0xFFu; // + DCHECK_EQ(kTableHeaderSize, i); + + // CRC. + uint32_t crc = + crc32(0xFFFFFFFFu, header_dst, static_cast<size_t>(crc_dst - header_dst)); + i = 0u; + // Avoid swapping the crc by reversing write order. + crc_dst[i++] = crc & 0xFFu; + crc_dst[i++] = (crc >> 8) & 0xFFu; + crc_dst[i++] = (crc >> 16) & 0xFFu; + crc_dst[i++] = (crc >> 24) & 0xFFu; + DCHECK_EQ(kCrcSize, i); + return i; +} + +size_t FillInTableSyntax(uint8_t* dst, + uint16_t table_id_extension, + uint8_t version_number) { + size_t i = 0u; + dst[i++] = table_id_extension >> 8; + dst[i++] = table_id_extension & 0xFFu; + dst[i++] = (0x3u << 6) | // Reserved bits (both bits on) + ((version_number & 0x1Fu) << 1) | // Version number (5 bits) + (0x1u << 0); // Current indicator + dst[i++] = 0u; // Section number + dst[i++] = 0u; // Last section number + return i; +} + +size_t FillInProgramAssociationTableEntry(uint8_t* dst, + uint16_t program_number, + unsigned pmt_packet_id) { + size_t i = 0u; + dst[i++] = program_number >> 8; + dst[i++] = program_number & 0xFFu; + dst[i++] = (0x7u << 5) | // Reserved bits (all 3 bits on) + ((pmt_packet_id >> 8) & 0x1Fu); // Packet identifier (13 bits) + dst[i++] = pmt_packet_id & 0xFFu; // + return i; +} + +size_t FillInProgramMapTableData(uint8_t* dst, unsigned pcr_packet_id) { + size_t i = 0u; + dst[i++] = (0x7u << 5) | // Reserved bits (all 3 bits on) + ((pcr_packet_id >> 8) & 0x1Fu); // Packet identifier (13 bits) + dst[i++] = pcr_packet_id & 0xFFu; // + dst[i++] = (0xFu << 4) | // Reserved bits (all 4 bits on) + (0x0u << 2) | // Program info length unused bits (both bits off) + ((0u >> 8) & 0x3u); // Program info length (10 bits) + dst[i++] = 0u & 0xFFu; // + // No program descriptors + return i; +} + +size_t CalculateElementaryStreamInfoLength( + const std::vector<WiFiDisplayElementaryStreamDescriptor>& es_descriptors) { + size_t es_info_length = 0u; + for (const auto& es_descriptor : es_descriptors) + es_info_length += es_descriptor.size(); + DCHECK_EQ(0u, es_info_length >> 8); + return es_info_length; +} + +size_t FillInProgramMapTableElementaryStreamEntry( + uint8_t* dst, + uint8_t stream_type, + unsigned es_packet_id, + size_t es_info_length, + const std::vector<WiFiDisplayElementaryStreamDescriptor>& es_descriptors) { + DCHECK_EQ(CalculateElementaryStreamInfoLength(es_descriptors), + es_info_length); + DCHECK_EQ(0u, es_info_length >> 10); + size_t i = 0u; + dst[i++] = stream_type; + dst[i++] = (0x7u << 5) | // Reserved bits (all 3 bits on) + ((es_packet_id >> 8) & 0x1Fu); // Packet identifier (13 bits) + dst[i++] = es_packet_id & 0xFFu; // + dst[i++] = (0xFu << 4) | // Reserved bits (all 4 bits on) + (0x0u << 2) | // ES info length unused bits (both bits off) + ((es_info_length >> 8) & 0x3u); // ES info length (10 bits) + dst[i++] = es_info_length & 0xFFu; // + for (const auto& es_descriptor : es_descriptors) { + std::memcpy(&dst[i], es_descriptor.data(), es_descriptor.size()); + i += es_descriptor.size(); + } + return i; +} + +} // namespace psi + +// Code and parameters related to the MPEG Transport Stream (MPEG-TS) +// specification. +namespace ts { + +const size_t kAdaptationFieldLengthSize = 1u; +const size_t kAdaptationFieldFlagsSize = 1u; +const size_t kPacketHeaderSize = 4u; +const size_t kProgramClockReferenceSize = 6u; + +size_t FillInProgramClockReference(uint8_t* dst, const base::TimeTicks& pcr) { + // Convert to the number of 27 MHz ticks since some epoch. + const uint64_t us = + static_cast<uint64_t>((pcr - base::TimeTicks()).InMicroseconds()); + const uint64_t n = 27u * us; + const uint64_t base = n / 300u; // 90 kHz + const uint64_t extension = n % 300u; + + size_t i = 0u; + dst[i++] = (base >> 25) & 0xFFu; // Base (33 bits) + dst[i++] = (base >> 17) & 0xFFu; // + dst[i++] = (base >> 9) & 0xFFu; // + dst[i++] = (base >> 1) & 0xFFu; // + dst[i++] = ((base & 0x01u) << 7) | // + (0x3Fu << 1) | // Reserved bits (all 6 bits on) + ((extension >> 8) & 0x1u); // Extension (9 bits) + dst[i++] = extension & 0xFFu; // + DCHECK_EQ(kProgramClockReferenceSize, i); + return i; +} + +size_t FillInAdaptationFieldLengthFromSize(uint8_t* dst, size_t size) { + size_t i = 0u; + dst[i++] = size - 1u; + DCHECK_EQ(kAdaptationFieldLengthSize, i); + return i; +} + +size_t FillInAdaptationFieldFlags(uint8_t* dst, + bool random_access_indicator, + const base::TimeTicks& pcr) { + size_t i = 0u; + dst[i++] = (0x0u << 7) | // Discontinuity indicator + (random_access_indicator << 6) | // Random access indicator + (0x0u << 5) | // Elementary stream priority indicator + ((!pcr.is_null()) << 4) | // PCR flag + (0x0u << 3) | // OPCR flag + (0x0u << 2) | // Splicing point flag + (0x0u << 1) | // Transport private data flag + (0x0u << 0); // Adaptation field extension flag + DCHECK_EQ(kAdaptationFieldFlagsSize, i); + return i; +} + +size_t FillInAdaptationField(uint8_t* dst, + bool random_access_indicator, + size_t min_size) { + // Reserve space for a length. + size_t i = kAdaptationFieldLengthSize; + + if (random_access_indicator || i < min_size) { + const base::TimeTicks pcr; + i += FillInAdaptationFieldFlags(&dst[i], random_access_indicator, pcr); + + if (i < min_size) { + std::memset(&dst[i], 0xFF, min_size - i); // Stuffing bytes. + i = min_size; + } + } + + // Fill in a length now that the size is known. + FillInAdaptationFieldLengthFromSize(dst, i); + + return i; +} + +size_t FillInPacketHeader(uint8_t* dst, + bool payload_unit_start_indicator, + unsigned packet_id, + bool adaptation_field_flag, + unsigned continuity_counter) { + size_t i = 0u; + dst[i++] = 0x47; // Sync byte ('G') + dst[i++] = + (0x0u << 7) | // Transport error indicator + (payload_unit_start_indicator << 6) | // Payload unit start indicator + (0x0 << 5) | // Transport priority + ((packet_id >> 8) & 0x1Fu); // Packet identifier (13 bits) + dst[i++] = packet_id & 0xFFu; // + dst[i++] = (0x0u << 6) | // Scrambling control (0b00 for not) + (adaptation_field_flag << 5) | // Adaptation field flag + (0x1u << 4) | // Payload flag + (continuity_counter & 0xFu); // Continuity counter + DCHECK_EQ(kPacketHeaderSize, i); + return i; +} + +} // namespace ts + +// Code and parameters related to the WiFi Display specification. +namespace widi { + +const size_t kUnitHeaderMaxSize = 4u; + +// Maximum interval between meta information which includes: +// * Program Association Table (PAT) +// * Program Map Table (PMT) +// * Program Clock Reference (PCR) +const int kMaxMillisecondsBetweenMetaInformation = 100u; + +const unsigned kProgramAssociationTablePacketId = 0x0000u; +const unsigned kProgramMapTablePacketId = 0x0100u; +const unsigned kProgramClockReferencePacketId = 0x1000u; +const unsigned kVideoStreamPacketId = 0x1011u; +const unsigned kFirstAudioStreamPacketId = 0x1100u; + +size_t FillInUnitHeader(uint8_t* dst, + const WiFiDisplayElementaryStreamInfo& stream_info) { + size_t i = 0u; + + if (stream_info.type() == WiFiDisplayElementaryStreamInfo::AUDIO_LPCM) { + // Convert an LPCM audio stream descriptor to an LPCM unit header. + if (const auto* lpcm_descriptor = + stream_info.FindDescriptor< + WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream>()) { + dst[i++] = 0xA0u; // Sub stream ID (0th sub stream) + dst[i++] = WiFiDisplayTransportStreamPacketizer::LPCM::kFramesPerUnit; + dst[i++] = ((0x00u << 1) | // Reserved (all 7 bits off) + (lpcm_descriptor->emphasis_flag() << 0)); + dst[i++] = ((lpcm_descriptor->bits_per_sample() << 6) | + (lpcm_descriptor->sampling_frequency() << 3) | + (lpcm_descriptor->number_of_channels() << 0)); + } + } + + DCHECK_LE(i, kUnitHeaderMaxSize); + return i; +} + +} // namespace widi + +} // namespace + +WiFiDisplayTransportStreamPacket::WiFiDisplayTransportStreamPacket( + const uint8_t* header_data, + size_t header_size) + : header_(header_data, header_size), + payload_(header_.end(), 0u), + filler_(kPacketSize - header_size) {} + +WiFiDisplayTransportStreamPacket::WiFiDisplayTransportStreamPacket( + const uint8_t* header_data, + size_t header_size, + const uint8_t* payload_data) + : header_(header_data, header_size), + payload_(payload_data, kPacketSize - header_size), + filler_(0u) {} + +struct WiFiDisplayTransportStreamPacketizer::ElementaryStreamState { + ElementaryStreamState(WiFiDisplayElementaryStreamInfo info, + uint16_t packet_id, + uint8_t stream_id) + : info(std::move(info)), + info_length( + psi::CalculateElementaryStreamInfoLength(this->info.descriptors())), + packet_id(packet_id), + stream_id(stream_id) {} + + WiFiDisplayElementaryStreamInfo info; + uint8_t info_length; + struct { + uint8_t continuity = 0u; + } counters; + uint16_t packet_id; + uint8_t stream_id; +}; + +WiFiDisplayTransportStreamPacketizer::WiFiDisplayTransportStreamPacketizer( + const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos) + : delay_for_unit_time_stamps_(delay_for_unit_time_stamps) { + std::memset(&counters_, 0x00, sizeof(counters_)); + if (!stream_infos.empty()) + CHECK(SetElementaryStreams(std::move(stream_infos))); +} + +WiFiDisplayTransportStreamPacketizer::~WiFiDisplayTransportStreamPacketizer() {} + +bool WiFiDisplayTransportStreamPacketizer::EncodeElementaryStreamUnit( + unsigned stream_index, + const uint8_t* unit_data, + size_t unit_size, + bool random_access, + base::TimeTicks pts, + base::TimeTicks dts, + bool flush) { + DCHECK(CalledOnValidThread()); + DCHECK_LT(stream_index, stream_states_.size()); + ElementaryStreamState& stream_state = stream_states_[stream_index]; + + if (program_clock_reference_.is_null() || + base::TimeTicks::Now() - program_clock_reference_ > + base::TimeDelta::FromMilliseconds( + widi::kMaxMillisecondsBetweenMetaInformation) / + 2) { + if (!EncodeMetaInformation(false)) + return false; + } + + uint8_t unit_header_data[widi::kUnitHeaderMaxSize]; + const size_t unit_header_size = + widi::FillInUnitHeader(unit_header_data, stream_state.info); + + UpdateDelayForUnitTimeStamps(pts, dts); + NormalizeUnitTimeStamps(&pts, &dts); + + WiFiDisplayElementaryStreamPacketizer elementary_stream_packetizer; + WiFiDisplayElementaryStreamPacket elementary_stream_packet = + elementary_stream_packetizer.EncodeElementaryStreamUnit( + stream_state.stream_id, unit_header_data, unit_header_size, unit_data, + unit_size, pts, dts); + + size_t adaptation_field_min_size = 0u; + uint8_t header_data[WiFiDisplayTransportStreamPacket::kPacketSize]; + bool is_payload_unit_end; + bool is_payload_unit_start = true; + size_t remaining_unit_size = elementary_stream_packet.unit().size(); + do { + // Fill in headers and an adaptation field: + // * Transport stream packet header + // * Transport stream adaptation field + // (only for the first and/or the last packet): + // - for the first packet to hold flags + // - for the last packet to hold padding + // * PES packet header (only for the first packet): + // - PES packet header base + // - Optional PES header base + // - Optional PES header optional fields: + // - Presentation time stamp + // - Decoding time stamp + bool adaptation_field_flag = false; + size_t header_min_size; + if (is_payload_unit_start || is_payload_unit_end) { + header_min_size = ts::kPacketHeaderSize; + if (is_payload_unit_start) { + header_min_size += elementary_stream_packet.header().size() + + elementary_stream_packet.unit_header().size(); + } + adaptation_field_min_size = + std::max( + WiFiDisplayTransportStreamPacket::kPacketSize - header_min_size, + remaining_unit_size) - + remaining_unit_size; + adaptation_field_flag = adaptation_field_min_size > 0 || + (is_payload_unit_start && random_access); + } + size_t i = 0u; + i += ts::FillInPacketHeader(&header_data[i], is_payload_unit_start, + stream_state.packet_id, adaptation_field_flag, + stream_state.counters.continuity++); + if (is_payload_unit_start) { + size_t adaptation_field_size = adaptation_field_min_size; + if (adaptation_field_flag) { + adaptation_field_size = ts::FillInAdaptationField( + &header_data[i], random_access, adaptation_field_min_size); + i += adaptation_field_size; + DCHECK_GE(adaptation_field_size, adaptation_field_min_size); + } + std::memcpy(&header_data[i], elementary_stream_packet.header().data(), + elementary_stream_packet.header().size()); + i += elementary_stream_packet.header().size(); + std::memcpy(&header_data[i], + elementary_stream_packet.unit_header().data(), + elementary_stream_packet.unit_header().size()); + i += elementary_stream_packet.unit_header().size(); + DCHECK_EQ(header_min_size + adaptation_field_size, i); + } else if (is_payload_unit_end) { + if (adaptation_field_flag) { + // Fill in an adaptation field only for padding. + i += ts::FillInAdaptationField(&header_data[i], false, + adaptation_field_min_size); + } + DCHECK_EQ(header_min_size + adaptation_field_min_size, i); + } + + // Delegate the packet. + WiFiDisplayTransportStreamPacket packet( + header_data, i, + elementary_stream_packet.unit().end() - remaining_unit_size); + DCHECK_LE(packet.payload().size(), remaining_unit_size); + remaining_unit_size -= packet.payload().size(); + if (!OnPacketizedTransportStreamPacket( + packet, flush && remaining_unit_size == 0u)) { + return false; + } + + // Prepare for the next packet. + is_payload_unit_end = + remaining_unit_size <= + WiFiDisplayTransportStreamPacket::kPacketSize - ts::kPacketHeaderSize; + is_payload_unit_start = false; + } while (remaining_unit_size > 0u); + + DCHECK_EQ(0u, remaining_unit_size); + + return true; +} + +bool WiFiDisplayTransportStreamPacketizer::EncodeMetaInformation(bool flush) { + DCHECK(CalledOnValidThread()); + + return (EncodeProgramAssociationTable(false) && + EncodeProgramMapTables(false) && EncodeProgramClockReference(flush)); +} + +bool WiFiDisplayTransportStreamPacketizer::EncodeProgramAssociationTable( + bool flush) { + DCHECK(CalledOnValidThread()); + + const uint16_t transport_stream_id = 0x0001u; + + uint8_t header_data[WiFiDisplayTransportStreamPacket::kPacketSize]; + size_t i = 0u; + + // Fill in a packet header. + i += ts::FillInPacketHeader(&header_data[i], true, + widi::kProgramAssociationTablePacketId, false, + counters_.program_association_table_continuity++); + + // Fill in a minimal table pointer. + i += psi::FillInTablePointer(&header_data[i], 0u); + + // Reserve space for a table header. + const size_t table_header_index = i; + i += psi::kTableHeaderSize; + + // Fill in a table syntax. + const uint8_t version_number = 0u; + i += psi::FillInTableSyntax(&header_data[i], transport_stream_id, + version_number); + + // Fill in program association table data. + i += psi::FillInProgramAssociationTableEntry(&header_data[i], + psi::kFirstProgramNumber, + widi::kProgramMapTablePacketId); + + // Fill in a table header and a CRC now that the table size is known. + i += psi::FillInTableHeaderAndCrc(&header_data[table_header_index], + &header_data[i], + psi::kProgramAssociationTableId); + + // Delegate the packet. + return OnPacketizedTransportStreamPacket( + WiFiDisplayTransportStreamPacket(header_data, i), flush); +} + +bool WiFiDisplayTransportStreamPacketizer::EncodeProgramClockReference( + bool flush) { + DCHECK(CalledOnValidThread()); + + program_clock_reference_ = base::TimeTicks::Now(); + + uint8_t header_data[ts::kPacketHeaderSize + ts::kAdaptationFieldLengthSize + + ts::kAdaptationFieldFlagsSize + + ts::kProgramClockReferenceSize]; + size_t i = 0u; + + // Fill in a packet header. + i += ts::FillInPacketHeader(&header_data[i], true, + widi::kProgramClockReferencePacketId, true, + counters_.program_clock_reference_continuity++); + + // Fill in an adaptation field. + i += ts::FillInAdaptationFieldLengthFromSize( + &header_data[i], WiFiDisplayTransportStreamPacket::kPacketSize - i); + i += ts::FillInAdaptationFieldFlags(&header_data[i], false, + program_clock_reference_); + i += ts::FillInProgramClockReference(&header_data[i], + program_clock_reference_); + + DCHECK_EQ(std::end(header_data), header_data + i); + + // Delegate the packet. + return OnPacketizedTransportStreamPacket( + WiFiDisplayTransportStreamPacket(header_data, i), flush); +} + +bool WiFiDisplayTransportStreamPacketizer::EncodeProgramMapTables(bool flush) { + DCHECK(CalledOnValidThread()); + DCHECK(!stream_states_.empty()); + + const uint16_t program_number = psi::kFirstProgramNumber; + + uint8_t header_data[WiFiDisplayTransportStreamPacket::kPacketSize]; + size_t i = 0u; + + // Fill in a packet header. + i += ts::FillInPacketHeader(&header_data[i], true, + widi::kProgramMapTablePacketId, false, + counters_.program_map_table_continuity++); + + // Fill in a minimal table pointer. + i += psi::FillInTablePointer(&header_data[i], 0u); + + // Reserve space for a table header. + const size_t table_header_index = i; + i += psi::kTableHeaderSize; + + // Fill in a table syntax. + i += psi::FillInTableSyntax(&header_data[i], program_number, + counters_.program_map_table_version); + + // Fill in program map table data. + i += psi::FillInProgramMapTableData(&header_data[i], + widi::kProgramClockReferencePacketId); + for (const auto& stream_state : stream_states_) { + DCHECK_LE(i + psi::kProgramMapTableElementaryStreamEntryBaseSize + + stream_state.info_length + psi::kCrcSize, + WiFiDisplayTransportStreamPacket::kPacketSize); + i += psi::FillInProgramMapTableElementaryStreamEntry( + &header_data[i], stream_state.info.type(), stream_state.packet_id, + stream_state.info_length, stream_state.info.descriptors()); + } + + // Fill in a table header and a CRC now that the table size is known. + i += psi::FillInTableHeaderAndCrc(&header_data[table_header_index], + &header_data[i], psi::kProgramMapTableId); + + // Delegate the packet. + return OnPacketizedTransportStreamPacket( + WiFiDisplayTransportStreamPacket(header_data, i), flush); +} + +void WiFiDisplayTransportStreamPacketizer::NormalizeUnitTimeStamps( + base::TimeTicks* pts, + base::TimeTicks* dts) const { + DCHECK(CalledOnValidThread()); + + // Normalize a presentation time stamp. + if (!pts || pts->is_null()) + return; + *pts += delay_for_unit_time_stamps_; + DCHECK_LE(program_clock_reference_, *pts); + + // Normalize a decoding time stamp. + if (!dts || dts->is_null()) + return; + *dts += delay_for_unit_time_stamps_; + DCHECK_LE(program_clock_reference_, *dts); + DCHECK_LE(*dts, *pts); +} + +bool WiFiDisplayTransportStreamPacketizer::SetElementaryStreams( + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos) { + DCHECK(CalledOnValidThread()); + + std::vector<ElementaryStreamState> new_stream_states; + new_stream_states.reserve(stream_infos.size()); + + uint8_t audio_stream_id = + WiFiDisplayElementaryStreamPacketizer::kFirstAudioStreamId; + uint16_t audio_stream_packet_id = widi::kFirstAudioStreamPacketId; + uint8_t private_stream_1_id = + WiFiDisplayElementaryStreamPacketizer::kPrivateStream1Id; + uint16_t video_stream_packet_id = widi::kVideoStreamPacketId; + + for (auto& stream_info : stream_infos) { + uint16_t packet_id; + uint8_t stream_id; + + switch (stream_info.type()) { + case AUDIO_AAC: + packet_id = audio_stream_packet_id++; + stream_id = audio_stream_id++; + break; + case AUDIO_AC3: + case AUDIO_LPCM: + if (private_stream_1_id != + WiFiDisplayElementaryStreamPacketizer::kPrivateStream1Id) { + return false; + } + packet_id = audio_stream_packet_id++; + stream_id = private_stream_1_id++; + break; + case VIDEO_H264: + if (video_stream_packet_id != widi::kVideoStreamPacketId) + return false; + packet_id = video_stream_packet_id++; + stream_id = WiFiDisplayElementaryStreamPacketizer::kFirstVideoStreamId; + break; + } + + new_stream_states.emplace_back(std::move(stream_info), packet_id, + stream_id); + } + + // If there are no previous states, there is no previous program map table + // to change, either. This ensures that the first encoded program map table + // has version 0. + if (!stream_states_.empty()) + ++counters_.program_map_table_version; + + stream_states_.swap(new_stream_states); + + return true; +} + +void WiFiDisplayTransportStreamPacketizer::UpdateDelayForUnitTimeStamps( + const base::TimeTicks& pts, + const base::TimeTicks& dts) { + DCHECK(CalledOnValidThread()); + + if (pts.is_null()) + return; + + const base::TimeTicks now = base::TimeTicks::Now(); + DCHECK_LE(program_clock_reference_, now); + + // Ensure that delayed time stamps are greater than or equal to now. + const base::TimeTicks ts_min = + (dts.is_null() ? pts : dts) + delay_for_unit_time_stamps_; + if (now > ts_min) { + const base::TimeDelta error = now - ts_min; + delay_for_unit_time_stamps_ += 2 * error; + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h new file mode 100644 index 00000000000..0cdeb6a750d --- /dev/null +++ b/chromium/extensions/renderer/api/display_source/wifi_display/wifi_display_transport_stream_packetizer.h @@ -0,0 +1,176 @@ +// Copyright 2016 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. + +#ifndef EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_TRANSPORT_STREAM_PACKETIZER_H_ +#define EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_TRANSPORT_STREAM_PACKETIZER_H_ + +#include <vector> + +#include "base/threading/non_thread_safe.h" +#include "base/time/time.h" +#include "extensions/renderer/api/display_source/wifi_display/wifi_display_stream_packet_part.h" + +namespace extensions { + +class WiFiDisplayElementaryStreamInfo; +class WiFiDisplayElementaryStreamPacket; + +// This class represents an MPEG Transport Stream (MPEG-TS) packet containing +// WiFi Display elementary stream unit data or related meta information. +class WiFiDisplayTransportStreamPacket { + public: + enum { kPacketSize = 188u }; + + using Part = WiFiDisplayStreamPacketPart; + + // This class represents a possibly empty padding part in the end of + // a transport stream packet. The padding part consists of repeated bytes + // having the same value. + class PaddingPart { + public: + explicit PaddingPart(unsigned size) : size_(size) {} + + unsigned size() const { return size_; } + uint8_t value() const { return 0xFFu; } + + private: + const unsigned size_; + + DISALLOW_COPY_AND_ASSIGN(PaddingPart); + }; + + WiFiDisplayTransportStreamPacket(const uint8_t* header_data, + size_t header_size); + WiFiDisplayTransportStreamPacket(const uint8_t* header_data, + size_t header_size, + const uint8_t* payload_data); + + const Part& header() const { return header_; } + const Part& payload() const { return payload_; } + const PaddingPart& filler() const { return filler_; } + + private: + const Part header_; + const Part payload_; + const PaddingPart filler_; + + DISALLOW_COPY_AND_ASSIGN(WiFiDisplayTransportStreamPacket); +}; + +// The WiFi Display transport stream packetizer packetizes unit buffers to +// MPEG Transport Stream (MPEG-TS) packets containing either meta information +// or Packetized Elementary Stream (PES) packets containing unit data. +// +// Whenever a Transport Stream (TS) packet is fully created and thus ready for +// further processing, a pure virtual member function +// |OnPacketizedTransportStreamPacket| is called. +class WiFiDisplayTransportStreamPacketizer : public base::NonThreadSafe { + public: + enum ElementaryStreamType : uint8_t { + AUDIO_AAC = 0x0Fu, + AUDIO_AC3 = 0x81u, + AUDIO_LPCM = 0x83u, + VIDEO_H264 = 0x1Bu, + }; + + // Fixed coding parameters for Linear Pulse-Code Modulation (LPCM) audio + // streams. See |WiFiDisplayElementaryStreamDescriptor::LPCMAudioStream| for + // variable ones. + struct LPCM { + enum { + kFramesPerUnit = 6u, + kChannelSamplesPerFrame = 80u, + kChannelSamplesPerUnit = kChannelSamplesPerFrame * kFramesPerUnit + }; + }; + + WiFiDisplayTransportStreamPacketizer( + const base::TimeDelta& delay_for_unit_time_stamps, + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos); + virtual ~WiFiDisplayTransportStreamPacketizer(); + + // Encodes one elementary stream unit buffer (such as one video frame or + // 2 * |LPCM::kChannelSamplesPerUnit| two-channel LPCM audio samples) into + // packets: + // 1) Encodes meta information into meta information packets (by calling + // |EncodeMetaInformation|) if needed. + // 2) Normalizes unit time stamps (|pts| and |dts|) so that they are never + // smaller than a program clock reference. + // 3) Encodes the elementary stream unit buffer to unit data packets. + // Returns false in the case of an error in which case the caller should stop + // encoding. + // + // In order to minimize encoding delays, |flush| should be true unless + // the caller is about to continue encoding immediately. + // + // Precondition: Elementary streams are configured either using a constructor + // or using the |SetElementaryStreams| member function. + bool EncodeElementaryStreamUnit(unsigned stream_index, + const uint8_t* unit_data, + size_t unit_size, + bool random_access, + base::TimeTicks pts, + base::TimeTicks dts, + bool flush); + + // Encodes meta information (program association table, program map table and + // program clock reference). Returns false in the case of an error in which + // case the caller should stop encoding. + // + // The |EncodeElementaryStreamUnit| member function calls this member function + // when needed, thus the caller is responsible for calling this member + // function explicitly only if the caller does silence suppression and does + // thus not encode all elementary stream units by calling + // the |EncodeElementaryStreamUnit| member function. + // + // In order to minimize encoding delays, |flush| should be true unless + // the caller is about to continue encoding immediately. + // + // Precondition: Elementary streams are configured either using a constructor + // or using the |SetElementaryStreams| member function. + bool EncodeMetaInformation(bool flush); + + bool SetElementaryStreams( + std::vector<WiFiDisplayElementaryStreamInfo> stream_infos); + + void DetachFromThread() { base::NonThreadSafe::DetachFromThread(); } + + protected: + bool EncodeProgramAssociationTable(bool flush); + bool EncodeProgramClockReference(bool flush); + bool EncodeProgramMapTables(bool flush); + + // Normalizes unit time stamps by delaying them in order to ensure that unit + // time stamps are never smaller than a program clock reference. + // Precondition: The |UpdateDelayForUnitTimeStamps| member function is called. + void NormalizeUnitTimeStamps(base::TimeTicks* pts, + base::TimeTicks* dts) const; + // Update unit time stamp delay in order to ensure that normalized unit time + // stamps are never smaller than a program clock reference. + void UpdateDelayForUnitTimeStamps(const base::TimeTicks& pts, + const base::TimeTicks& dts); + + // Called whenever a Transport Stream (TS) packet is fully created and thus + // ready for further processing. + virtual bool OnPacketizedTransportStreamPacket( + const WiFiDisplayTransportStreamPacket& transport_stream_packet, + bool flush) = 0; + + private: + struct ElementaryStreamState; + + struct { + uint8_t program_association_table_continuity; + uint8_t program_map_table_continuity; + uint8_t program_map_table_version; + uint8_t program_clock_reference_continuity; + } counters_; + base::TimeDelta delay_for_unit_time_stamps_; + base::TimeTicks program_clock_reference_; + std::vector<ElementaryStreamState> stream_states_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DISPLAY_SOURCE_WIFI_DISPLAY_WIFI_DISPLAY_TRANSPORT_STREAM_PACKETIZER_H_ diff --git a/chromium/extensions/renderer/api/mojo_private/mojo_private_unittest.cc b/chromium/extensions/renderer/api/mojo_private/mojo_private_unittest.cc new file mode 100644 index 00000000000..344cbe9da6c --- /dev/null +++ b/chromium/extensions/renderer/api/mojo_private/mojo_private_unittest.cc @@ -0,0 +1,59 @@ +// Copyright 2015 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/api_test_base.h" + +#include "base/macros.h" +#include "gin/modules/module_registry.h" + +// A test launcher for tests for the mojoPrivate API defined in +// extensions/test/data/mojo_private_unittest.js. + +namespace extensions { + +class MojoPrivateApiTest : public ApiTestBase { + public: + MojoPrivateApiTest() = default; + + gin::ModuleRegistry* module_registry() { + return gin::ModuleRegistry::From(env()->context()->v8_context()); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MojoPrivateApiTest); +}; + +TEST_F(MojoPrivateApiTest, Define) { + ASSERT_NO_FATAL_FAILURE(RunTest("mojo_private_unittest.js", "testDefine")); + EXPECT_EQ(1u, module_registry()->available_modules().count("testModule")); +} + +TEST_F(MojoPrivateApiTest, DefineRegistersModule) { + ASSERT_NO_FATAL_FAILURE( + RunTest("mojo_private_unittest.js", "testDefineRegistersModule")); + EXPECT_EQ(1u, module_registry()->available_modules().count("testModule")); + EXPECT_EQ(1u, module_registry()->available_modules().count("dependency")); +} + +TEST_F(MojoPrivateApiTest, DefineModuleDoesNotExist) { + ASSERT_NO_FATAL_FAILURE( + RunTest("mojo_private_unittest.js", "testDefineModuleDoesNotExist")); + EXPECT_EQ(0u, module_registry()->available_modules().count("testModule")); + EXPECT_EQ(0u, + module_registry()->available_modules().count("does not exist!")); + EXPECT_EQ(1u, module_registry()->unsatisfied_dependencies().count( + "does not exist!")); +} + +TEST_F(MojoPrivateApiTest, RequireAsync) { + ASSERT_NO_FATAL_FAILURE( + RunTest("mojo_private_unittest.js", "testRequireAsync")); +} + +TEST_F(MojoPrivateApiTest, DefineAndRequire) { + ASSERT_NO_FATAL_FAILURE( + RunTest("mojo_private_unittest.js", "testDefineAndRequire")); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/serial/DEPS b/chromium/extensions/renderer/api/serial/DEPS new file mode 100644 index 00000000000..e273c393ba2 --- /dev/null +++ b/chromium/extensions/renderer/api/serial/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+device/serial", +] diff --git a/chromium/extensions/renderer/api/serial/data_receiver_unittest.cc b/chromium/extensions/renderer/api/serial/data_receiver_unittest.cc new file mode 100644 index 00000000000..0c7dc000d8a --- /dev/null +++ b/chromium/extensions/renderer/api/serial/data_receiver_unittest.cc @@ -0,0 +1,202 @@ +// 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 <stdint.h> + +#include <queue> +#include <utility> + +#include "base/macros.h" +#include "device/serial/data_source_sender.h" +#include "device/serial/data_stream.mojom.h" +#include "extensions/renderer/api_test_base.h" +#include "gin/dictionary.h" +#include "gin/wrappable.h" +#include "grit/extensions_renderer_resources.h" +#include "mojo/edk/js/handle.h" + +namespace extensions { + +class DataReceiverFactory : public gin::Wrappable<DataReceiverFactory> { + public: + using Callback = base::Callback<void( + mojo::InterfaceRequest<device::serial::DataSource>, + mojo::InterfacePtr<device::serial::DataSourceClient>)>; + static gin::Handle<DataReceiverFactory> Create(v8::Isolate* isolate, + const Callback& callback) { + return gin::CreateHandle(isolate, + new DataReceiverFactory(callback, isolate)); + } + + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override { + return Wrappable<DataReceiverFactory>::GetObjectTemplateBuilder(isolate) + .SetMethod("create", &DataReceiverFactory::CreateReceiver); + } + + gin::Dictionary CreateReceiver() { + mojo::InterfacePtr<device::serial::DataSource> sink; + mojo::InterfacePtr<device::serial::DataSourceClient> client; + mojo::InterfaceRequest<device::serial::DataSourceClient> client_request = + mojo::GetProxy(&client); + callback_.Run(mojo::GetProxy(&sink), std::move(client)); + + gin::Dictionary result = gin::Dictionary::CreateEmpty(isolate_); + result.Set("source", sink.PassInterface().PassHandle().release()); + result.Set("client", client_request.PassMessagePipe().release()); + return result; + } + + static gin::WrapperInfo kWrapperInfo; + + private: + DataReceiverFactory(const Callback& callback, v8::Isolate* isolate) + : callback_(callback), isolate_(isolate) {} + + base::Callback<void(mojo::InterfaceRequest<device::serial::DataSource>, + mojo::InterfacePtr<device::serial::DataSourceClient>)> + callback_; + v8::Isolate* isolate_; +}; + +gin::WrapperInfo DataReceiverFactory::kWrapperInfo = {gin::kEmbedderNativeGin}; + +// Runs tests defined in extensions/test/data/data_receiver_unittest.js +class DataReceiverTest : public ApiTestBase { + public: + DataReceiverTest() {} + + void SetUp() override { + ApiTestBase::SetUp(); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule(env()->isolate(), + "device/serial/data_receiver_test_factory", + DataReceiverFactory::Create( + env()->isolate(), + base::Bind(&DataReceiverTest::CreateDataSource, + base::Unretained(this))).ToV8()); + } + + void TearDown() override { + if (sender_.get()) { + sender_->ShutDown(); + sender_ = NULL; + } + ApiTestBase::TearDown(); + } + + std::queue<int32_t> error_to_send_; + std::queue<std::string> data_to_send_; + + private: + void CreateDataSource( + mojo::InterfaceRequest<device::serial::DataSource> request, + mojo::InterfacePtr<device::serial::DataSourceClient> client) { + sender_ = new device::DataSourceSender( + std::move(request), std::move(client), + base::Bind(&DataReceiverTest::ReadyToSend, base::Unretained(this)), + base::Bind(base::DoNothing)); + } + + void ReadyToSend(scoped_ptr<device::WritableBuffer> buffer) { + if (data_to_send_.empty() && error_to_send_.empty()) + return; + + std::string data; + int32_t error = 0; + if (!data_to_send_.empty()) { + data = data_to_send_.front(); + data_to_send_.pop(); + } + if (!error_to_send_.empty()) { + error = error_to_send_.front(); + error_to_send_.pop(); + } + DCHECK(buffer->GetSize() >= static_cast<uint32_t>(data.size())); + memcpy(buffer->GetData(), data.c_str(), data.size()); + if (error) + buffer->DoneWithError(data.size(), error); + else + buffer->Done(data.size()); + } + + scoped_refptr<device::DataSourceSender> sender_; + + DISALLOW_COPY_AND_ASSIGN(DataReceiverTest); +}; + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_Receive DISABLED_Receive +#else +#define MAYBE_Receive Receive +#endif +TEST_F(DataReceiverTest, MAYBE_Receive) { + data_to_send_.push("a"); + RunTest("data_receiver_unittest.js", "testReceive"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_ReceiveError DISABLED_ReceiveError +#else +#define MAYBE_ReceiveError ReceiveError +#endif +TEST_F(DataReceiverTest, MAYBE_ReceiveError) { + error_to_send_.push(1); + RunTest("data_receiver_unittest.js", "testReceiveError"); +} + +TEST_F(DataReceiverTest, ReceiveDataAndError) { + data_to_send_.push("a"); + data_to_send_.push("b"); + error_to_send_.push(1); + RunTest("data_receiver_unittest.js", "testReceiveDataAndError"); +} + +TEST_F(DataReceiverTest, ReceiveErrorThenData) { + data_to_send_.push(""); + data_to_send_.push("a"); + error_to_send_.push(1); + RunTest("data_receiver_unittest.js", "testReceiveErrorThenData"); +} + +TEST_F(DataReceiverTest, ReceiveBeforeAndAfterSerialization) { + data_to_send_.push("a"); + data_to_send_.push("b"); + RunTest("data_receiver_unittest.js", + "testReceiveBeforeAndAfterSerialization"); +} + +TEST_F(DataReceiverTest, ReceiveErrorSerialization) { + error_to_send_.push(1); + error_to_send_.push(3); + RunTest("data_receiver_unittest.js", "testReceiveErrorSerialization"); +} + +TEST_F(DataReceiverTest, ReceiveDataAndErrorSerialization) { + data_to_send_.push("a"); + data_to_send_.push("b"); + error_to_send_.push(1); + error_to_send_.push(3); + RunTest("data_receiver_unittest.js", "testReceiveDataAndErrorSerialization"); +} + +TEST_F(DataReceiverTest, SerializeDuringReceive) { + data_to_send_.push("a"); + RunTest("data_receiver_unittest.js", "testSerializeDuringReceive"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_SerializeAfterClose DISABLED_SerializeAfterClose +#else +#define MAYBE_SerializeAfterClose SerializeAfterClose +#endif +TEST_F(DataReceiverTest, MAYBE_SerializeAfterClose) { + data_to_send_.push("a"); + RunTest("data_receiver_unittest.js", "testSerializeAfterClose"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/serial/data_sender_unittest.cc b/chromium/extensions/renderer/api/serial/data_sender_unittest.cc new file mode 100644 index 00000000000..a38dd10f201 --- /dev/null +++ b/chromium/extensions/renderer/api/serial/data_sender_unittest.cc @@ -0,0 +1,213 @@ +// 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 <stdint.h> + +#include <queue> +#include <utility> + +#include "base/macros.h" +#include "device/serial/data_sink_receiver.h" +#include "device/serial/data_stream.mojom.h" +#include "extensions/renderer/api_test_base.h" +#include "grit/extensions_renderer_resources.h" + +namespace extensions { + +// Runs tests defined in extensions/test/data/data_sender_unittest.js +class DataSenderTest : public ApiTestBase { + public: + DataSenderTest() {} + + void SetUp() override { + ApiTestBase::SetUp(); + service_provider()->AddService( + base::Bind(&DataSenderTest::CreateDataSink, base::Unretained(this))); + } + + void TearDown() override { + if (receiver_.get()) { + receiver_->ShutDown(); + receiver_ = NULL; + } + EXPECT_FALSE(buffer_); + buffer_.reset(); + ApiTestBase::TearDown(); + } + + std::queue<int32_t> error_to_report_; + std::queue<std::string> expected_data_; + + private: + void CreateDataSink( + mojo::InterfaceRequest<device::serial::DataSink> request) { + receiver_ = new device::DataSinkReceiver( + std::move(request), + base::Bind(&DataSenderTest::ReadyToReceive, base::Unretained(this)), + base::Bind(&DataSenderTest::OnCancel, base::Unretained(this)), + base::Bind(base::DoNothing)); + } + + void ReadyToReceive(scoped_ptr<device::ReadOnlyBuffer> buffer) { + std::string data(buffer->GetData(), buffer->GetSize()); + if (expected_data_.empty()) { + buffer_ = std::move(buffer); + return; + } + + std::string& expected = expected_data_.front(); + if (expected.size() > buffer->GetSize()) { + EXPECT_EQ(expected.substr(0, buffer->GetSize()), data); + expected = expected.substr(buffer->GetSize()); + buffer->Done(buffer->GetSize()); + return; + } + if (expected.size() < buffer->GetSize()) + data = data.substr(0, expected.size()); + EXPECT_EQ(expected, data); + expected_data_.pop(); + int32_t error = 0; + if (!error_to_report_.empty()) { + error = error_to_report_.front(); + error_to_report_.pop(); + } + if (error) + buffer->DoneWithError(data.size(), error); + else + buffer->Done(data.size()); + } + + void OnCancel(int32_t error) { + ASSERT_TRUE(buffer_); + buffer_->DoneWithError(0, error); + buffer_.reset(); + } + + scoped_refptr<device::DataSinkReceiver> receiver_; + scoped_ptr<device::ReadOnlyBuffer> buffer_; + + DISALLOW_COPY_AND_ASSIGN(DataSenderTest); +}; + +TEST_F(DataSenderTest, Send) { + expected_data_.push("aa"); + RunTest("data_sender_unittest.js", "testSend"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_LargeSend DISABLED_LargeSend +#else +#define MAYBE_LargeSend LargeSend +#endif +TEST_F(DataSenderTest, MAYBE_LargeSend) { + std::string pattern = "123"; + std::string expected_data; + for (int i = 0; i < 11; i++) + expected_data += pattern; + expected_data_.push(expected_data); + RunTest("data_sender_unittest.js", "testLargeSend"); +} + +TEST_F(DataSenderTest, SendError) { + expected_data_.push(""); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendError"); +} + +TEST_F(DataSenderTest, SendErrorPartialSuccess) { + expected_data_.push(std::string(5, 'b')); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorPartialSuccess"); +} + +TEST_F(DataSenderTest, SendErrorBetweenPackets) { + expected_data_.push(std::string(2, 'b')); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorBetweenPackets"); +} + +TEST_F(DataSenderTest, SendErrorInSecondPacket) { + expected_data_.push(std::string(3, 'b')); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorInSecondPacket"); +} + +TEST_F(DataSenderTest, SendErrorInLargeSend) { + expected_data_.push("123456789012"); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorInLargeSend"); +} + +TEST_F(DataSenderTest, SendErrorBeforeLargeSend) { + expected_data_.push(std::string(2, 'b')); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorBeforeLargeSend"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_CancelWithoutSend DISABLED_CancelWithoutSend +#else +#define MAYBE_CancelWithoutSend CancelWithoutSend +#endif +TEST_F(DataSenderTest, MAYBE_CancelWithoutSend) { + RunTest("data_sender_unittest.js", "testCancelWithoutSend"); +} + +TEST_F(DataSenderTest, Cancel) { + RunTest("data_sender_unittest.js", "testCancel"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_Close DISABLED_Close +#else +#define MAYBE_Close Close +#endif +TEST_F(DataSenderTest, MAYBE_Close) { + RunTest("data_sender_unittest.js", "testClose"); +} + +TEST_F(DataSenderTest, SendAfterSerialization) { + expected_data_.push("aa"); + RunTest("data_sender_unittest.js", "testSendAfterSerialization"); +} + +TEST_F(DataSenderTest, SendErrorAfterSerialization) { + expected_data_.push(""); + expected_data_.push("a"); + error_to_report_.push(1); + RunTest("data_sender_unittest.js", "testSendErrorAfterSerialization"); +} + +TEST_F(DataSenderTest, CancelAfterSerialization) { + RunTest("data_sender_unittest.js", "testCancelAfterSerialization"); +} + +TEST_F(DataSenderTest, SerializeCancelsSendsInProgress) { + RunTest("data_sender_unittest.js", "testSerializeCancelsSendsInProgress"); +} + +TEST_F(DataSenderTest, SerializeWaitsForCancel) { + RunTest("data_sender_unittest.js", "testSerializeWaitsForCancel"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_SerializeAfterClose DISABLED_SerializeAfterClose +#else +#define MAYBE_SerializeAfterClose SerializeAfterClose +#endif +TEST_F(DataSenderTest, MAYBE_SerializeAfterClose) { + RunTest("data_sender_unittest.js", "testSerializeAfterClose"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api/serial/serial_api_unittest.cc b/chromium/extensions/renderer/api/serial/serial_api_unittest.cc new file mode 100644 index 00000000000..82f8f2a36f1 --- /dev/null +++ b/chromium/extensions/renderer/api/serial/serial_api_unittest.cc @@ -0,0 +1,738 @@ +// 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 <stddef.h> +#include <stdint.h> + +#include <utility> + +#include "base/macros.h" +#include "base/thread_task_runner_handle.h" +#include "device/serial/serial_device_enumerator.h" +#include "device/serial/serial_service_impl.h" +#include "device/serial/test_serial_io_handler.h" +#include "extensions/browser/mojo/stash_backend.h" +#include "extensions/common/mojo/keep_alive.mojom.h" +#include "extensions/renderer/api_test_base.h" +#include "grit/extensions_renderer_resources.h" + +// A test launcher for tests for the serial API defined in +// extensions/test/data/serial_unittest.js. Each C++ test function sets up a +// fake DeviceEnumerator or SerialIoHandler expecting or returning particular +// values for that test. + +namespace extensions { + +namespace { + +class FakeSerialDeviceEnumerator : public device::SerialDeviceEnumerator { + mojo::Array<device::serial::DeviceInfoPtr> GetDevices() override { + mojo::Array<device::serial::DeviceInfoPtr> result(3); + result[0] = device::serial::DeviceInfo::New(); + result[0]->path = "device"; + result[0]->vendor_id = 1234; + result[0]->has_vendor_id = true; + result[0]->product_id = 5678; + result[0]->has_product_id = true; + result[0]->display_name = "foo"; + result[1] = device::serial::DeviceInfo::New(); + result[1]->path = "another_device"; + // These IDs should be ignored. + result[1]->vendor_id = 1234; + result[1]->product_id = 5678; + result[2] = device::serial::DeviceInfo::New(); + result[2]->path = ""; + result[2]->display_name = ""; + return result; + } +}; + +enum OptionalValue { + OPTIONAL_VALUE_UNSET, + OPTIONAL_VALUE_FALSE, + OPTIONAL_VALUE_TRUE, +}; + +device::serial::HostControlSignals GenerateControlSignals(OptionalValue dtr, + OptionalValue rts) { + device::serial::HostControlSignals result; + switch (dtr) { + case OPTIONAL_VALUE_UNSET: + break; + case OPTIONAL_VALUE_FALSE: + result.dtr = false; + result.has_dtr = true; + break; + case OPTIONAL_VALUE_TRUE: + result.dtr = true; + result.has_dtr = true; + break; + } + switch (rts) { + case OPTIONAL_VALUE_UNSET: + break; + case OPTIONAL_VALUE_FALSE: + result.rts = false; + result.has_rts = true; + break; + case OPTIONAL_VALUE_TRUE: + result.rts = true; + result.has_rts = true; + break; + } + return result; +} + +device::serial::ConnectionOptions GenerateConnectionOptions( + int bitrate, + device::serial::DataBits data_bits, + device::serial::ParityBit parity_bit, + device::serial::StopBits stop_bits, + OptionalValue cts_flow_control) { + device::serial::ConnectionOptions result; + result.bitrate = bitrate; + result.data_bits = data_bits; + result.parity_bit = parity_bit; + result.stop_bits = stop_bits; + switch (cts_flow_control) { + case OPTIONAL_VALUE_UNSET: + break; + case OPTIONAL_VALUE_FALSE: + result.cts_flow_control = false; + result.has_cts_flow_control = true; + break; + case OPTIONAL_VALUE_TRUE: + result.cts_flow_control = true; + result.has_cts_flow_control = true; + break; + } + return result; +} + +class TestIoHandlerBase : public device::TestSerialIoHandler { + public: + TestIoHandlerBase() : calls_(0) {} + + size_t num_calls() const { return calls_; } + + protected: + ~TestIoHandlerBase() override {} + void record_call() const { calls_++; } + + private: + mutable size_t calls_; + + DISALLOW_COPY_AND_ASSIGN(TestIoHandlerBase); +}; + +class SetControlSignalsTestIoHandler : public TestIoHandlerBase { + public: + SetControlSignalsTestIoHandler() {} + + bool SetControlSignals( + const device::serial::HostControlSignals& signals) override { + static const device::serial::HostControlSignals expected_signals[] = { + GenerateControlSignals(OPTIONAL_VALUE_UNSET, OPTIONAL_VALUE_UNSET), + GenerateControlSignals(OPTIONAL_VALUE_FALSE, OPTIONAL_VALUE_UNSET), + GenerateControlSignals(OPTIONAL_VALUE_TRUE, OPTIONAL_VALUE_UNSET), + GenerateControlSignals(OPTIONAL_VALUE_UNSET, OPTIONAL_VALUE_FALSE), + GenerateControlSignals(OPTIONAL_VALUE_FALSE, OPTIONAL_VALUE_FALSE), + GenerateControlSignals(OPTIONAL_VALUE_TRUE, OPTIONAL_VALUE_FALSE), + GenerateControlSignals(OPTIONAL_VALUE_UNSET, OPTIONAL_VALUE_TRUE), + GenerateControlSignals(OPTIONAL_VALUE_FALSE, OPTIONAL_VALUE_TRUE), + GenerateControlSignals(OPTIONAL_VALUE_TRUE, OPTIONAL_VALUE_TRUE), + }; + if (num_calls() >= arraysize(expected_signals)) + return false; + + EXPECT_EQ(expected_signals[num_calls()].has_dtr, signals.has_dtr); + EXPECT_EQ(expected_signals[num_calls()].dtr, signals.dtr); + EXPECT_EQ(expected_signals[num_calls()].has_rts, signals.has_rts); + EXPECT_EQ(expected_signals[num_calls()].rts, signals.rts); + record_call(); + return true; + } + + private: + ~SetControlSignalsTestIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(SetControlSignalsTestIoHandler); +}; + +class GetControlSignalsTestIoHandler : public TestIoHandlerBase { + public: + GetControlSignalsTestIoHandler() {} + + device::serial::DeviceControlSignalsPtr GetControlSignals() const override { + device::serial::DeviceControlSignalsPtr signals( + device::serial::DeviceControlSignals::New()); + signals->dcd = num_calls() & 1; + signals->cts = num_calls() & 2; + signals->ri = num_calls() & 4; + signals->dsr = num_calls() & 8; + record_call(); + return signals; + } + + private: + ~GetControlSignalsTestIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(GetControlSignalsTestIoHandler); +}; + +class ConfigurePortTestIoHandler : public TestIoHandlerBase { + public: + ConfigurePortTestIoHandler() {} + bool ConfigurePortImpl() override { + static const device::serial::ConnectionOptions expected_options[] = { + // Each JavaScript call to chrome.serial.update only modifies a single + // property of the connection however this function can only check the + // final value of all options. The modified option is marked with "set". + GenerateConnectionOptions(9600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::NO, + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions( + 57600, // set + device::serial::DataBits::EIGHT, device::serial::ParityBit::NO, + device::serial::StopBits::ONE, OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, + device::serial::DataBits::SEVEN, // set + device::serial::ParityBit::NO, + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, + device::serial::DataBits::EIGHT, // set + device::serial::ParityBit::NO, + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::NO, // set + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::ODD, // set + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::EVEN, // set + device::serial::StopBits::ONE, + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::EVEN, + device::serial::StopBits::ONE, // set + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::EVEN, + device::serial::StopBits::TWO, // set + OPTIONAL_VALUE_FALSE), + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::EVEN, + device::serial::StopBits::TWO, + OPTIONAL_VALUE_FALSE), // set + GenerateConnectionOptions(57600, device::serial::DataBits::EIGHT, + device::serial::ParityBit::EVEN, + device::serial::StopBits::TWO, + OPTIONAL_VALUE_TRUE), // set + }; + + if (!TestIoHandlerBase::ConfigurePortImpl()) { + return false; + } + + if (num_calls() >= arraysize(expected_options)) { + return false; + } + + EXPECT_EQ(expected_options[num_calls()].bitrate, options().bitrate); + EXPECT_EQ(expected_options[num_calls()].data_bits, options().data_bits); + EXPECT_EQ(expected_options[num_calls()].parity_bit, options().parity_bit); + EXPECT_EQ(expected_options[num_calls()].stop_bits, options().stop_bits); + EXPECT_EQ(expected_options[num_calls()].has_cts_flow_control, + options().has_cts_flow_control); + EXPECT_EQ(expected_options[num_calls()].cts_flow_control, + options().cts_flow_control); + record_call(); + return true; + } + + private: + ~ConfigurePortTestIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(ConfigurePortTestIoHandler); +}; + +class FlushTestIoHandler : public TestIoHandlerBase { + public: + FlushTestIoHandler() {} + + bool Flush() const override { + record_call(); + return true; + } + + private: + ~FlushTestIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(FlushTestIoHandler); +}; + +class FailToConnectTestIoHandler : public TestIoHandlerBase { + public: + FailToConnectTestIoHandler() {} + void Open(const std::string& port, + const device::serial::ConnectionOptions& options, + const OpenCompleteCallback& callback) override { + callback.Run(false); + return; + } + + private: + ~FailToConnectTestIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(FailToConnectTestIoHandler); +}; + +class FailToGetInfoTestIoHandler : public TestIoHandlerBase { + public: + explicit FailToGetInfoTestIoHandler(int times_to_succeed) + : times_to_succeed_(times_to_succeed) {} + device::serial::ConnectionInfoPtr GetPortInfo() const override { + if (times_to_succeed_-- > 0) + return device::TestSerialIoHandler::GetPortInfo(); + return device::serial::ConnectionInfoPtr(); + } + + private: + ~FailToGetInfoTestIoHandler() override {} + + mutable int times_to_succeed_; + + DISALLOW_COPY_AND_ASSIGN(FailToGetInfoTestIoHandler); +}; + +class SendErrorTestIoHandler : public TestIoHandlerBase { + public: + explicit SendErrorTestIoHandler(device::serial::SendError error) + : error_(error) {} + + void WriteImpl() override { QueueWriteCompleted(0, error_); } + + private: + ~SendErrorTestIoHandler() override {} + + device::serial::SendError error_; + + DISALLOW_COPY_AND_ASSIGN(SendErrorTestIoHandler); +}; + +class FixedDataReceiveTestIoHandler : public TestIoHandlerBase { + public: + explicit FixedDataReceiveTestIoHandler(const std::string& data) + : data_(data) {} + + void ReadImpl() override { + if (pending_read_buffer_len() < data_.size()) + return; + memcpy(pending_read_buffer(), data_.c_str(), data_.size()); + QueueReadCompleted(static_cast<uint32_t>(data_.size()), + device::serial::ReceiveError::NONE); + } + + private: + ~FixedDataReceiveTestIoHandler() override {} + + const std::string data_; + + DISALLOW_COPY_AND_ASSIGN(FixedDataReceiveTestIoHandler); +}; + +class ReceiveErrorTestIoHandler : public TestIoHandlerBase { + public: + explicit ReceiveErrorTestIoHandler(device::serial::ReceiveError error) + : error_(error) {} + + void ReadImpl() override { QueueReadCompleted(0, error_); } + + private: + ~ReceiveErrorTestIoHandler() override {} + + device::serial::ReceiveError error_; + + DISALLOW_COPY_AND_ASSIGN(ReceiveErrorTestIoHandler); +}; + +class SendDataWithErrorIoHandler : public TestIoHandlerBase { + public: + SendDataWithErrorIoHandler() : sent_error_(false) {} + void WriteImpl() override { + if (sent_error_) { + WriteCompleted(pending_write_buffer_len(), + device::serial::SendError::NONE); + return; + } + sent_error_ = true; + // We expect the JS test code to send a 4 byte buffer. + ASSERT_LT(2u, pending_write_buffer_len()); + WriteCompleted(2, device::serial::SendError::SYSTEM_ERROR); + } + + private: + ~SendDataWithErrorIoHandler() override {} + + bool sent_error_; + + DISALLOW_COPY_AND_ASSIGN(SendDataWithErrorIoHandler); +}; + +class BlockSendsForeverSendIoHandler : public TestIoHandlerBase { + public: + BlockSendsForeverSendIoHandler() {} + void WriteImpl() override {} + + private: + ~BlockSendsForeverSendIoHandler() override {} + + DISALLOW_COPY_AND_ASSIGN(BlockSendsForeverSendIoHandler); +}; + +} // namespace + +class SerialApiTest : public ApiTestBase { + public: + SerialApiTest() {} + + void SetUp() override { + ApiTestBase::SetUp(); + stash_backend_.reset(new StashBackend(base::Closure())); + PrepareEnvironment(api_test_env(), stash_backend_.get()); + } + + void PrepareEnvironment(ApiTestEnvironment* environment, + StashBackend* stash_backend) { + environment->env()->RegisterModule("serial", IDR_SERIAL_CUSTOM_BINDINGS_JS); + environment->service_provider()->AddService<device::serial::SerialService>( + base::Bind(&SerialApiTest::CreateSerialService, + base::Unretained(this))); + environment->service_provider()->AddService(base::Bind( + &StashBackend::BindToRequest, base::Unretained(stash_backend))); + environment->service_provider()->IgnoreServiceRequests<KeepAlive>(); + } + + scoped_refptr<TestIoHandlerBase> io_handler_; + + scoped_ptr<StashBackend> stash_backend_; + + private: + scoped_refptr<device::SerialIoHandler> GetIoHandler() { + if (!io_handler_.get()) + io_handler_ = new TestIoHandlerBase; + return io_handler_; + } + + void CreateSerialService( + mojo::InterfaceRequest<device::serial::SerialService> request) { + new device::SerialServiceImpl( + new device::SerialConnectionFactory( + base::Bind(&SerialApiTest::GetIoHandler, base::Unretained(this)), + base::ThreadTaskRunnerHandle::Get()), + scoped_ptr<device::SerialDeviceEnumerator>( + new FakeSerialDeviceEnumerator), + std::move(request)); + } + + DISALLOW_COPY_AND_ASSIGN(SerialApiTest); +}; + +TEST_F(SerialApiTest, GetDevices) { + RunTest("serial_unittest.js", "testGetDevices"); +} + +TEST_F(SerialApiTest, ConnectFail) { + io_handler_ = new FailToConnectTestIoHandler; + RunTest("serial_unittest.js", "testConnectFail"); +} + +TEST_F(SerialApiTest, GetInfoFailOnConnect) { + io_handler_ = new FailToGetInfoTestIoHandler(0); + RunTest("serial_unittest.js", "testGetInfoFailOnConnect"); +} + +TEST_F(SerialApiTest, Connect) { + RunTest("serial_unittest.js", "testConnect"); +} + +TEST_F(SerialApiTest, ConnectDefaultOptions) { + RunTest("serial_unittest.js", "testConnectDefaultOptions"); +} + +TEST_F(SerialApiTest, ConnectInvalidBitrate) { + RunTest("serial_unittest.js", "testConnectInvalidBitrate"); +} + +TEST_F(SerialApiTest, GetInfo) { + RunTest("serial_unittest.js", "testGetInfo"); +} + +TEST_F(SerialApiTest, GetInfoAfterSerialization) { + RunTest("serial_unittest.js", "testGetInfoAfterSerialization"); +} + +TEST_F(SerialApiTest, GetInfoFailToGetPortInfo) { + io_handler_ = new FailToGetInfoTestIoHandler(1); + RunTest("serial_unittest.js", "testGetInfoFailToGetPortInfo"); +} + +TEST_F(SerialApiTest, GetConnections) { + RunTest("serial_unittest.js", "testGetConnections"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_GetControlSignals DISABLED_GetControlSignals +#else +#define MAYBE_GetControlSignals GetControlSignals +#endif +TEST_F(SerialApiTest, MAYBE_GetControlSignals) { + io_handler_ = new GetControlSignalsTestIoHandler; + RunTest("serial_unittest.js", "testGetControlSignals"); + EXPECT_EQ(16u, io_handler_->num_calls()); +} + +TEST_F(SerialApiTest, SetControlSignals) { + io_handler_ = new SetControlSignalsTestIoHandler; + RunTest("serial_unittest.js", "testSetControlSignals"); + EXPECT_EQ(9u, io_handler_->num_calls()); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_Update DISABLED_Update +#else +#define MAYBE_Update Update +#endif +TEST_F(SerialApiTest, MAYBE_Update) { + io_handler_ = new ConfigurePortTestIoHandler; + RunTest("serial_unittest.js", "testUpdate"); + EXPECT_EQ(11u, io_handler_->num_calls()); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_UpdateAcrossSerialization DISABLED_UpdateAcrossSerialization +#else +#define MAYBE_UpdateAcrossSerialization UpdateAcrossSerialization +#endif +TEST_F(SerialApiTest, MAYBE_UpdateAcrossSerialization) { + io_handler_ = new ConfigurePortTestIoHandler; + RunTest("serial_unittest.js", "testUpdateAcrossSerialization"); + EXPECT_EQ(11u, io_handler_->num_calls()); +} + +TEST_F(SerialApiTest, UpdateInvalidBitrate) { + io_handler_ = new ConfigurePortTestIoHandler; + RunTest("serial_unittest.js", "testUpdateInvalidBitrate"); + EXPECT_EQ(1u, io_handler_->num_calls()); +} + +TEST_F(SerialApiTest, Flush) { + io_handler_ = new FlushTestIoHandler; + RunTest("serial_unittest.js", "testFlush"); + EXPECT_EQ(1u, io_handler_->num_calls()); +} + +TEST_F(SerialApiTest, SetPaused) { + RunTest("serial_unittest.js", "testSetPaused"); +} + +TEST_F(SerialApiTest, Echo) { + RunTest("serial_unittest.js", "testEcho"); +} + +TEST_F(SerialApiTest, EchoAfterSerialization) { + RunTest("serial_unittest.js", "testEchoAfterSerialization"); +} + +TEST_F(SerialApiTest, SendDuringExistingSend) { + RunTest("serial_unittest.js", "testSendDuringExistingSend"); +} + +TEST_F(SerialApiTest, SendAfterSuccessfulSend) { + RunTest("serial_unittest.js", "testSendAfterSuccessfulSend"); +} + +TEST_F(SerialApiTest, SendPartialSuccessWithError) { + io_handler_ = new SendDataWithErrorIoHandler(); + RunTest("serial_unittest.js", "testSendPartialSuccessWithError"); +} + +TEST_F(SerialApiTest, SendTimeout) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testSendTimeout"); +} + +TEST_F(SerialApiTest, SendTimeoutAfterSerialization) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testSendTimeoutAfterSerialization"); +} + +TEST_F(SerialApiTest, DisableSendTimeout) { + io_handler_ = new BlockSendsForeverSendIoHandler(); + RunTest("serial_unittest.js", "testDisableSendTimeout"); +} + +TEST_F(SerialApiTest, PausedReceive) { + io_handler_ = new FixedDataReceiveTestIoHandler("data"); + RunTest("serial_unittest.js", "testPausedReceive"); +} + +TEST_F(SerialApiTest, PausedReceiveError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::DEVICE_LOST); + RunTest("serial_unittest.js", "testPausedReceiveError"); +} + +TEST_F(SerialApiTest, ReceiveTimeout) { + RunTest("serial_unittest.js", "testReceiveTimeout"); +} + +TEST_F(SerialApiTest, ReceiveTimeoutAfterSerialization) { + RunTest("serial_unittest.js", "testReceiveTimeoutAfterSerialization"); +} + +TEST_F(SerialApiTest, DisableReceiveTimeout) { + RunTest("serial_unittest.js", "testDisableReceiveTimeout"); +} + +TEST_F(SerialApiTest, ReceiveErrorDisconnected) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::DISCONNECTED); + RunTest("serial_unittest.js", "testReceiveErrorDisconnected"); +} + +TEST_F(SerialApiTest, ReceiveErrorDeviceLost) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::DEVICE_LOST); + RunTest("serial_unittest.js", "testReceiveErrorDeviceLost"); +} + +TEST_F(SerialApiTest, ReceiveErrorBreak) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::BREAK); + RunTest("serial_unittest.js", "testReceiveErrorBreak"); +} + +TEST_F(SerialApiTest, ReceiveErrorFrameError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::FRAME_ERROR); + RunTest("serial_unittest.js", "testReceiveErrorFrameError"); +} + +TEST_F(SerialApiTest, ReceiveErrorOverrun) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::OVERRUN); + RunTest("serial_unittest.js", "testReceiveErrorOverrun"); +} + +TEST_F(SerialApiTest, ReceiveErrorBufferOverflow) { + io_handler_ = new ReceiveErrorTestIoHandler( + device::serial::ReceiveError::BUFFER_OVERFLOW); + RunTest("serial_unittest.js", "testReceiveErrorBufferOverflow"); +} + +TEST_F(SerialApiTest, ReceiveErrorParityError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::PARITY_ERROR); + RunTest("serial_unittest.js", "testReceiveErrorParityError"); +} + +TEST_F(SerialApiTest, ReceiveErrorSystemError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::SYSTEM_ERROR); + RunTest("serial_unittest.js", "testReceiveErrorSystemError"); +} + +TEST_F(SerialApiTest, SendErrorDisconnected) { + io_handler_ = + new SendErrorTestIoHandler(device::serial::SendError::DISCONNECTED); + RunTest("serial_unittest.js", "testSendErrorDisconnected"); +} + +TEST_F(SerialApiTest, SendErrorSystemError) { + io_handler_ = + new SendErrorTestIoHandler(device::serial::SendError::SYSTEM_ERROR); + RunTest("serial_unittest.js", "testSendErrorSystemError"); +} + +TEST_F(SerialApiTest, DisconnectUnknownConnectionId) { + RunTest("serial_unittest.js", "testDisconnectUnknownConnectionId"); +} + +TEST_F(SerialApiTest, GetInfoUnknownConnectionId) { + RunTest("serial_unittest.js", "testGetInfoUnknownConnectionId"); +} + +TEST_F(SerialApiTest, UpdateUnknownConnectionId) { + RunTest("serial_unittest.js", "testUpdateUnknownConnectionId"); +} + +TEST_F(SerialApiTest, SetControlSignalsUnknownConnectionId) { + RunTest("serial_unittest.js", "testSetControlSignalsUnknownConnectionId"); +} + +TEST_F(SerialApiTest, GetControlSignalsUnknownConnectionId) { + RunTest("serial_unittest.js", "testGetControlSignalsUnknownConnectionId"); +} + +TEST_F(SerialApiTest, FlushUnknownConnectionId) { + RunTest("serial_unittest.js", "testFlushUnknownConnectionId"); +} + +TEST_F(SerialApiTest, SetPausedUnknownConnectionId) { + RunTest("serial_unittest.js", "testSetPausedUnknownConnectionId"); +} + +TEST_F(SerialApiTest, SendUnknownConnectionId) { + RunTest("serial_unittest.js", "testSendUnknownConnectionId"); +} + +// Note: these tests are disabled, since there is no good story for persisting +// the stashed handles when an extension process is shut down. See +// https://crbug.com/538774 +TEST_F(SerialApiTest, DISABLED_StashAndRestoreDuringEcho) { + ASSERT_NO_FATAL_FAILURE(RunTest("serial_unittest.js", "testSendAndStash")); + scoped_ptr<ModuleSystemTestEnvironment> new_env(CreateEnvironment()); + ApiTestEnvironment new_api_test_env(new_env.get()); + PrepareEnvironment(&new_api_test_env, stash_backend_.get()); + new_api_test_env.RunTest("serial_unittest.js", "testRestoreAndReceive"); +} + +TEST_F(SerialApiTest, DISABLED_StashAndRestoreDuringEchoError) { + io_handler_ = + new ReceiveErrorTestIoHandler(device::serial::ReceiveError::DEVICE_LOST); + ASSERT_NO_FATAL_FAILURE( + RunTest("serial_unittest.js", "testRestoreAndReceiveErrorSetUp")); + scoped_ptr<ModuleSystemTestEnvironment> new_env(CreateEnvironment()); + ApiTestEnvironment new_api_test_env(new_env.get()); + PrepareEnvironment(&new_api_test_env, stash_backend_.get()); + new_api_test_env.RunTest("serial_unittest.js", "testRestoreAndReceiveError"); +} + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_StashAndRestoreNoConnections DISABLED_StashAndRestoreNoConnections +#else +#define MAYBE_StashAndRestoreNoConnections StashAndRestoreNoConnections +#endif +TEST_F(SerialApiTest, MAYBE_StashAndRestoreNoConnections) { + ASSERT_NO_FATAL_FAILURE( + RunTest("serial_unittest.js", "testStashNoConnections")); + io_handler_ = nullptr; + scoped_ptr<ModuleSystemTestEnvironment> new_env(CreateEnvironment()); + ApiTestEnvironment new_api_test_env(new_env.get()); + PrepareEnvironment(&new_api_test_env, stash_backend_.get()); + new_api_test_env.RunTest("serial_unittest.js", "testRestoreNoConnections"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api_activity_logger.cc b/chromium/extensions/renderer/api_activity_logger.cc new file mode 100644 index 00000000000..e78c27452f1 --- /dev/null +++ b/chromium/extensions/renderer/api_activity_logger.cc @@ -0,0 +1,82 @@ +// 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 <stddef.h> + +#include <string> + +#include "base/bind.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/activity_log_converter_strategy.h" +#include "extensions/renderer/api_activity_logger.h" +#include "extensions/renderer/script_context.h" + +using content::V8ValueConverter; + +namespace extensions { + +APIActivityLogger::APIActivityLogger(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("LogEvent", base::Bind(&APIActivityLogger::LogEvent)); + RouteFunction("LogAPICall", base::Bind(&APIActivityLogger::LogAPICall)); +} + +// static +void APIActivityLogger::LogAPICall( + const v8::FunctionCallbackInfo<v8::Value>& args) { + LogInternal(APICALL, args); +} + +// static +void APIActivityLogger::LogEvent( + const v8::FunctionCallbackInfo<v8::Value>& args) { + LogInternal(EVENT, args); +} + +// static +void APIActivityLogger::LogInternal( + const CallType call_type, + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_GT(args.Length(), 2); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsString()); + CHECK(args[2]->IsArray()); + + std::string ext_id = *v8::String::Utf8Value(args[0]); + ExtensionHostMsg_APIActionOrEvent_Params params; + params.api_call = *v8::String::Utf8Value(args[1]); + if (args.Length() == 4) // Extras are optional. + params.extra = *v8::String::Utf8Value(args[3]); + else + params.extra = ""; + + // Get the array of api call arguments. + v8::Local<v8::Array> arg_array = v8::Local<v8::Array>::Cast(args[2]); + if (arg_array->Length() > 0) { + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + ActivityLogConverterStrategy strategy; + converter->SetFunctionAllowed(true); + converter->SetStrategy(&strategy); + scoped_ptr<base::ListValue> arg_list(new base::ListValue()); + for (size_t i = 0; i < arg_array->Length(); ++i) { + arg_list->Set( + i, + converter->FromV8Value(arg_array->Get(i), + args.GetIsolate()->GetCurrentContext())); + } + params.arguments.Swap(arg_list.get()); + } + + if (call_type == APICALL) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_AddAPIActionToActivityLog(ext_id, params)); + } else if (call_type == EVENT) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_AddEventToActivityLog(ext_id, params)); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api_activity_logger.h b/chromium/extensions/renderer/api_activity_logger.h new file mode 100644 index 00000000000..63a4699a88d --- /dev/null +++ b/chromium/extensions/renderer/api_activity_logger.h @@ -0,0 +1,52 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_API_ACTIVITY_LOGGER_H_ +#define EXTENSIONS_RENDERER_API_ACTIVITY_LOGGER_H_ + +#include <string> + +#include "base/macros.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { + +// Used to log extension API calls and events that are implemented with custom +// bindings.The actions are sent via IPC to extensions::ActivityLog for +// recording and display. +class APIActivityLogger : public ObjectBackedNativeHandler { + public: + explicit APIActivityLogger(ScriptContext* context); + + private: + // Used to distinguish API calls & events from each other in LogInternal. + enum CallType { APICALL, EVENT }; + + // This is ultimately invoked in bindings.js with JavaScript arguments. + // arg0 - extension ID as a string + // arg1 - API call name as a string + // arg2 - arguments to the API call + // arg3 - any extra logging info as a string (optional) + static void LogAPICall(const v8::FunctionCallbackInfo<v8::Value>& args); + + // This is ultimately invoked in bindings.js with JavaScript arguments. + // arg0 - extension ID as a string + // arg1 - Event name as a string + // arg2 - Event arguments + // arg3 - any extra logging info as a string (optional) + static void LogEvent(const v8::FunctionCallbackInfo<v8::Value>& args); + + // LogAPICall and LogEvent are really the same underneath except for + // how they are ultimately dispatched to the log. + static void LogInternal(const CallType call_type, + const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(APIActivityLogger); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_ACTIVITY_LOGGER_H_ diff --git a/chromium/extensions/renderer/api_definitions_natives.cc b/chromium/extensions/renderer/api_definitions_natives.cc new file mode 100644 index 00000000000..96f1f2c6eb8 --- /dev/null +++ b/chromium/extensions/renderer/api_definitions_natives.cc @@ -0,0 +1,37 @@ +// 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/renderer/api_definitions_natives.h" + +#include "extensions/common/features/feature.h" +#include "extensions/common/features/feature_provider.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +ApiDefinitionsNatives::ApiDefinitionsNatives(Dispatcher* dispatcher, + ScriptContext* context) + : ObjectBackedNativeHandler(context), dispatcher_(dispatcher) { + RouteFunction( + "GetExtensionAPIDefinitionsForTest", + base::Bind(&ApiDefinitionsNatives::GetExtensionAPIDefinitionsForTest, + base::Unretained(this))); +} + +void ApiDefinitionsNatives::GetExtensionAPIDefinitionsForTest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + std::vector<std::string> apis; + const FeatureProvider* feature_provider = FeatureProvider::GetAPIFeatures(); + for (const auto& map_entry : feature_provider->GetAllFeatures()) { + if (!feature_provider->GetParent(map_entry.second.get()) && + context()->GetAvailability(map_entry.first).is_available()) { + apis.push_back(map_entry.first); + } + } + args.GetReturnValue().Set( + dispatcher_->v8_schema_registry()->GetSchemas(apis)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api_definitions_natives.h b/chromium/extensions/renderer/api_definitions_natives.h new file mode 100644 index 00000000000..dcc1b732feb --- /dev/null +++ b/chromium/extensions/renderer/api_definitions_natives.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_API_DEFINITIONS_NATIVES_H_ +#define EXTENSIONS_RENDERER_API_DEFINITIONS_NATIVES_H_ + +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { +class Dispatcher; +class ScriptContext; + +// Native functions for JS to get access to the schemas for extension APIs. +class ApiDefinitionsNatives : public ObjectBackedNativeHandler { + public: + ApiDefinitionsNatives(Dispatcher* dispatcher, ScriptContext* context); + + private: + // Returns the list of all schemas that are available to the calling context. + void GetExtensionAPIDefinitionsForTest( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // Not owned. + Dispatcher* dispatcher_; + + DISALLOW_COPY_AND_ASSIGN(ApiDefinitionsNatives); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_DEFINITIONS_NATIVES_H_ diff --git a/chromium/extensions/renderer/api_test_base.cc b/chromium/extensions/renderer/api_test_base.cc new file mode 100644 index 00000000000..81ec07f18d6 --- /dev/null +++ b/chromium/extensions/renderer/api_test_base.cc @@ -0,0 +1,240 @@ +// 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/renderer/api_test_base.h" + +#include <utility> +#include <vector> + +#include "base/run_loop.h" +#include "extensions/common/extension_urls.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/process_info_native_handler.h" +#include "gin/converter.h" +#include "gin/dictionary.h" +#include "mojo/edk/js/core.h" +#include "mojo/edk/js/handle.h" +#include "mojo/edk/js/support.h" +#include "mojo/public/cpp/bindings/interface_request.h" +#include "mojo/public/cpp/system/core.h" + +namespace extensions { +namespace { + +// Natives for the implementation of the unit test version of chrome.test. Calls +// the provided |quit_closure| when either notifyPass or notifyFail is called. +class TestNatives : public gin::Wrappable<TestNatives> { + public: + static gin::Handle<TestNatives> Create(v8::Isolate* isolate, + const base::Closure& quit_closure) { + return gin::CreateHandle(isolate, new TestNatives(quit_closure)); + } + + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override { + return Wrappable<TestNatives>::GetObjectTemplateBuilder(isolate) + .SetMethod("Log", &TestNatives::Log) + .SetMethod("NotifyPass", &TestNatives::NotifyPass) + .SetMethod("NotifyFail", &TestNatives::NotifyFail); + } + + void Log(const std::string& value) { logs_ += value + "\n"; } + void NotifyPass() { FinishTesting(); } + + void NotifyFail(const std::string& message) { + FinishTesting(); + FAIL() << logs_ << message; + } + + void FinishTesting() { + base::MessageLoop::current()->PostTask(FROM_HERE, quit_closure_); + } + + static gin::WrapperInfo kWrapperInfo; + + private: + explicit TestNatives(const base::Closure& quit_closure) + : quit_closure_(quit_closure) {} + + const base::Closure quit_closure_; + std::string logs_; +}; + +gin::WrapperInfo TestNatives::kWrapperInfo = {gin::kEmbedderNativeGin}; + +} // namespace + +gin::WrapperInfo TestServiceProvider::kWrapperInfo = {gin::kEmbedderNativeGin}; + +gin::Handle<TestServiceProvider> TestServiceProvider::Create( + v8::Isolate* isolate) { + return gin::CreateHandle(isolate, new TestServiceProvider()); +} + +TestServiceProvider::~TestServiceProvider() { +} + +gin::ObjectTemplateBuilder TestServiceProvider::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return Wrappable<TestServiceProvider>::GetObjectTemplateBuilder(isolate) + .SetMethod("connectToService", &TestServiceProvider::ConnectToService); +} + +mojo::Handle TestServiceProvider::ConnectToService( + const std::string& service_name) { + EXPECT_EQ(1u, service_factories_.count(service_name)) + << "Unregistered service " << service_name << " requested."; + mojo::MessagePipe pipe; + std::map<std::string, + base::Callback<void(mojo::ScopedMessagePipeHandle)> >::iterator it = + service_factories_.find(service_name); + if (it != service_factories_.end()) + it->second.Run(std::move(pipe.handle0)); + return pipe.handle1.release(); +} + +TestServiceProvider::TestServiceProvider() { +} + +// static +void TestServiceProvider::IgnoreHandle(mojo::ScopedMessagePipeHandle handle) { +} + +ApiTestEnvironment::ApiTestEnvironment( + ModuleSystemTestEnvironment* environment) { + env_ = environment; + InitializeEnvironment(); + RegisterModules(); +} + +ApiTestEnvironment::~ApiTestEnvironment() { +} + +void ApiTestEnvironment::RegisterModules() { + v8_schema_registry_.reset(new V8SchemaRegistry); + const std::vector<std::pair<std::string, int> > resources = + Dispatcher::GetJsResources(); + for (std::vector<std::pair<std::string, int> >::const_iterator resource = + resources.begin(); + resource != resources.end(); + ++resource) { + if (resource->first != "test_environment_specific_bindings") + env()->RegisterModule(resource->first, resource->second); + } + Dispatcher::RegisterNativeHandlers(env()->module_system(), + env()->context(), + NULL, + NULL, + v8_schema_registry_.get()); + env()->module_system()->RegisterNativeHandler( + "process", + scoped_ptr<NativeHandler>(new ProcessInfoNativeHandler( + env()->context(), + env()->context()->GetExtensionID(), + env()->context()->GetContextTypeDescription(), + false, + false, + 2, + false))); + env()->RegisterTestFile("test_environment_specific_bindings", + "unit_test_environment_specific_bindings.js"); + + env()->OverrideNativeHandler("activityLogger", + "exports.$set('LogAPICall', function() {});"); + env()->OverrideNativeHandler( + "apiDefinitions", + "exports.$set('GetExtensionAPIDefinitionsForTest'," + "function() { return [] });"); + env()->OverrideNativeHandler( + "event_natives", + "exports.$set('AttachEvent', function() {});" + "exports.$set('DetachEvent', function() {});" + "exports.$set('AttachFilteredEvent', function() {});" + "exports.$set('AttachFilteredEvent', function() {});" + "exports.$set('MatchAgainstEventFilter', function() { return [] });"); + + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule(env()->isolate(), mojo::edk::js::Core::kModuleName, + mojo::edk::js::Core::GetModule(env()->isolate())); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule(env()->isolate(), mojo::edk::js::Support::kModuleName, + mojo::edk::js::Support::GetModule(env()->isolate())); + gin::Handle<TestServiceProvider> service_provider = + TestServiceProvider::Create(env()->isolate()); + service_provider_ = service_provider.get(); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule(env()->isolate(), + "content/public/renderer/frame_service_registry", + service_provider.ToV8()); +} + +void ApiTestEnvironment::InitializeEnvironment() { + gin::Dictionary global(env()->isolate(), + env()->context()->v8_context()->Global()); + gin::Dictionary navigator(gin::Dictionary::CreateEmpty(env()->isolate())); + navigator.Set("appVersion", base::StringPiece("")); + global.Set("navigator", navigator); + gin::Dictionary chrome(gin::Dictionary::CreateEmpty(env()->isolate())); + global.Set("chrome", chrome); + gin::Dictionary extension(gin::Dictionary::CreateEmpty(env()->isolate())); + chrome.Set("extension", extension); + gin::Dictionary runtime(gin::Dictionary::CreateEmpty(env()->isolate())); + chrome.Set("runtime", runtime); +} + +void ApiTestEnvironment::RunTest(const std::string& file_name, + const std::string& test_name) { + env()->RegisterTestFile("testBody", file_name); + base::RunLoop run_loop; + gin::ModuleRegistry::From(env()->context()->v8_context())->AddBuiltinModule( + env()->isolate(), + "testNatives", + TestNatives::Create(env()->isolate(), run_loop.QuitClosure()).ToV8()); + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ApiTestEnvironment::RunTestInner, base::Unretained(this), + test_name, run_loop.QuitClosure())); + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&ApiTestEnvironment::RunPromisesAgain, + base::Unretained(this))); + run_loop.Run(); +} + +void ApiTestEnvironment::RunTestInner(const std::string& test_name, + const base::Closure& quit_closure) { + v8::HandleScope scope(env()->isolate()); + ModuleSystem::NativesEnabledScope natives_enabled(env()->module_system()); + v8::Local<v8::Value> result = + env()->module_system()->CallModuleMethod("testBody", test_name); + if (!result->IsTrue()) { + base::MessageLoop::current()->PostTask(FROM_HERE, quit_closure); + FAIL() << "Failed to run test \"" << test_name << "\""; + } +} + +void ApiTestEnvironment::RunPromisesAgain() { + v8::MicrotasksScope::PerformCheckpoint(env()->isolate()); + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&ApiTestEnvironment::RunPromisesAgain, + base::Unretained(this))); +} + +ApiTestBase::ApiTestBase() { +} + +ApiTestBase::~ApiTestBase() { +} + +void ApiTestBase::SetUp() { + ModuleSystemTest::SetUp(); + test_env_.reset(new ApiTestEnvironment(env())); +} + +void ApiTestBase::RunTest(const std::string& file_name, + const std::string& test_name) { + ExpectNoAssertionsMade(); + test_env_->RunTest(file_name, test_name); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/api_test_base.h b/chromium/extensions/renderer/api_test_base.h new file mode 100644 index 00000000000..8b62049924c --- /dev/null +++ b/chromium/extensions/renderer/api_test_base.h @@ -0,0 +1,124 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_API_TEST_BASE_H_ +#define EXTENSIONS_RENDERER_API_TEST_BASE_H_ + +#include <map> +#include <string> +#include <utility> + +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "extensions/renderer/module_system_test.h" +#include "extensions/renderer/v8_schema_registry.h" +#include "gin/handle.h" +#include "gin/modules/module_registry.h" +#include "gin/object_template_builder.h" +#include "gin/wrappable.h" +#include "mojo/public/cpp/bindings/interface_request.h" +#include "mojo/public/cpp/system/core.h" + +namespace extensions { + +class V8SchemaRegistry; + +// A ServiceProvider that provides access from JS modules to services registered +// by AddService() calls. +class TestServiceProvider : public gin::Wrappable<TestServiceProvider> { + public: + static gin::Handle<TestServiceProvider> Create(v8::Isolate* isolate); + ~TestServiceProvider() override; + + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override; + + template <typename Interface> + void AddService(const base::Callback<void(mojo::InterfaceRequest<Interface>)> + service_factory) { + service_factories_.insert(std::make_pair( + Interface::Name_, + base::Bind(ForwardToServiceFactory<Interface>, service_factory))); + } + + // Ignore requests for the Interface service. + template <typename Interface> + void IgnoreServiceRequests() { + service_factories_.insert(std::make_pair( + Interface::Name_, base::Bind(&TestServiceProvider::IgnoreHandle))); + } + + static gin::WrapperInfo kWrapperInfo; + + private: + TestServiceProvider(); + + mojo::Handle ConnectToService(const std::string& service_name); + + template <typename Interface> + static void ForwardToServiceFactory( + const base::Callback<void(mojo::InterfaceRequest<Interface>)> + service_factory, + mojo::ScopedMessagePipeHandle handle) { + service_factory.Run(mojo::MakeRequest<Interface>(std::move(handle))); + } + + static void IgnoreHandle(mojo::ScopedMessagePipeHandle handle); + + std::map<std::string, base::Callback<void(mojo::ScopedMessagePipeHandle)> > + service_factories_; +}; + +// An environment for unit testing apps/extensions API custom bindings +// implemented on Mojo services. This augments a ModuleSystemTestEnvironment +// with a TestServiceProvider and other modules available in a real extensions +// environment. +class ApiTestEnvironment { + public: + explicit ApiTestEnvironment(ModuleSystemTestEnvironment* environment); + ~ApiTestEnvironment(); + void RunTest(const std::string& file_name, const std::string& test_name); + TestServiceProvider* service_provider() { return service_provider_; } + ModuleSystemTestEnvironment* env() { return env_; } + + private: + void RegisterModules(); + void InitializeEnvironment(); + void RunTestInner(const std::string& test_name, + const base::Closure& quit_closure); + void RunPromisesAgain(); + + ModuleSystemTestEnvironment* env_; + TestServiceProvider* service_provider_; + scoped_ptr<V8SchemaRegistry> v8_schema_registry_; +}; + +// A base class for unit testing apps/extensions API custom bindings implemented +// on Mojo services. To use: +// 1. Register test Mojo service implementations on service_provider(). +// 2. Write JS tests in extensions/test/data/test_file.js. +// 3. Write one C++ test function for each JS test containing +// RunTest("test_file.js", "testFunctionName"). +// See extensions/renderer/api_test_base_unittest.cc and +// extensions/test/data/api_test_base_unittest.js for sample usage. +class ApiTestBase : public ModuleSystemTest { + protected: + ApiTestBase(); + ~ApiTestBase() override; + void SetUp() override; + void RunTest(const std::string& file_name, const std::string& test_name); + + ApiTestEnvironment* api_test_env() { return test_env_.get(); } + TestServiceProvider* service_provider() { + return test_env_->service_provider(); + } + + private: + base::MessageLoop message_loop_; + scoped_ptr<ApiTestEnvironment> test_env_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_API_TEST_BASE_H_ diff --git a/chromium/extensions/renderer/api_test_base_unittest.cc b/chromium/extensions/renderer/api_test_base_unittest.cc new file mode 100644 index 00000000000..149e34eb22a --- /dev/null +++ b/chromium/extensions/renderer/api_test_base_unittest.cc @@ -0,0 +1,34 @@ +// 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/renderer/api_test_base.h" + +namespace extensions { + +class ApiTestBaseTest : public ApiTestBase { + public: + void SetUp() override { ApiTestBase::SetUp(); } +}; + +TEST_F(ApiTestBaseTest, TestEnvironment) { + RunTest("api_test_base_unittest.js", "testEnvironment"); +} + +TEST_F(ApiTestBaseTest, TestPromisesRun) { + RunTest("api_test_base_unittest.js", "testPromisesRun"); +} + +TEST_F(ApiTestBaseTest, TestCommonModulesAreAvailable) { + RunTest("api_test_base_unittest.js", "testCommonModulesAreAvailable"); +} + +TEST_F(ApiTestBaseTest, TestMojoModulesAreAvailable) { + RunTest("api_test_base_unittest.js", "testMojoModulesAreAvailable"); +} + +TEST_F(ApiTestBaseTest, TestTestBindings) { + RunTest("api_test_base_unittest.js", "testTestBindings"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/app_window_custom_bindings.cc b/chromium/extensions/renderer/app_window_custom_bindings.cc new file mode 100644 index 00000000000..492731d1a82 --- /dev/null +++ b/chromium/extensions/renderer/app_window_custom_bindings.cc @@ -0,0 +1,82 @@ +// Copyright (c) 2012 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/app_window_custom_bindings.h" + +#include "base/command_line.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_view.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/switches.h" +#include "extensions/renderer/script_context.h" +#include "grit/extensions_renderer_resources.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" +#include "ui/base/resource/resource_bundle.h" +#include "v8/include/v8.h" + +namespace extensions { + +AppWindowCustomBindings::AppWindowCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("GetFrame", base::Bind(&AppWindowCustomBindings::GetFrame, + base::Unretained(this))); + + RouteFunction("GetWindowControlsHtmlTemplate", + base::Bind(&AppWindowCustomBindings::GetWindowControlsHtmlTemplate, + base::Unretained(this))); +} + +void AppWindowCustomBindings::GetFrame( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // TODO(jeremya): convert this to IDL nocompile to get validation, and turn + // these argument checks into CHECK(). + if (args.Length() != 2) + return; + + if (!args[0]->IsInt32() || !args[1]->IsBoolean()) + return; + + int frame_id = args[0]->Int32Value(); + bool notify_browser = args[1]->BooleanValue(); + + if (frame_id == MSG_ROUTING_NONE) + return; + + content::RenderFrame* app_frame = + content::RenderFrame::FromRoutingID(frame_id); + if (!app_frame) + return; + + if (notify_browser) { + content::RenderThread::Get()->Send(new ExtensionHostMsg_AppWindowReady( + app_frame->GetRenderView()->GetRoutingID())); + } + + v8::Local<v8::Value> window = + app_frame->GetWebFrame()->mainWorldScriptContext()->Global(); + args.GetReturnValue().Set(window); +} + +void AppWindowCustomBindings::GetWindowControlsHtmlTemplate( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(args.Length(), 0); + + v8::Local<v8::Value> result = v8::String::Empty(args.GetIsolate()); + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableAppWindowControls)) { + base::StringValue value( + ResourceBundle::GetSharedInstance() + .GetRawDataResource(IDR_WINDOW_CONTROLS_TEMPLATE_HTML) + .as_string()); + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + result = converter->ToV8Value(&value, context()->v8_context()); + } + args.GetReturnValue().Set(result); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/app_window_custom_bindings.h b/chromium/extensions/renderer/app_window_custom_bindings.h new file mode 100644 index 00000000000..3fbab04f0fd --- /dev/null +++ b/chromium/extensions/renderer/app_window_custom_bindings.h @@ -0,0 +1,32 @@ +// Copyright (c) 2012 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. + +#ifndef EXTENSIONS_RENDERER_APP_WINDOW_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_APP_WINDOW_CUSTOM_BINDINGS_H_ + +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContextSet; + +// Implements custom bindings for the app.window API. +class AppWindowCustomBindings : public ObjectBackedNativeHandler { + public: + AppWindowCustomBindings(ScriptContext* context); + + private: + void GetFrame(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Return string containing the HTML <template> for the <window-controls> + // custom element. + void GetWindowControlsHtmlTemplate( + const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(AppWindowCustomBindings); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_APP_WINDOW_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/binding_generating_native_handler.cc b/chromium/extensions/renderer/binding_generating_native_handler.cc new file mode 100644 index 00000000000..8912017e7f1 --- /dev/null +++ b/chromium/extensions/renderer/binding_generating_native_handler.cc @@ -0,0 +1,112 @@ +// 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/renderer/binding_generating_native_handler.h" + +#include "base/macros.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/v8_helpers.h" + +namespace extensions { + +using namespace v8_helpers; + +BindingGeneratingNativeHandler::BindingGeneratingNativeHandler( + ScriptContext* context, + const std::string& api_name, + const std::string& bind_to) + : context_(context), api_name_(api_name), bind_to_(bind_to) {} + +v8::Local<v8::Object> BindingGeneratingNativeHandler::NewInstance() { + // This long sequence of commands effectively runs the JavaScript code, + // such that result[bind_to] is the compiled schema for |api_name|: + // + // var result = {}; + // result[bind_to] = require('binding').Binding.create(api_name).generate(); + // return result; + // + // Unfortunately using the v8 APIs makes that quite verbose. + // Each stage is marked with the code it executes. + v8::Isolate* isolate = context_->isolate(); + v8::EscapableHandleScope scope(isolate); + + // Convert |api_name| and |bind_to| into their v8::Strings to pass + // through the v8 APIs. + v8::Local<v8::String> v8_api_name; + v8::Local<v8::String> v8_bind_to; + if (!ToV8String(isolate, api_name_, &v8_api_name) || + !ToV8String(isolate, bind_to_, &v8_bind_to)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + + v8::Local<v8::Context> v8_context = context_->v8_context(); + + // require('binding'); + v8::Local<v8::Object> binding_module; + if (!context_->module_system()->Require("binding").ToLocal(&binding_module)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + + // require('binding').Binding; + v8::Local<v8::Value> binding_value; + v8::Local<v8::Object> binding; + if (!GetProperty(v8_context, binding_module, "Binding", &binding_value) || + !binding_value->ToObject(v8_context).ToLocal(&binding)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + + // require('binding').Binding.create; + v8::Local<v8::Value> create_binding_value; + if (!GetProperty(v8_context, binding, "create", &create_binding_value) || + !create_binding_value->IsFunction()) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + v8::Local<v8::Function> create_binding = + create_binding_value.As<v8::Function>(); + + // require('Binding').Binding.create(api_name); + v8::Local<v8::Value> argv[] = {v8_api_name}; + v8::Local<v8::Value> binding_instance_value; + v8::Local<v8::Object> binding_instance; + if (!CallFunction(v8_context, create_binding, binding, arraysize(argv), argv, + &binding_instance_value) || + !binding_instance_value->ToObject(v8_context) + .ToLocal(&binding_instance)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + + // require('binding').Binding.create(api_name).generate; + v8::Local<v8::Value> generate_value; + if (!GetProperty(v8_context, binding_instance, "generate", &generate_value) || + !generate_value->IsFunction()) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + v8::Local<v8::Function> generate = generate_value.As<v8::Function>(); + + // require('binding').Binding.create(api_name).generate(); + v8::Local<v8::Object> object = v8::Object::New(isolate); + v8::Local<v8::Value> compiled_schema; + if (!CallFunction(v8_context, generate, binding_instance, 0, nullptr, + &compiled_schema)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + + // var result = {}; + // result[bind_to] = ...; + if (!SetProperty(v8_context, object, v8_bind_to, compiled_schema)) { + NOTREACHED(); + return v8::Local<v8::Object>(); + } + // return result; + return scope.Escape(object); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/binding_generating_native_handler.h b/chromium/extensions/renderer/binding_generating_native_handler.h new file mode 100644 index 00000000000..d4e4b0aa722 --- /dev/null +++ b/chromium/extensions/renderer/binding_generating_native_handler.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_BINDING_GENERATING_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_BINDING_GENERATING_NATIVE_HANDLER_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "extensions/renderer/native_handler.h" + +namespace extensions { + +class ScriptContext; + +// Generates API bindings based on the JSON/IDL schemas. This is done by +// creating a |Binding| (from binding.js) for the schema and generating the +// bindings from that. +class BindingGeneratingNativeHandler : public NativeHandler { + public: + // Generates binding for |api_name|, and sets the |bind_to| property on the + // Object returned by |NewInstance| to the generated binding. + BindingGeneratingNativeHandler(ScriptContext* context, + const std::string& api_name, + const std::string& bind_to); + + v8::Local<v8::Object> NewInstance() override; + + private: + ScriptContext* context_; + std::string api_name_; + std::string bind_to_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_BINDING_GENERATING_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/blob_native_handler.cc b/chromium/extensions/renderer/blob_native_handler.cc new file mode 100644 index 00000000000..4161fbc2184 --- /dev/null +++ b/chromium/extensions/renderer/blob_native_handler.cc @@ -0,0 +1,54 @@ +// 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/renderer/blob_native_handler.h" + +#include "base/bind.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/platform/WebURL.h" +#include "third_party/WebKit/public/web/WebBlob.h" + +namespace { + +// Expects a single Blob argument. Returns the Blob's UUID. +void GetBlobUuid(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + blink::WebBlob blob = blink::WebBlob::fromV8Value(args[0]); + args.GetReturnValue().Set( + v8::String::NewFromUtf8(args.GetIsolate(), blob.uuid().utf8().data())); +} + +} // namespace + +namespace extensions { + +BlobNativeHandler::BlobNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("GetBlobUuid", base::Bind(&GetBlobUuid)); + RouteFunction("TakeBrowserProcessBlob", + base::Bind(&BlobNativeHandler::TakeBrowserProcessBlob, + base::Unretained(this))); +} + +// Take ownership of a Blob created on the browser process. Expects the Blob's +// UUID, type, and size as arguments. Returns the Blob we just took to +// Javascript. The Blob reference in the browser process is dropped through +// a separate flow to avoid leaking Blobs if the script context is destroyed. +void BlobNativeHandler::TakeBrowserProcessBlob( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(3, args.Length()); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsString()); + CHECK(args[2]->IsInt32()); + std::string uuid(*v8::String::Utf8Value(args[0])); + std::string type(*v8::String::Utf8Value(args[1])); + blink::WebBlob blob = + blink::WebBlob::createFromUUID(blink::WebString::fromUTF8(uuid), + blink::WebString::fromUTF8(type), + args[2]->Int32Value()); + args.GetReturnValue().Set(blob.toV8Value( + context()->v8_context()->Global(), args.GetIsolate())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/blob_native_handler.h b/chromium/extensions/renderer/blob_native_handler.h new file mode 100644 index 00000000000..6f986e56647 --- /dev/null +++ b/chromium/extensions/renderer/blob_native_handler.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_BLOB_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_BLOB_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// This native handler is used to extract Blobs' UUIDs and pass them over to the +// browser process extension implementation via argument modification. This is +// necessary to support extension functions that take Blob parameters, as Blobs +// are not serialized and sent over to the browser process in the normal way. +// +// Blobs sent via this method don't have their ref-counts incremented, so the +// app using this technique must be sure to keep a reference. +class BlobNativeHandler : public ObjectBackedNativeHandler { + public: + explicit BlobNativeHandler(ScriptContext* context); + + private: + void TakeBrowserProcessBlob(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_BLOB_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/console.cc b/chromium/extensions/renderer/console.cc new file mode 100644 index 00000000000..64160743f77 --- /dev/null +++ b/chromium/extensions/renderer/console.cc @@ -0,0 +1,127 @@ +// 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/renderer/console.h" + +#include "base/compiler_specific.h" +#include "base/debug/alias.h" +#include "base/lazy_instance.h" +#include "base/macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/v8_helpers.h" + +namespace extensions { +namespace console { + +using namespace v8_helpers; + +namespace { + +// Writes |message| to stack to show up in minidump, then crashes. +void CheckWithMinidump(const std::string& message) { + char minidump[1024]; + base::debug::Alias(&minidump); + base::snprintf( + minidump, arraysize(minidump), "e::console: %s", message.c_str()); + CHECK(false) << message; +} + +typedef void (*LogMethod)(content::RenderFrame* render_frame, + const std::string& message); + +void BoundLogMethodCallback(const v8::FunctionCallbackInfo<v8::Value>& info) { + std::string message; + for (int i = 0; i < info.Length(); ++i) { + if (i > 0) + message += " "; + message += *v8::String::Utf8Value(info[i]); + } + + v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext(); + if (context.IsEmpty()) { + LOG(WARNING) << "Could not log \"" << message << "\": no context given"; + return; + } + + ScriptContext* script_context = + ScriptContextSet::GetContextByV8Context(context); + LogMethod log_method = + reinterpret_cast<LogMethod>(info.Data().As<v8::External>()->Value()); + (*log_method)(script_context ? script_context->GetRenderFrame() : nullptr, + message); +} + +void BindLogMethod(v8::Isolate* isolate, + v8::Local<v8::Object> target, + const std::string& name, + LogMethod log_method) { + v8::Local<v8::FunctionTemplate> tmpl = v8::FunctionTemplate::New( + isolate, + &BoundLogMethodCallback, + v8::External::New(isolate, reinterpret_cast<void*>(log_method))); + v8::Local<v8::Function> function; + if (!tmpl->GetFunction(isolate->GetCurrentContext()).ToLocal(&function)) { + LOG(FATAL) << "Could not create log function \"" << name << "\""; + return; + } + v8::Local<v8::String> v8_name = ToV8StringUnsafe(isolate, name); + if (!SetProperty(isolate->GetCurrentContext(), target, v8_name, function)) { + LOG(WARNING) << "Could not bind log method \"" << name << "\""; + } + SetProperty(isolate->GetCurrentContext(), target, v8_name, + tmpl->GetFunction()); +} + +} // namespace + +void Debug(content::RenderFrame* render_frame, const std::string& message) { + AddMessage(render_frame, content::CONSOLE_MESSAGE_LEVEL_DEBUG, message); +} + +void Log(content::RenderFrame* render_frame, const std::string& message) { + AddMessage(render_frame, content::CONSOLE_MESSAGE_LEVEL_LOG, message); +} + +void Warn(content::RenderFrame* render_frame, const std::string& message) { + AddMessage(render_frame, content::CONSOLE_MESSAGE_LEVEL_WARNING, message); +} + +void Error(content::RenderFrame* render_frame, const std::string& message) { + AddMessage(render_frame, content::CONSOLE_MESSAGE_LEVEL_ERROR, message); +} + +void Fatal(content::RenderFrame* render_frame, const std::string& message) { + Error(render_frame, message); + CheckWithMinidump(message); +} + +void AddMessage(content::RenderFrame* render_frame, + content::ConsoleMessageLevel level, + const std::string& message) { + if (!render_frame) { + LOG(WARNING) << "Could not log \"" << message + << "\": no render frame found"; + } else { + render_frame->AddMessageToConsole(level, message); + } +} + +v8::Local<v8::Object> AsV8Object(v8::Isolate* isolate) { + v8::EscapableHandleScope handle_scope(isolate); + v8::Local<v8::Object> console_object = v8::Object::New(isolate); + BindLogMethod(isolate, console_object, "debug", &Debug); + BindLogMethod(isolate, console_object, "log", &Log); + BindLogMethod(isolate, console_object, "warn", &Warn); + BindLogMethod(isolate, console_object, "error", &Error); + return handle_scope.Escape(console_object); +} + +} // namespace console +} // namespace extensions diff --git a/chromium/extensions/renderer/console.h b/chromium/extensions/renderer/console.h new file mode 100644 index 00000000000..79d338e773a --- /dev/null +++ b/chromium/extensions/renderer/console.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_CONSOLE_H_ +#define EXTENSIONS_RENDERER_CONSOLE_H_ + +#include <string> + +#include "content/public/common/console_message_level.h" +#include "v8/include/v8.h" + +namespace content { +class RenderFrame; +} + +namespace extensions { + +// Utility for logging messages to RenderFrames. +namespace console { + +// Adds |message| to the console of |render_frame|. If |render_frame| is null, +// LOG()s the message instead. +void Debug(content::RenderFrame* render_frame, const std::string& message); +void Log(content::RenderFrame* render_frame, const std::string& message); +void Warn(content::RenderFrame* render_frame, const std::string& message); +void Error(content::RenderFrame* render_frame, const std::string& message); + +// Logs an Error then crashes the current process. +void Fatal(content::RenderFrame* render_frame, const std::string& message); + +void AddMessage(content::RenderFrame* render_frame, + content::ConsoleMessageLevel level, + const std::string& message); + +// Returns a new v8::Object with each standard log method (Debug/Log/Warn/Error) +// bound to respective debug/log/warn/error methods. This is a direct drop-in +// replacement for the standard devtools console.* methods usually accessible +// from JS. +v8::Local<v8::Object> AsV8Object(v8::Isolate* isolate); + +} // namespace console + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_CONSOLE_H_ diff --git a/chromium/extensions/renderer/console_apitest.cc b/chromium/extensions/renderer/console_apitest.cc new file mode 100644 index 00000000000..de6e3b10a95 --- /dev/null +++ b/chromium/extensions/renderer/console_apitest.cc @@ -0,0 +1,16 @@ +// Copyright (c) 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 "chrome/browser/extensions/extension_apitest.h" + +namespace extensions { +namespace { + +IN_PROC_BROWSER_TEST_F(ExtensionApiTest, UncaughtExceptionLogging) { + ASSERT_TRUE(StartEmbeddedTestServer()); + ASSERT_TRUE(RunExtensionTest("uncaught_exception_logging")) << message_; +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/content_watcher.cc b/chromium/extensions/renderer/content_watcher.cc new file mode 100644 index 00000000000..8c99e11364c --- /dev/null +++ b/chromium/extensions/renderer/content_watcher.cc @@ -0,0 +1,125 @@ +// 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/renderer/content_watcher.h" + +#include <stddef.h> + +#include "content/public/renderer/render_view.h" +#include "content/public/renderer/render_view_visitor.h" +#include "extensions/common/extension_messages.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebElement.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace extensions { + +using blink::WebString; +using blink::WebVector; +using blink::WebView; + +ContentWatcher::ContentWatcher() {} +ContentWatcher::~ContentWatcher() {} + +void ContentWatcher::OnWatchPages( + const std::vector<std::string>& new_css_selectors_utf8) { + blink::WebVector<blink::WebString> new_css_selectors( + new_css_selectors_utf8.size()); + bool changed = new_css_selectors.size() != css_selectors_.size(); + for (size_t i = 0; i < new_css_selectors.size(); ++i) { + new_css_selectors[i] = + blink::WebString::fromUTF8(new_css_selectors_utf8[i]); + if (!changed && new_css_selectors[i] != css_selectors_[i]) + changed = true; + } + + if (!changed) + return; + + css_selectors_.swap(new_css_selectors); + + // Tell each frame's document about the new set of watched selectors. These + // will trigger calls to DidMatchCSS after Blink has a chance to apply the new + // style, which will in turn notify the browser about the changes. + struct WatchSelectors : public content::RenderViewVisitor { + explicit WatchSelectors(const WebVector<WebString>& css_selectors) + : css_selectors_(css_selectors) {} + + bool Visit(content::RenderView* view) override { + // TODO(dcheng): This should be rewritten to be frame-oriented. It + // probably breaks declarative content for OOPI. + for (blink::WebFrame* frame = view->GetWebView()->mainFrame(); frame; + frame = frame->traverseNext(/*wrap=*/false)) { + if (frame->isWebRemoteFrame()) + continue; + frame->toWebLocalFrame()->document().watchCSSSelectors(css_selectors_); + } + + return true; // Continue visiting. + } + + const WebVector<WebString>& css_selectors_; + }; + WatchSelectors visitor(css_selectors_); + content::RenderView::ForEach(&visitor); +} + +void ContentWatcher::DidCreateDocumentElement(blink::WebLocalFrame* frame) { + frame->document().watchCSSSelectors(css_selectors_); +} + +void ContentWatcher::DidMatchCSS( + blink::WebLocalFrame* frame, + const WebVector<WebString>& newly_matching_selectors, + const WebVector<WebString>& stopped_matching_selectors) { + std::set<std::string>& frame_selectors = matching_selectors_[frame]; + for (size_t i = 0; i < stopped_matching_selectors.size(); ++i) + frame_selectors.erase(stopped_matching_selectors[i].utf8()); + for (size_t i = 0; i < newly_matching_selectors.size(); ++i) + frame_selectors.insert(newly_matching_selectors[i].utf8()); + + if (frame_selectors.empty()) + matching_selectors_.erase(frame); + + NotifyBrowserOfChange(frame); +} + +void ContentWatcher::NotifyBrowserOfChange( + blink::WebLocalFrame* changed_frame) const { + blink::WebFrame* const top_frame = changed_frame->top(); + const blink::WebSecurityOrigin top_origin = top_frame->getSecurityOrigin(); + // Want to aggregate matched selectors from all frames where an + // extension with access to top_origin could run on the frame. + if (!top_origin.canAccess(changed_frame->document().getSecurityOrigin())) { + // If the changed frame can't be accessed by the top frame, then + // no change in it could affect the set of selectors we'd send back. + return; + } + + std::set<base::StringPiece> transitive_selectors; + for (blink::WebFrame* frame = top_frame; frame; + frame = frame->traverseNext(/*wrap=*/false)) { + if (top_origin.canAccess(frame->getSecurityOrigin())) { + std::map<blink::WebFrame*, std::set<std::string> >::const_iterator + frame_selectors = matching_selectors_.find(frame); + if (frame_selectors != matching_selectors_.end()) { + transitive_selectors.insert(frame_selectors->second.begin(), + frame_selectors->second.end()); + } + } + } + std::vector<std::string> selector_strings; + for (std::set<base::StringPiece>::const_iterator it = + transitive_selectors.begin(); + it != transitive_selectors.end(); + ++it) + selector_strings.push_back(it->as_string()); + content::RenderView* view = + content::RenderView::FromWebView(top_frame->view()); + view->Send(new ExtensionHostMsg_OnWatchedPageChange(view->GetRoutingID(), + selector_strings)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/content_watcher.h b/chromium/extensions/renderer/content_watcher.h new file mode 100644 index 00000000000..4480ce99cf7 --- /dev/null +++ b/chromium/extensions/renderer/content_watcher.h @@ -0,0 +1,73 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_CONTENT_WATCHER_H_ +#define EXTENSIONS_RENDERER_CONTENT_WATCHER_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "third_party/WebKit/public/platform/WebVector.h" + +namespace blink { +class WebFrame; +class WebLocalFrame; +class WebString; +} + +namespace extensions { +class Dispatcher; +class Extension; +class NativeHandler; + +// Watches the content of WebFrames to notify extensions when they match various +// patterns. This class tracks the set of relevant patterns (set by +// ExtensionMsg_WatchPages) and the set that match on each WebFrame, and sends a +// ExtensionHostMsg_OnWatchedPageChange whenever a RenderView's set changes. +// +// There's one ContentWatcher per Dispatcher rather than per RenderView because +// WebFrames can move between RenderViews through adoptNode. +// TODO(devlin): This class isn't OOPI-safe. +class ContentWatcher { + public: + ContentWatcher(); + ~ContentWatcher(); + + // Handler for ExtensionMsg_WatchPages. + void OnWatchPages(const std::vector<std::string>& css_selectors); + + // Uses WebDocument::watchCSSSelectors to watch the selectors in + // css_selectors_ and get a callback into DidMatchCSS() whenever the set of + // matching selectors in |frame| changes. + void DidCreateDocumentElement(blink::WebLocalFrame* frame); + + // Records that |newly_matching_selectors| have started matching on |*frame|, + // and |stopped_matching_selectors| have stopped matching. + void DidMatchCSS( + blink::WebLocalFrame* frame, + const blink::WebVector<blink::WebString>& newly_matching_selectors, + const blink::WebVector<blink::WebString>& stopped_matching_selectors); + + private: + // Given that we saw a change in the CSS selectors that |changed_frame| + // matched, tell the browser about the new set of matching selectors in its + // top-level page. We filter this so that if an extension were to be granted + // activeTab permission on that top-level page, we only send CSS selectors for + // frames that it could run on. + void NotifyBrowserOfChange(blink::WebLocalFrame* changed_frame) const; + + // If any of these selectors match on a page, we need to send an + // ExtensionHostMsg_OnWatchedPageChange back to the browser. + blink::WebVector<blink::WebString> css_selectors_; + + // Maps live WebFrames to the set of CSS selectors they match. Blink sends + // back diffs, which we apply to these sets. + std::map<blink::WebFrame*, std::set<std::string> > matching_selectors_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_CONTENT_WATCHER_H_ diff --git a/chromium/extensions/renderer/context_menus_custom_bindings.cc b/chromium/extensions/renderer/context_menus_custom_bindings.cc new file mode 100644 index 00000000000..40f4705ebf3 --- /dev/null +++ b/chromium/extensions/renderer/context_menus_custom_bindings.cc @@ -0,0 +1,32 @@ +// 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/renderer/context_menus_custom_bindings.h" + +#include <stdint.h> + +#include "base/bind.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "v8/include/v8.h" + +namespace { + +void GetNextContextMenuId(const v8::FunctionCallbackInfo<v8::Value>& args) { + int context_menu_id = -1; + content::RenderThread::Get()->Send( + new ExtensionHostMsg_GenerateUniqueID(&context_menu_id)); + args.GetReturnValue().Set(static_cast<int32_t>(context_menu_id)); +} + +} // namespace + +namespace extensions { + +ContextMenusCustomBindings::ContextMenusCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("GetNextContextMenuId", base::Bind(&GetNextContextMenuId)); +} + +} // extensions diff --git a/chromium/extensions/renderer/context_menus_custom_bindings.h b/chromium/extensions/renderer/context_menus_custom_bindings.h new file mode 100644 index 00000000000..7c625b48a83 --- /dev/null +++ b/chromium/extensions/renderer/context_menus_custom_bindings.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_CONTEXT_MENUS_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_CONTEXT_MENUS_CUSTOM_BINDINGS_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Implements custom bindings for the contextMenus API. +class ContextMenusCustomBindings : public ObjectBackedNativeHandler { + public: + ContextMenusCustomBindings(ScriptContext* context); +}; + +} // extensions + +#endif // EXTENSIONS_RENDERER_CONTEXT_MENUS_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/css_native_handler.cc b/chromium/extensions/renderer/css_native_handler.cc new file mode 100644 index 00000000000..d7cdc39f164 --- /dev/null +++ b/chromium/extensions/renderer/css_native_handler.cc @@ -0,0 +1,36 @@ +// 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/renderer/css_native_handler.h" + +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/v8_helpers.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/web/WebSelector.h" + +namespace extensions { + +using blink::WebString; + +CssNativeHandler::CssNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("CanonicalizeCompoundSelector", + base::Bind(&CssNativeHandler::CanonicalizeCompoundSelector, + base::Unretained(this))); +} + +void CssNativeHandler::CanonicalizeCompoundSelector( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + std::string input_selector = *v8::String::Utf8Value(args[0]); + // TODO(esprehn): This API shouldn't exist, the extension code should be + // moved into blink. + WebString output_selector = blink::canonicalizeSelector( + WebString::fromUTF8(input_selector), blink::WebSelectorTypeCompound); + args.GetReturnValue().Set(v8_helpers::ToV8StringUnsafe( + args.GetIsolate(), output_selector.utf8().c_str())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/css_native_handler.h b/chromium/extensions/renderer/css_native_handler.h new file mode 100644 index 00000000000..c3d1bc34649 --- /dev/null +++ b/chromium/extensions/renderer/css_native_handler.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_CSS_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_CSS_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +class CssNativeHandler : public ObjectBackedNativeHandler { + public: + explicit CssNativeHandler(ScriptContext* context); + + private: + // Expects one string argument that's a comma-separated list of compound CSS + // selectors (http://dev.w3.org/csswg/selectors4/#compound), and returns its + // Blink-canonicalized form. If the selector is invalid, returns an empty + // string. + void CanonicalizeCompoundSelector( + const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_CSS_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/dispatcher.cc b/chromium/extensions/renderer/dispatcher.cc new file mode 100644 index 00000000000..60a4a11bfec --- /dev/null +++ b/chromium/extensions/renderer/dispatcher.cc @@ -0,0 +1,1629 @@ +// 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/renderer/dispatcher.h" + +#include <stddef.h> + +#include <utility> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/command_line.h" +#include "base/debug/alias.h" +#include "base/lazy_instance.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics_action.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/values.h" +#include "build/build_config.h" +#include "content/grit/content_resources.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/common/browser_plugin_guest_mode.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/api/messaging/message.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension_api.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/extension_urls.h" +#include "extensions/common/feature_switch.h" +#include "extensions/common/features/behavior_feature.h" +#include "extensions/common/features/feature.h" +#include "extensions/common/features/feature_provider.h" +#include "extensions/common/manifest.h" +#include "extensions/common/manifest_constants.h" +#include "extensions/common/manifest_handlers/background_info.h" +#include "extensions/common/manifest_handlers/content_capabilities_handler.h" +#include "extensions/common/manifest_handlers/externally_connectable.h" +#include "extensions/common/manifest_handlers/options_page_info.h" +#include "extensions/common/message_bundle.h" +#include "extensions/common/permissions/permission_set.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/common/switches.h" +#include "extensions/common/view_type.h" +#include "extensions/renderer/api_activity_logger.h" +#include "extensions/renderer/api_definitions_natives.h" +#include "extensions/renderer/app_window_custom_bindings.h" +#include "extensions/renderer/binding_generating_native_handler.h" +#include "extensions/renderer/blob_native_handler.h" +#include "extensions/renderer/content_watcher.h" +#include "extensions/renderer/context_menus_custom_bindings.h" +#include "extensions/renderer/css_native_handler.h" +#include "extensions/renderer/dispatcher_delegate.h" +#include "extensions/renderer/display_source_custom_bindings.h" +#include "extensions/renderer/document_custom_bindings.h" +#include "extensions/renderer/dom_activity_logger.h" +#include "extensions/renderer/event_bindings.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/extension_helper.h" +#include "extensions/renderer/extensions_renderer_client.h" +#include "extensions/renderer/file_system_natives.h" +#include "extensions/renderer/guest_view/guest_view_internal_custom_bindings.h" +#include "extensions/renderer/i18n_custom_bindings.h" +#include "extensions/renderer/id_generator_custom_bindings.h" +#include "extensions/renderer/lazy_background_page_native_handler.h" +#include "extensions/renderer/logging_native_handler.h" +#include "extensions/renderer/messaging_bindings.h" +#include "extensions/renderer/module_system.h" +#include "extensions/renderer/print_native_handler.h" +#include "extensions/renderer/process_info_native_handler.h" +#include "extensions/renderer/render_frame_observer_natives.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "extensions/renderer/request_sender.h" +#include "extensions/renderer/runtime_custom_bindings.h" +#include "extensions/renderer/safe_builtins.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/script_injection_manager.h" +#include "extensions/renderer/send_request_natives.h" +#include "extensions/renderer/set_icon_natives.h" +#include "extensions/renderer/static_v8_external_one_byte_string_resource.h" +#include "extensions/renderer/test_features_native_handler.h" +#include "extensions/renderer/test_native_handler.h" +#include "extensions/renderer/user_gestures_native_handler.h" +#include "extensions/renderer/utils_native_handler.h" +#include "extensions/renderer/v8_context_native_handler.h" +#include "extensions/renderer/v8_helpers.h" +#include "extensions/renderer/wake_event_page.h" +#include "extensions/renderer/worker_script_context_set.h" +#include "grit/extensions_renderer_resources.h" +#include "mojo/public/js/constants.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/platform/WebURLRequest.h" +#include "third_party/WebKit/public/web/WebCustomElement.h" +#include "third_party/WebKit/public/web/WebDataSource.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebRuntimeFeatures.h" +#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebSecurityPolicy.h" +#include "third_party/WebKit/public/web/WebView.h" +#include "ui/base/layout.h" +#include "ui/base/resource/resource_bundle.h" +#include "v8/include/v8.h" + +using base::UserMetricsAction; +using blink::WebDataSource; +using blink::WebDocument; +using blink::WebScopedUserGesture; +using blink::WebSecurityPolicy; +using blink::WebString; +using blink::WebVector; +using blink::WebView; +using content::RenderThread; + +namespace extensions { + +namespace { + +static const int64_t kInitialExtensionIdleHandlerDelayMs = 5 * 1000; +static const int64_t kMaxExtensionIdleHandlerDelayMs = 5 * 60 * 1000; +static const char kEventDispatchFunction[] = "dispatchEvent"; +static const char kOnSuspendEvent[] = "runtime.onSuspend"; +static const char kOnSuspendCanceledEvent[] = "runtime.onSuspendCanceled"; + +void CrashOnException(const v8::TryCatch& trycatch) { + NOTREACHED(); +}; + +// Returns the global value for "chrome" from |context|. If one doesn't exist +// creates a new object for it. +// +// Note that this isn't necessarily an object, since webpages can write, for +// example, "window.chrome = true". +v8::Local<v8::Value> GetOrCreateChrome(ScriptContext* context) { + v8::Local<v8::String> chrome_string( + v8::String::NewFromUtf8(context->isolate(), "chrome")); + v8::Local<v8::Object> global(context->v8_context()->Global()); + v8::Local<v8::Value> chrome(global->Get(chrome_string)); + if (chrome->IsUndefined()) { + chrome = v8::Object::New(context->isolate()); + global->Set(chrome_string, chrome); + } + return chrome; +} + +// Returns |value| cast to an object if possible, else an empty handle. +v8::Local<v8::Object> AsObjectOrEmpty(v8::Local<v8::Value> value) { + return value->IsObject() ? value.As<v8::Object>() : v8::Local<v8::Object>(); +} + +// Calls a method |method_name| in a module |module_name| belonging to the +// module system from |context|. Intended as a callback target from +// ScriptContextSet::ForEach. +void CallModuleMethod(const std::string& module_name, + const std::string& method_name, + const base::ListValue* args, + ScriptContext* context) { + v8::HandleScope handle_scope(context->isolate()); + v8::Context::Scope context_scope(context->v8_context()); + + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + + std::vector<v8::Local<v8::Value>> arguments; + for (base::ListValue::const_iterator it = args->begin(); it != args->end(); + ++it) { + arguments.push_back(converter->ToV8Value(*it, context->v8_context())); + } + + context->module_system()->CallModuleMethod( + module_name, method_name, &arguments); +} + +// This handles the "chrome." root API object in script contexts. +class ChromeNativeHandler : public ObjectBackedNativeHandler { + public: + explicit ChromeNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetChrome", + base::Bind(&ChromeNativeHandler::GetChrome, base::Unretained(this))); + } + + void GetChrome(const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(GetOrCreateChrome(context())); + } +}; + +base::LazyInstance<WorkerScriptContextSet> g_worker_script_context_set = + LAZY_INSTANCE_INITIALIZER; + +} // namespace + +// Note that we can't use Blink public APIs in the constructor becase Blink +// is not initialized at the point we create Dispatcher. +Dispatcher::Dispatcher(DispatcherDelegate* delegate) + : delegate_(delegate), + content_watcher_(new ContentWatcher()), + source_map_(&ResourceBundle::GetSharedInstance()), + v8_schema_registry_(new V8SchemaRegistry), + user_script_set_manager_observer_(this), + webrequest_used_(false) { + const base::CommandLine& command_line = + *(base::CommandLine::ForCurrentProcess()); + set_idle_notifications_ = + command_line.HasSwitch(switches::kExtensionProcess) || + command_line.HasSwitch(::switches::kSingleProcess); + + if (set_idle_notifications_) { + RenderThread::Get()->SetIdleNotificationDelayInMs( + kInitialExtensionIdleHandlerDelayMs); + } + + script_context_set_.reset(new ScriptContextSet(&active_extension_ids_)); + user_script_set_manager_.reset(new UserScriptSetManager()); + script_injection_manager_.reset( + new ScriptInjectionManager(user_script_set_manager_.get())); + user_script_set_manager_observer_.Add(user_script_set_manager_.get()); + request_sender_.reset(new RequestSender(this)); + PopulateSourceMap(); + WakeEventPage::Get()->Init(RenderThread::Get()); + + RenderThread::Get()->RegisterExtension(SafeBuiltins::CreateV8Extension()); + + // WebSecurityPolicy whitelists. They should be registered for both + // chrome-extension: and chrome-extension-resource. + using RegisterFunction = void (*)(const WebString&); + RegisterFunction register_functions[] = { + // Treat as secure because communication with them is entirely in the + // browser, so there is no danger of manipulation or eavesdropping on + // communication with them by third parties. + WebSecurityPolicy::registerURLSchemeAsSecure, + // As far as Blink is concerned, they should be allowed to receive CORS + // requests. At the Extensions layer, requests will actually be blocked + // unless overridden by the web_accessible_resources manifest key. + // TODO(kalman): See what happens with a service worker. + WebSecurityPolicy::registerURLSchemeAsCORSEnabled, + // Resources should bypass Content Security Policy checks when included in + // protected resources. TODO(kalman): What are "protected resources"? + WebSecurityPolicy::registerURLSchemeAsBypassingContentSecurityPolicy, + // Extension resources are HTTP-like and safe to expose to the fetch API. + // The rules for the fetch API are consistent with XHR. + WebSecurityPolicy::registerURLSchemeAsSupportingFetchAPI, + // Extension resources, when loaded as the top-level document, should + // bypass Blink's strict first-party origin checks. + WebSecurityPolicy::registerURLSchemeAsFirstPartyWhenTopLevel, + }; + + WebString extension_scheme(base::ASCIIToUTF16(kExtensionScheme)); + WebString extension_resource_scheme(base::ASCIIToUTF16( + kExtensionResourceScheme)); + for (RegisterFunction func : register_functions) { + func(extension_scheme); + func(extension_resource_scheme); + } + + // For extensions, we want to ensure we call the IdleHandler every so often, + // even if the extension keeps up activity. + if (set_idle_notifications_) { + forced_idle_timer_.reset(new base::RepeatingTimer); + forced_idle_timer_->Start( + FROM_HERE, + base::TimeDelta::FromMilliseconds(kMaxExtensionIdleHandlerDelayMs), + RenderThread::Get(), + &RenderThread::IdleHandler); + } + + // Initialize host permissions for any extensions that were activated before + // WebKit was initialized. + for (const std::string& extension_id : active_extension_ids_) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + CHECK(extension); + InitOriginPermissions(extension); + } + + EnableCustomElementWhiteList(); +} + +Dispatcher::~Dispatcher() { +} + +void Dispatcher::OnRenderFrameCreated(content::RenderFrame* render_frame) { + script_injection_manager_->OnRenderFrameCreated(render_frame); +} + +bool Dispatcher::IsExtensionActive(const std::string& extension_id) const { + bool is_active = + active_extension_ids_.find(extension_id) != active_extension_ids_.end(); + if (is_active) + CHECK(RendererExtensionRegistry::Get()->Contains(extension_id)); + return is_active; +} + +void Dispatcher::DidCreateScriptContext( + blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& v8_context, + int extension_group, + int world_id) { + const base::TimeTicks start_time = base::TimeTicks::Now(); + + ScriptContext* context = script_context_set_->Register( + frame, v8_context, extension_group, world_id); + + // Initialize origin permissions for content scripts, which can't be + // initialized in |OnActivateExtension|. + if (context->context_type() == Feature::CONTENT_SCRIPT_CONTEXT) + InitOriginPermissions(context->extension()); + + { + scoped_ptr<ModuleSystem> module_system( + new ModuleSystem(context, &source_map_)); + context->set_module_system(std::move(module_system)); + } + ModuleSystem* module_system = context->module_system(); + + // Enable natives in startup. + ModuleSystem::NativesEnabledScope natives_enabled_scope(module_system); + + RegisterNativeHandlers(module_system, context); + + // chrome.Event is part of the public API (although undocumented). Make it + // lazily evalulate to Event from event_bindings.js. For extensions only + // though, not all webpages! + if (context->extension()) { + v8::Local<v8::Object> chrome = AsObjectOrEmpty(GetOrCreateChrome(context)); + if (!chrome.IsEmpty()) + module_system->SetLazyField(chrome, "Event", kEventBindings, "Event"); + } + + UpdateBindingsForContext(context); + + bool is_within_platform_app = IsWithinPlatformApp(); + // Inject custom JS into the platform app context. + if (is_within_platform_app) { + module_system->Require("platformApp"); + } + + RequireGuestViewModules(context); + delegate_->RequireAdditionalModules(context, is_within_platform_app); + + const base::TimeDelta elapsed = base::TimeTicks::Now() - start_time; + switch (context->context_type()) { + case Feature::UNSPECIFIED_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_Unspecified", + elapsed); + break; + case Feature::BLESSED_EXTENSION_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_Blessed", elapsed); + break; + case Feature::UNBLESSED_EXTENSION_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_Unblessed", + elapsed); + break; + case Feature::CONTENT_SCRIPT_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_ContentScript", + elapsed); + break; + case Feature::WEB_PAGE_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_WebPage", elapsed); + break; + case Feature::BLESSED_WEB_PAGE_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_BlessedWebPage", + elapsed); + break; + case Feature::WEBUI_CONTEXT: + UMA_HISTOGRAM_TIMES("Extensions.DidCreateScriptContext_WebUI", elapsed); + break; + case Feature::SERVICE_WORKER_CONTEXT: + // Handled in DidInitializeServiceWorkerContextOnWorkerThread(). + NOTREACHED(); + break; + } + + VLOG(1) << "Num tracked contexts: " << script_context_set_->size(); +} + +// static +void Dispatcher::DidInitializeServiceWorkerContextOnWorkerThread( + v8::Local<v8::Context> v8_context, + const GURL& url) { + const base::TimeTicks start_time = base::TimeTicks::Now(); + + if (!url.SchemeIs(kExtensionScheme) && + !url.SchemeIs(kExtensionResourceScheme)) { + // Early-out if this isn't a chrome-extension:// or resource scheme, + // because looking up the extension registry is unnecessary if it's not. + // Checking this will also skip over hosted apps, which is the desired + // behavior - hosted app service workers are not our concern. + return; + } + + const Extension* extension = + RendererExtensionRegistry::Get()->GetExtensionOrAppByURL(url); + + if (!extension) { + // TODO(kalman): This is no good. Instead we need to either: + // + // - Hold onto the v8::Context and create the ScriptContext and install + // our bindings when this extension is loaded. + // - Deal with there being an extension ID (url.host()) but no + // extension associated with it, then document that getBackgroundClient + // may fail if the extension hasn't loaded yet. + // + // The former is safer, but is unfriendly to caching (e.g. session restore). + // It seems to contradict the service worker idiom. + // + // The latter is friendly to caching, but running extension code without an + // installed extension makes me nervous, and means that we won't be able to + // expose arbitrary (i.e. capability-checked) extension APIs to service + // workers. We will probably need to relax some assertions - we just need + // to find them. + // + // Perhaps this could be solved with our own event on the service worker + // saying that an extension is ready, and documenting that extension APIs + // won't work before that event has fired? + return; + } + + ScriptContext* context = new ScriptContext( + v8_context, nullptr, extension, Feature::SERVICE_WORKER_CONTEXT, + extension, Feature::SERVICE_WORKER_CONTEXT); + context->set_url(url); + + g_worker_script_context_set.Get().Insert(make_scoped_ptr(context)); + + v8::Isolate* isolate = context->isolate(); + + // Fetch the source code for service_worker_bindings.js. + base::StringPiece script_resource = + ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_SERVICE_WORKER_BINDINGS_JS); + v8::Local<v8::String> script = v8::String::NewExternal( + isolate, new StaticV8ExternalOneByteStringResource(script_resource)); + + // Run service_worker.js to get the main function. + v8::Local<v8::Function> main_function; + { + v8::Local<v8::Value> result = context->RunScript( + v8_helpers::ToV8StringUnsafe(isolate, "service_worker"), script, + base::Bind(&CrashOnException)); + CHECK(result->IsFunction()); + main_function = result.As<v8::Function>(); + } + + // Expose CHECK/DCHECK/NOTREACHED to the main function with a + // LoggingNativeHandler. Admire the neat base::Bind trick to both Invalidate + // and delete the native handler. + LoggingNativeHandler* logging = new LoggingNativeHandler(context); + context->AddInvalidationObserver( + base::Bind(&NativeHandler::Invalidate, base::Owned(logging))); + + // Execute the main function with its dependencies passed in as arguments. + v8::Local<v8::Value> args[] = { + // The extension's background URL. + v8_helpers::ToV8StringUnsafe( + isolate, BackgroundInfo::GetBackgroundURL(extension).spec()), + // The wake-event-page native function. + WakeEventPage::Get()->GetForContext(context), + // The logging module. + logging->NewInstance(), + }; + context->CallFunction(main_function, arraysize(args), args); + + const base::TimeDelta elapsed = base::TimeTicks::Now() - start_time; + UMA_HISTOGRAM_TIMES( + "Extensions.DidInitializeServiceWorkerContextOnWorkerThread", elapsed); +} + +void Dispatcher::WillReleaseScriptContext( + blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& v8_context, + int world_id) { + ScriptContext* context = script_context_set_->GetByV8Context(v8_context); + if (!context) + return; + + // TODO(kalman): Make |request_sender| use |context->AddInvalidationObserver|. + // In fact |request_sender_| should really be owned by ScriptContext. + request_sender_->InvalidateSource(context); + + script_context_set_->Remove(context); + VLOG(1) << "Num tracked contexts: " << script_context_set_->size(); +} + +// static +void Dispatcher::WillDestroyServiceWorkerContextOnWorkerThread( + v8::Local<v8::Context> v8_context, + const GURL& url) { + if (url.SchemeIs(kExtensionScheme) || + url.SchemeIs(kExtensionResourceScheme)) { + // See comment in DidInitializeServiceWorkerContextOnWorkerThread. + g_worker_script_context_set.Get().Remove(v8_context, url); + } +} + +void Dispatcher::DidCreateDocumentElement(blink::WebLocalFrame* frame) { + // Note: use GetEffectiveDocumentURL not just frame->document()->url() + // so that this also injects the stylesheet on about:blank frames that + // are hosted in the extension process. + GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL( + frame, frame->document().url(), true /* match_about_blank */); + + const Extension* extension = + RendererExtensionRegistry::Get()->GetExtensionOrAppByURL( + effective_document_url); + + if (extension && + (extension->is_extension() || extension->is_platform_app())) { + int resource_id = extension->is_platform_app() ? IDR_PLATFORM_APP_CSS + : IDR_EXTENSION_FONTS_CSS; + std::string stylesheet = ResourceBundle::GetSharedInstance() + .GetRawDataResource(resource_id) + .as_string(); + base::ReplaceFirstSubstringAfterOffset( + &stylesheet, 0, "$FONTFAMILY", system_font_family_); + base::ReplaceFirstSubstringAfterOffset( + &stylesheet, 0, "$FONTSIZE", system_font_size_); + + // Blink doesn't let us define an additional user agent stylesheet, so + // we insert the default platform app or extension stylesheet into all + // documents that are loaded in each app or extension. + frame->document().insertStyleSheet(WebString::fromUTF8(stylesheet)); + } + + // If this is an extension options page, and the extension has opted into + // using Chrome styles, then insert the Chrome extension stylesheet. + if (extension && extension->is_extension() && + OptionsPageInfo::ShouldUseChromeStyle(extension) && + effective_document_url == OptionsPageInfo::GetOptionsPage(extension)) { + frame->document().insertStyleSheet( + WebString::fromUTF8(ResourceBundle::GetSharedInstance() + .GetRawDataResource(IDR_EXTENSION_CSS) + .as_string())); + } + + // In testing, the document lifetime events can happen after the render + // process shutdown event. + // See: http://crbug.com/21508 and http://crbug.com/500851 + if (content_watcher_) { + content_watcher_->DidCreateDocumentElement(frame); + } +} + +void Dispatcher::RunScriptsAtDocumentStart(content::RenderFrame* render_frame) { + ExtensionFrameHelper* frame_helper = ExtensionFrameHelper::Get(render_frame); + if (!frame_helper) + return; // The frame is invisible to extensions. + + frame_helper->RunScriptsAtDocumentStart(); + // |frame_helper| and |render_frame| might be dead by now. +} + +void Dispatcher::RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) { + ExtensionFrameHelper* frame_helper = ExtensionFrameHelper::Get(render_frame); + if (!frame_helper) + return; // The frame is invisible to extensions. + + frame_helper->RunScriptsAtDocumentEnd(); + // |frame_helper| and |render_frame| might be dead by now. +} + +void Dispatcher::OnExtensionResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error) { + request_sender_->HandleResponse(request_id, success, response, error); +} + +void Dispatcher::DispatchEvent(const std::string& extension_id, + const std::string& event_name) const { + base::ListValue args; + args.Set(0, new base::StringValue(event_name)); + args.Set(1, new base::ListValue()); + + // Needed for Windows compilation, since kEventBindings is declared extern. + const char* local_event_bindings = kEventBindings; + script_context_set_->ForEach( + extension_id, base::Bind(&CallModuleMethod, local_event_bindings, + kEventDispatchFunction, &args)); +} + +void Dispatcher::InvokeModuleSystemMethod(content::RenderFrame* render_frame, + const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture) { + scoped_ptr<WebScopedUserGesture> web_user_gesture; + if (user_gesture) + web_user_gesture.reset(new WebScopedUserGesture); + + script_context_set_->ForEach( + extension_id, render_frame, + base::Bind(&CallModuleMethod, module_name, function_name, &args)); + + // Reset the idle handler each time there's any activity like event or message + // dispatch, for which Invoke is the chokepoint. + if (set_idle_notifications_) { + RenderThread::Get()->ScheduleIdleHandler( + kInitialExtensionIdleHandlerDelayMs); + } + + // Tell the browser process when an event has been dispatched with a lazy + // background page active. + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + if (extension && BackgroundInfo::HasLazyBackgroundPage(extension) && + module_name == kEventBindings && + function_name == kEventDispatchFunction) { + content::RenderFrame* background_frame = + ExtensionFrameHelper::GetBackgroundPageFrame(extension_id); + if (background_frame) { + int message_id; + args.GetInteger(3, &message_id); + background_frame->Send(new ExtensionHostMsg_EventAck( + background_frame->GetRoutingID(), message_id)); + } + } +} + +void Dispatcher::ClearPortData(int port_id) { + // Only the target port side has entries in |port_to_tab_id_map_|. If + // |port_id| is a source port, std::map::erase() will just silently fail + // here as a no-op. + port_to_tab_id_map_.erase(port_id); +} + +// static +std::vector<std::pair<std::string, int> > Dispatcher::GetJsResources() { + std::vector<std::pair<std::string, int> > resources; + + // Libraries. + resources.push_back(std::make_pair("appView", IDR_APP_VIEW_JS)); + resources.push_back(std::make_pair("entryIdManager", IDR_ENTRY_ID_MANAGER)); + resources.push_back(std::make_pair(kEventBindings, IDR_EVENT_BINDINGS_JS)); + resources.push_back(std::make_pair("extensionOptions", + IDR_EXTENSION_OPTIONS_JS)); + resources.push_back(std::make_pair("extensionOptionsAttributes", + IDR_EXTENSION_OPTIONS_ATTRIBUTES_JS)); + resources.push_back(std::make_pair("extensionOptionsConstants", + IDR_EXTENSION_OPTIONS_CONSTANTS_JS)); + resources.push_back(std::make_pair("extensionOptionsEvents", + IDR_EXTENSION_OPTIONS_EVENTS_JS)); + resources.push_back(std::make_pair("extensionView", IDR_EXTENSION_VIEW_JS)); + resources.push_back(std::make_pair("extensionViewApiMethods", + IDR_EXTENSION_VIEW_API_METHODS_JS)); + resources.push_back(std::make_pair("extensionViewAttributes", + IDR_EXTENSION_VIEW_ATTRIBUTES_JS)); + resources.push_back(std::make_pair("extensionViewConstants", + IDR_EXTENSION_VIEW_CONSTANTS_JS)); + resources.push_back(std::make_pair("extensionViewEvents", + IDR_EXTENSION_VIEW_EVENTS_JS)); + resources.push_back(std::make_pair( + "extensionViewInternal", IDR_EXTENSION_VIEW_INTERNAL_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("guestView", IDR_GUEST_VIEW_JS)); + resources.push_back(std::make_pair("guestViewAttributes", + IDR_GUEST_VIEW_ATTRIBUTES_JS)); + resources.push_back(std::make_pair("guestViewContainer", + IDR_GUEST_VIEW_CONTAINER_JS)); + resources.push_back(std::make_pair("guestViewDeny", IDR_GUEST_VIEW_DENY_JS)); + resources.push_back(std::make_pair("guestViewEvents", + IDR_GUEST_VIEW_EVENTS_JS)); + + if (content::BrowserPluginGuestMode::UseCrossProcessFramesForGuests()) { + resources.push_back(std::make_pair("guestViewIframe", + IDR_GUEST_VIEW_IFRAME_JS)); + resources.push_back(std::make_pair("guestViewIframeContainer", + IDR_GUEST_VIEW_IFRAME_CONTAINER_JS)); + } + + resources.push_back(std::make_pair("imageUtil", IDR_IMAGE_UTIL_JS)); + resources.push_back(std::make_pair("json_schema", IDR_JSON_SCHEMA_JS)); + resources.push_back(std::make_pair("lastError", IDR_LAST_ERROR_JS)); + resources.push_back(std::make_pair("messaging", IDR_MESSAGING_JS)); + resources.push_back(std::make_pair("messaging_utils", + IDR_MESSAGING_UTILS_JS)); + resources.push_back(std::make_pair(kSchemaUtils, IDR_SCHEMA_UTILS_JS)); + resources.push_back(std::make_pair("sendRequest", IDR_SEND_REQUEST_JS)); + resources.push_back(std::make_pair("setIcon", IDR_SET_ICON_JS)); + resources.push_back(std::make_pair("test", IDR_TEST_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("test_environment_specific_bindings", + IDR_BROWSER_TEST_ENVIRONMENT_SPECIFIC_BINDINGS_JS)); + resources.push_back(std::make_pair("uncaught_exception_handler", + IDR_UNCAUGHT_EXCEPTION_HANDLER_JS)); + resources.push_back(std::make_pair("utils", IDR_UTILS_JS)); + resources.push_back(std::make_pair("webRequest", + IDR_WEB_REQUEST_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("webRequestInternal", + IDR_WEB_REQUEST_INTERNAL_CUSTOM_BINDINGS_JS)); + // Note: webView not webview so that this doesn't interfere with the + // chrome.webview API bindings. + resources.push_back(std::make_pair("webView", IDR_WEB_VIEW_JS)); + resources.push_back(std::make_pair("webViewActionRequests", + IDR_WEB_VIEW_ACTION_REQUESTS_JS)); + resources.push_back(std::make_pair("webViewApiMethods", + IDR_WEB_VIEW_API_METHODS_JS)); + resources.push_back(std::make_pair("webViewAttributes", + IDR_WEB_VIEW_ATTRIBUTES_JS)); + resources.push_back(std::make_pair("webViewConstants", + IDR_WEB_VIEW_CONSTANTS_JS)); + resources.push_back(std::make_pair("webViewEvents", IDR_WEB_VIEW_EVENTS_JS)); + resources.push_back(std::make_pair("webViewInternal", + IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("webViewExperimental", IDR_WEB_VIEW_EXPERIMENTAL_JS)); + resources.push_back( + std::make_pair(mojo::kBindingsModuleName, IDR_MOJO_BINDINGS_JS)); + resources.push_back( + std::make_pair(mojo::kBufferModuleName, IDR_MOJO_BUFFER_JS)); + resources.push_back( + std::make_pair(mojo::kCodecModuleName, IDR_MOJO_CODEC_JS)); + resources.push_back( + std::make_pair(mojo::kConnectionModuleName, IDR_MOJO_CONNECTION_JS)); + resources.push_back( + std::make_pair(mojo::kConnectorModuleName, IDR_MOJO_CONNECTOR_JS)); + resources.push_back( + std::make_pair(mojo::kRouterModuleName, IDR_MOJO_ROUTER_JS)); + resources.push_back( + std::make_pair(mojo::kUnicodeModuleName, IDR_MOJO_UNICODE_JS)); + resources.push_back( + std::make_pair(mojo::kValidatorModuleName, IDR_MOJO_VALIDATOR_JS)); + resources.push_back(std::make_pair("async_waiter", IDR_ASYNC_WAITER_JS)); + resources.push_back(std::make_pair("data_receiver", IDR_DATA_RECEIVER_JS)); + resources.push_back(std::make_pair("data_sender", IDR_DATA_SENDER_JS)); + resources.push_back(std::make_pair("keep_alive", IDR_KEEP_ALIVE_JS)); + resources.push_back(std::make_pair("extensions/common/mojo/keep_alive.mojom", + IDR_KEEP_ALIVE_MOJOM_JS)); + resources.push_back(std::make_pair("device/serial/data_stream.mojom", + IDR_DATA_STREAM_MOJOM_JS)); + resources.push_back( + std::make_pair("device/serial/data_stream_serialization.mojom", + IDR_DATA_STREAM_SERIALIZATION_MOJOM_JS)); + resources.push_back(std::make_pair("stash_client", IDR_STASH_CLIENT_JS)); + resources.push_back( + std::make_pair("extensions/common/mojo/stash.mojom", IDR_STASH_MOJOM_JS)); + + // Custom bindings. + resources.push_back( + std::make_pair("app.runtime", IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("app.window", IDR_APP_WINDOW_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("declarativeWebRequest", + IDR_DECLARATIVE_WEBREQUEST_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("displaySource", + IDR_DISPLAY_SOURCE_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("contextMenus", IDR_CONTEXT_MENUS_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("contextMenusHandlers", IDR_CONTEXT_MENUS_HANDLERS_JS)); + resources.push_back( + std::make_pair("extension", IDR_EXTENSION_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("i18n", IDR_I18N_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair( + "mimeHandlerPrivate", IDR_MIME_HANDLER_PRIVATE_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("extensions/common/api/mime_handler.mojom", + IDR_MIME_HANDLER_MOJOM_JS)); + resources.push_back( + std::make_pair("mojoPrivate", IDR_MOJO_PRIVATE_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("permissions", IDR_PERMISSIONS_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("printerProvider", + IDR_PRINTER_PROVIDER_CUSTOM_BINDINGS_JS)); + resources.push_back( + std::make_pair("runtime", IDR_RUNTIME_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("windowControls", IDR_WINDOW_CONTROLS_JS)); + resources.push_back( + std::make_pair("webViewRequest", + IDR_WEB_VIEW_REQUEST_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("binding", IDR_BINDING_JS)); + + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableMojoSerialService)) { + resources.push_back( + std::make_pair("serial", IDR_SERIAL_CUSTOM_BINDINGS_JS)); + } + resources.push_back(std::make_pair("serial_service", IDR_SERIAL_SERVICE_JS)); + resources.push_back( + std::make_pair("device/serial/serial.mojom", IDR_SERIAL_MOJOM_JS)); + resources.push_back(std::make_pair("device/serial/serial_serialization.mojom", + IDR_SERIAL_SERIALIZATION_MOJOM_JS)); + + // Custom types sources. + resources.push_back(std::make_pair("StorageArea", IDR_STORAGE_AREA_JS)); + + // Platform app sources that are not API-specific.. + resources.push_back(std::make_pair("platformApp", IDR_PLATFORM_APP_JS)); + +#if defined(ENABLE_MEDIA_ROUTER) + resources.push_back( + std::make_pair("chrome/browser/media/router/mojo/media_router.mojom", + IDR_MEDIA_ROUTER_MOJOM_JS)); + resources.push_back( + std::make_pair("media_router_bindings", IDR_MEDIA_ROUTER_BINDINGS_JS)); +#endif // defined(ENABLE_MEDIA_ROUTER) + + return resources; +} + +// NOTE: please use the naming convention "foo_natives" for these. +// static +void Dispatcher::RegisterNativeHandlers(ModuleSystem* module_system, + ScriptContext* context, + Dispatcher* dispatcher, + RequestSender* request_sender, + V8SchemaRegistry* v8_schema_registry) { + module_system->RegisterNativeHandler( + "chrome", scoped_ptr<NativeHandler>(new ChromeNativeHandler(context))); + module_system->RegisterNativeHandler( + "lazy_background_page", + scoped_ptr<NativeHandler>(new LazyBackgroundPageNativeHandler(context))); + module_system->RegisterNativeHandler( + "logging", scoped_ptr<NativeHandler>(new LoggingNativeHandler(context))); + module_system->RegisterNativeHandler("schema_registry", + v8_schema_registry->AsNativeHandler()); + module_system->RegisterNativeHandler( + "print", scoped_ptr<NativeHandler>(new PrintNativeHandler(context))); + module_system->RegisterNativeHandler( + "test_features", + scoped_ptr<NativeHandler>(new TestFeaturesNativeHandler(context))); + module_system->RegisterNativeHandler( + "test_native_handler", + scoped_ptr<NativeHandler>(new TestNativeHandler(context))); + module_system->RegisterNativeHandler( + "user_gestures", + scoped_ptr<NativeHandler>(new UserGesturesNativeHandler(context))); + module_system->RegisterNativeHandler( + "utils", scoped_ptr<NativeHandler>(new UtilsNativeHandler(context))); + module_system->RegisterNativeHandler( + "v8_context", + scoped_ptr<NativeHandler>(new V8ContextNativeHandler(context))); + module_system->RegisterNativeHandler( + "event_natives", scoped_ptr<NativeHandler>(new EventBindings(context))); + module_system->RegisterNativeHandler( + "messaging_natives", + scoped_ptr<NativeHandler>(MessagingBindings::Get(dispatcher, context))); + module_system->RegisterNativeHandler( + "apiDefinitions", + scoped_ptr<NativeHandler>( + new ApiDefinitionsNatives(dispatcher, context))); + module_system->RegisterNativeHandler( + "sendRequest", + scoped_ptr<NativeHandler>( + new SendRequestNatives(request_sender, context))); + module_system->RegisterNativeHandler( + "setIcon", + scoped_ptr<NativeHandler>(new SetIconNatives(context))); + module_system->RegisterNativeHandler( + "activityLogger", + scoped_ptr<NativeHandler>(new APIActivityLogger(context))); + module_system->RegisterNativeHandler( + "renderFrameObserverNatives", + scoped_ptr<NativeHandler>(new RenderFrameObserverNatives(context))); + + // Natives used by multiple APIs. + module_system->RegisterNativeHandler( + "file_system_natives", + scoped_ptr<NativeHandler>(new FileSystemNatives(context))); + + // Custom bindings. + module_system->RegisterNativeHandler( + "app_window_natives", + scoped_ptr<NativeHandler>(new AppWindowCustomBindings(context))); + module_system->RegisterNativeHandler( + "blob_natives", + scoped_ptr<NativeHandler>(new BlobNativeHandler(context))); + module_system->RegisterNativeHandler( + "context_menus", + scoped_ptr<NativeHandler>(new ContextMenusCustomBindings(context))); + module_system->RegisterNativeHandler( + "css_natives", scoped_ptr<NativeHandler>(new CssNativeHandler(context))); + module_system->RegisterNativeHandler( + "document_natives", + scoped_ptr<NativeHandler>(new DocumentCustomBindings(context))); + module_system->RegisterNativeHandler( + "guest_view_internal", + scoped_ptr<NativeHandler>( + new GuestViewInternalCustomBindings(context))); + module_system->RegisterNativeHandler( + "i18n", scoped_ptr<NativeHandler>(new I18NCustomBindings(context))); + module_system->RegisterNativeHandler( + "id_generator", + scoped_ptr<NativeHandler>(new IdGeneratorCustomBindings(context))); + module_system->RegisterNativeHandler( + "runtime", scoped_ptr<NativeHandler>(new RuntimeCustomBindings(context))); + module_system->RegisterNativeHandler( + "display_source", + scoped_ptr<NativeHandler>(new DisplaySourceCustomBindings(context))); +} + +bool Dispatcher::OnControlMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(Dispatcher, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_ActivateExtension, OnActivateExtension) + IPC_MESSAGE_HANDLER(ExtensionMsg_CancelSuspend, OnCancelSuspend) + IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnDeliverMessage) + IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnConnect, OnDispatchOnConnect) + IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnDisconnect, OnDispatchOnDisconnect) + IPC_MESSAGE_HANDLER(ExtensionMsg_Loaded, OnLoaded) + IPC_MESSAGE_HANDLER(ExtensionMsg_MessageInvoke, OnMessageInvoke) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetChannel, OnSetChannel) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetScriptingWhitelist, + OnSetScriptingWhitelist) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetSystemFont, OnSetSystemFont) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetWebViewPartitionID, + OnSetWebViewPartitionID) + IPC_MESSAGE_HANDLER(ExtensionMsg_ShouldSuspend, OnShouldSuspend) + IPC_MESSAGE_HANDLER(ExtensionMsg_Suspend, OnSuspend) + IPC_MESSAGE_HANDLER(ExtensionMsg_TransferBlobs, OnTransferBlobs) + IPC_MESSAGE_HANDLER(ExtensionMsg_Unloaded, OnUnloaded) + IPC_MESSAGE_HANDLER(ExtensionMsg_UpdatePermissions, OnUpdatePermissions) + IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateTabSpecificPermissions, + OnUpdateTabSpecificPermissions) + IPC_MESSAGE_HANDLER(ExtensionMsg_ClearTabSpecificPermissions, + OnClearTabSpecificPermissions) + IPC_MESSAGE_HANDLER(ExtensionMsg_UsingWebRequestAPI, OnUsingWebRequestAPI) + IPC_MESSAGE_FORWARD(ExtensionMsg_WatchPages, + content_watcher_.get(), + ContentWatcher::OnWatchPages) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + + return handled; +} +void Dispatcher::IdleNotification() { + if (set_idle_notifications_ && forced_idle_timer_) { + // Dampen the forced delay as well if the extension stays idle for long + // periods of time. (forced_idle_timer_ can be NULL after + // OnRenderProcessShutdown has been called.) + int64_t forced_delay_ms = + std::max(RenderThread::Get()->GetIdleNotificationDelayInMs(), + kMaxExtensionIdleHandlerDelayMs); + forced_idle_timer_->Stop(); + forced_idle_timer_->Start( + FROM_HERE, + base::TimeDelta::FromMilliseconds(forced_delay_ms), + RenderThread::Get(), + &RenderThread::IdleHandler); + } +} + +void Dispatcher::OnRenderProcessShutdown() { + v8_schema_registry_.reset(); + forced_idle_timer_.reset(); + content_watcher_.reset(); + script_context_set_->ForEach( + std::string(), nullptr, + base::Bind(&ScriptContextSet::Remove, + base::Unretained(script_context_set_.get()))); +} + +void Dispatcher::OnActivateExtension(const std::string& extension_id) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + if (!extension) { + // Extension was activated but was never loaded. This probably means that + // the renderer failed to load it (or the browser failed to tell us when it + // did). Failures shouldn't happen, but instead of crashing there (which + // executes on all renderers) be conservative and only crash in the renderer + // of the extension which failed to load; this one. + std::string& error = extension_load_errors_[extension_id]; + char minidump[256]; + base::debug::Alias(&minidump); + base::snprintf(minidump, + arraysize(minidump), + "e::dispatcher:%s:%s", + extension_id.c_str(), + error.c_str()); + LOG(FATAL) << extension_id << " was never loaded: " << error; + } + + active_extension_ids_.insert(extension_id); + + // This is called when starting a new extension page, so start the idle + // handler ticking. + RenderThread::Get()->ScheduleIdleHandler(kInitialExtensionIdleHandlerDelayMs); + + DOMActivityLogger::AttachToWorld( + DOMActivityLogger::kMainWorldId, extension_id); + + InitOriginPermissions(extension); + + UpdateActiveExtensions(); +} + +void Dispatcher::OnCancelSuspend(const std::string& extension_id) { + DispatchEvent(extension_id, kOnSuspendCanceledEvent); +} + +void Dispatcher::OnDeliverMessage(int target_port_id, const Message& message) { + scoped_ptr<RequestSender::ScopedTabID> scoped_tab_id; + std::map<int, int>::const_iterator it = + port_to_tab_id_map_.find(target_port_id); + if (it != port_to_tab_id_map_.end()) { + scoped_tab_id.reset( + new RequestSender::ScopedTabID(request_sender(), it->second)); + } + + MessagingBindings::DeliverMessage(*script_context_set_, target_port_id, + message, + NULL); // All render frames. +} + +void Dispatcher::OnDispatchOnConnect( + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id) { + DCHECK(!ContainsKey(port_to_tab_id_map_, target_port_id)); + DCHECK_EQ(1, target_port_id % 2); // target renderer ports have odd IDs. + int sender_tab_id = -1; + source.tab.GetInteger("id", &sender_tab_id); + port_to_tab_id_map_[target_port_id] = sender_tab_id; + + MessagingBindings::DispatchOnConnect(*script_context_set_, target_port_id, + channel_name, source, info, + tls_channel_id, + NULL); // All render frames. +} + +void Dispatcher::OnDispatchOnDisconnect(int port_id, + const std::string& error_message) { + MessagingBindings::DispatchOnDisconnect(*script_context_set_, port_id, + error_message, + NULL); // All render frames. +} + +void Dispatcher::OnLoaded( + const std::vector<ExtensionMsg_Loaded_Params>& loaded_extensions) { + for (const auto& param : loaded_extensions) { + std::string error; + scoped_refptr<const Extension> extension = param.ConvertToExtension(&error); + if (!extension.get()) { + NOTREACHED() << error; + // Note: in tests |param.id| has been observed to be empty (see comment + // just below) so this isn't all that reliable. + extension_load_errors_[param.id] = error; + continue; + } + + RendererExtensionRegistry* extension_registry = + RendererExtensionRegistry::Get(); + // TODO(kalman): This test is deliberately not a CHECK (though I wish it + // could be) and uses extension->id() not params.id: + // 1. For some reason params.id can be empty. I've only seen it with + // the webstore extension, in tests, and I've spent some time trying to + // figure out why - but cost/benefit won. + // 2. The browser only sends this IPC to RenderProcessHosts once, but the + // Dispatcher is attached to a RenderThread. Presumably there is a + // mismatch there. In theory one would think it's possible for the + // browser to figure this out itself - but again, cost/benefit. + if (!extension_registry->Contains(extension->id())) + extension_registry->Insert(extension); + } + + // Update the available bindings for all contexts. These may have changed if + // an externally_connectable extension was loaded that can connect to an + // open webpage. + UpdateBindings(""); +} + +void Dispatcher::OnMessageInvoke(const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture) { + InvokeModuleSystemMethod( + NULL, extension_id, module_name, function_name, args, user_gesture); +} + +void Dispatcher::OnSetChannel(int channel) { + delegate_->SetChannel(channel); +} + +void Dispatcher::OnSetScriptingWhitelist( + const ExtensionsClient::ScriptingWhitelist& extension_ids) { + ExtensionsClient::Get()->SetScriptingWhitelist(extension_ids); +} + +void Dispatcher::OnSetSystemFont(const std::string& font_family, + const std::string& font_size) { + system_font_family_ = font_family; + system_font_size_ = font_size; +} + +void Dispatcher::OnSetWebViewPartitionID(const std::string& partition_id) { + // |webview_partition_id_| cannot be changed once set. + CHECK(webview_partition_id_.empty() || webview_partition_id_ == partition_id); + webview_partition_id_ = partition_id; +} + +void Dispatcher::OnShouldSuspend(const std::string& extension_id, + uint64_t sequence_id) { + RenderThread::Get()->Send( + new ExtensionHostMsg_ShouldSuspendAck(extension_id, sequence_id)); +} + +void Dispatcher::OnSuspend(const std::string& extension_id) { + // Dispatch the suspend event. This doesn't go through the standard event + // dispatch machinery because it requires special handling. We need to let + // the browser know when we are starting and stopping the event dispatch, so + // that it still considers the extension idle despite any activity the suspend + // event creates. + DispatchEvent(extension_id, kOnSuspendEvent); + RenderThread::Get()->Send(new ExtensionHostMsg_SuspendAck(extension_id)); +} + +void Dispatcher::OnTransferBlobs(const std::vector<std::string>& blob_uuids) { + RenderThread::Get()->Send(new ExtensionHostMsg_TransferBlobsAck(blob_uuids)); +} + +void Dispatcher::OnUnloaded(const std::string& id) { + // See comment in OnLoaded for why it would be nice, but perhaps incorrect, + // to CHECK here rather than guarding. + if (!RendererExtensionRegistry::Get()->Remove(id)) + return; + + active_extension_ids_.erase(id); + + script_injection_manager_->OnExtensionUnloaded(id); + + // If the extension is later reloaded with a different set of permissions, + // we'd like it to get a new isolated world ID, so that it can pick up the + // changed origin whitelist. + ScriptInjection::RemoveIsolatedWorld(id); + + // Invalidate all of the contexts that were removed. + // TODO(kalman): add an invalidation observer interface to ScriptContext. + std::set<ScriptContext*> removed_contexts = + script_context_set_->OnExtensionUnloaded(id); + for (ScriptContext* context : removed_contexts) { + request_sender_->InvalidateSource(context); + } + + // Update the available bindings for the remaining contexts. These may have + // changed if an externally_connectable extension is unloaded and a webpage + // is no longer accessible. + UpdateBindings(""); + + // Invalidates the messages map for the extension in case the extension is + // reloaded with a new messages map. + EraseL10nMessagesMap(id); + + // We don't do anything with existing platform-app stylesheets. They will + // stay resident, but the URL pattern corresponding to the unloaded + // extension's URL just won't match anything anymore. +} + +void Dispatcher::OnUpdatePermissions( + const ExtensionMsg_UpdatePermissions_Params& params) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(params.extension_id); + if (!extension) + return; + + scoped_ptr<const PermissionSet> active = + params.active_permissions.ToPermissionSet(); + scoped_ptr<const PermissionSet> withheld = + params.withheld_permissions.ToPermissionSet(); + + UpdateOriginPermissions( + extension->url(), + extension->permissions_data()->GetEffectiveHostPermissions(), + active->effective_hosts()); + + extension->permissions_data()->SetPermissions(std::move(active), + std::move(withheld)); + UpdateBindings(extension->id()); +} + +void Dispatcher::OnUpdateTabSpecificPermissions(const GURL& visible_url, + const std::string& extension_id, + const URLPatternSet& new_hosts, + bool update_origin_whitelist, + int tab_id) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + if (!extension) + return; + + URLPatternSet old_effective = + extension->permissions_data()->GetEffectiveHostPermissions(); + extension->permissions_data()->UpdateTabSpecificPermissions( + tab_id, + extensions::PermissionSet(extensions::APIPermissionSet(), + extensions::ManifestPermissionSet(), new_hosts, + extensions::URLPatternSet())); + + if (update_origin_whitelist) { + UpdateOriginPermissions( + extension->url(), + old_effective, + extension->permissions_data()->GetEffectiveHostPermissions()); + } +} + +void Dispatcher::OnClearTabSpecificPermissions( + const std::vector<std::string>& extension_ids, + bool update_origin_whitelist, + int tab_id) { + for (const std::string& id : extension_ids) { + const Extension* extension = RendererExtensionRegistry::Get()->GetByID(id); + if (extension) { + URLPatternSet old_effective = + extension->permissions_data()->GetEffectiveHostPermissions(); + extension->permissions_data()->ClearTabSpecificPermissions(tab_id); + if (update_origin_whitelist) { + UpdateOriginPermissions( + extension->url(), + old_effective, + extension->permissions_data()->GetEffectiveHostPermissions()); + } + } + } +} + +void Dispatcher::OnUsingWebRequestAPI(bool webrequest_used) { + webrequest_used_ = webrequest_used; +} + +void Dispatcher::OnUserScriptsUpdated(const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) { + UpdateActiveExtensions(); +} + +void Dispatcher::UpdateActiveExtensions() { + std::set<std::string> active_extensions = active_extension_ids_; + user_script_set_manager_->GetAllActiveExtensionIds(&active_extensions); + delegate_->OnActiveExtensionsUpdated(active_extensions); +} + +void Dispatcher::InitOriginPermissions(const Extension* extension) { + delegate_->InitOriginPermissions(extension, + IsExtensionActive(extension->id())); + UpdateOriginPermissions( + extension->url(), + URLPatternSet(), // No old permissions. + extension->permissions_data()->GetEffectiveHostPermissions()); +} + +void Dispatcher::UpdateOriginPermissions(const GURL& extension_url, + const URLPatternSet& old_patterns, + const URLPatternSet& new_patterns) { + static const char* kSchemes[] = { + url::kHttpScheme, + url::kHttpsScheme, + url::kFileScheme, + content::kChromeUIScheme, + url::kFtpScheme, +#if defined(OS_CHROMEOS) + content::kExternalFileScheme, +#endif + extensions::kExtensionScheme, + }; + for (size_t i = 0; i < arraysize(kSchemes); ++i) { + const char* scheme = kSchemes[i]; + // Remove all old patterns... + for (URLPatternSet::const_iterator pattern = old_patterns.begin(); + pattern != old_patterns.end(); ++pattern) { + if (pattern->MatchesScheme(scheme)) { + WebSecurityPolicy::removeOriginAccessWhitelistEntry( + extension_url, + WebString::fromUTF8(scheme), + WebString::fromUTF8(pattern->host()), + pattern->match_subdomains()); + } + } + // ...And add the new ones. + for (URLPatternSet::const_iterator pattern = new_patterns.begin(); + pattern != new_patterns.end(); ++pattern) { + if (pattern->MatchesScheme(scheme)) { + WebSecurityPolicy::addOriginAccessWhitelistEntry( + extension_url, + WebString::fromUTF8(scheme), + WebString::fromUTF8(pattern->host()), + pattern->match_subdomains()); + } + } + } +} + +void Dispatcher::EnableCustomElementWhiteList() { + blink::WebCustomElement::addEmbedderCustomElementName("appview"); + blink::WebCustomElement::addEmbedderCustomElementName("appviewbrowserplugin"); + blink::WebCustomElement::addEmbedderCustomElementName("extensionoptions"); + blink::WebCustomElement::addEmbedderCustomElementName( + "extensionoptionsbrowserplugin"); + blink::WebCustomElement::addEmbedderCustomElementName("extensionview"); + blink::WebCustomElement::addEmbedderCustomElementName( + "extensionviewbrowserplugin"); + blink::WebCustomElement::addEmbedderCustomElementName("webview"); + blink::WebCustomElement::addEmbedderCustomElementName("webviewbrowserplugin"); +} + +void Dispatcher::UpdateBindings(const std::string& extension_id) { + script_context_set().ForEach(extension_id, + base::Bind(&Dispatcher::UpdateBindingsForContext, + base::Unretained(this))); +} + +void Dispatcher::UpdateBindingsForContext(ScriptContext* context) { + v8::HandleScope handle_scope(context->isolate()); + v8::Context::Scope context_scope(context->v8_context()); + + // TODO(kalman): Make the bindings registration have zero overhead then run + // the same code regardless of context type. + switch (context->context_type()) { + case Feature::UNSPECIFIED_CONTEXT: + case Feature::WEB_PAGE_CONTEXT: + case Feature::BLESSED_WEB_PAGE_CONTEXT: + // Hard-code registration of any APIs that are exposed to webpage-like + // contexts, because it's too expensive to run the full bindings code. + // All of the same permission checks will still apply. + if (context->GetAvailability("app").is_available()) + RegisterBinding("app", context); + if (context->GetAvailability("webstore").is_available()) + RegisterBinding("webstore", context); + if (context->GetAvailability("dashboardPrivate").is_available()) + RegisterBinding("dashboardPrivate", context); + if (IsRuntimeAvailableToContext(context)) + RegisterBinding("runtime", context); + UpdateContentCapabilities(context); + break; + + case Feature::BLESSED_EXTENSION_CONTEXT: + case Feature::UNBLESSED_EXTENSION_CONTEXT: + case Feature::CONTENT_SCRIPT_CONTEXT: + case Feature::WEBUI_CONTEXT: { + // Extension context; iterate through all the APIs and bind the available + // ones. + const FeatureProvider* api_feature_provider = + FeatureProvider::GetAPIFeatures(); + for (const auto& map_entry : api_feature_provider->GetAllFeatures()) { + // Internal APIs are included via require(api_name) from internal code + // rather than chrome[api_name]. + if (map_entry.second->IsInternal()) + continue; + + // If this API has a parent feature (and isn't marked 'noparent'), + // then this must be a function or event, so we should not register. + if (api_feature_provider->GetParent(map_entry.second.get()) != nullptr) + continue; + + // Skip chrome.test if this isn't a test. + if (map_entry.first == "test" && + !base::CommandLine::ForCurrentProcess()->HasSwitch( + ::switches::kTestType)) { + continue; + } + + if (context->IsAnyFeatureAvailableToContext(*map_entry.second.get())) + RegisterBinding(map_entry.first, context); + } + break; + } + case Feature::SERVICE_WORKER_CONTEXT: + // Handled in DidInitializeServiceWorkerContextOnWorkerThread(). + NOTREACHED(); + break; + } +} + +void Dispatcher::RegisterBinding(const std::string& api_name, + ScriptContext* context) { + std::string bind_name; + v8::Local<v8::Object> bind_object = + GetOrCreateBindObjectIfAvailable(api_name, &bind_name, context); + + // Empty if the bind object failed to be created, probably because the + // extension overrode chrome with a non-object, e.g. window.chrome = true. + if (bind_object.IsEmpty()) + return; + + v8::Local<v8::String> v8_bind_name = + v8::String::NewFromUtf8(context->isolate(), bind_name.c_str()); + if (bind_object->HasRealNamedProperty(v8_bind_name)) { + // The bind object may already have the property if the API has been + // registered before (or if the extension has put something there already, + // but, whatevs). + // + // In the former case, we need to re-register the bindings for the APIs + // which the extension now has permissions for (if any), but not touch any + // others so that we don't destroy state such as event listeners. + // + // TODO(kalman): Only register available APIs to make this all moot. + if (bind_object->HasRealNamedCallbackProperty(v8_bind_name)) + return; // lazy binding still there, nothing to do + if (bind_object->Get(v8_bind_name)->IsObject()) + return; // binding has already been fully installed + } + + ModuleSystem* module_system = context->module_system(); + if (!source_map_.Contains(api_name)) { + module_system->RegisterNativeHandler( + api_name, + scoped_ptr<NativeHandler>(new BindingGeneratingNativeHandler( + context, api_name, "binding"))); + module_system->SetNativeLazyField( + bind_object, bind_name, api_name, "binding"); + } else { + module_system->SetLazyField(bind_object, bind_name, api_name, "binding"); + } +} + +// NOTE: please use the naming convention "foo_natives" for these. +void Dispatcher::RegisterNativeHandlers(ModuleSystem* module_system, + ScriptContext* context) { + RegisterNativeHandlers(module_system, + context, + this, + request_sender_.get(), + v8_schema_registry_.get()); + const Extension* extension = context->extension(); + int manifest_version = extension ? extension->manifest_version() : 1; + bool is_component_extension = + extension && Manifest::IsComponentLocation(extension->location()); + bool send_request_disabled = + (extension && Manifest::IsUnpackedLocation(extension->location()) && + BackgroundInfo::HasLazyBackgroundPage(extension)); + module_system->RegisterNativeHandler( + "process", + scoped_ptr<NativeHandler>(new ProcessInfoNativeHandler( + context, + context->GetExtensionID(), + context->GetContextTypeDescription(), + ExtensionsRendererClient::Get()->IsIncognitoProcess(), + is_component_extension, + manifest_version, + send_request_disabled))); + + delegate_->RegisterNativeHandlers(this, module_system, context); +} + +bool Dispatcher::IsRuntimeAvailableToContext(ScriptContext* context) { + for (const auto& extension : + *RendererExtensionRegistry::Get()->GetMainThreadExtensionSet()) { + ExternallyConnectableInfo* info = static_cast<ExternallyConnectableInfo*>( + extension->GetManifestData(manifest_keys::kExternallyConnectable)); + if (info && info->matches.MatchesURL(context->url())) + return true; + } + return false; +} + +void Dispatcher::UpdateContentCapabilities(ScriptContext* context) { + APIPermissionSet permissions; + for (const auto& extension : + *RendererExtensionRegistry::Get()->GetMainThreadExtensionSet()) { + const ContentCapabilitiesInfo& info = + ContentCapabilitiesInfo::Get(extension.get()); + if (info.url_patterns.MatchesURL(context->url())) { + APIPermissionSet new_permissions; + APIPermissionSet::Union(permissions, info.permissions, &new_permissions); + permissions = new_permissions; + } + } + context->set_content_capabilities(permissions); +} + +void Dispatcher::PopulateSourceMap() { + const std::vector<std::pair<std::string, int> > resources = GetJsResources(); + for (std::vector<std::pair<std::string, int> >::const_iterator resource = + resources.begin(); + resource != resources.end(); + ++resource) { + source_map_.RegisterSource(resource->first, resource->second); + } + delegate_->PopulateSourceMap(&source_map_); +} + +bool Dispatcher::IsWithinPlatformApp() { + for (std::set<std::string>::iterator iter = active_extension_ids_.begin(); + iter != active_extension_ids_.end(); + ++iter) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(*iter); + if (extension && extension->is_platform_app()) + return true; + } + return false; +} + +v8::Local<v8::Object> Dispatcher::GetOrCreateObject( + const v8::Local<v8::Object>& object, + const std::string& field, + v8::Isolate* isolate) { + v8::Local<v8::String> key = v8::String::NewFromUtf8(isolate, field.c_str()); + // If the object has a callback property, it is assumed it is an unavailable + // API, so it is safe to delete. This is checked before GetOrCreateObject is + // called. + if (object->HasRealNamedCallbackProperty(key)) { + object->Delete(key); + } else if (object->HasRealNamedProperty(key)) { + v8::Local<v8::Value> value = object->Get(key); + CHECK(value->IsObject()); + return v8::Local<v8::Object>::Cast(value); + } + + v8::Local<v8::Object> new_object = v8::Object::New(isolate); + object->Set(key, new_object); + return new_object; +} + +v8::Local<v8::Object> Dispatcher::GetOrCreateBindObjectIfAvailable( + const std::string& api_name, + std::string* bind_name, + ScriptContext* context) { + std::vector<std::string> split = base::SplitString( + api_name, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + + v8::Local<v8::Object> bind_object; + + // Check if this API has an ancestor. If the API's ancestor is available and + // the API is not available, don't install the bindings for this API. If + // the API is available and its ancestor is not, delete the ancestor and + // install the bindings for the API. This is to prevent loading the ancestor + // API schema if it will not be needed. + // + // For example: + // If app is available and app.window is not, just install app. + // If app.window is available and app is not, delete app and install + // app.window on a new object so app does not have to be loaded. + const FeatureProvider* api_feature_provider = + FeatureProvider::GetAPIFeatures(); + std::string ancestor_name; + bool only_ancestor_available = false; + + for (size_t i = 0; i < split.size() - 1; ++i) { + ancestor_name += (i ? "." : "") + split[i]; + if (api_feature_provider->GetFeature(ancestor_name) && + context->GetAvailability(ancestor_name).is_available() && + !context->GetAvailability(api_name).is_available()) { + only_ancestor_available = true; + break; + } + + if (bind_object.IsEmpty()) { + bind_object = AsObjectOrEmpty(GetOrCreateChrome(context)); + if (bind_object.IsEmpty()) + return v8::Local<v8::Object>(); + } + bind_object = GetOrCreateObject(bind_object, split[i], context->isolate()); + } + + if (only_ancestor_available) + return v8::Local<v8::Object>(); + + if (bind_name) + *bind_name = split.back(); + + return bind_object.IsEmpty() ? AsObjectOrEmpty(GetOrCreateChrome(context)) + : bind_object; +} + +void Dispatcher::RequireGuestViewModules(ScriptContext* context) { + Feature::Context context_type = context->context_type(); + ModuleSystem* module_system = context->module_system(); + + // Only set if |context| is capable of running guests in OOPIF. Used to + // require additional module overrides. + bool guest_view_required = false; + + // Require AppView. + if (context->GetAvailability("appViewEmbedderInternal").is_available()) { + module_system->Require("appView"); + } + + // Require ExtensionOptions. + if (context->GetAvailability("extensionOptionsInternal").is_available()) { + module_system->Require("extensionOptions"); + module_system->Require("extensionOptionsAttributes"); + + guest_view_required = true; + } + + // Require ExtensionView. + if (context->GetAvailability("extensionViewInternal").is_available()) { + module_system->Require("extensionView"); + module_system->Require("extensionViewApiMethods"); + module_system->Require("extensionViewAttributes"); + } + + // Require WebView. + if (context->GetAvailability("webViewInternal").is_available()) { + module_system->Require("webView"); + module_system->Require("webViewApiMethods"); + module_system->Require("webViewAttributes"); + if (context->GetAvailability("webViewExperimentalInternal") + .is_available()) { + module_system->Require("webViewExperimental"); + } + + guest_view_required = true; + } + + if (guest_view_required && + content::BrowserPluginGuestMode::UseCrossProcessFramesForGuests()) { + module_system->Require("guestViewIframe"); + module_system->Require("guestViewIframeContainer"); + } + + // The "guestViewDeny" module must always be loaded last. It registers + // error-providing custom elements for the GuestView types that are not + // available, and thus all of those types must have been checked and loaded + // (or not loaded) beforehand. + if (context_type == Feature::BLESSED_EXTENSION_CONTEXT) { + module_system->Require("guestViewDeny"); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/dispatcher.h b/chromium/extensions/renderer/dispatcher.h new file mode 100644 index 00000000000..37ab0a312dc --- /dev/null +++ b/chromium/extensions/renderer/dispatcher.h @@ -0,0 +1,318 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_DISPATCHER_H_ +#define EXTENSIONS_RENDERER_DISPATCHER_H_ + +#include <stdint.h> + +#include <map> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/scoped_observer.h" +#include "base/timer/timer.h" +#include "content/public/renderer/render_process_observer.h" +#include "extensions/common/event_filter.h" +#include "extensions/common/extension.h" +#include "extensions/common/extensions_client.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/resource_bundle_source_map.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/user_script_set_manager.h" +#include "extensions/renderer/v8_schema_registry.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/platform/WebVector.h" +#include "v8/include/v8.h" + +class ChromeRenderViewTest; +class GURL; +class ModuleSystem; +class URLPattern; +struct ExtensionMsg_ExternalConnectionInfo; +struct ExtensionMsg_Loaded_Params; +struct ExtensionMsg_TabConnectionInfo; +struct ExtensionMsg_UpdatePermissions_Params; + +namespace blink { +class WebFrame; +class WebLocalFrame; +class WebSecurityOrigin; +} + +namespace base { +class ListValue; +} + +namespace content { +class RenderThread; +} + +namespace extensions { +class ContentWatcher; +class DispatcherDelegate; +class FilteredEventRouter; +class ManifestPermissionSet; +class RequestSender; +class ScriptContext; +class ScriptInjectionManager; +struct Message; + +// Dispatches extension control messages sent to the renderer and stores +// renderer extension related state. +class Dispatcher : public content::RenderProcessObserver, + public UserScriptSetManager::Observer { + public: + explicit Dispatcher(DispatcherDelegate* delegate); + ~Dispatcher() override; + + const ScriptContextSet& script_context_set() const { + return *script_context_set_; + } + + V8SchemaRegistry* v8_schema_registry() { return v8_schema_registry_.get(); } + + ContentWatcher* content_watcher() { return content_watcher_.get(); } + + RequestSender* request_sender() { return request_sender_.get(); } + + const std::string& webview_partition_id() { return webview_partition_id_; } + + void OnRenderFrameCreated(content::RenderFrame* render_frame); + + bool IsExtensionActive(const std::string& extension_id) const; + + void DidCreateScriptContext(blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& context, + int extension_group, + int world_id); + + // Runs on a different thread and should not use any member variables. + static void DidInitializeServiceWorkerContextOnWorkerThread( + v8::Local<v8::Context> v8_context, + const GURL& url); + + void WillReleaseScriptContext(blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& context, + int world_id); + + // Runs on a different thread and should not use any member variables. + static void WillDestroyServiceWorkerContextOnWorkerThread( + v8::Local<v8::Context> v8_context, + const GURL& url); + + // This method is not allowed to run JavaScript code in the frame. + void DidCreateDocumentElement(blink::WebLocalFrame* frame); + + // These methods may run (untrusted) JavaScript code in the frame, and + // cause |render_frame| to become invalid. + void RunScriptsAtDocumentStart(content::RenderFrame* render_frame); + void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame); + + void OnExtensionResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error); + + // Dispatches the event named |event_name| to all render views. + void DispatchEvent(const std::string& extension_id, + const std::string& event_name) const; + + // Shared implementation of the various MessageInvoke IPCs. + void InvokeModuleSystemMethod(content::RenderFrame* render_frame, + const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture); + + void ClearPortData(int port_id); + + // Returns a list of (module name, resource id) pairs for the JS modules to + // add to the source map. + static std::vector<std::pair<std::string, int> > GetJsResources(); + static void RegisterNativeHandlers(ModuleSystem* module_system, + ScriptContext* context, + Dispatcher* dispatcher, + RequestSender* request_sender, + V8SchemaRegistry* v8_schema_registry); + + bool WasWebRequestUsedBySomeExtensions() const { return webrequest_used_; } + + private: + // The RendererPermissionsPolicyDelegateTest.CannotScriptWebstore test needs + // to call the OnActivateExtension IPCs. + friend class ::ChromeRenderViewTest; + FRIEND_TEST_ALL_PREFIXES(RendererPermissionsPolicyDelegateTest, + CannotScriptWebstore); + + // RenderProcessObserver implementation: + bool OnControlMessageReceived(const IPC::Message& message) override; + void IdleNotification() override; + void OnRenderProcessShutdown() override; + + void OnActivateExtension(const std::string& extension_id); + void OnCancelSuspend(const std::string& extension_id); + void OnDeliverMessage(int target_port_id, const Message& message); + void OnDispatchOnConnect(int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id); + void OnDispatchOnDisconnect(int port_id, const std::string& error_message); + void OnLoaded( + const std::vector<ExtensionMsg_Loaded_Params>& loaded_extensions); + void OnMessageInvoke(const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture); + void OnSetChannel(int channel); + void OnSetScriptingWhitelist( + const ExtensionsClient::ScriptingWhitelist& extension_ids); + void OnSetSystemFont(const std::string& font_family, + const std::string& font_size); + void OnSetWebViewPartitionID(const std::string& partition_id); + void OnShouldSuspend(const std::string& extension_id, uint64_t sequence_id); + void OnSuspend(const std::string& extension_id); + void OnTransferBlobs(const std::vector<std::string>& blob_uuids); + void OnUnloaded(const std::string& id); + void OnUpdatePermissions(const ExtensionMsg_UpdatePermissions_Params& params); + void OnUpdateTabSpecificPermissions(const GURL& visible_url, + const std::string& extension_id, + const URLPatternSet& new_hosts, + bool update_origin_whitelist, + int tab_id); + void OnClearTabSpecificPermissions( + const std::vector<std::string>& extension_ids, + bool update_origin_whitelist, + int tab_id); + void OnUsingWebRequestAPI(bool webrequest_used); + + // UserScriptSetManager::Observer implementation. + void OnUserScriptsUpdated(const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) override; + + void UpdateActiveExtensions(); + + // Sets up the host permissions for |extension|. + void InitOriginPermissions(const Extension* extension); + + // Updates the host permissions for the extension url to include only those in + // |new_patterns|, and remove from |old_patterns| that are no longer allowed. + void UpdateOriginPermissions(const GURL& extension_url, + const URLPatternSet& old_patterns, + const URLPatternSet& new_patterns); + + // Enable custom element whitelist in Apps. + void EnableCustomElementWhiteList(); + + // Adds or removes bindings for every context belonging to |extension_id|, or + // or all contexts if |extension_id| is empty. + void UpdateBindings(const std::string& extension_id); + + void UpdateBindingsForContext(ScriptContext* context); + + void RegisterBinding(const std::string& api_name, ScriptContext* context); + + void RegisterNativeHandlers(ModuleSystem* module_system, + ScriptContext* context); + + // Determines if a ScriptContext can connect to any externally_connectable- + // enabled extension. + bool IsRuntimeAvailableToContext(ScriptContext* context); + + // Updates a web page context with any content capabilities granted by active + // extensions. + void UpdateContentCapabilities(ScriptContext* context); + + // Inserts static source code into |source_map_|. + void PopulateSourceMap(); + + // Returns whether the current renderer hosts a platform app. + bool IsWithinPlatformApp(); + + // Gets |field| from |object| or creates it as an empty object if it doesn't + // exist. + v8::Local<v8::Object> GetOrCreateObject(const v8::Local<v8::Object>& object, + const std::string& field, + v8::Isolate* isolate); + + v8::Local<v8::Object> GetOrCreateBindObjectIfAvailable( + const std::string& api_name, + std::string* bind_name, + ScriptContext* context); + + // Requires the GuestView modules in the module system of the ScriptContext + // |context|. + void RequireGuestViewModules(ScriptContext* context); + + // The delegate for this dispatcher. Not owned, but must extend beyond the + // Dispatcher's own lifetime. + DispatcherDelegate* delegate_; + + // True if the IdleNotification timer should be set. + bool set_idle_notifications_; + + // The IDs of extensions that failed to load, mapped to the error message + // generated on failure. + std::map<std::string, std::string> extension_load_errors_; + + // All the bindings contexts that are currently loaded for this renderer. + // There is zero or one for each v8 context. + scoped_ptr<ScriptContextSet> script_context_set_; + + scoped_ptr<ContentWatcher> content_watcher_; + + scoped_ptr<UserScriptSetManager> user_script_set_manager_; + + scoped_ptr<ScriptInjectionManager> script_injection_manager_; + + // Same as above, but on a longer timer and will run even if the process is + // not idle, to ensure that IdleHandle gets called eventually. + scoped_ptr<base::RepeatingTimer> forced_idle_timer_; + + // The extensions and apps that are active in this process. + ExtensionIdSet active_extension_ids_; + + ResourceBundleSourceMap source_map_; + + // Cache for the v8 representation of extension API schemas. + scoped_ptr<V8SchemaRegistry> v8_schema_registry_; + + // Sends API requests to the extension host. + scoped_ptr<RequestSender> request_sender_; + + // The platforms system font family and size; + std::string system_font_family_; + std::string system_font_size_; + + // Mapping of port IDs to tabs. If there is no tab, the value would be -1. + std::map<int, int> port_to_tab_id_map_; + + // It is important for this to come after the ScriptInjectionManager, so that + // the observer is destroyed before the UserScriptSet. + ScopedObserver<UserScriptSetManager, UserScriptSetManager::Observer> + user_script_set_manager_observer_; + + // Status of webrequest usage. + bool webrequest_used_; + + // The WebView partition ID associated with this process's storage partition, + // if this renderer is a WebView guest render process. Otherwise, this will be + // empty. + std::string webview_partition_id_; + + DISALLOW_COPY_AND_ASSIGN(Dispatcher); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_DISPATCHER_H_ diff --git a/chromium/extensions/renderer/dispatcher_delegate.h b/chromium/extensions/renderer/dispatcher_delegate.h new file mode 100644 index 00000000000..f00c357b7ef --- /dev/null +++ b/chromium/extensions/renderer/dispatcher_delegate.h @@ -0,0 +1,59 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_DISPATCHER_DELEGATE_H_ +#define EXTENSIONS_RENDERER_DISPATCHER_DELEGATE_H_ + +#include <set> +#include <string> + +namespace blink { +class WebFrame; +} + +namespace extensions { +class Dispatcher; +class Extension; +class ModuleSystem; +class ResourceBundleSourceMap; +class ScriptContext; +class URLPatternSet; + +// Base class and default implementation for an extensions::Dispacher delegate. +// DispatcherDelegate can be used to override and extend the behavior of the +// extensions system's renderer side. +class DispatcherDelegate { + public: + virtual ~DispatcherDelegate() {} + + // Initializes origin permissions for a newly created extension context. + virtual void InitOriginPermissions(const Extension* extension, + bool is_extension_active) {} + + // Includes additional native handlers in a ScriptContext's ModuleSystem. + virtual void RegisterNativeHandlers(Dispatcher* dispatcher, + ModuleSystem* module_system, + ScriptContext* context) {} + + // Includes additional source resources into the resource map. + virtual void PopulateSourceMap(ResourceBundleSourceMap* source_map) {} + + // Requires additional modules within an extension context's module system. + virtual void RequireAdditionalModules(ScriptContext* context, + bool is_within_platform_app) {} + + // Allows the delegate to respond to an updated set of active extensions in + // the Dispatcher. + virtual void OnActiveExtensionsUpdated( + const std::set<std::string>& extension_ids) {} + + // Sets the current Chrome channel. + // TODO(rockot): This doesn't belong in a generic extensions system interface. + // See http://crbug.com/368431. + virtual void SetChannel(int channel) {} +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_DISPATCHER_DELEGATE_H_ diff --git a/chromium/extensions/renderer/display_source_custom_bindings.cc b/chromium/extensions/renderer/display_source_custom_bindings.cc new file mode 100644 index 00000000000..1a021c157b3 --- /dev/null +++ b/chromium/extensions/renderer/display_source_custom_bindings.cc @@ -0,0 +1,306 @@ +// Copyright 2015 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/display_source_custom_bindings.h" + +#include <stdint.h> + +#include "base/bind.h" +#include "content/public/child/v8_value_converter.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/platform/WebMediaStream.h" +#include "third_party/WebKit/public/platform/WebMediaStreamTrack.h" +#include "third_party/WebKit/public/web/WebDOMMediaStreamTrack.h" +#include "v8/include/v8.h" + +namespace extensions { + +using content::V8ValueConverter; + +namespace { +const char kErrorNotSupported[] = "Not supported"; +const char kInvalidStreamArgs[] = "Invalid stream arguments"; +const char kSessionAlreadyStarted[] = "The session has been already started"; +const char kSessionAlreadyTerminating[] = "The session is already terminating"; +const char kSessionNotFound[] = "Session not found"; +} // namespace + +DisplaySourceCustomBindings::DisplaySourceCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context), + weak_factory_(this) { + RouteFunction("StartSession", + base::Bind(&DisplaySourceCustomBindings::StartSession, + weak_factory_.GetWeakPtr())); + RouteFunction("TerminateSession", + base::Bind(&DisplaySourceCustomBindings::TerminateSession, + weak_factory_.GetWeakPtr())); +} + +DisplaySourceCustomBindings::~DisplaySourceCustomBindings() { +} + +void DisplaySourceCustomBindings::Invalidate() { + session_map_.clear(); + weak_factory_.InvalidateWeakPtrs(); + ObjectBackedNativeHandler::Invalidate(); +} + +namespace { + +v8::Local<v8::Value> GetChildValue(v8::Local<v8::Object> value, + const std::string& key_name, + v8::Isolate* isolate) { + v8::Local<v8::Array> property_names(value->GetOwnPropertyNames()); + for (uint32_t i = 0; i < property_names->Length(); ++i) { + v8::Local<v8::Value> key(property_names->Get(i)); + if (key_name == *v8::String::Utf8Value(key)) { + v8::TryCatch try_catch(isolate); + v8::Local<v8::Value> child_v8 = value->Get(key); + if (try_catch.HasCaught()) { + return v8::Null(isolate); + } + return child_v8; + } + } + + return v8::Null(isolate); +} + +int32_t GetCallbackId() { + static int32_t sCallId = 0; + return ++sCallId; +} + +} // namespace + +void DisplaySourceCustomBindings::StartSession( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsObject()); + + v8::Isolate* isolate = context()->isolate(); + v8::Local<v8::Object> start_info = args[0].As<v8::Object>(); + + v8::Local<v8::Value> sink_id_val = + GetChildValue(start_info, "sinkId", isolate); + CHECK(sink_id_val->IsInt32()); + const int sink_id = sink_id_val->ToInt32(isolate)->Value(); + if (GetDisplaySession(sink_id)) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8( + isolate, kSessionAlreadyStarted))); + return; + } + + v8::Local<v8::Value> video_stream_val = + GetChildValue(start_info, "videoTrack", isolate); + v8::Local<v8::Value> audio_stream_val = + GetChildValue(start_info, "audioTrack", isolate); + + if ((video_stream_val->IsNull() || video_stream_val->IsUndefined()) && + (audio_stream_val->IsNull() || audio_stream_val->IsUndefined())) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kInvalidStreamArgs))); + return; + } + + blink::WebMediaStreamTrack audio_track, video_track; + + if (!video_stream_val->IsNull() && !video_stream_val->IsUndefined()) { + CHECK(video_stream_val->IsObject()); + video_track = + blink::WebDOMMediaStreamTrack::fromV8Value( + video_stream_val).component(); + if (video_track.isNull()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kInvalidStreamArgs))); + return; + } + } + if (!audio_stream_val->IsNull() && !audio_stream_val->IsUndefined()) { + CHECK(audio_stream_val->IsObject()); + audio_track = + blink::WebDOMMediaStreamTrack::fromV8Value( + audio_stream_val).component(); + if (audio_track.isNull()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kInvalidStreamArgs))); + return; + } + } + + scoped_ptr<DisplaySourceAuthInfo> auth_info; + v8::Local<v8::Value> auth_info_v8_val = + GetChildValue(start_info, "authenticationInfo", isolate); + if (!auth_info_v8_val->IsNull()) { + CHECK(auth_info_v8_val->IsObject()); + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + scoped_ptr<base::Value> auth_info_val( + converter->FromV8Value(auth_info_v8_val, context()->v8_context())); + CHECK(auth_info_val); + auth_info = DisplaySourceAuthInfo::FromValue(*auth_info_val); + } + + DisplaySourceSessionParams session_params; + session_params.sink_id = sink_id; + session_params.video_track = video_track; + session_params.audio_track = audio_track; + session_params.render_frame = context()->GetRenderFrame(); + if (auth_info) { + session_params.auth_method = auth_info->method; + session_params.auth_data = auth_info->data ? *auth_info->data : ""; + } + scoped_ptr<DisplaySourceSession> session = + DisplaySourceSessionFactory::CreateSession(session_params); + if (!session) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8( + isolate, kErrorNotSupported))); + return; + } + + auto on_terminated_callback = + base::Bind(&DisplaySourceCustomBindings::OnSessionTerminated, + weak_factory_.GetWeakPtr(), sink_id); + auto on_error_callback = + base::Bind(&DisplaySourceCustomBindings::OnSessionError, + weak_factory_.GetWeakPtr(), sink_id); + session->SetNotificationCallbacks(on_terminated_callback, on_error_callback); + + int32_t call_id = GetCallbackId(); + args.GetReturnValue().Set(call_id); + + auto on_call_completed = + base::Bind(&DisplaySourceCustomBindings::OnSessionStarted, + weak_factory_.GetWeakPtr(), sink_id, call_id); + session->Start(on_call_completed); + session_map_.insert(std::make_pair(sink_id, std::move(session))); +} + +void DisplaySourceCustomBindings::TerminateSession( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsInt32()); + + v8::Isolate* isolate = context()->isolate(); + int sink_id = args[0]->ToInt32(args.GetIsolate())->Value(); + DisplaySourceSession* session = GetDisplaySession(sink_id); + if (!session) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8( + isolate, kSessionNotFound))); + return; + } + + DisplaySourceSession::State state = session->state(); + DCHECK_NE(state, DisplaySourceSession::Idle); + if (state == DisplaySourceSession::Establishing) { + // 'session started' callback has not yet been invoked. + // This session is not existing for the user. + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kSessionNotFound))); + return; + } + + if (state == DisplaySourceSession::Terminating) { + isolate->ThrowException(v8::Exception::Error(v8::String::NewFromUtf8( + isolate, kSessionAlreadyTerminating))); + return; + } + + int32_t call_id = GetCallbackId(); + args.GetReturnValue().Set(call_id); + + auto on_call_completed = + base::Bind(&DisplaySourceCustomBindings::OnCallCompleted, + weak_factory_.GetWeakPtr(), call_id); + // The session will get removed from session_map_ in OnSessionTerminated. + session->Terminate(on_call_completed); +} + +void DisplaySourceCustomBindings::OnCallCompleted( + int call_id, + bool success, + const std::string& error_message) { + v8::Isolate* isolate = context()->isolate(); + ModuleSystem* module_system = context()->module_system(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context()->v8_context()); + + v8::Local<v8::Value> callback_args[2]; + callback_args[0] = v8::Integer::New(isolate, call_id); + if (success) + callback_args[1] = v8::Null(isolate); + else + callback_args[1] = v8::String::NewFromUtf8(isolate, error_message.c_str()); + + module_system->CallModuleMethod("displaySource", "callCompletionCallback", 2, + callback_args); +} + +void DisplaySourceCustomBindings::OnSessionStarted( + int sink_id, + int call_id, + bool success, + const std::string& error_message) { + CHECK(GetDisplaySession(sink_id)); + if (!success) { + // Session has failed to start, removing it. + session_map_.erase(sink_id); + } + OnCallCompleted(call_id, success, error_message); +} + +void DisplaySourceCustomBindings::DispatchSessionTerminated(int sink_id) const { + v8::Isolate* isolate = context()->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context()->v8_context()); + v8::Local<v8::Array> event_args = v8::Array::New(isolate, 1); + event_args->Set(0, v8::Integer::New(isolate, sink_id)); + context()->DispatchEvent("displaySource.onSessionTerminated", event_args); +} + +void DisplaySourceCustomBindings::DispatchSessionError( + int sink_id, + DisplaySourceErrorType type, + const std::string& message) const { + v8::Isolate* isolate = context()->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context()->v8_context()); + + api::display_source::ErrorInfo error_info; + error_info.type = type; + if (!message.empty()) + error_info.description.reset(new std::string(message)); + + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + v8::Local<v8::Value> info_arg = + converter->ToV8Value(error_info.ToValue().get(), + context()->v8_context()); + + v8::Local<v8::Array> event_args = v8::Array::New(isolate, 2); + event_args->Set(0, v8::Integer::New(isolate, sink_id)); + event_args->Set(1, info_arg); + context()->DispatchEvent("displaySource.onSessionErrorOccured", event_args); +} + +DisplaySourceSession* DisplaySourceCustomBindings::GetDisplaySession( + int sink_id) const { + auto iter = session_map_.find(sink_id); + if (iter != session_map_.end()) + return iter->second.get(); + return nullptr; +} + +void DisplaySourceCustomBindings::OnSessionTerminated(int sink_id) { + CHECK(GetDisplaySession(sink_id)); + session_map_.erase(sink_id); + DispatchSessionTerminated(sink_id); +} + +void DisplaySourceCustomBindings::OnSessionError(int sink_id, + DisplaySourceErrorType type, + const std::string& message) { + CHECK(GetDisplaySession(sink_id)); + DispatchSessionError(sink_id, type, message); +} + +} // extensions diff --git a/chromium/extensions/renderer/display_source_custom_bindings.h b/chromium/extensions/renderer/display_source_custom_bindings.h new file mode 100644 index 00000000000..b7e5e967ac7 --- /dev/null +++ b/chromium/extensions/renderer/display_source_custom_bindings.h @@ -0,0 +1,63 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_DISPLAY_SOURCE_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_DISPLAY_SOURCE_CUSTOM_BINDINGS_H_ + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "extensions/common/api/display_source.h" +#include "extensions/renderer/api/display_source/display_source_session.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { +class ScriptContext; + +// Implements custom bindings for the displaySource API. +class DisplaySourceCustomBindings : public ObjectBackedNativeHandler { + public: + explicit DisplaySourceCustomBindings(ScriptContext* context); + + ~DisplaySourceCustomBindings() override; + + private: + // ObjectBackedNativeHandler override. + void Invalidate() override; + + void StartSession( + const v8::FunctionCallbackInfo<v8::Value>& args); + void TerminateSession( + const v8::FunctionCallbackInfo<v8::Value>& args); + // Call completion callbacks. + void OnCallCompleted(int call_id, + bool success, + const std::string& error_message); + void OnSessionStarted(int sink_id, + int call_id, + bool success, + const std::string& error_message); + // Dispatch events + void DispatchSessionTerminated(int sink_id) const; + void DispatchSessionError(int sink_id, + DisplaySourceErrorType type, + const std::string& message) const; + + // DisplaySession notification callbacks. + void OnSessionTerminated(int sink_id); + void OnSessionError(int sink_id, + DisplaySourceErrorType type, + const std::string& message); + + DisplaySourceSession* GetDisplaySession(int sink_id) const; + + std::map<int, scoped_ptr<DisplaySourceSession>> session_map_; + base::WeakPtrFactory<DisplaySourceCustomBindings> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(DisplaySourceCustomBindings); +}; + +} // extensions + +#endif // EXTENSIONS_RENDERER_DISPLAY_SOURCE_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/document_custom_bindings.cc b/chromium/extensions/renderer/document_custom_bindings.cc new file mode 100644 index 00000000000..5f81ee8e1b2 --- /dev/null +++ b/chromium/extensions/renderer/document_custom_bindings.cc @@ -0,0 +1,42 @@ +// 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/renderer/document_custom_bindings.h" + +#include <string> + +#include "base/bind.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "v8/include/v8.h" + +namespace extensions { + +DocumentCustomBindings::DocumentCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("RegisterElement", + base::Bind(&DocumentCustomBindings::RegisterElement, + base::Unretained(this))); +} + +// Attach an event name to an object. +void DocumentCustomBindings::RegisterElement( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() != 2 || !args[0]->IsString() || !args[1]->IsObject()) { + NOTREACHED(); + return; + } + + std::string element_name(*v8::String::Utf8Value(args[0])); + v8::Local<v8::Object> options = v8::Local<v8::Object>::Cast(args[1]); + + blink::WebExceptionCode ec = 0; + blink::WebDocument document = context()->web_frame()->document(); + v8::Local<v8::Value> constructor = document.registerEmbedderCustomElement( + blink::WebString::fromUTF8(element_name), options, ec); + args.GetReturnValue().Set(constructor); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/document_custom_bindings.h b/chromium/extensions/renderer/document_custom_bindings.h new file mode 100644 index 00000000000..6c07eab0fa2 --- /dev/null +++ b/chromium/extensions/renderer/document_custom_bindings.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_DOCUMENT_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_DOCUMENT_CUSTOM_BINDINGS_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Implements custom bindings for document-level operations. +class DocumentCustomBindings : public ObjectBackedNativeHandler { + public: + DocumentCustomBindings(ScriptContext* context); + + private: + // Registers the provided element as a custom element in Blink. + void RegisterElement(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_DOCUMENT_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/dom_activity_logger.cc b/chromium/extensions/renderer/dom_activity_logger.cc new file mode 100644 index 00000000000..92055232c71 --- /dev/null +++ b/chromium/extensions/renderer/dom_activity_logger.cc @@ -0,0 +1,133 @@ +// 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/renderer/dom_activity_logger.h" + +#include <utility> + +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/dom_action_types.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/activity_log_converter_strategy.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/platform/WebURL.h" + +using content::V8ValueConverter; +using blink::WebString; +using blink::WebURL; + +namespace extensions { + +namespace { + +// Converts the given |v8_value| and appends it to the given |list|, if the +// conversion succeeds. +void AppendV8Value(const std::string& api_name, + const v8::Local<v8::Value>& v8_value, + base::ListValue* list) { + DCHECK(list); + std::unique_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + ActivityLogConverterStrategy strategy; + converter->SetFunctionAllowed(true); + converter->SetStrategy(&strategy); + std::unique_ptr<base::Value> value(converter->FromV8Value( + v8_value, v8::Isolate::GetCurrent()->GetCurrentContext())); + + if (value.get()) + list->Append(value.release()); +} + +} // namespace + +DOMActivityLogger::DOMActivityLogger(const std::string& extension_id) + : extension_id_(extension_id) { +} + +DOMActivityLogger::~DOMActivityLogger() {} + +void DOMActivityLogger::AttachToWorld(int world_id, + const std::string& extension_id) { + // If there is no logger registered for world_id, construct a new logger + // and register it with world_id. + if (!blink::hasDOMActivityLogger(world_id, + WebString::fromUTF8(extension_id))) { + DOMActivityLogger* logger = new DOMActivityLogger(extension_id); + blink::setDOMActivityLogger(world_id, + WebString::fromUTF8(extension_id), + logger); + } +} + +void DOMActivityLogger::logGetter(const WebString& api_name, + const WebURL& url, + const WebString& title) { + SendDomActionMessage(api_name.utf8(), url, title, DomActionType::GETTER, + std::unique_ptr<base::ListValue>(new base::ListValue())); +} + +void DOMActivityLogger::logSetter(const WebString& api_name, + const v8::Local<v8::Value>& new_value, + const WebURL& url, + const WebString& title) { + logSetter(api_name, new_value, v8::Local<v8::Value>(), url, title); +} + +void DOMActivityLogger::logSetter(const WebString& api_name, + const v8::Local<v8::Value>& new_value, + const v8::Local<v8::Value>& old_value, + const WebURL& url, + const WebString& title) { + std::unique_ptr<base::ListValue> args(new base::ListValue); + std::string api_name_utf8 = api_name.utf8(); + AppendV8Value(api_name_utf8, new_value, args.get()); + if (!old_value.IsEmpty()) + AppendV8Value(api_name_utf8, old_value, args.get()); + SendDomActionMessage(api_name_utf8, url, title, DomActionType::SETTER, + std::move(args)); +} + +void DOMActivityLogger::logMethod(const WebString& api_name, + int argc, + const v8::Local<v8::Value>* argv, + const WebURL& url, + const WebString& title) { + std::unique_ptr<base::ListValue> args(new base::ListValue); + std::string api_name_utf8 = api_name.utf8(); + for (int i = 0; i < argc; ++i) + AppendV8Value(api_name_utf8, argv[i], args.get()); + SendDomActionMessage(api_name_utf8, url, title, DomActionType::METHOD, + std::move(args)); +} + +void DOMActivityLogger::logEvent(const WebString& event_name, + int argc, + const WebString* argv, + const WebURL& url, + const WebString& title) { + std::unique_ptr<base::ListValue> args(new base::ListValue); + std::string event_name_utf8 = event_name.utf8(); + for (int i = 0; i < argc; ++i) + args->Append(new base::StringValue(argv[i])); + SendDomActionMessage(event_name_utf8, url, title, DomActionType::METHOD, + std::move(args)); +} + +void DOMActivityLogger::SendDomActionMessage( + const std::string& api_call, + const GURL& url, + const base::string16& url_title, + DomActionType::Type call_type, + std::unique_ptr<base::ListValue> args) { + ExtensionHostMsg_DOMAction_Params params; + params.api_call = api_call; + params.url = url; + params.url_title = url_title; + params.call_type = call_type; + params.arguments.Swap(args.get()); + content::RenderThread::Get()->Send( + new ExtensionHostMsg_AddDOMActionToActivityLog(extension_id_, params)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/dom_activity_logger.h b/chromium/extensions/renderer/dom_activity_logger.h new file mode 100644 index 00000000000..3956c4847b4 --- /dev/null +++ b/chromium/extensions/renderer/dom_activity_logger.h @@ -0,0 +1,91 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_DOM_ACTIVITY_LOGGER_H_ +#define EXTENSIONS_RENDERER_DOM_ACTIVITY_LOGGER_H_ + +#include <memory> +#include <string> + +#include "base/macros.h" +#include "extensions/common/dom_action_types.h" +#include "third_party/WebKit/public/web/WebDOMActivityLogger.h" +#include "v8/include/v8.h" + +namespace base { +class ListValue; +} + +namespace blink { +class WebString; +class WebURL; +} + +namespace content { +class V8ValueConverter; +} + +namespace extensions { + +// Used to log DOM API calls from within WebKit. The events are sent via IPC to +// extensions::ActivityLog for recording and display. +class DOMActivityLogger: public blink::WebDOMActivityLogger { + public: + static const int kMainWorldId = 0; + explicit DOMActivityLogger(const std::string& extension_id); + ~DOMActivityLogger() override; + + // Check (using the WebKit API) if there is no logger attached to the world + // corresponding to world_id, and if so, construct a new logger and attach it. + // world_id = 0 indicates the main world. + static void AttachToWorld(int world_id, + const std::string& extension_id); + + private: + // blink::WebDOMActivityLogger implementation. + // Marshals the arguments into an ExtensionHostMsg_DOMAction_Params and sends + // it over to the browser (via IPC) for appending it to the extension activity + // log. + // These methods don't have the override keyword due to the complexities it + // introduces when changes blink apis. + void logGetter(const blink::WebString& api_name, + const blink::WebURL& url, + const blink::WebString& title) override; + void logSetter(const blink::WebString& api_name, + const v8::Local<v8::Value>& new_value, + const blink::WebURL& url, + const blink::WebString& title) override; + virtual void logSetter(const blink::WebString& api_name, + const v8::Local<v8::Value>& new_value, + const v8::Local<v8::Value>& old_value, + const blink::WebURL& url, + const blink::WebString& title); + void logMethod(const blink::WebString& api_name, + int argc, + const v8::Local<v8::Value>* argv, + const blink::WebURL& url, + const blink::WebString& title) override; + void logEvent(const blink::WebString& event_name, + int argc, + const blink::WebString* argv, + const blink::WebURL& url, + const blink::WebString& title) override; + + // Helper function to actually send the message across IPC. + void SendDomActionMessage(const std::string& api_call, + const GURL& url, + const base::string16& url_title, + DomActionType::Type call_type, + std::unique_ptr<base::ListValue> args); + + // The id of the extension with which this logger is associated. + std::string extension_id_; + + DISALLOW_COPY_AND_ASSIGN(DOMActivityLogger); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_DOM_ACTIVITY_LOGGER_H_ + diff --git a/chromium/extensions/renderer/event_bindings.cc b/chromium/extensions/renderer/event_bindings.cc new file mode 100644 index 00000000000..87b8349d098 --- /dev/null +++ b/chromium/extensions/renderer/event_bindings.cc @@ -0,0 +1,367 @@ +// 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/renderer/event_bindings.h" + +#include <stdint.h> + +#include <map> +#include <utility> + +#include "base/bind.h" +#include "base/lazy_instance.h" +#include "base/memory/scoped_ptr.h" +#include "components/crx_file/id_util.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_view.h" +#include "extensions/common/event_filter.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/value_counter.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/script_context.h" +#include "url/gurl.h" + +namespace extensions { + +namespace { + +// A map of event names to the number of contexts listening to that event. +// We notify the browser about event listeners when we transition between 0 +// and 1. +typedef std::map<std::string, int> EventListenerCounts; + +// A map of extension IDs to listener counts for that extension. +base::LazyInstance<std::map<std::string, EventListenerCounts>> + g_listener_counts = LAZY_INSTANCE_INITIALIZER; + +// A map of (extension ID, event name) pairs to the filtered listener counts +// for that pair. The map is used to keep track of which filters are in effect +// for which events. We notify the browser about filtered event listeners when +// we transition between 0 and 1. +using FilteredEventListenerKey = std::pair<std::string, std::string>; +using FilteredEventListenerCounts = + std::map<FilteredEventListenerKey, scoped_ptr<ValueCounter>>; +base::LazyInstance<FilteredEventListenerCounts> g_filtered_listener_counts = + LAZY_INSTANCE_INITIALIZER; + +base::LazyInstance<EventFilter> g_event_filter = LAZY_INSTANCE_INITIALIZER; + +// Gets a unique string key identifier for a ScriptContext. +// TODO(kalman): Just use pointer equality...? +std::string GetKeyForScriptContext(ScriptContext* script_context) { + const std::string& extension_id = script_context->GetExtensionID(); + CHECK(crx_file::id_util::IdIsValid(extension_id) || + script_context->url().is_valid()); + return crx_file::id_util::IdIsValid(extension_id) + ? extension_id + : script_context->url().spec(); +} + +// Increments the number of event-listeners for the given |event_name| and +// ScriptContext. Returns the count after the increment. +int IncrementEventListenerCount(ScriptContext* script_context, + const std::string& event_name) { + return ++g_listener_counts + .Get()[GetKeyForScriptContext(script_context)][event_name]; +} + +// Decrements the number of event-listeners for the given |event_name| and +// ScriptContext. Returns the count after the increment. +int DecrementEventListenerCount(ScriptContext* script_context, + const std::string& event_name) { + return --g_listener_counts + .Get()[GetKeyForScriptContext(script_context)][event_name]; +} + +EventFilteringInfo ParseFromObject(v8::Local<v8::Object> object, + v8::Isolate* isolate) { + EventFilteringInfo info; + v8::Local<v8::String> url(v8::String::NewFromUtf8(isolate, "url")); + if (object->Has(url)) { + v8::Local<v8::Value> url_value(object->Get(url)); + info.SetURL(GURL(*v8::String::Utf8Value(url_value))); + } + v8::Local<v8::String> instance_id( + v8::String::NewFromUtf8(isolate, "instanceId")); + if (object->Has(instance_id)) { + v8::Local<v8::Value> instance_id_value(object->Get(instance_id)); + info.SetInstanceID(instance_id_value->IntegerValue()); + } + v8::Local<v8::String> service_type( + v8::String::NewFromUtf8(isolate, "serviceType")); + if (object->Has(service_type)) { + v8::Local<v8::Value> service_type_value(object->Get(service_type)); + info.SetServiceType(*v8::String::Utf8Value(service_type_value)); + } + v8::Local<v8::String> window_types( + v8::String::NewFromUtf8(isolate, "windowType")); + if (object->Has(window_types)) { + v8::Local<v8::Value> window_types_value(object->Get(window_types)); + info.SetWindowType(*v8::String::Utf8Value(window_types_value)); + } + + v8::Local<v8::String> window_exposed( + v8::String::NewFromUtf8(isolate, "windowExposedByDefault")); + if (object->Has(window_exposed)) { + v8::Local<v8::Value> window_exposed_value(object->Get(window_exposed)); + info.SetWindowExposedByDefault( + window_exposed_value.As<v8::Boolean>()->Value()); + } + + return info; +} + +// Add a filter to |event_name| in |extension_id|, returning true if it +// was the first filter for that event in that extension. +bool AddFilter(const std::string& event_name, + const std::string& extension_id, + const base::DictionaryValue& filter) { + FilteredEventListenerKey key(extension_id, event_name); + FilteredEventListenerCounts& all_counts = g_filtered_listener_counts.Get(); + FilteredEventListenerCounts::const_iterator counts = all_counts.find(key); + if (counts == all_counts.end()) { + counts = all_counts.insert(std::make_pair( + key, make_scoped_ptr(new ValueCounter()))) + .first; + } + return counts->second->Add(filter); +} + +// Remove a filter from |event_name| in |extension_id|, returning true if it +// was the last filter for that event in that extension. +bool RemoveFilter(const std::string& event_name, + const std::string& extension_id, + base::DictionaryValue* filter) { + FilteredEventListenerKey key(extension_id, event_name); + FilteredEventListenerCounts& all_counts = g_filtered_listener_counts.Get(); + FilteredEventListenerCounts::const_iterator counts = all_counts.find(key); + if (counts == all_counts.end()) + return false; + // Note: Remove() returns true if it removed the last filter equivalent to + // |filter|. If there are more equivalent filters, or if there weren't any in + // the first place, it returns false. + if (counts->second->Remove(*filter)) { + if (counts->second->is_empty()) + all_counts.erase(counts); // Clean up if there are no more filters. + return true; + } + return false; +} + +} // namespace + +EventBindings::EventBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("AttachEvent", base::Bind(&EventBindings::AttachEventHandler, + base::Unretained(this))); + RouteFunction("DetachEvent", base::Bind(&EventBindings::DetachEventHandler, + base::Unretained(this))); + RouteFunction( + "AttachFilteredEvent", + base::Bind(&EventBindings::AttachFilteredEvent, base::Unretained(this))); + RouteFunction("DetachFilteredEvent", + base::Bind(&EventBindings::DetachFilteredEventHandler, + base::Unretained(this))); + RouteFunction("MatchAgainstEventFilter", + base::Bind(&EventBindings::MatchAgainstEventFilter, + base::Unretained(this))); + + // It's safe to use base::Unretained here because |context| will always + // outlive us. + context->AddInvalidationObserver( + base::Bind(&EventBindings::OnInvalidated, base::Unretained(this))); +} + +EventBindings::~EventBindings() {} + +void EventBindings::AttachEventHandler( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsString()); + AttachEvent(*v8::String::Utf8Value(args[0])); +} + +void EventBindings::AttachEvent(const std::string& event_name) { + if (!context()->HasAccessOrThrowError(event_name)) + return; + + // Record the attachment for this context so that events can be detached when + // the context is destroyed. + // + // Ideally we'd CHECK that it's not already attached, however that's not + // possible because extensions can create and attach events themselves. Very + // silly, but that's the way it is. For an example of this, see + // chrome/test/data/extensions/api_test/events/background.js. + attached_event_names_.insert(event_name); + + const std::string& extension_id = context()->GetExtensionID(); + if (IncrementEventListenerCount(context(), event_name) == 1) { + content::RenderThread::Get()->Send(new ExtensionHostMsg_AddListener( + extension_id, context()->url(), event_name)); + } + + // This is called the first time the page has added a listener. Since + // the background page is the only lazy page, we know this is the first + // time this listener has been registered. + if (ExtensionFrameHelper::IsContextForEventPage(context())) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_AddLazyListener(extension_id, event_name)); + } +} + +void EventBindings::DetachEventHandler( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(2, args.Length()); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsBoolean()); + DetachEvent(*v8::String::Utf8Value(args[0]), args[1]->BooleanValue()); +} + +void EventBindings::DetachEvent(const std::string& event_name, bool is_manual) { + // See comment in AttachEvent(). + attached_event_names_.erase(event_name); + + const std::string& extension_id = context()->GetExtensionID(); + + if (DecrementEventListenerCount(context(), event_name) == 0) { + content::RenderThread::Get()->Send(new ExtensionHostMsg_RemoveListener( + extension_id, context()->url(), event_name)); + } + + // DetachEvent is called when the last listener for the context is + // removed. If the context is the background page, and it removes the + // last listener manually, then we assume that it is no longer interested + // in being awakened for this event. + if (is_manual && ExtensionFrameHelper::IsContextForEventPage(context())) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_RemoveLazyListener(extension_id, event_name)); + } +} + +// MatcherID AttachFilteredEvent(string event_name, object filter) +// event_name - Name of the event to attach. +// filter - Which instances of the named event are we interested in. +// returns the id assigned to the listener, which will be returned from calls +// to MatchAgainstEventFilter where this listener matches. +void EventBindings::AttachFilteredEvent( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(2, args.Length()); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsObject()); + + std::string event_name = *v8::String::Utf8Value(args[0]); + if (!context()->HasAccessOrThrowError(event_name)) + return; + + scoped_ptr<base::DictionaryValue> filter; + { + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + scoped_ptr<base::Value> filter_value(converter->FromV8Value( + v8::Local<v8::Object>::Cast(args[1]), context()->v8_context())); + if (!filter_value || !filter_value->IsType(base::Value::TYPE_DICTIONARY)) { + args.GetReturnValue().Set(static_cast<int32_t>(-1)); + return; + } + filter = base::DictionaryValue::From(std::move(filter_value)); + } + + // Hold onto a weak reference to |filter| so that it can be used after passing + // ownership to |event_filter|. + base::DictionaryValue* filter_weak = filter.get(); + int id = g_event_filter.Get().AddEventMatcher( + event_name, ParseEventMatcher(std::move(filter))); + attached_matcher_ids_.insert(id); + + // Only send IPCs the first time a filter gets added. + std::string extension_id = context()->GetExtensionID(); + if (AddFilter(event_name, extension_id, *filter_weak)) { + bool lazy = ExtensionFrameHelper::IsContextForEventPage(context()); + content::RenderThread::Get()->Send(new ExtensionHostMsg_AddFilteredListener( + extension_id, event_name, *filter_weak, lazy)); + } + + args.GetReturnValue().Set(static_cast<int32_t>(id)); +} + +void EventBindings::DetachFilteredEventHandler( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(2, args.Length()); + CHECK(args[0]->IsInt32()); + CHECK(args[1]->IsBoolean()); + DetachFilteredEvent(args[0]->Int32Value(), args[1]->BooleanValue()); +} + +void EventBindings::DetachFilteredEvent(int matcher_id, bool is_manual) { + EventFilter& event_filter = g_event_filter.Get(); + EventMatcher* event_matcher = event_filter.GetEventMatcher(matcher_id); + + const std::string& event_name = event_filter.GetEventName(matcher_id); + + // Only send IPCs the last time a filter gets removed. + std::string extension_id = context()->GetExtensionID(); + if (RemoveFilter(event_name, extension_id, event_matcher->value())) { + bool remove_lazy = + is_manual && ExtensionFrameHelper::IsContextForEventPage(context()); + content::RenderThread::Get()->Send( + new ExtensionHostMsg_RemoveFilteredListener( + extension_id, event_name, *event_matcher->value(), remove_lazy)); + } + + event_filter.RemoveEventMatcher(matcher_id); + attached_matcher_ids_.erase(matcher_id); +} + +void EventBindings::MatchAgainstEventFilter( + const v8::FunctionCallbackInfo<v8::Value>& args) { + v8::Isolate* isolate = args.GetIsolate(); + typedef std::set<EventFilter::MatcherID> MatcherIDs; + EventFilter& event_filter = g_event_filter.Get(); + std::string event_name = *v8::String::Utf8Value(args[0]); + EventFilteringInfo info = + ParseFromObject(args[1]->ToObject(isolate), isolate); + // Only match events routed to this context's RenderFrame or ones that don't + // have a routingId in their filter. + MatcherIDs matched_event_filters = event_filter.MatchEvent( + event_name, info, context()->GetRenderFrame()->GetRoutingID()); + v8::Local<v8::Array> array( + v8::Array::New(isolate, matched_event_filters.size())); + int i = 0; + for (MatcherIDs::iterator it = matched_event_filters.begin(); + it != matched_event_filters.end(); + ++it) { + array->Set(v8::Integer::New(isolate, i++), v8::Integer::New(isolate, *it)); + } + args.GetReturnValue().Set(array); +} + +scoped_ptr<EventMatcher> EventBindings::ParseEventMatcher( + scoped_ptr<base::DictionaryValue> filter) { + return make_scoped_ptr(new EventMatcher( + std::move(filter), context()->GetRenderFrame()->GetRoutingID())); +} + +void EventBindings::OnInvalidated() { + // Detach all attached events that weren't attached. Iterate over a copy + // because it will be mutated. + std::set<std::string> attached_event_names_safe = attached_event_names_; + for (const std::string& event_name : attached_event_names_safe) { + DetachEvent(event_name, false /* is_manual */); + } + DCHECK(attached_event_names_.empty()) + << "Events cannot be attached during invalidation"; + + // Same for filtered events. + std::set<int> attached_matcher_ids_safe = attached_matcher_ids_; + for (int matcher_id : attached_matcher_ids_safe) { + DetachFilteredEvent(matcher_id, false /* is_manual */); + } + DCHECK(attached_matcher_ids_.empty()) + << "Filtered events cannot be attached during invalidation"; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/event_bindings.h b/chromium/extensions/renderer/event_bindings.h new file mode 100644 index 00000000000..8c179983717 --- /dev/null +++ b/chromium/extensions/renderer/event_bindings.h @@ -0,0 +1,89 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_EVENT_BINDINGS_H_ +#define EXTENSIONS_RENDERER_EVENT_BINDINGS_H_ + +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace base { +class DictionaryValue; +} + +namespace extensions { +class EventMatcher; + +// This class deals with the javascript bindings related to Event objects. +class EventBindings : public ObjectBackedNativeHandler { + public: + explicit EventBindings(ScriptContext* context); + ~EventBindings() override; + + private: + // JavaScript handler which forwards to AttachEvent(). + // args[0] forwards to |event_name|. + void AttachEventHandler(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Attach an event name to an object. + // |event_name| The name of the event to attach. + void AttachEvent(const std::string& event_name); + + // JavaScript handler which forwards to DetachEvent(). + // args[0] forwards to |event_name|. + // args[1] forwards to |is_manual|. + void DetachEventHandler(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Detaches an event name from an object. + // |event_name| The name of the event to stop listening to. + // |is_manual| True if this detach was done by the user via removeListener() + // as opposed to automatically during shutdown, in which case we should inform + // the browser we are no longer interested in that event. + void DetachEvent(const std::string& event_name, bool is_manual); + + // MatcherID AttachFilteredEvent(string event_name, object filter) + // |event_name| Name of the event to attach. + // |filter| Which instances of the named event are we interested in. + // returns the id assigned to the listener, which will be returned from calls + // to MatchAgainstEventFilter where this listener matches. + void AttachFilteredEvent(const v8::FunctionCallbackInfo<v8::Value>& args); + + // JavaScript handler which forwards to DetachFilteredEvent. + // void DetachFilteredEvent(int id, bool manual) + // args[0] forwards to |matcher_id| + // args[1] forwards to |is_manual| + void DetachFilteredEventHandler( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // Detaches a filtered event. Unlike a normal event, a filtered event is + // identified by a unique ID per filter, not its name. + // |matcher_id| The ID of the filtered event. + // |is_manual| false if this is part of the extension unload process where all + // listeners are automatically detached. + void DetachFilteredEvent(int matcher_id, bool is_manual); + + void MatchAgainstEventFilter(const v8::FunctionCallbackInfo<v8::Value>& args); + + scoped_ptr<EventMatcher> ParseEventMatcher( + scoped_ptr<base::DictionaryValue> filter); + + // Called when our context, and therefore us, is invalidated. Run any cleanup. + void OnInvalidated(); + + // The set of attached events and filtered events. Maintain these so that we + // can detch them on unload. + std::set<std::string> attached_event_names_; + std::set<int> attached_matcher_ids_; + + DISALLOW_COPY_AND_ASSIGN(EventBindings); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EVENT_BINDINGS_H_ diff --git a/chromium/extensions/renderer/event_unittest.cc b/chromium/extensions/renderer/event_unittest.cc new file mode 100644 index 00000000000..59694623dca --- /dev/null +++ b/chromium/extensions/renderer/event_unittest.cc @@ -0,0 +1,259 @@ +// 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/extension_urls.h" +#include "extensions/renderer/module_system_test.h" +#include "grit/extensions_renderer_resources.h" + +namespace extensions { +namespace { + +class EventUnittest : public ModuleSystemTest { + void SetUp() override { + ModuleSystemTest::SetUp(); + + env()->RegisterModule(kEventBindings, IDR_EVENT_BINDINGS_JS); + env()->RegisterModule("json_schema", IDR_JSON_SCHEMA_JS); + env()->RegisterModule(kSchemaUtils, IDR_SCHEMA_UTILS_JS); + env()->RegisterModule("uncaught_exception_handler", + IDR_UNCAUGHT_EXCEPTION_HANDLER_JS); + env()->RegisterModule("utils", IDR_UTILS_JS); + + // Mock out the native handler for event_bindings. These mocks will fail if + // any invariants maintained by the real event_bindings are broken. + env()->OverrideNativeHandler( + "event_natives", + "var assert = requireNative('assert');" + "exports.$set('attachedListeners', {});" + "var attachedListeners = exports.attachedListeners;" + "exports.$set('attachedFilteredListeners', {});" + "var attachedFilteredListeners = exports.attachedFilteredListeners;" + "var nextId = 0;" + "var idToName = {};" + "exports.$set('AttachEvent', function(eventName) {" + " assert.AssertFalse(!!attachedListeners[eventName]);" + " attachedListeners[eventName] = 1;" + "});" + "exports.$set('DetachEvent', function(eventName) {" + " assert.AssertTrue(!!attachedListeners[eventName]);" + " delete attachedListeners[eventName];" + "});" + "exports.$set('IsEventAttached', function(eventName) {" + " return !!attachedListeners[eventName];" + "});" + "exports.$set('AttachFilteredEvent', function(name, filters) {" + " var id = nextId++;" + " idToName[id] = name;" + " attachedFilteredListeners[name] =" + " attachedFilteredListeners[name] || [];" + " attachedFilteredListeners[name][id] = filters;" + " return id;" + "});" + "exports.$set('DetachFilteredEvent', function(id, manual) {" + " var i = attachedFilteredListeners[idToName[id]].indexOf(id);" + " attachedFilteredListeners[idToName[id]].splice(i, 1);" + "});" + "exports.$set('HasFilteredListener', function(name) {" + " return attachedFilteredListeners[name].length;" + "});"); + env()->OverrideNativeHandler("sendRequest", + "exports.$set('sendRequest', function() {});"); + env()->OverrideNativeHandler( + "apiDefinitions", + "exports.$set('GetExtensionAPIDefinitionsForTest', function() {});"); + env()->OverrideNativeHandler("logging", + "exports.$set('DCHECK', function() {});"); + env()->OverrideNativeHandler("schema_registry", + "exports.$set('GetSchema', function() {});"); + } +}; + +TEST_F(EventUnittest, TestNothing) { + ExpectNoAssertionsMade(); +} + +TEST_F(EventUnittest, AddRemoveTwoListeners) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var assert = requireNative('assert');" + "var Event = require('event_bindings').Event;" + "var eventNatives = requireNative('event_natives');" + "var myEvent = new Event('named-event');" + "var cb1 = function() {};" + "var cb2 = function() {};" + "myEvent.addListener(cb1);" + "myEvent.addListener(cb2);" + "myEvent.removeListener(cb1);" + "assert.AssertTrue(!!eventNatives.attachedListeners['named-event']);" + "myEvent.removeListener(cb2);" + "assert.AssertFalse(!!eventNatives.attachedListeners['named-event']);"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, EventsThatSupportRulesMustHaveAName) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var eventOpts = {supportsRules: true};" + "var assert = requireNative('assert');" + "var caught = false;" + "try {" + " var myEvent = new Event(undefined, undefined, eventOpts);" + "} catch (e) {" + " caught = true;" + "}" + "assert.AssertTrue(caught);"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, NamedEventDispatch) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var dispatchEvent = require('event_bindings').dispatchEvent;" + "var assert = requireNative('assert');" + "var e = new Event('myevent');" + "var called = false;" + "e.addListener(function() { called = true; });" + "dispatchEvent('myevent', []);" + "assert.AssertTrue(called);"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, AddListenerWithFiltersThrowsErrorByDefault) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var e = new Event('myevent');" + "var filter = [{" + " url: {hostSuffix: 'google.com'}," + "}];" + "var caught = false;" + "try {" + " e.addListener(function() {}, filter);" + "} catch (e) {" + " caught = true;" + "}" + "assert.AssertTrue(caught);"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, FilteredEventsAttachment) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var bindings = requireNative('event_natives');" + "var eventOpts = {supportsListeners: true, supportsFilters: true};" + "var e = new Event('myevent', undefined, eventOpts);" + "var cb = function() {};" + "var filters = {url: [{hostSuffix: 'google.com'}]};" + "e.addListener(cb, filters);" + "assert.AssertTrue(bindings.HasFilteredListener('myevent'));" + "e.removeListener(cb);" + "assert.AssertFalse(bindings.HasFilteredListener('myevent'));"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, DetachFilteredEvent) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var bindings = requireNative('event_natives');" + "var eventOpts = {supportsListeners: true, supportsFilters: true};" + "var e = new Event('myevent', undefined, eventOpts);" + "var cb1 = function() {};" + "var cb2 = function() {};" + "var filters = {url: [{hostSuffix: 'google.com'}]};" + "e.addListener(cb1, filters);" + "e.addListener(cb2, filters);" + "privates(e).impl.detach_();" + "assert.AssertFalse(bindings.HasFilteredListener('myevent'));"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, AttachAndRemoveSameFilteredEventListener) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var bindings = requireNative('event_natives');" + "var eventOpts = {supportsListeners: true, supportsFilters: true};" + "var e = new Event('myevent', undefined, eventOpts);" + "var cb = function() {};" + "var filters = {url: [{hostSuffix: 'google.com'}]};" + "e.addListener(cb, filters);" + "e.addListener(cb, filters);" + "assert.AssertTrue(bindings.HasFilteredListener('myevent'));" + "e.removeListener(cb);" + "assert.AssertTrue(bindings.HasFilteredListener('myevent'));" + "e.removeListener(cb);" + "assert.AssertFalse(bindings.HasFilteredListener('myevent'));"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, AddingFilterWithUrlFieldNotAListThrowsException) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var eventOpts = {supportsListeners: true, supportsFilters: true};" + "var e = new Event('myevent', undefined, eventOpts);" + "var cb = function() {};" + "var filters = {url: {hostSuffix: 'google.com'}};" + "var caught = false;" + "try {" + " e.addListener(cb, filters);" + "} catch (e) {" + " caught = true;" + "}" + "assert.AssertTrue(caught);"); + env()->module_system()->Require("test"); +} + +TEST_F(EventUnittest, MaxListeners) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var Event = require('event_bindings').Event;" + "var assert = requireNative('assert');" + "var eventOpts = {supportsListeners: true, maxListeners: 1};" + "var e = new Event('myevent', undefined, eventOpts);" + "var cb = function() {};" + "var caught = false;" + "try {" + " e.addListener(cb);" + "} catch (e) {" + " caught = true;" + "}" + "assert.AssertTrue(!caught);" + "try {" + " e.addListener(cb);" + "} catch (e) {" + " caught = true;" + "}" + "assert.AssertTrue(caught);"); + env()->module_system()->Require("test"); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/extension_frame_helper.cc b/chromium/extensions/renderer/extension_frame_helper.cc new file mode 100644 index 00000000000..ba47a7c7488 --- /dev/null +++ b/chromium/extensions/renderer/extension_frame_helper.cc @@ -0,0 +1,277 @@ +// Copyright 2013 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/extension_frame_helper.h" + +#include "base/strings/string_util.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/api/messaging/message.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/manifest_handlers/background_info.h" +#include "extensions/renderer/console.h" +#include "extensions/renderer/content_watcher.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/messaging_bindings.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/platform/WebSecurityOrigin.h" +#include "third_party/WebKit/public/web/WebConsoleMessage.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" + +namespace extensions { + +namespace { + +base::LazyInstance<std::set<const ExtensionFrameHelper*>> g_frame_helpers = + LAZY_INSTANCE_INITIALIZER; + +// Returns true if the render frame corresponding with |frame_helper| matches +// the given criteria. +bool RenderFrameMatches(const ExtensionFrameHelper* frame_helper, + ViewType match_view_type, + int match_window_id, + const std::string& match_extension_id) { + if (match_view_type != VIEW_TYPE_INVALID && + frame_helper->view_type() != match_view_type) + return false; + + // Not all frames have a valid ViewType, e.g. devtools, most GuestViews, and + // unclassified detached WebContents. + if (frame_helper->view_type() == VIEW_TYPE_INVALID) + return false; + + // This logic matches ExtensionWebContentsObserver::GetExtensionFromFrame. + blink::WebSecurityOrigin origin = + frame_helper->render_frame()->GetWebFrame()->getSecurityOrigin(); + if (origin.isUnique() || + !base::EqualsASCII(base::StringPiece16(origin.protocol()), + kExtensionScheme) || + !base::EqualsASCII(base::StringPiece16(origin.host()), + match_extension_id.c_str())) + return false; + + if (match_window_id != extension_misc::kUnknownWindowId && + frame_helper->browser_window_id() != match_window_id) + return false; + return true; +} + +// Runs every callback in |callbacks_to_be_run_and_cleared| while |frame_helper| +// is valid, and clears |callbacks_to_be_run_and_cleared|. +void RunCallbacksWhileFrameIsValid( + base::WeakPtr<ExtensionFrameHelper> frame_helper, + std::vector<base::Closure>* callbacks_to_be_run_and_cleared) { + // The JavaScript code can cause re-entrancy. To avoid a deadlock, don't run + // callbacks that are added during the iteration. + std::vector<base::Closure> callbacks; + callbacks_to_be_run_and_cleared->swap(callbacks); + for (auto& callback : callbacks) { + callback.Run(); + if (!frame_helper.get()) + return; // Frame and ExtensionFrameHelper invalidated by callback. + } +} + +} // namespace + +ExtensionFrameHelper::ExtensionFrameHelper(content::RenderFrame* render_frame, + Dispatcher* extension_dispatcher) + : content::RenderFrameObserver(render_frame), + content::RenderFrameObserverTracker<ExtensionFrameHelper>(render_frame), + view_type_(VIEW_TYPE_INVALID), + tab_id_(-1), + browser_window_id_(-1), + extension_dispatcher_(extension_dispatcher), + did_create_current_document_element_(false), + weak_ptr_factory_(this) { + g_frame_helpers.Get().insert(this); +} + +ExtensionFrameHelper::~ExtensionFrameHelper() { + g_frame_helpers.Get().erase(this); +} + +// static +std::vector<content::RenderFrame*> ExtensionFrameHelper::GetExtensionFrames( + const std::string& extension_id, + int browser_window_id, + ViewType view_type) { + std::vector<content::RenderFrame*> render_frames; + for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) { + if (RenderFrameMatches(helper, view_type, browser_window_id, extension_id)) + render_frames.push_back(helper->render_frame()); + } + return render_frames; +} + +// static +content::RenderFrame* ExtensionFrameHelper::GetBackgroundPageFrame( + const std::string& extension_id) { + for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) { + if (RenderFrameMatches(helper, VIEW_TYPE_EXTENSION_BACKGROUND_PAGE, + extension_misc::kUnknownWindowId, extension_id)) { + blink::WebLocalFrame* web_frame = helper->render_frame()->GetWebFrame(); + // Check if this is the top frame. + if (web_frame->top() == web_frame) + return helper->render_frame(); + } + } + return nullptr; +} + +// static +bool ExtensionFrameHelper::IsContextForEventPage(const ScriptContext* context) { + content::RenderFrame* render_frame = context->GetRenderFrame(); + return context->extension() && render_frame && + BackgroundInfo::HasLazyBackgroundPage(context->extension()) && + ExtensionFrameHelper::Get(render_frame)->view_type() == + VIEW_TYPE_EXTENSION_BACKGROUND_PAGE; +} + +void ExtensionFrameHelper::DidCreateDocumentElement() { + did_create_current_document_element_ = true; + extension_dispatcher_->DidCreateDocumentElement( + render_frame()->GetWebFrame()); +} + +void ExtensionFrameHelper::DidCreateNewDocument() { + did_create_current_document_element_ = false; +} + +void ExtensionFrameHelper::RunScriptsAtDocumentStart() { + DCHECK(did_create_current_document_element_); + RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), + &document_element_created_callbacks_); + // |this| might be dead by now. +} + +void ExtensionFrameHelper::RunScriptsAtDocumentEnd() { + RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), + &document_load_finished_callbacks_); + // |this| might be dead by now. +} + +void ExtensionFrameHelper::ScheduleAtDocumentStart( + const base::Closure& callback) { + document_element_created_callbacks_.push_back(callback); +} + +void ExtensionFrameHelper::ScheduleAtDocumentEnd( + const base::Closure& callback) { + document_load_finished_callbacks_.push_back(callback); +} + +void ExtensionFrameHelper::DidMatchCSS( + const blink::WebVector<blink::WebString>& newly_matching_selectors, + const blink::WebVector<blink::WebString>& stopped_matching_selectors) { + extension_dispatcher_->content_watcher()->DidMatchCSS( + render_frame()->GetWebFrame(), newly_matching_selectors, + stopped_matching_selectors); +} + +void ExtensionFrameHelper::DidCreateScriptContext( + v8::Local<v8::Context> context, + int extension_group, + int world_id) { + extension_dispatcher_->DidCreateScriptContext( + render_frame()->GetWebFrame(), context, extension_group, world_id); +} + +void ExtensionFrameHelper::WillReleaseScriptContext( + v8::Local<v8::Context> context, + int world_id) { + extension_dispatcher_->WillReleaseScriptContext( + render_frame()->GetWebFrame(), context, world_id); +} + +bool ExtensionFrameHelper::OnMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(ExtensionFrameHelper, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnConnect, + OnExtensionDispatchOnConnect) + IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnExtensionDeliverMessage) + IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnDisconnect, + OnExtensionDispatchOnDisconnect) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetTabId, OnExtensionSetTabId) + IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateBrowserWindowId, + OnUpdateBrowserWindowId) + IPC_MESSAGE_HANDLER(ExtensionMsg_NotifyRenderViewType, + OnNotifyRendererViewType) + IPC_MESSAGE_HANDLER(ExtensionMsg_Response, OnExtensionResponse) + IPC_MESSAGE_HANDLER(ExtensionMsg_MessageInvoke, OnExtensionMessageInvoke) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void ExtensionFrameHelper::OnExtensionDispatchOnConnect( + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id) { + MessagingBindings::DispatchOnConnect( + extension_dispatcher_->script_context_set(), + target_port_id, + channel_name, + source, + info, + tls_channel_id, + render_frame()); +} + +void ExtensionFrameHelper::OnExtensionDeliverMessage(int target_id, + const Message& message) { + MessagingBindings::DeliverMessage( + extension_dispatcher_->script_context_set(), target_id, message, + render_frame()); +} + +void ExtensionFrameHelper::OnExtensionDispatchOnDisconnect( + int port_id, + const std::string& error_message) { + MessagingBindings::DispatchOnDisconnect( + extension_dispatcher_->script_context_set(), port_id, error_message, + render_frame()); +} + +void ExtensionFrameHelper::OnExtensionSetTabId(int tab_id) { + CHECK_EQ(tab_id_, -1); + CHECK_GE(tab_id, 0); + tab_id_ = tab_id; +} + +void ExtensionFrameHelper::OnUpdateBrowserWindowId(int browser_window_id) { + browser_window_id_ = browser_window_id; +} + +void ExtensionFrameHelper::OnNotifyRendererViewType(ViewType type) { + // TODO(devlin): It'd be really nice to be able to + // DCHECK_EQ(VIEW_TYPE_INVALID, view_type_) here. + view_type_ = type; +} + +void ExtensionFrameHelper::OnExtensionResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error) { + extension_dispatcher_->OnExtensionResponse(request_id, + success, + response, + error); +} + +void ExtensionFrameHelper::OnExtensionMessageInvoke( + const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture) { + extension_dispatcher_->InvokeModuleSystemMethod(render_frame(), extension_id, + module_name, function_name, + args, user_gesture); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/extension_frame_helper.h b/chromium/extensions/renderer/extension_frame_helper.h new file mode 100644 index 00000000000..57ac3b66b23 --- /dev/null +++ b/chromium/extensions/renderer/extension_frame_helper.h @@ -0,0 +1,148 @@ +// Copyright 2013 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSION_FRAME_HELPER_H_ +#define EXTENSIONS_RENDERER_EXTENSION_FRAME_HELPER_H_ + +#include <vector> + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "content/public/common/console_message_level.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_frame_observer_tracker.h" +#include "extensions/common/view_type.h" + +struct ExtensionMsg_ExternalConnectionInfo; +struct ExtensionMsg_TabConnectionInfo; + +namespace base { +class ListValue; +} + +namespace extensions { + +class Dispatcher; +struct Message; +class ScriptContext; + +// RenderFrame-level plumbing for extension features. +class ExtensionFrameHelper + : public content::RenderFrameObserver, + public content::RenderFrameObserverTracker<ExtensionFrameHelper> { + public: + ExtensionFrameHelper(content::RenderFrame* render_frame, + Dispatcher* extension_dispatcher); + ~ExtensionFrameHelper() override; + + // Returns a list of extension RenderFrames that match the given filter + // criteria. A |browser_window_id| of extension_misc::kUnknownWindowId + // specifies "all", as does a |view_type| of VIEW_TYPE_INVALID. + static std::vector<content::RenderFrame*> GetExtensionFrames( + const std::string& extension_id, + int browser_window_id, + ViewType view_type); + + // Returns the main frame of the extension's background page, or null if there + // isn't one in this process. + static content::RenderFrame* GetBackgroundPageFrame( + const std::string& extension_id); + + // Returns true if the given |context| is for any frame in the extension's + // event page. + // TODO(devlin): This isn't really used properly, and should probably be + // deleted. + static bool IsContextForEventPage(const ScriptContext* context); + + ViewType view_type() const { return view_type_; } + int tab_id() const { return tab_id_; } + int browser_window_id() const { return browser_window_id_; } + bool did_create_current_document_element() const { + return did_create_current_document_element_; + } + + // Called when the document element has been inserted in this frame. This + // method may invoke untrusted JavaScript code that invalidate the frame and + // this ExtensionFrameHelper. + void RunScriptsAtDocumentStart(); + + // Called after the DOMContentLoaded event has fired. + void RunScriptsAtDocumentEnd(); + + // Schedule a callback, to be run at the next RunScriptsAtDocumentStart + // notification. Only call this when you are certain that there will be such a + // notification, e.g. from RenderFrameObserver::DidCreateDocumentElement. + // Otherwise the callback is never invoked, or invoked for a document that you + // were not expecting. + void ScheduleAtDocumentStart(const base::Closure& callback); + + // Schedule a callback, to be run at the next RunScriptsAtDocumentEnd call. + void ScheduleAtDocumentEnd(const base::Closure& callback); + + private: + // RenderFrameObserver implementation. + void DidCreateDocumentElement() override; + void DidCreateNewDocument() override; + void DidMatchCSS( + const blink::WebVector<blink::WebString>& newly_matching_selectors, + const blink::WebVector<blink::WebString>& stopped_matching_selectors) + override; + void DidCreateScriptContext(v8::Local<v8::Context>, + int extension_group, + int world_id) override; + void WillReleaseScriptContext(v8::Local<v8::Context>, int world_id) override; + bool OnMessageReceived(const IPC::Message& message) override; + + // IPC handlers. + void OnExtensionDispatchOnConnect( + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id); + void OnExtensionDeliverMessage(int target_port_id, + const Message& message); + void OnExtensionDispatchOnDisconnect(int port_id, + const std::string& error_message); + void OnExtensionSetTabId(int tab_id); + void OnUpdateBrowserWindowId(int browser_window_id); + void OnNotifyRendererViewType(ViewType view_type); + void OnExtensionResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error); + void OnExtensionMessageInvoke(const std::string& extension_id, + const std::string& module_name, + const std::string& function_name, + const base::ListValue& args, + bool user_gesture); + + // Type of view associated with the RenderFrame. + ViewType view_type_; + + // The id of the tab the render frame is attached to. + int tab_id_; + + // The id of the browser window the render frame is attached to. + int browser_window_id_; + + Dispatcher* extension_dispatcher_; + + // Whether or not the current document element has been created. + bool did_create_current_document_element_; + + // Callbacks to be run at the next RunScriptsAtDocumentStart notification. + std::vector<base::Closure> document_element_created_callbacks_; + + // Callbacks to be run at the next RunScriptsAtDocumentEnd notification. + std::vector<base::Closure> document_load_finished_callbacks_; + + base::WeakPtrFactory<ExtensionFrameHelper> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionFrameHelper); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EXTENSION_FRAME_HELPER_H_ diff --git a/chromium/extensions/renderer/extension_groups.h b/chromium/extensions/renderer/extension_groups.h new file mode 100644 index 00000000000..9766fa4ed45 --- /dev/null +++ b/chromium/extensions/renderer/extension_groups.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSION_GROUPS_H_ +#define EXTENSIONS_RENDERER_EXTENSION_GROUPS_H_ + +namespace extensions { + +// A set of extension groups for use with blink::registerExtension and +// WebFrame::ExecuteScriptInNewWorld to control which extensions get loaded +// into which contexts. +// TODO(kalman): Remove this when https://crbug.com/481699 is fixed. +enum ExtensionGroups { + // Use this to mark extensions to be loaded into content scripts only. + EXTENSION_GROUP_CONTENT_SCRIPTS = 1, +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EXTENSION_GROUPS_H_ diff --git a/chromium/extensions/renderer/extension_helper.cc b/chromium/extensions/renderer/extension_helper.cc new file mode 100644 index 00000000000..4d3bd2711dc --- /dev/null +++ b/chromium/extensions/renderer/extension_helper.cc @@ -0,0 +1,76 @@ +// 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/renderer/extension_helper.h" + +#include <stddef.h> + +#include "content/public/renderer/render_view.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/common/url_pattern_set.h" +#include "extensions/renderer/api/automation/automation_api_helper.h" +#include "extensions/renderer/dispatcher.h" +#include "third_party/WebKit/public/platform/WebURLRequest.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace extensions { + +ExtensionHelper::ExtensionHelper(content::RenderView* render_view, + Dispatcher* dispatcher) + : content::RenderViewObserver(render_view), + dispatcher_(dispatcher) { + // Lifecycle managed by RenderViewObserver. + new AutomationApiHelper(render_view); +} + +ExtensionHelper::~ExtensionHelper() { +} + +bool ExtensionHelper::OnMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(ExtensionHelper, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_SetFrameName, OnSetFrameName) + IPC_MESSAGE_HANDLER(ExtensionMsg_AppWindowClosed, + OnAppWindowClosed) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void ExtensionHelper::DraggableRegionsChanged(blink::WebFrame* frame) { + blink::WebVector<blink::WebDraggableRegion> webregions = + frame->document().draggableRegions(); + std::vector<DraggableRegion> regions; + for (size_t i = 0; i < webregions.size(); ++i) { + DraggableRegion region; + render_view()->ConvertViewportToWindowViaWidget(&webregions[i].bounds); + region.bounds = webregions[i].bounds; + region.draggable = webregions[i].draggable; + regions.push_back(region); + } + Send(new ExtensionHostMsg_UpdateDraggableRegions(routing_id(), regions)); +} + +void ExtensionHelper::OnSetFrameName(const std::string& name) { + blink::WebView* web_view = render_view()->GetWebView(); + if (web_view) + web_view->mainFrame()->setName(blink::WebString::fromUTF8(name)); +} + +void ExtensionHelper::OnAppWindowClosed() { + v8::HandleScope scope(v8::Isolate::GetCurrent()); + v8::Local<v8::Context> v8_context = + render_view()->GetWebView()->mainFrame()->mainWorldScriptContext(); + ScriptContext* script_context = + dispatcher_->script_context_set().GetByV8Context(v8_context); + if (!script_context) + return; + script_context->module_system()->CallModuleMethod("app.window", + "onAppWindowClosed"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/extension_helper.h b/chromium/extensions/renderer/extension_helper.h new file mode 100644 index 00000000000..a25a0a3f4ca --- /dev/null +++ b/chromium/extensions/renderer/extension_helper.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSION_HELPER_H_ +#define EXTENSIONS_RENDERER_EXTENSION_HELPER_H_ + +#include <string> + +#include "base/macros.h" +#include "content/public/renderer/render_view_observer.h" + +namespace extensions { +class Dispatcher; + +// RenderView-level plumbing for extension features. +class ExtensionHelper : public content::RenderViewObserver { + public: + ExtensionHelper(content::RenderView* render_view, Dispatcher* dispatcher); + ~ExtensionHelper() override; + + private: + // RenderViewObserver implementation. + bool OnMessageReceived(const IPC::Message& message) override; + void DraggableRegionsChanged(blink::WebFrame* frame) override; + + void OnAppWindowClosed(); + void OnSetFrameName(const std::string& name); + + Dispatcher* dispatcher_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionHelper); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EXTENSION_HELPER_H_ diff --git a/chromium/extensions/renderer/extension_injection_host.cc b/chromium/extensions/renderer/extension_injection_host.cc new file mode 100644 index 00000000000..6e5b0422871 --- /dev/null +++ b/chromium/extensions/renderer/extension_injection_host.cc @@ -0,0 +1,88 @@ +// Copyright 2015 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/extension_injection_host.h" + +#include "content/public/renderer/render_frame.h" +#include "extensions/common/constants.h" +#include "extensions/common/manifest_handlers/csp_info.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "third_party/WebKit/public/platform/WebSecurityOrigin.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" + +namespace extensions { + +ExtensionInjectionHost::ExtensionInjectionHost( + const Extension* extension) + : InjectionHost(HostID(HostID::EXTENSIONS, extension->id())), + extension_(extension) { +} + +ExtensionInjectionHost::~ExtensionInjectionHost() { +} + +// static +scoped_ptr<const InjectionHost> ExtensionInjectionHost::Create( + const std::string& extension_id) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + if (!extension) + return scoped_ptr<const ExtensionInjectionHost>(); + return scoped_ptr<const ExtensionInjectionHost>( + new ExtensionInjectionHost(extension)); +} + +std::string ExtensionInjectionHost::GetContentSecurityPolicy() const { + return CSPInfo::GetContentSecurityPolicy(extension_); +} + +const GURL& ExtensionInjectionHost::url() const { + return extension_->url(); +} + +const std::string& ExtensionInjectionHost::name() const { + return extension_->name(); +} + +PermissionsData::AccessType ExtensionInjectionHost::CanExecuteOnFrame( + const GURL& document_url, + content::RenderFrame* render_frame, + int tab_id, + bool is_declarative) const { + blink::WebSecurityOrigin top_frame_security_origin = + render_frame->GetWebFrame()->top()->getSecurityOrigin(); + // Only whitelisted extensions may run scripts on another extension's page. + if (top_frame_security_origin.protocol().utf8() == kExtensionScheme && + top_frame_security_origin.host().utf8() != extension_->id() && + !PermissionsData::CanExecuteScriptEverywhere(extension_)) + return PermissionsData::ACCESS_DENIED; + + // Declarative user scripts use "page access" (from "permissions" section in + // manifest) whereas non-declarative user scripts use custom + // "content script access" logic. + PermissionsData::AccessType access = PermissionsData::ACCESS_ALLOWED; + if (is_declarative) { + access = extension_->permissions_data()->GetPageAccess( + extension_, + document_url, + tab_id, + nullptr /* ignore error */); + } else { + access = extension_->permissions_data()->GetContentScriptAccess( + extension_, + document_url, + tab_id, + nullptr /* ignore error */); + } + if (access == PermissionsData::ACCESS_WITHHELD && + (tab_id == -1 || render_frame->GetWebFrame()->parent())) { + // Note: we don't consider ACCESS_WITHHELD for child frames or for frames + // outside of tabs because there is nowhere to surface a request. + // TODO(devlin): We should ask for permission somehow. crbug.com/491402. + access = PermissionsData::ACCESS_DENIED; + } + return access; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/extension_injection_host.h b/chromium/extensions/renderer/extension_injection_host.h new file mode 100644 index 00000000000..a3c9d320dfd --- /dev/null +++ b/chromium/extensions/renderer/extension_injection_host.h @@ -0,0 +1,45 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSION_INJECTION_HOST_H_ +#define EXTENSIONS_RENDERER_EXTENSION_INJECTION_HOST_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "extensions/common/extension.h" +#include "extensions/renderer/injection_host.h" + +namespace extensions { + +// A wrapper class that holds an extension and implements the InjectionHost +// interface. +class ExtensionInjectionHost : public InjectionHost { + public: + ExtensionInjectionHost(const Extension* extension); + ~ExtensionInjectionHost() override; + + // Create an ExtensionInjectionHost object. If the extension is gone, returns + // a null scoped ptr. + static scoped_ptr<const InjectionHost> Create( + const std::string& extension_id); + + private: + // InjectionHost: + std::string GetContentSecurityPolicy() const override; + const GURL& url() const override; + const std::string& name() const override; + PermissionsData::AccessType CanExecuteOnFrame( + const GURL& document_url, + content::RenderFrame* render_frame, + int tab_id, + bool is_declarative) const override; + + const Extension* extension_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionInjectionHost); +}; + +} // namespace extesions + +#endif // EXTENSIONS_RENDERER_EXTENSION_INJECTION_HOST_H_ diff --git a/chromium/extensions/renderer/extensions_render_frame_observer.cc b/chromium/extensions/renderer/extensions_render_frame_observer.cc new file mode 100644 index 00000000000..3f66f46256a --- /dev/null +++ b/chromium/extensions/renderer/extensions_render_frame_observer.cc @@ -0,0 +1,100 @@ +// 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/renderer/extensions_render_frame_observer.h" + +#include <stddef.h> + +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/stack_frame.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" + +namespace extensions { + +namespace { + +// The delimiter for a stack trace provided by WebKit. +const char kStackFrameDelimiter[] = "\n at "; + +// Get a stack trace from a WebKit console message. +// There are three possible scenarios: +// 1. WebKit gives us a stack trace in |stack_trace|. +// 2. The stack trace is embedded in the error |message| by an internal +// script. This will be more useful than |stack_trace|, since |stack_trace| +// will include the internal bindings trace, instead of a developer's code. +// 3. No stack trace is included. In this case, we should mock one up from +// the given line number and source. +// |message| will be populated with the error message only (i.e., will not +// include any stack trace). +StackTrace GetStackTraceFromMessage(base::string16* message, + const base::string16& source, + const base::string16& stack_trace, + int32_t line_number) { + StackTrace result; + std::vector<base::string16> pieces; + size_t index = 0; + + if (message->find(base::UTF8ToUTF16(kStackFrameDelimiter)) != + base::string16::npos) { + base::SplitStringUsingSubstr(*message, + base::UTF8ToUTF16(kStackFrameDelimiter), + &pieces); + *message = pieces[0]; + index = 1; + } else if (!stack_trace.empty()) { + base::SplitStringUsingSubstr(stack_trace, + base::UTF8ToUTF16(kStackFrameDelimiter), + &pieces); + } + + // If we got a stack trace, parse each frame from the text. + if (index < pieces.size()) { + for (; index < pieces.size(); ++index) { + scoped_ptr<StackFrame> frame = StackFrame::CreateFromText(pieces[index]); + if (frame.get()) + result.push_back(*frame); + } + } + + if (result.empty()) { // If we don't have a stack trace, mock one up. + result.push_back( + StackFrame(line_number, + 1u, // column number + source, + base::string16() /* no function name */ )); + } + + return result; +} + +} // namespace + +ExtensionsRenderFrameObserver::ExtensionsRenderFrameObserver( + content::RenderFrame* render_frame) + : content::RenderFrameObserver(render_frame) { +} + +ExtensionsRenderFrameObserver::~ExtensionsRenderFrameObserver() { +} + +void ExtensionsRenderFrameObserver::DetailedConsoleMessageAdded( + const base::string16& message, + const base::string16& source, + const base::string16& stack_trace_string, + uint32_t line_number, + int32_t severity_level) { + base::string16 trimmed_message = message; + StackTrace stack_trace = GetStackTraceFromMessage( + &trimmed_message, + source, + stack_trace_string, + line_number); + Send(new ExtensionHostMsg_DetailedConsoleMessageAdded( + routing_id(), trimmed_message, source, stack_trace, severity_level)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/extensions_render_frame_observer.h b/chromium/extensions/renderer/extensions_render_frame_observer.h new file mode 100644 index 00000000000..55521b2b7b5 --- /dev/null +++ b/chromium/extensions/renderer/extensions_render_frame_observer.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSIONS_RENDER_FRAME_OBSERVER_H_ +#define EXTENSIONS_RENDERER_EXTENSIONS_RENDER_FRAME_OBSERVER_H_ + +#include <stdint.h> + +#include "base/macros.h" +#include "content/public/renderer/render_frame_observer.h" + +namespace extensions { + +// This class holds the extensions specific parts of RenderFrame, and has the +// same lifetime. +class ExtensionsRenderFrameObserver + : public content::RenderFrameObserver { + public: + explicit ExtensionsRenderFrameObserver( + content::RenderFrame* render_frame); + ~ExtensionsRenderFrameObserver() override; + + private: + // RenderFrameObserver implementation. + void DetailedConsoleMessageAdded(const base::string16& message, + const base::string16& source, + const base::string16& stack_trace, + uint32_t line_number, + int32_t severity_level) override; + + DISALLOW_COPY_AND_ASSIGN(ExtensionsRenderFrameObserver); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EXTENSIONS_RENDER_FRAME_OBSERVER_H_ + diff --git a/chromium/extensions/renderer/extensions_renderer_client.cc b/chromium/extensions/renderer/extensions_renderer_client.cc new file mode 100644 index 00000000000..26c46ed70ed --- /dev/null +++ b/chromium/extensions/renderer/extensions_renderer_client.cc @@ -0,0 +1,26 @@ +// 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/renderer/extensions_renderer_client.h" + +#include "base/logging.h" + +namespace extensions { + +namespace { + +ExtensionsRendererClient* g_client = NULL; + +} // namespace + +ExtensionsRendererClient* ExtensionsRendererClient::Get() { + CHECK(g_client); + return g_client; +} + +void ExtensionsRendererClient::Set(ExtensionsRendererClient* client) { + g_client = client; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/extensions_renderer_client.h b/chromium/extensions/renderer/extensions_renderer_client.h new file mode 100644 index 00000000000..ae4a31ba689 --- /dev/null +++ b/chromium/extensions/renderer/extensions_renderer_client.h @@ -0,0 +1,39 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_EXTENSIONS_RENDERER_CLIENT_H_ +#define EXTENSIONS_RENDERER_EXTENSIONS_RENDERER_CLIENT_H_ + +class ResourceBundleSourceMap; + +namespace extensions { + +// Interface to allow the extensions module to make render-process-specific +// queries of the embedder. Should be Set() once in the render process. +// +// NOTE: Methods that do not require knowledge of renderer concepts should be +// added in ExtensionsClient (extensions/common/extensions_client.h) even if +// they are only used in the renderer process. +class ExtensionsRendererClient { + public: + virtual ~ExtensionsRendererClient() {} + + // Returns true if the current render process was launched incognito. + virtual bool IsIncognitoProcess() const = 0; + + // Returns the lowest isolated world ID available to extensions. + // Must be greater than 0. See blink::WebFrame::executeScriptInIsolatedWorld + // (third_party/WebKit/public/web/WebFrame.h) for additional context. + virtual int GetLowestIsolatedWorldId() const = 0; + + // Returns the single instance of |this|. + static ExtensionsRendererClient* Get(); + + // Initialize the single instance. + static void Set(ExtensionsRendererClient* client); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_EXTENSIONS_RENDERER_CLIENT_H_ diff --git a/chromium/extensions/renderer/file_system_natives.cc b/chromium/extensions/renderer/file_system_natives.cc new file mode 100644 index 00000000000..2b7006299a2 --- /dev/null +++ b/chromium/extensions/renderer/file_system_natives.cc @@ -0,0 +1,124 @@ +// 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/renderer/file_system_natives.h" + +#include <string> + +#include "extensions/common/constants.h" +#include "extensions/renderer/script_context.h" +#include "storage/common/fileapi/file_system_types.h" +#include "storage/common/fileapi/file_system_util.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/web/WebDOMFileSystem.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "url/origin.h" + +namespace extensions { + +FileSystemNatives::FileSystemNatives(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetFileEntry", + base::Bind(&FileSystemNatives::GetFileEntry, base::Unretained(this))); + RouteFunction("GetIsolatedFileSystem", + base::Bind(&FileSystemNatives::GetIsolatedFileSystem, + base::Unretained(this))); + RouteFunction("CrackIsolatedFileSystemName", + base::Bind(&FileSystemNatives::CrackIsolatedFileSystemName, + base::Unretained(this))); +} + +void FileSystemNatives::GetIsolatedFileSystem( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(args.Length() == 1 || args.Length() == 2); + CHECK(args[0]->IsString()); + std::string file_system_id(*v8::String::Utf8Value(args[0])); + blink::WebLocalFrame* webframe = + blink::WebLocalFrame::frameForContext(context()->v8_context()); + DCHECK(webframe); + + GURL context_url = + extensions::ScriptContext::GetDataSourceURLForFrame(webframe); + CHECK(context_url.SchemeIs(extensions::kExtensionScheme)); + + const GURL origin(url::Origin(context_url).Serialize()); + std::string name(storage::GetIsolatedFileSystemName(origin, file_system_id)); + + // The optional second argument is the subfolder within the isolated file + // system at which to root the DOMFileSystem we're returning to the caller. + std::string optional_root_name; + if (args.Length() == 2) { + CHECK(args[1]->IsString()); + optional_root_name = *v8::String::Utf8Value(args[1]); + } + + GURL root_url(storage::GetIsolatedFileSystemRootURIString( + origin, file_system_id, optional_root_name)); + + args.GetReturnValue().Set( + blink::WebDOMFileSystem::create(webframe, + blink::WebFileSystemTypeIsolated, + blink::WebString::fromUTF8(name), + root_url) + .toV8Value(context()->v8_context()->Global(), args.GetIsolate())); +} + +void FileSystemNatives::GetFileEntry( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(5, args.Length()); + CHECK(args[0]->IsString()); + std::string type_string = *v8::String::Utf8Value(args[0]); + blink::WebFileSystemType type; + bool is_valid_type = storage::GetFileSystemPublicType(type_string, &type); + DCHECK(is_valid_type); + if (is_valid_type == false) { + return; + } + + CHECK(args[1]->IsString()); + CHECK(args[2]->IsString()); + CHECK(args[3]->IsString()); + std::string file_system_name(*v8::String::Utf8Value(args[1])); + GURL file_system_root_url(*v8::String::Utf8Value(args[2])); + std::string file_path_string(*v8::String::Utf8Value(args[3])); + base::FilePath file_path = base::FilePath::FromUTF8Unsafe(file_path_string); + DCHECK(storage::VirtualPath::IsAbsolute(file_path.value())); + + CHECK(args[4]->IsBoolean()); + blink::WebDOMFileSystem::EntryType entry_type = + args[4]->BooleanValue() ? blink::WebDOMFileSystem::EntryTypeDirectory + : blink::WebDOMFileSystem::EntryTypeFile; + + blink::WebLocalFrame* webframe = + blink::WebLocalFrame::frameForContext(context()->v8_context()); + DCHECK(webframe); + args.GetReturnValue().Set( + blink::WebDOMFileSystem::create( + webframe, + type, + blink::WebString::fromUTF8(file_system_name), + file_system_root_url) + .createV8Entry(blink::WebString::fromUTF8(file_path_string), + entry_type, + context()->v8_context()->Global(), + args.GetIsolate())); +} + +void FileSystemNatives::CrackIsolatedFileSystemName( + const v8::FunctionCallbackInfo<v8::Value>& args) { + DCHECK_EQ(args.Length(), 1); + DCHECK(args[0]->IsString()); + std::string filesystem_name = *v8::String::Utf8Value(args[0]); + std::string filesystem_id; + if (!storage::CrackIsolatedFileSystemName(filesystem_name, &filesystem_id)) + return; + + args.GetReturnValue().Set(v8::String::NewFromUtf8(args.GetIsolate(), + filesystem_id.c_str(), + v8::String::kNormalString, + filesystem_id.size())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/file_system_natives.h b/chromium/extensions/renderer/file_system_natives.h new file mode 100644 index 00000000000..102f2200644 --- /dev/null +++ b/chromium/extensions/renderer/file_system_natives.h @@ -0,0 +1,31 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_FILE_SYSTEM_NATIVES_H_ +#define EXTENSIONS_RENDERER_FILE_SYSTEM_NATIVES_H_ + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Custom bindings for the nativeFileSystem API. +class FileSystemNatives : public ObjectBackedNativeHandler { + public: + explicit FileSystemNatives(ScriptContext* context); + + private: + void GetFileEntry(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetIsolatedFileSystem(const v8::FunctionCallbackInfo<v8::Value>& args); + void CrackIsolatedFileSystemName( + const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(FileSystemNatives); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_FILE_SYSTEM_NATIVES_H_ diff --git a/chromium/extensions/renderer/gc_callback.cc b/chromium/extensions/renderer/gc_callback.cc new file mode 100644 index 00000000000..46b7f8acdf1 --- /dev/null +++ b/chromium/extensions/renderer/gc_callback.cc @@ -0,0 +1,58 @@ +// Copyright 2015 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/gc_callback.h" + +#include "base/bind.h" +#include "base/message_loop/message_loop.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +GCCallback::GCCallback(ScriptContext* context, + const v8::Local<v8::Object>& object, + const v8::Local<v8::Function>& callback, + const base::Closure& fallback) + : context_(context), + object_(context->isolate(), object), + callback_(context->isolate(), callback), + fallback_(fallback), + weak_ptr_factory_(this) { + object_.SetWeak(this, OnObjectGC, v8::WeakCallbackType::kParameter); + context->AddInvalidationObserver(base::Bind(&GCCallback::OnContextInvalidated, + weak_ptr_factory_.GetWeakPtr())); +} + +GCCallback::~GCCallback() {} + +// static +void GCCallback::OnObjectGC(const v8::WeakCallbackInfo<GCCallback>& data) { + // Usually FirstWeakCallback should do nothing other than reset |object_| + // and then set a second weak callback to run later. We can sidestep that, + // because posting a task to the current message loop is all but free - but + // DO NOT add any more work to this method. The only acceptable place to add + // code is RunCallback. + GCCallback* self = data.GetParameter(); + self->object_.Reset(); + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&GCCallback::RunCallback, + self->weak_ptr_factory_.GetWeakPtr())); +} + +void GCCallback::RunCallback() { + fallback_.Reset(); + v8::Isolate* isolate = context_->isolate(); + v8::HandleScope handle_scope(isolate); + context_->CallFunction(v8::Local<v8::Function>::New(isolate, callback_)); + delete this; +} + +void GCCallback::OnContextInvalidated() { + if (!fallback_.is_null()) { + fallback_.Run(); + delete this; + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/gc_callback.h b/chromium/extensions/renderer/gc_callback.h new file mode 100644 index 00000000000..96dc2441aed --- /dev/null +++ b/chromium/extensions/renderer/gc_callback.h @@ -0,0 +1,56 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_GC_CALLBACK_H_ +#define EXTENSIONS_RENDERER_GC_CALLBACK_H_ + +#include <map> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "v8/include/v8.h" + +namespace extensions { + +class ScriptContext; + +// Runs |callback| when v8 garbage collects |object|, or |fallback| if +// |context| is invalidated first. Exactly one of |callback| or |fallback| will +// be called, after which it deletes itself. +class GCCallback { + public: + GCCallback(ScriptContext* context, + const v8::Local<v8::Object>& object, + const v8::Local<v8::Function>& callback, + const base::Closure& fallback); + ~GCCallback(); + + private: + static void OnObjectGC(const v8::WeakCallbackInfo<GCCallback>& data); + void RunCallback(); + void OnContextInvalidated(); + + // The context which owns |object_|. + ScriptContext* context_; + + // The object this GCCallback is bound to. + v8::Global<v8::Object> object_; + + // The function to run when |object_| is garbage collected. + v8::Global<v8::Function> callback_; + + // The function to run if |context_| is invalidated before we have a chance + // to execute |callback_|. + base::Closure fallback_; + + base::WeakPtrFactory<GCCallback> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(GCCallback); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_GC_CALLBACK_H_ diff --git a/chromium/extensions/renderer/gc_callback_unittest.cc b/chromium/extensions/renderer/gc_callback_unittest.cc new file mode 100644 index 00000000000..f1b70ba3e24 --- /dev/null +++ b/chromium/extensions/renderer/gc_callback_unittest.cc @@ -0,0 +1,161 @@ +// Copyright 2015 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 "base/bind.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_set.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/gc_callback.h" +#include "extensions/renderer/scoped_web_frame.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "gin/function_template.h" +#include "gin/public/context_holder.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "v8/include/v8.h" + +namespace extensions { +namespace { + +void SetToTrue(bool* value) { + if (*value) + ADD_FAILURE() << "Value is already true"; + *value = true; +} + +class GCCallbackTest : public testing::Test { + public: + GCCallbackTest() : script_context_set_(&active_extensions_) {} + + protected: + base::MessageLoop& message_loop() { return message_loop_; } + + ScriptContextSet& script_context_set() { return script_context_set_; } + + v8::Local<v8::Context> v8_context() { + return v8::Local<v8::Context>::New(v8::Isolate::GetCurrent(), v8_context_); + } + + ScriptContext* RegisterScriptContext() { + // No extension group or world ID. + return script_context_set_.Register( + web_frame_.frame(), + v8::Local<v8::Context>::New(v8::Isolate::GetCurrent(), v8_context_), 0, + 0); + } + + void RequestGarbageCollection() { + v8::Isolate::GetCurrent()->RequestGarbageCollectionForTesting( + v8::Isolate::kFullGarbageCollection); + } + + private: + void SetUp() override { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Context> local_v8_context = v8::Context::New(isolate); + v8_context_.Reset(isolate, local_v8_context); + // ScriptContexts rely on gin. + gin_context_holder_.reset(new gin::ContextHolder(isolate)); + gin_context_holder_->SetContext(local_v8_context); + } + + void TearDown() override { + gin_context_holder_.reset(); + v8_context_.Reset(); + RequestGarbageCollection(); + } + + base::MessageLoop message_loop_; + ScopedWebFrame web_frame_; // (this will construct the v8::Isolate) + ExtensionIdSet active_extensions_; + ScriptContextSet script_context_set_; + v8::Global<v8::Context> v8_context_; + scoped_ptr<gin::ContextHolder> gin_context_holder_; + + DISALLOW_COPY_AND_ASSIGN(GCCallbackTest); +}; + +TEST_F(GCCallbackTest, GCBeforeContextInvalidated) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(v8_context()); + + ScriptContext* script_context = RegisterScriptContext(); + + bool callback_invoked = false; + bool fallback_invoked = false; + + { + // Nest another HandleScope so that |object| and |unreachable_function|'s + // handles will be garbage collected. + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Object> object = v8::Object::New(isolate); + v8::Local<v8::FunctionTemplate> unreachable_function = + gin::CreateFunctionTemplate(isolate, + base::Bind(SetToTrue, &callback_invoked)); + // The GCCallback will delete itself, or memory tests will complain. + new GCCallback(script_context, object, unreachable_function->GetFunction(), + base::Bind(SetToTrue, &fallback_invoked)); + } + + // Trigger a GC. Only the callback should be invoked. + RequestGarbageCollection(); + message_loop().RunUntilIdle(); + + EXPECT_TRUE(callback_invoked); + EXPECT_FALSE(fallback_invoked); + + // Invalidate the context. The fallback should not be invoked because the + // callback was already invoked. + script_context_set().Remove(script_context); + message_loop().RunUntilIdle(); + + EXPECT_FALSE(fallback_invoked); +} + +TEST_F(GCCallbackTest, ContextInvalidatedBeforeGC) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(v8_context()); + + ScriptContext* script_context = RegisterScriptContext(); + + bool callback_invoked = false; + bool fallback_invoked = false; + + { + // Nest another HandleScope so that |object| and |unreachable_function|'s + // handles will be garbage collected. + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Object> object = v8::Object::New(isolate); + v8::Local<v8::FunctionTemplate> unreachable_function = + gin::CreateFunctionTemplate(isolate, + base::Bind(SetToTrue, &callback_invoked)); + // The GCCallback will delete itself, or memory tests will complain. + new GCCallback(script_context, object, unreachable_function->GetFunction(), + base::Bind(SetToTrue, &fallback_invoked)); + } + + // Invalidate the context. Only the fallback should be invoked. + script_context_set().Remove(script_context); + message_loop().RunUntilIdle(); + + EXPECT_FALSE(callback_invoked); + EXPECT_TRUE(fallback_invoked); + + // Trigger a GC. The callback should not be invoked because the fallback was + // already invoked. + RequestGarbageCollection(); + message_loop().RunUntilIdle(); + + EXPECT_FALSE(callback_invoked); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/OWNERS b/chromium/extensions/renderer/guest_view/OWNERS new file mode 100644 index 00000000000..51bd1721001 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/OWNERS @@ -0,0 +1,5 @@ +fsamuel@chromium.org +lazyboy@chromium.org +lfg@chromium.org +hanxi@chromium.org +wjmaclean@chromium.org diff --git a/chromium/extensions/renderer/guest_view/extensions_guest_view_container.cc b/chromium/extensions/renderer/guest_view/extensions_guest_view_container.cc new file mode 100644 index 00000000000..ffa3580e3fe --- /dev/null +++ b/chromium/extensions/renderer/guest_view/extensions_guest_view_container.cc @@ -0,0 +1,64 @@ +// 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/renderer/guest_view/extensions_guest_view_container.h" + +#include "content/public/renderer/render_frame.h" +#include "ui/gfx/geometry/size.h" + +namespace extensions { + +ExtensionsGuestViewContainer::ExtensionsGuestViewContainer( + content::RenderFrame* render_frame) + : GuestViewContainer(render_frame), + element_resize_isolate_(nullptr), + weak_ptr_factory_(this) { +} + +ExtensionsGuestViewContainer::~ExtensionsGuestViewContainer() { +} + +void ExtensionsGuestViewContainer::OnDestroy(bool embedder_frame_destroyed) { +} + +void ExtensionsGuestViewContainer::RegisterElementResizeCallback( + v8::Local<v8::Function> callback, + v8::Isolate* isolate) { + element_resize_callback_.Reset(isolate, callback); + element_resize_isolate_ = isolate; +} + +void ExtensionsGuestViewContainer::DidResizeElement(const gfx::Size& new_size) { + // Call the element resize callback, if one is registered. + if (element_resize_callback_.IsEmpty()) + return; + + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ExtensionsGuestViewContainer::CallElementResizeCallback, + weak_ptr_factory_.GetWeakPtr(), new_size)); +} + +void ExtensionsGuestViewContainer::CallElementResizeCallback( + const gfx::Size& new_size) { + v8::HandleScope handle_scope(element_resize_isolate_); + v8::Local<v8::Function> callback = v8::Local<v8::Function>::New( + element_resize_isolate_, element_resize_callback_); + v8::Local<v8::Context> context = callback->CreationContext(); + if (context.IsEmpty()) + return; + + const int argc = 2; + v8::Local<v8::Value> argv[argc] = { + v8::Integer::New(element_resize_isolate_, new_size.width()), + v8::Integer::New(element_resize_isolate_, new_size.height())}; + + v8::Context::Scope context_scope(context); + v8::MicrotasksScope microtasks( + element_resize_isolate_, v8::MicrotasksScope::kDoNotRunMicrotasks); + + callback->Call(context->Global(), argc, argv); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/extensions_guest_view_container.h b/chromium/extensions/renderer/guest_view/extensions_guest_view_container.h new file mode 100644 index 00000000000..bc95b187f80 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/extensions_guest_view_container.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_GUEST_VIEW_EXTENSIONS_GUEST_VIEW_CONTAINER_H_ +#define EXTENSIONS_RENDERER_GUEST_VIEW_EXTENSIONS_GUEST_VIEW_CONTAINER_H_ + +#include <queue> + +#include "base/macros.h" +#include "components/guest_view/renderer/guest_view_container.h" +#include "v8/include/v8.h" + +namespace gfx { +class Size; +} + +namespace extensions { + +class ExtensionsGuestViewContainer : public guest_view::GuestViewContainer { + public: + explicit ExtensionsGuestViewContainer(content::RenderFrame* render_frame); + + void RegisterElementResizeCallback(v8::Local<v8::Function> callback, + v8::Isolate* isolate); + + // BrowserPluginDelegate implementation. + void DidResizeElement(const gfx::Size& new_size) override; + + protected: + ~ExtensionsGuestViewContainer() override; + + private: + void CallElementResizeCallback(const gfx::Size& new_size); + + // GuestViewContainer implementation. + void OnDestroy(bool embedder_frame_destroyed) override; + + v8::Global<v8::Function> element_resize_callback_; + v8::Isolate* element_resize_isolate_; + + // Weak pointer factory used for calling the element resize callback. + base::WeakPtrFactory<ExtensionsGuestViewContainer> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionsGuestViewContainer); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_GUEST_VIEW_EXTENSIONS_GUEST_VIEW_CONTAINER_H_ diff --git a/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.cc b/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.cc new file mode 100644 index 00000000000..16f1f4771e4 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.cc @@ -0,0 +1,26 @@ +// Copyright 2015 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/guest_view/extensions_guest_view_container_dispatcher.h" + +#include "ipc/ipc_message.h" +#include "ipc/ipc_message_macros.h" + +namespace extensions { + +ExtensionsGuestViewContainerDispatcher:: + ExtensionsGuestViewContainerDispatcher() { +} + +ExtensionsGuestViewContainerDispatcher:: + ~ExtensionsGuestViewContainerDispatcher() { +} + +bool ExtensionsGuestViewContainerDispatcher::HandlesMessage( + const IPC::Message& message) { + return GuestViewContainerDispatcher::HandlesMessage(message) || + (IPC_MESSAGE_CLASS(message) == ExtensionsGuestViewMsgStart); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.h b/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.h new file mode 100644 index 00000000000..b04ee04f1e0 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/extensions_guest_view_container_dispatcher.h @@ -0,0 +1,23 @@ +// Copyright 2015 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 "base/macros.h" +#include "components/guest_view/renderer/guest_view_container_dispatcher.h" + +namespace extensions { + +class ExtensionsGuestViewContainerDispatcher + : public guest_view::GuestViewContainerDispatcher { + public: + ExtensionsGuestViewContainerDispatcher(); + ~ExtensionsGuestViewContainerDispatcher() override; + + private: + // guest_view::GuestViewContainerDispatcher implementation. + bool HandlesMessage(const IPC::Message& message) override; + + DISALLOW_COPY_AND_ASSIGN(ExtensionsGuestViewContainerDispatcher); +}; + +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.cc b/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.cc new file mode 100644 index 00000000000..f54c87891c7 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.cc @@ -0,0 +1,438 @@ +// 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/renderer/guest_view/guest_view_internal_custom_bindings.h" + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "components/guest_view/common/guest_view_constants.h" +#include "components/guest_view/common/guest_view_messages.h" +#include "components/guest_view/renderer/guest_view_request.h" +#include "components/guest_view/renderer/iframe_guest_view_container.h" +#include "components/guest_view/renderer/iframe_guest_view_request.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_view.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/guest_view/extensions_guest_view_messages.h" +#include "extensions/renderer/guest_view/extensions_guest_view_container.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebRemoteFrame.h" +#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebView.h" +#include "v8/include/v8.h" + +using content::V8ValueConverter; + +namespace { + +// A map from view instance ID to view object (stored via weak V8 reference). +// Views are registered into this map via +// GuestViewInternalCustomBindings::RegisterView(), and accessed via +// GuestViewInternalCustomBindings::GetViewFromID(). +using ViewMap = std::map<int, v8::Global<v8::Object>*>; +static base::LazyInstance<ViewMap> weak_view_map = LAZY_INSTANCE_INITIALIZER; + +} // namespace + +namespace extensions { + +namespace { + +content::RenderFrame* GetRenderFrame(v8::Handle<v8::Value> value) { + v8::Local<v8::Context> context = + v8::Local<v8::Object>::Cast(value)->CreationContext(); + if (context.IsEmpty()) + return nullptr; + blink::WebLocalFrame* frame = blink::WebLocalFrame::frameForContext(context); + if (!frame) + return nullptr; + return content::RenderFrame::FromWebFrame(frame); +} + +} // namespace + +GuestViewInternalCustomBindings::GuestViewInternalCustomBindings( + ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("AttachGuest", + base::Bind(&GuestViewInternalCustomBindings::AttachGuest, + base::Unretained(this))); + RouteFunction("DetachGuest", + base::Bind(&GuestViewInternalCustomBindings::DetachGuest, + base::Unretained(this))); + RouteFunction("AttachIframeGuest", + base::Bind(&GuestViewInternalCustomBindings::AttachIframeGuest, + base::Unretained(this))); + RouteFunction("DestroyContainer", + base::Bind(&GuestViewInternalCustomBindings::DestroyContainer, + base::Unretained(this))); + RouteFunction("GetContentWindow", + base::Bind(&GuestViewInternalCustomBindings::GetContentWindow, + base::Unretained(this))); + RouteFunction("GetViewFromID", + base::Bind(&GuestViewInternalCustomBindings::GetViewFromID, + base::Unretained(this))); + RouteFunction( + "RegisterDestructionCallback", + base::Bind(&GuestViewInternalCustomBindings::RegisterDestructionCallback, + base::Unretained(this))); + RouteFunction( + "RegisterElementResizeCallback", + base::Bind( + &GuestViewInternalCustomBindings::RegisterElementResizeCallback, + base::Unretained(this))); + RouteFunction("RegisterView", + base::Bind(&GuestViewInternalCustomBindings::RegisterView, + base::Unretained(this))); + RouteFunction( + "RunWithGesture", + base::Bind(&GuestViewInternalCustomBindings::RunWithGesture, + base::Unretained(this))); +} + +GuestViewInternalCustomBindings::~GuestViewInternalCustomBindings() {} + +// static +void GuestViewInternalCustomBindings::ResetMapEntry( + const v8::WeakCallbackInfo<int>& data) { + int* param = data.GetParameter(); + int view_instance_id = *param; + delete param; + ViewMap& view_map = weak_view_map.Get(); + auto entry = view_map.find(view_instance_id); + if (entry == view_map.end()) + return; + + // V8 says we need to explicitly reset weak handles from their callbacks. + // It is not implicit as one might expect. + entry->second->Reset(); + delete entry->second; + view_map.erase(entry); + + // Let the GuestViewManager know that a GuestView has been garbage collected. + content::RenderThread::Get()->Send( + new GuestViewHostMsg_ViewGarbageCollected(view_instance_id)); +} + +void GuestViewInternalCustomBindings::AttachGuest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Allow for an optional callback parameter. + CHECK(args.Length() >= 3 && args.Length() <= 4); + // Element Instance ID. + CHECK(args[0]->IsInt32()); + // Guest Instance ID. + CHECK(args[1]->IsInt32()); + // Attach Parameters. + CHECK(args[2]->IsObject()); + // Optional Callback Function. + CHECK(args.Length() < 4 || args[3]->IsFunction()); + + int element_instance_id = args[0]->Int32Value(); + // An element instance ID uniquely identifies a GuestViewContainer. + auto guest_view_container = + guest_view::GuestViewContainer::FromID(element_instance_id); + + // TODO(fsamuel): Should we be reporting an error if the element instance ID + // is invalid? + if (!guest_view_container) + return; + + int guest_instance_id = args[1]->Int32Value(); + + scoped_ptr<base::DictionaryValue> params; + { + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + scoped_ptr<base::Value> params_as_value( + converter->FromV8Value(args[2], context()->v8_context())); + params = base::DictionaryValue::From(std::move(params_as_value)); + CHECK(params); + } + + // Add flag to |params| to indicate that the element size is specified in + // logical units. + params->SetBoolean(guest_view::kElementSizeIsLogical, true); + + linked_ptr<guest_view::GuestViewRequest> request( + new guest_view::GuestViewAttachRequest( + guest_view_container, guest_instance_id, std::move(params), + args.Length() == 4 ? args[3].As<v8::Function>() + : v8::Local<v8::Function>(), + args.GetIsolate())); + guest_view_container->IssueRequest(request); + + args.GetReturnValue().Set(v8::Boolean::New(context()->isolate(), true)); +} + +void GuestViewInternalCustomBindings::DetachGuest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Allow for an optional callback parameter. + CHECK(args.Length() >= 1 && args.Length() <= 2); + // Element Instance ID. + CHECK(args[0]->IsInt32()); + // Optional Callback Function. + CHECK(args.Length() < 2 || args[1]->IsFunction()); + + int element_instance_id = args[0]->Int32Value(); + // An element instance ID uniquely identifies a GuestViewContainer. + auto guest_view_container = + guest_view::GuestViewContainer::FromID(element_instance_id); + + // TODO(fsamuel): Should we be reporting an error if the element instance ID + // is invalid? + if (!guest_view_container) + return; + + linked_ptr<guest_view::GuestViewRequest> request( + new guest_view::GuestViewDetachRequest( + guest_view_container, args.Length() == 2 ? args[1].As<v8::Function>() + : v8::Local<v8::Function>(), + args.GetIsolate())); + guest_view_container->IssueRequest(request); + + args.GetReturnValue().Set(v8::Boolean::New(context()->isolate(), true)); +} + +void GuestViewInternalCustomBindings::AttachIframeGuest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Allow for an optional callback parameter. + const int num_required_params = 4; + CHECK(args.Length() >= num_required_params && + args.Length() <= (num_required_params + 1)); + // Element Instance ID. + CHECK(args[0]->IsInt32()); + // Guest Instance ID. + CHECK(args[1]->IsInt32()); + // Attach Parameters. + CHECK(args[2]->IsObject()); + // <iframe>.contentWindow. + CHECK(args[3]->IsObject()); + // Optional Callback Function. + CHECK(args.Length() <= num_required_params || + args[num_required_params]->IsFunction()); + + int element_instance_id = args[0]->Int32Value(); + int guest_instance_id = args[1]->Int32Value(); + + scoped_ptr<base::DictionaryValue> params; + { + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + scoped_ptr<base::Value> params_as_value( + converter->FromV8Value(args[2], context()->v8_context())); + params = base::DictionaryValue::From(std::move(params_as_value)); + CHECK(params); + } + + // Add flag to |params| to indicate that the element size is specified in + // logical units. + params->SetBoolean(guest_view::kElementSizeIsLogical, true); + + content::RenderFrame* render_frame = GetRenderFrame(args[3]); + blink::WebLocalFrame* frame = render_frame->GetWebFrame(); + + // Parent must exist. + blink::WebFrame* parent_frame = frame->parent(); + DCHECK(parent_frame); + DCHECK(parent_frame->isWebLocalFrame()); + + content::RenderFrame* embedder_parent_frame = + content::RenderFrame::FromWebFrame(parent_frame); + + // Create a GuestViewContainer if it does not exist. + // An element instance ID uniquely identifies an IframeGuestViewContainer + // within a RenderView. + auto* guest_view_container = + static_cast<guest_view::IframeGuestViewContainer*>( + guest_view::GuestViewContainer::FromID(element_instance_id)); + // This is the first time we hear about the |element_instance_id|. + DCHECK(!guest_view_container); + // The <webview> element's GC takes ownership of |guest_view_container|. + guest_view_container = + new guest_view::IframeGuestViewContainer(embedder_parent_frame); + guest_view_container->SetElementInstanceID(element_instance_id); + + linked_ptr<guest_view::GuestViewRequest> request( + new guest_view::GuestViewAttachIframeRequest( + guest_view_container, render_frame->GetRoutingID(), guest_instance_id, + std::move(params), args.Length() == (num_required_params + 1) + ? args[num_required_params].As<v8::Function>() + : v8::Local<v8::Function>(), + args.GetIsolate())); + guest_view_container->IssueRequest(request); + + args.GetReturnValue().Set(v8::Boolean::New(context()->isolate(), true)); +} + +void GuestViewInternalCustomBindings::DestroyContainer( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().SetNull(); + + if (args.Length() != 1) + return; + + // Element Instance ID. + if (!args[0]->IsInt32()) + return; + + int element_instance_id = args[0]->Int32Value(); + auto* guest_view_container = + guest_view::GuestViewContainer::FromID(element_instance_id); + if (!guest_view_container) + return; + + // Note: |guest_view_container| is deleted. + // GuestViewContainer::DidDestroyElement() currently also destroys + // a GuestViewContainer. That won't be necessary once GuestViewContainer + // always runs w/o plugin. + guest_view_container->Destroy(false /* embedder_frame_destroyed */); +} + +void GuestViewInternalCustomBindings::GetContentWindow( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Default to returning null. + args.GetReturnValue().SetNull(); + + if (args.Length() != 1) + return; + + // The routing ID for the RenderView. + if (!args[0]->IsInt32()) + return; + + int view_id = args[0]->Int32Value(); + if (view_id == MSG_ROUTING_NONE) + return; + + content::RenderView* view = content::RenderView::FromRoutingID(view_id); + if (!view) + return; + + blink::WebFrame* frame = view->GetWebView()->mainFrame(); + // TODO(lazyboy,nasko): The WebLocalFrame branch is not used when running + // on top of out-of-process iframes. Remove it once the code is converted. + v8::Local<v8::Value> window; + if (frame->isWebLocalFrame()) { + window = frame->mainWorldScriptContext()->Global(); + } else { + window = + frame->toWebRemoteFrame()->deprecatedMainWorldScriptContext()->Global(); + } + args.GetReturnValue().Set(window); +} + +void GuestViewInternalCustomBindings::GetViewFromID( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Default to returning null. + args.GetReturnValue().SetNull(); + // There is one argument. + CHECK(args.Length() == 1); + // The view ID. + CHECK(args[0]->IsInt32()); + int view_id = args[0]->Int32Value(); + + ViewMap& view_map = weak_view_map.Get(); + auto map_entry = view_map.find(view_id); + if (map_entry == view_map.end()) + return; + + auto return_object = v8::Handle<v8::Object>::New(args.GetIsolate(), + *map_entry->second); + args.GetReturnValue().Set(return_object); +} + +void GuestViewInternalCustomBindings::RegisterDestructionCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // There are two parameters. + CHECK(args.Length() == 2); + // Element Instance ID. + CHECK(args[0]->IsInt32()); + // Callback function. + CHECK(args[1]->IsFunction()); + + int element_instance_id = args[0]->Int32Value(); + // An element instance ID uniquely identifies a GuestViewContainer within a + // RenderView. + auto* guest_view_container = + guest_view::GuestViewContainer::FromID(element_instance_id); + if (!guest_view_container) + return; + + guest_view_container->RegisterDestructionCallback(args[1].As<v8::Function>(), + args.GetIsolate()); + + args.GetReturnValue().Set(v8::Boolean::New(context()->isolate(), true)); +} + +void GuestViewInternalCustomBindings::RegisterElementResizeCallback( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // There are two parameters. + CHECK(args.Length() == 2); + // Element Instance ID. + CHECK(args[0]->IsInt32()); + // Callback function. + CHECK(args[1]->IsFunction()); + + int element_instance_id = args[0]->Int32Value(); + // An element instance ID uniquely identifies a ExtensionsGuestViewContainer + // within a RenderView. + auto guest_view_container = static_cast<ExtensionsGuestViewContainer*>( + guest_view::GuestViewContainer::FromID(element_instance_id)); + if (!guest_view_container) + return; + + guest_view_container->RegisterElementResizeCallback( + args[1].As<v8::Function>(), args.GetIsolate()); + + args.GetReturnValue().Set(v8::Boolean::New(context()->isolate(), true)); +} + +void GuestViewInternalCustomBindings::RegisterView( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // There are three parameters. + CHECK(args.Length() == 3); + // View Instance ID. + CHECK(args[0]->IsInt32()); + // View element. + CHECK(args[1]->IsObject()); + // View type (e.g. "webview"). + CHECK(args[2]->IsString()); + + // A reference to the view object is stored in |weak_view_map| using its view + // ID as the key. The reference is made weak so that it will not extend the + // lifetime of the object. + int view_instance_id = args[0]->Int32Value(); + auto object = + new v8::Global<v8::Object>(args.GetIsolate(), args[1].As<v8::Object>()); + weak_view_map.Get().insert(std::make_pair(view_instance_id, object)); + + // The |view_instance_id| is given to the SetWeak callback so that that view's + // entry in |weak_view_map| can be cleared when the view object is garbage + // collected. + object->SetWeak(new int(view_instance_id), + &GuestViewInternalCustomBindings::ResetMapEntry, + v8::WeakCallbackType::kParameter); + + // Let the GuestViewManager know that a GuestView has been created. + const std::string& view_type = *v8::String::Utf8Value(args[2]); + content::RenderThread::Get()->Send( + new GuestViewHostMsg_ViewCreated(view_instance_id, view_type)); +} + +void GuestViewInternalCustomBindings::RunWithGesture( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Gesture is required to request fullscreen. + blink::WebScopedUserGesture user_gesture; + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + v8::Local<v8::Value> no_args; + context()->CallFunction(v8::Local<v8::Function>::Cast(args[0]), 0, &no_args); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.h b/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.h new file mode 100644 index 00000000000..f18e15cd7ab --- /dev/null +++ b/chromium/extensions/renderer/guest_view/guest_view_internal_custom_bindings.h @@ -0,0 +1,98 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_GUEST_VIEW_GUEST_VIEW_INTERNAL_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_GUEST_VIEW_GUEST_VIEW_INTERNAL_CUSTOM_BINDINGS_H_ + +#include <map> + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class Dispatcher; + +// Implements custom bindings for the guestViewInternal API. +class GuestViewInternalCustomBindings : public ObjectBackedNativeHandler { + public: + explicit GuestViewInternalCustomBindings(ScriptContext* context); + ~GuestViewInternalCustomBindings() override; + + private: + // ResetMapEntry is called as a callback to SetWeak(). It resets the + // weak view reference held in |view_map_|. + static void ResetMapEntry(const v8::WeakCallbackInfo<int>& data); + + // AttachGuest attaches a GuestView to a provided container element. Once + // attached, the GuestView will participate in layout of the container page + // and become visible on screen. + // AttachGuest takes four parameters: + // |element_instance_id| uniquely identifies a container within the content + // module is able to host GuestViews. + // |guest_instance_id| uniquely identifies an unattached GuestView. + // |attach_params| is typically used to convey the current state of the + // container element at the time of attachment. These parameters are passed + // down to the GuestView. The GuestView may use these parameters to update the + // state of the guest hosted in another process. + // |callback| is an optional callback that is called once attachment is + // complete. The callback takes in a parameter for the WindowProxy of the + // guest identified by |guest_instance_id|. + void AttachGuest(const v8::FunctionCallbackInfo<v8::Value>& args); + + // DetachGuest detaches the container container specified from the associated + // GuestViewBase. DetachGuest takes two parameters: + // |element_instance_id| uniquely identifies a container within the content + // module is able to host GuestViews. + // |callback| is an optional callback that is called once the container has + // been detached. + void DetachGuest(const v8::FunctionCallbackInfo<v8::Value>& args); + + // AttachIframeGuest is --site-per-process variant of AttachGuest(). + // + // AttachIframeGuest takes a |contentWindow| parameter in addition to the + // parameters to AttachGuest. That parameter is used to identify the + // RenderFrame of the <iframe> container element. + void AttachIframeGuest(const v8::FunctionCallbackInfo<v8::Value>& args); + + // GetContentWindow takes in a RenderView routing ID and returns the + // Window JavaScript object for that RenderView. + void GetContentWindow(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Destroys the GuestViewContainer given an element instance ID in |args|. + void DestroyContainer(const v8::FunctionCallbackInfo<v8::Value>& args); + + // GetViewFromID takes a view ID, and returns the GuestView element associated + // with that ID, if one exists. Otherwise, null is returned. + void GetViewFromID(const v8::FunctionCallbackInfo<v8::Value>& args); + + // RegisterDestructionCallback registers a JavaScript callback function to be + // called when the guestview's container is destroyed. + // RegisterDestructionCallback takes in a single paramater, |callback|. + void RegisterDestructionCallback( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // RegisterElementResizeCallback registers a JavaScript callback function to + // be called when the element is resized. RegisterElementResizeCallback takes + // a single parameter, |callback|. + void RegisterElementResizeCallback( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // RegisterView takes in a view ID and a GuestView element, and stores the + // pair as an entry in |view_map_|. The view can then be retrieved using + // GetViewFromID. + void RegisterView(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Runs a JavaScript function with user gesture. + // + // This is used to request webview element to enter fullscreen (from the + // embedder). + // Note that the guest requesting fullscreen means it has already been + // triggered by a user gesture and we get to this point if embedder allows + // the fullscreen request to proceed. + void RunWithGesture( + const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_GUEST_VIEW_GUEST_VIEW_INTERNAL_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/guest_view/mime_handler_view/OWNERS b/chromium/extensions/renderer/guest_view/mime_handler_view/OWNERS new file mode 100644 index 00000000000..db781ac5adc --- /dev/null +++ b/chromium/extensions/renderer/guest_view/mime_handler_view/OWNERS @@ -0,0 +1 @@ +raymes@chromium.org diff --git a/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.cc b/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.cc new file mode 100644 index 00000000000..91f8197e853 --- /dev/null +++ b/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.cc @@ -0,0 +1,340 @@ +// 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/renderer/guest_view/mime_handler_view/mime_handler_view_container.h" + +#include <map> +#include <set> + +#include "base/macros.h" +#include "components/guest_view/common/guest_view_constants.h" +#include "components/guest_view/common/guest_view_messages.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_view.h" +#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_constants.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/guest_view/extensions_guest_view_messages.h" +#include "gin/arguments.h" +#include "gin/dictionary.h" +#include "gin/handle.h" +#include "gin/interceptor.h" +#include "gin/object_template_builder.h" +#include "gin/wrappable.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebRemoteFrame.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace extensions { + +namespace { + +const char kPostMessageName[] = "postMessage"; + +// The gin-backed scriptable object which is exposed by the BrowserPlugin for +// MimeHandlerViewContainer. This currently only implements "postMessage". +class ScriptableObject : public gin::Wrappable<ScriptableObject>, + public gin::NamedPropertyInterceptor { + public: + static gin::WrapperInfo kWrapperInfo; + + static v8::Local<v8::Object> Create( + v8::Isolate* isolate, + base::WeakPtr<MimeHandlerViewContainer> container) { + ScriptableObject* scriptable_object = + new ScriptableObject(isolate, container); + return gin::CreateHandle(isolate, scriptable_object) + .ToV8() + .As<v8::Object>(); + } + + // gin::NamedPropertyInterceptor + v8::Local<v8::Value> GetNamedProperty( + v8::Isolate* isolate, + const std::string& identifier) override { + if (identifier == kPostMessageName) { + if (post_message_function_template_.IsEmpty()) { + post_message_function_template_.Reset( + isolate, + gin::CreateFunctionTemplate( + isolate, base::Bind(&MimeHandlerViewContainer::PostMessage, + container_, isolate))); + } + v8::Local<v8::FunctionTemplate> function_template = + v8::Local<v8::FunctionTemplate>::New(isolate, + post_message_function_template_); + v8::Local<v8::Function> function; + if (function_template->GetFunction(isolate->GetCurrentContext()) + .ToLocal(&function)) + return function; + } + return v8::Local<v8::Value>(); + } + + private: + ScriptableObject(v8::Isolate* isolate, + base::WeakPtr<MimeHandlerViewContainer> container) + : gin::NamedPropertyInterceptor(isolate, this), + container_(container) {} + + // gin::Wrappable + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override { + return gin::Wrappable<ScriptableObject>::GetObjectTemplateBuilder(isolate) + .AddNamedPropertyInterceptor(); + } + + base::WeakPtr<MimeHandlerViewContainer> container_; + v8::Persistent<v8::FunctionTemplate> post_message_function_template_; +}; + +// static +gin::WrapperInfo ScriptableObject::kWrapperInfo = { gin::kEmbedderNativeGin }; + +// Maps from content::RenderFrame to the set of MimeHandlerViewContainers within +// it. +base::LazyInstance< + std::map<content::RenderFrame*, std::set<MimeHandlerViewContainer*>>> + g_mime_handler_view_container_map = LAZY_INSTANCE_INITIALIZER; + +} // namespace + +MimeHandlerViewContainer::MimeHandlerViewContainer( + content::RenderFrame* render_frame, + const std::string& mime_type, + const GURL& original_url) + : GuestViewContainer(render_frame), + mime_type_(mime_type), + original_url_(original_url), + guest_proxy_routing_id_(-1), + guest_loaded_(false), + weak_factory_(this) { + DCHECK(!mime_type_.empty()); + is_embedded_ = !render_frame->GetWebFrame()->document().isPluginDocument(); + g_mime_handler_view_container_map.Get()[render_frame].insert(this); +} + +MimeHandlerViewContainer::~MimeHandlerViewContainer() { + if (loader_) + loader_->cancel(); + + if (render_frame()) { + g_mime_handler_view_container_map.Get()[render_frame()].erase(this); + if (g_mime_handler_view_container_map.Get()[render_frame()].empty()) + g_mime_handler_view_container_map.Get().erase(render_frame()); + } +} + +// static +std::vector<MimeHandlerViewContainer*> +MimeHandlerViewContainer::FromRenderFrame(content::RenderFrame* render_frame) { + auto it = g_mime_handler_view_container_map.Get().find(render_frame); + if (it == g_mime_handler_view_container_map.Get().end()) + return std::vector<MimeHandlerViewContainer*>(); + + return std::vector<MimeHandlerViewContainer*>(it->second.begin(), + it->second.end()); +} + +void MimeHandlerViewContainer::OnReady() { + if (!render_frame()) + return; + + blink::WebFrame* frame = render_frame()->GetWebFrame(); + blink::WebURLLoaderOptions options; + // The embedded plugin is allowed to be cross-origin and we should always + // send credentials/cookies with the request. + options.crossOriginRequestPolicy = + blink::WebURLLoaderOptions::CrossOriginRequestPolicyAllow; + options.allowCredentials = true; + DCHECK(!loader_); + loader_.reset(frame->createAssociatedURLLoader(options)); + + blink::WebURLRequest request(original_url_); + request.setRequestContext(blink::WebURLRequest::RequestContextObject); + loader_->loadAsynchronously(request, this); +} + +bool MimeHandlerViewContainer::OnMessage(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(MimeHandlerViewContainer, message) + IPC_MESSAGE_HANDLER(ExtensionsGuestViewMsg_CreateMimeHandlerViewGuestACK, + OnCreateMimeHandlerViewGuestACK) + IPC_MESSAGE_HANDLER( + ExtensionsGuestViewMsg_MimeHandlerViewGuestOnLoadCompleted, + OnMimeHandlerViewGuestOnLoadCompleted) + IPC_MESSAGE_HANDLER(GuestViewMsg_GuestAttached, OnGuestAttached) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void MimeHandlerViewContainer::DidFinishLoading() { + DCHECK(!is_embedded_); + CreateMimeHandlerViewGuest(); +} + +void MimeHandlerViewContainer::OnRenderFrameDestroyed() { + g_mime_handler_view_container_map.Get().erase(render_frame()); +} + +void MimeHandlerViewContainer::DidReceiveData(const char* data, + int data_length) { + view_id_ += std::string(data, data_length); +} + + +void MimeHandlerViewContainer::DidResizeElement(const gfx::Size& new_size) { + element_size_ = new_size; + render_frame()->Send(new ExtensionsGuestViewHostMsg_ResizeGuest( + render_frame()->GetRoutingID(), element_instance_id(), new_size)); +} + +v8::Local<v8::Object> MimeHandlerViewContainer::V8ScriptableObject( + v8::Isolate* isolate) { + if (scriptable_object_.IsEmpty()) { + v8::Local<v8::Object> object = + ScriptableObject::Create(isolate, weak_factory_.GetWeakPtr()); + scriptable_object_.Reset(isolate, object); + } + return v8::Local<v8::Object>::New(isolate, scriptable_object_); +} + +void MimeHandlerViewContainer::didReceiveData(blink::WebURLLoader* /* unused */, + const char* data, + int data_length, + int /* unused */) { + view_id_ += std::string(data, data_length); +} + +void MimeHandlerViewContainer::didFinishLoading( + blink::WebURLLoader* /* unused */, + double /* unused */, + int64_t /* unused */) { + DCHECK(is_embedded_); + CreateMimeHandlerViewGuest(); +} + +void MimeHandlerViewContainer::PostMessage(v8::Isolate* isolate, + v8::Local<v8::Value> message) { + if (!guest_loaded_) { + linked_ptr<v8::Global<v8::Value>> global( + new v8::Global<v8::Value>(isolate, message)); + pending_messages_.push_back(global); + return; + } + + content::RenderView* guest_proxy_render_view = + content::RenderView::FromRoutingID(guest_proxy_routing_id_); + if (!guest_proxy_render_view) + return; + blink::WebFrame* guest_proxy_frame = + guest_proxy_render_view->GetWebView()->mainFrame(); + if (!guest_proxy_frame) + return; + + v8::Context::Scope context_scope( + render_frame()->GetWebFrame()->mainWorldScriptContext()); + + // TODO(lazyboy,nasko): The WebLocalFrame branch is not used when running + // on top of out-of-process iframes. Remove it once the code is converted. + v8::Local<v8::Object> guest_proxy_window; + if (guest_proxy_frame->isWebLocalFrame()) { + guest_proxy_window = + guest_proxy_frame->mainWorldScriptContext()->Global(); + } else { + guest_proxy_window = guest_proxy_frame->toWebRemoteFrame() + ->deprecatedMainWorldScriptContext() + ->Global(); + } + gin::Dictionary window_object(isolate, guest_proxy_window); + v8::Local<v8::Function> post_message; + if (!window_object.Get(std::string(kPostMessageName), &post_message)) + return; + + v8::Local<v8::Value> args[] = { + message, + // Post the message to any domain inside the browser plugin. The embedder + // should already know what is embedded. + gin::StringToV8(isolate, "*")}; + render_frame()->GetWebFrame()->callFunctionEvenIfScriptDisabled( + post_message.As<v8::Function>(), + guest_proxy_window, + arraysize(args), + args); +} + +void MimeHandlerViewContainer::PostMessageFromValue( + const base::Value& message) { + blink::WebFrame* frame = render_frame()->GetWebFrame(); + if (!frame) + return; + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(frame->mainWorldScriptContext()); + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + PostMessage(isolate, + converter->ToV8Value(&message, frame->mainWorldScriptContext())); +} + +void MimeHandlerViewContainer::OnCreateMimeHandlerViewGuestACK( + int element_instance_id) { + DCHECK_NE(this->element_instance_id(), guest_view::kInstanceIDNone); + DCHECK_EQ(this->element_instance_id(), element_instance_id); + + if (!render_frame()) + return; + + render_frame()->AttachGuest(element_instance_id); +} + +void MimeHandlerViewContainer::OnGuestAttached(int /* unused */, + int guest_proxy_routing_id) { + // Save the RenderView routing ID of the guest here so it can be used to route + // PostMessage calls. + guest_proxy_routing_id_ = guest_proxy_routing_id; +} + +void MimeHandlerViewContainer::OnMimeHandlerViewGuestOnLoadCompleted( + int /* unused */) { + if (!render_frame()) + return; + + guest_loaded_ = true; + if (pending_messages_.empty()) + return; + + // Now that the guest has loaded, flush any unsent messages. + blink::WebFrame* frame = render_frame()->GetWebFrame(); + if (!frame) + return; + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(frame->mainWorldScriptContext()); + for (const auto& pending_message : pending_messages_) + PostMessage(isolate, v8::Local<v8::Value>::New(isolate, *pending_message)); + + pending_messages_.clear(); +} + +void MimeHandlerViewContainer::CreateMimeHandlerViewGuest() { + // The loader has completed loading |view_id_| so we can dispose it. + loader_.reset(); + + DCHECK_NE(element_instance_id(), guest_view::kInstanceIDNone); + + if (!render_frame()) + return; + + render_frame()->Send( + new ExtensionsGuestViewHostMsg_CreateMimeHandlerViewGuest( + render_frame()->GetRoutingID(), view_id_, element_instance_id(), + element_size_)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.h b/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.h new file mode 100644 index 00000000000..6c98abefabf --- /dev/null +++ b/chromium/extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.h @@ -0,0 +1,132 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_GUEST_VIEW_MIME_HANDLER_VIEW_MIME_HANDLER_VIEW_CONTAINER_H_ +#define EXTENSIONS_RENDERER_GUEST_VIEW_MIME_HANDLER_VIEW_MIME_HANDLER_VIEW_CONTAINER_H_ + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/linked_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/guest_view/renderer/guest_view_container.h" +#include "third_party/WebKit/public/platform/WebURLLoader.h" +#include "third_party/WebKit/public/platform/WebURLLoaderClient.h" +#include "ui/gfx/geometry/size.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +namespace extensions { + +// A container for loading up an extension inside a BrowserPlugin to handle a +// MIME type. A request for the URL of the data to load inside the container is +// made and a url is sent back in response which points to the URL which the +// container should be navigated to. There are two cases for making this URL +// request, the case where the plugin is embedded and the case where it is top +// level: +// 1) In the top level case a URL request for the data to load has already been +// made by the renderer on behalf of the plugin. The |DidReceiveData| and +// |DidFinishLoading| callbacks (from BrowserPluginDelegate) will be called +// when data is received and when it has finished being received, +// respectively. +// 2) In the embedded case, no URL request is automatically made by the +// renderer. We make a URL request for the data inside the container using +// a WebURLLoader. In this case, the |didReceiveData| and |didFinishLoading| +// (from WebURLLoaderClient) when data is received and when it has finished +// being received. +class MimeHandlerViewContainer : public guest_view::GuestViewContainer, + public blink::WebURLLoaderClient { + public: + MimeHandlerViewContainer(content::RenderFrame* render_frame, + const std::string& mime_type, + const GURL& original_url); + + static std::vector<MimeHandlerViewContainer*> FromRenderFrame( + content::RenderFrame* render_frame); + + // GuestViewContainer implementation. + bool OnMessage(const IPC::Message& message) override; + void OnReady() override; + + // BrowserPluginDelegate implementation. + void DidFinishLoading() override; + void DidReceiveData(const char* data, int data_length) override; + void DidResizeElement(const gfx::Size& new_size) override; + v8::Local<v8::Object> V8ScriptableObject(v8::Isolate*) override; + + // WebURLLoaderClient overrides. + void didReceiveData(blink::WebURLLoader* loader, + const char* data, + int data_length, + int encoded_data_length) override; + void didFinishLoading(blink::WebURLLoader* loader, + double finish_time, + int64_t total_encoded_data_length) override; + + // GuestViewContainer overrides. + void OnRenderFrameDestroyed() override; + + // Post a JavaScript message to the guest. + void PostMessage(v8::Isolate* isolate, v8::Local<v8::Value> message); + + // Post |message| to the guest. + void PostMessageFromValue(const base::Value& message); + + protected: + ~MimeHandlerViewContainer() override; + + private: + // Message handlers. + void OnCreateMimeHandlerViewGuestACK(int element_instance_id); + void OnGuestAttached(int element_instance_id, + int guest_proxy_routing_id); + void OnMimeHandlerViewGuestOnLoadCompleted(int element_instance_id); + + void CreateMimeHandlerViewGuest(); + + // The MIME type of the plugin. + const std::string mime_type_; + + // The URL of the extension to navigate to. + std::string view_id_; + + // Whether the plugin is embedded or not. + bool is_embedded_; + + // The original URL of the plugin. + GURL original_url_; + + // The RenderView routing ID of the guest. + int guest_proxy_routing_id_; + + // A URL loader to load the |original_url_| when the plugin is embedded. In + // the embedded case, no URL request is made automatically. + scoped_ptr<blink::WebURLLoader> loader_; + + // The scriptable object that backs the plugin. + v8::Global<v8::Object> scriptable_object_; + + // Pending postMessage messages that need to be sent to the guest. These are + // queued while the guest is loading and once it is fully loaded they are + // delivered so that messages aren't lost. + std::vector<linked_ptr<v8::Global<v8::Value>>> pending_messages_; + + // True if the guest page has fully loaded and its JavaScript onload function + // has been called. + bool guest_loaded_; + + // The size of the element. + gfx::Size element_size_; + + base::WeakPtrFactory<MimeHandlerViewContainer> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(MimeHandlerViewContainer); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_GUEST_VIEW_MIME_HANDLER_VIEW_MIME_HANDLER_VIEW_CONTAINER_H_ diff --git a/chromium/extensions/renderer/i18n_custom_bindings.cc b/chromium/extensions/renderer/i18n_custom_bindings.cc new file mode 100644 index 00000000000..08d9e75aac3 --- /dev/null +++ b/chromium/extensions/renderer/i18n_custom_bindings.cc @@ -0,0 +1,245 @@ +// 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/renderer/i18n_custom_bindings.h" + +#include <stddef.h> +#include <stdint.h> + +#include <vector> + +#include "base/bind.h" +#include "base/macros.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/message_bundle.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/v8_helpers.h" +#include "third_party/cld_2/src/public/compact_lang_det.h" +#include "third_party/cld_2/src/public/encodings.h" + +namespace extensions { + +using namespace v8_helpers; + +namespace { + +// Max number of languages detected by CLD2. +const int kCldNumLangs = 3; + +struct DetectedLanguage { + DetectedLanguage(const std::string& language, int percentage) + : language(language), percentage(percentage) {} + ~DetectedLanguage() {} + + // Returns a new v8::Local<v8::Value> representing the serialized form of + // this DetectedLanguage object. + scoped_ptr<base::DictionaryValue> ToDictionary() const; + + std::string language; + int percentage; + + private: + DISALLOW_COPY_AND_ASSIGN(DetectedLanguage); +}; + +// LanguageDetectionResult object that holds detected langugae reliability and +// array of DetectedLanguage +struct LanguageDetectionResult { + explicit LanguageDetectionResult(bool is_reliable) + : is_reliable(is_reliable) {} + ~LanguageDetectionResult() {} + + // Returns a new v8::Local<v8::Value> representing the serialized form of + // this Result object. + v8::Local<v8::Value> ToValue(ScriptContext* context); + + // CLD detected language reliability + bool is_reliable; + + // Array of detectedLanguage of size 1-3. The null is returned if + // there were no languages detected + std::vector<scoped_ptr<DetectedLanguage>> languages; + + private: + DISALLOW_COPY_AND_ASSIGN(LanguageDetectionResult); +}; + +scoped_ptr<base::DictionaryValue> DetectedLanguage::ToDictionary() const { + scoped_ptr<base::DictionaryValue> dict_value(new base::DictionaryValue()); + dict_value->SetString("language", language.c_str()); + dict_value->SetInteger("percentage", percentage); + return dict_value; +} + +v8::Local<v8::Value> LanguageDetectionResult::ToValue(ScriptContext* context) { + base::DictionaryValue dict_value; + dict_value.SetBoolean("isReliable", is_reliable); + scoped_ptr<base::ListValue> languages_list(new base::ListValue()); + for (const auto& language : languages) + languages_list->Append(language->ToDictionary()); + dict_value.Set("languages", std::move(languages_list)); + + v8::Local<v8::Context> v8_context = context->v8_context(); + v8::Isolate* isolate = v8_context->GetIsolate(); + v8::EscapableHandleScope handle_scope(isolate); + + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + v8::Local<v8::Value> result = converter->ToV8Value(&dict_value, v8_context); + return handle_scope.Escape(result); +} + +void InitDetectedLanguages( + CLD2::Language* languages, + int* percents, + std::vector<scoped_ptr<DetectedLanguage>>* detected_languages) { + for (int i = 0; i < kCldNumLangs; i++) { + std::string language_code; + // Convert LanguageCode 'zh' to 'zh-CN' and 'zh-Hant' to 'zh-TW' for + // Translate server usage. see DetermineTextLanguage in + // components/translate/core/language_detection/language_detection_util.cc + if (languages[i] == CLD2::UNKNOWN_LANGUAGE) { + // Break from the loop since there is no need to save + // unknown languages + break; + } else { + language_code = + CLD2::LanguageCode(static_cast<CLD2::Language>(languages[i])); + } + detected_languages->push_back( + make_scoped_ptr(new DetectedLanguage(language_code, percents[i]))); + } +} + +} // namespace + +I18NCustomBindings::I18NCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetL10nMessage", + base::Bind(&I18NCustomBindings::GetL10nMessage, base::Unretained(this))); + RouteFunction("GetL10nUILanguage", + base::Bind(&I18NCustomBindings::GetL10nUILanguage, + base::Unretained(this))); + RouteFunction("DetectTextLanguage", + base::Bind(&I18NCustomBindings::DetectTextLanguage, + base::Unretained(this))); +} + +void I18NCustomBindings::GetL10nMessage( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() != 3 || !args[0]->IsString()) { + NOTREACHED() << "Bad arguments"; + return; + } + + std::string extension_id; + if (args[2]->IsNull() || !args[2]->IsString()) { + return; + } else { + extension_id = *v8::String::Utf8Value(args[2]); + if (extension_id.empty()) + return; + } + + L10nMessagesMap* l10n_messages = GetL10nMessagesMap(extension_id); + if (!l10n_messages) { + content::RenderFrame* render_frame = context()->GetRenderFrame(); + if (!render_frame) + return; + + L10nMessagesMap messages; + // A sync call to load message catalogs for current extension. + render_frame->Send( + new ExtensionHostMsg_GetMessageBundle(extension_id, &messages)); + + // Save messages we got. + ExtensionToL10nMessagesMap& l10n_messages_map = + *GetExtensionToL10nMessagesMap(); + l10n_messages_map[extension_id] = messages; + + l10n_messages = GetL10nMessagesMap(extension_id); + } + + std::string message_name = *v8::String::Utf8Value(args[0]); + std::string message = + MessageBundle::GetL10nMessage(message_name, *l10n_messages); + + v8::Isolate* isolate = args.GetIsolate(); + std::vector<std::string> substitutions; + if (args[1]->IsArray()) { + // chrome.i18n.getMessage("message_name", ["more", "params"]); + v8::Local<v8::Array> placeholders = v8::Local<v8::Array>::Cast(args[1]); + uint32_t count = placeholders->Length(); + if (count > 9) + return; + for (uint32_t i = 0; i < count; ++i) { + substitutions.push_back(*v8::String::Utf8Value(placeholders->Get( + v8::Integer::New(isolate, i)))); + } + } else if (args[1]->IsString()) { + // chrome.i18n.getMessage("message_name", "one param"); + substitutions.push_back(*v8::String::Utf8Value(args[1])); + } + + args.GetReturnValue().Set(v8::String::NewFromUtf8( + isolate, + base::ReplaceStringPlaceholders(message, substitutions, NULL).c_str())); +} + +void I18NCustomBindings::GetL10nUILanguage( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(v8::String::NewFromUtf8( + args.GetIsolate(), content::RenderThread::Get()->GetLocale().c_str())); +} + +void I18NCustomBindings::DetectTextLanguage( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(args.Length() == 1); + CHECK(args[0]->IsString()); + + std::string text = *v8::String::Utf8Value(args[0]); + CLD2::CLDHints cldhints = {nullptr, "", CLD2::UNKNOWN_ENCODING, + CLD2::UNKNOWN_LANGUAGE}; + + bool is_plain_text = true; // assume the text is a plain text + int flags = 0; // no flags, see compact_lang_det.h for details + int text_bytes; // amount of non-tag/letters-only text (assumed 0) + int valid_prefix_bytes; // amount of valid UTF8 character in the string + double normalized_score[kCldNumLangs]; + + CLD2::Language languages[kCldNumLangs]; + int percents[kCldNumLangs]; + bool is_reliable = false; + + // populating languages and percents + int cld_language = CLD2::ExtDetectLanguageSummaryCheckUTF8( + text.c_str(), static_cast<int>(text.size()), is_plain_text, &cldhints, + flags, languages, percents, normalized_score, + nullptr, // assumed no ResultChunkVector is used + &text_bytes, &is_reliable, &valid_prefix_bytes); + + // Check if non-UTF8 character is encountered + // See bug http://crbug.com/444258. + if (valid_prefix_bytes < static_cast<int>(text.size()) && + cld_language == CLD2::UNKNOWN_LANGUAGE) { + // Detect Language upto before the first non-UTF8 character + CLD2::ExtDetectLanguageSummary( + text.c_str(), valid_prefix_bytes, is_plain_text, &cldhints, flags, + languages, percents, normalized_score, + nullptr, // assumed no ResultChunkVector is used + &text_bytes, &is_reliable); + } + + LanguageDetectionResult result(is_reliable); + // populate LanguageDetectionResult with languages and percents + InitDetectedLanguages(languages, percents, &result.languages); + + args.GetReturnValue().Set(result.ToValue(context())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/i18n_custom_bindings.h b/chromium/extensions/renderer/i18n_custom_bindings.h new file mode 100644 index 00000000000..0ad2ffcdee9 --- /dev/null +++ b/chromium/extensions/renderer/i18n_custom_bindings.h @@ -0,0 +1,26 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_I18N_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_I18N_CUSTOM_BINDINGS_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Implements custom bindings for the i18n API. +class I18NCustomBindings : public ObjectBackedNativeHandler { + public: + explicit I18NCustomBindings(ScriptContext* context); + + private: + void GetL10nMessage(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetL10nUILanguage(const v8::FunctionCallbackInfo<v8::Value>& args); + void DetectTextLanguage(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_I18N_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/id_generator_custom_bindings.cc b/chromium/extensions/renderer/id_generator_custom_bindings.cc new file mode 100644 index 00000000000..182c4540d16 --- /dev/null +++ b/chromium/extensions/renderer/id_generator_custom_bindings.cc @@ -0,0 +1,31 @@ +// 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/renderer/id_generator_custom_bindings.h" + +#include <stdint.h> + +#include "base/bind.h" + +namespace extensions { + +IdGeneratorCustomBindings::IdGeneratorCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("GetNextId", + base::Bind(&IdGeneratorCustomBindings::GetNextId, + base::Unretained(this))); +} + +void IdGeneratorCustomBindings::GetNextId( + const v8::FunctionCallbackInfo<v8::Value>& args) { + static int32_t next_id = 0; + ++next_id; + // Make sure 0 is never returned because some APIs (particularly WebRequest) + // have special meaning for 0 IDs. + if (next_id == 0) + next_id = 1; + args.GetReturnValue().Set(next_id); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/id_generator_custom_bindings.h b/chromium/extensions/renderer/id_generator_custom_bindings.h new file mode 100644 index 00000000000..b8d79f6c6be --- /dev/null +++ b/chromium/extensions/renderer/id_generator_custom_bindings.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_ID_GENERATOR_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_ID_GENERATOR_CUSTOM_BINDINGS_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Implements function that can be used by JS layer to generate unique integer +// identifiers. +class IdGeneratorCustomBindings : public ObjectBackedNativeHandler { + public: + IdGeneratorCustomBindings(ScriptContext* context); + + private: + void GetNextId(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_ID_GENERATOR_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/injection_host.cc b/chromium/extensions/renderer/injection_host.cc new file mode 100644 index 00000000000..30e8679de88 --- /dev/null +++ b/chromium/extensions/renderer/injection_host.cc @@ -0,0 +1,12 @@ +// Copyright 2015 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/injection_host.h" + +InjectionHost::InjectionHost(const HostID& host_id) : + id_(host_id) { +} + +InjectionHost::~InjectionHost() { +} diff --git a/chromium/extensions/renderer/injection_host.h b/chromium/extensions/renderer/injection_host.h new file mode 100644 index 00000000000..21a4c0ed61a --- /dev/null +++ b/chromium/extensions/renderer/injection_host.h @@ -0,0 +1,47 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_INJECTION_HOST_H +#define EXTENSIONS_RENDERER_INJECTION_HOST_H + +#include "base/macros.h" +#include "extensions/common/host_id.h" +#include "extensions/common/permissions/permissions_data.h" +#include "url/gurl.h" + +namespace content { +class RenderFrame; +} + +// An interface for all kinds of hosts who own user scripts. +class InjectionHost { + public: + InjectionHost(const HostID& host_id); + virtual ~InjectionHost(); + + virtual std::string GetContentSecurityPolicy() const = 0; + + // The base url for the host. + virtual const GURL& url() const = 0; + + // The human-readable name of the host. + virtual const std::string& name() const = 0; + + // Returns true if the script should execute. + virtual extensions::PermissionsData::AccessType CanExecuteOnFrame( + const GURL& document_url, + content::RenderFrame* render_frame, + int tab_id, + bool is_declarative) const = 0; + + const HostID& id() const { return id_; } + + private: + // The ID of the host. + HostID id_; + + DISALLOW_COPY_AND_ASSIGN(InjectionHost); +}; + +#endif // EXTENSIONS_RENDERER_INJECTION_HOST_H diff --git a/chromium/extensions/renderer/json_schema_unittest.cc b/chromium/extensions/renderer/json_schema_unittest.cc new file mode 100644 index 00000000000..3a2191cad0f --- /dev/null +++ b/chromium/extensions/renderer/json_schema_unittest.cc @@ -0,0 +1,111 @@ +// 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/renderer/module_system_test.h" +#include "extensions/renderer/v8_schema_registry.h" +#include "gin/dictionary.h" +#include "grit/extensions_renderer_resources.h" + +namespace extensions { + +class JsonSchemaTest : public ModuleSystemTest { + public: + void SetUp() override { + ModuleSystemTest::SetUp(); + + env()->RegisterModule("json_schema", IDR_JSON_SCHEMA_JS); + env()->RegisterModule("utils", IDR_UTILS_JS); + + env()->module_system()->RegisterNativeHandler( + "schema_registry", schema_registry_.AsNativeHandler()); + + env()->RegisterTestFile("json_schema_test", "json_schema_test.js"); + } + + protected: + void TestFunction(const std::string& test_name) { + env()->module_system()->CallModuleMethod("json_schema_test", test_name); + } + + private: + V8SchemaRegistry schema_registry_; +}; + +TEST_F(JsonSchemaTest, TestFormatError) { + TestFunction("testFormatError"); +} + +TEST_F(JsonSchemaTest, TestComplex) { + TestFunction("testComplex"); +} + +TEST_F(JsonSchemaTest, TestEnum) { + TestFunction("testEnum"); +} + +TEST_F(JsonSchemaTest, TestExtends) { + TestFunction("testExtends"); +} + +TEST_F(JsonSchemaTest, TestObject) { + TestFunction("testObject"); +} + +TEST_F(JsonSchemaTest, TestArrayTuple) { + TestFunction("testArrayTuple"); +} + +TEST_F(JsonSchemaTest, TestArrayNonTuple) { + TestFunction("testArrayNonTuple"); +} + +TEST_F(JsonSchemaTest, TestString) { + TestFunction("testString"); +} + +TEST_F(JsonSchemaTest, TestNumber) { + TestFunction("testNumber"); +} + +TEST_F(JsonSchemaTest, TestIntegerBounds) { + TestFunction("testIntegerBounds"); +} + +TEST_F(JsonSchemaTest, TestType) { + gin::Dictionary array_buffer_container( + env()->isolate(), + env()->CreateGlobal("otherContextArrayBufferContainer")); + { + // Create an ArrayBuffer in another v8 context and pass it to the test + // through a global. + scoped_ptr<ModuleSystemTestEnvironment> other_env(CreateEnvironment()); + v8::Context::Scope scope(other_env->context()->v8_context()); + v8::Local<v8::ArrayBuffer> array_buffer( + v8::ArrayBuffer::New(env()->isolate(), 1)); + array_buffer_container.Set("value", array_buffer); + } + TestFunction("testType"); +} + +TEST_F(JsonSchemaTest, TestTypeReference) { + TestFunction("testTypeReference"); +} + +TEST_F(JsonSchemaTest, TestGetAllTypesForSchema) { + TestFunction("testGetAllTypesForSchema"); +} + +TEST_F(JsonSchemaTest, TestIsValidSchemaType) { + TestFunction("testIsValidSchemaType"); +} + +TEST_F(JsonSchemaTest, TestCheckSchemaOverlap) { + TestFunction("testCheckSchemaOverlap"); +} + +TEST_F(JsonSchemaTest, TestInstanceOf) { + TestFunction("testInstanceOf"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/lazy_background_page_native_handler.cc b/chromium/extensions/renderer/lazy_background_page_native_handler.cc new file mode 100644 index 00000000000..f09beec7a7f --- /dev/null +++ b/chromium/extensions/renderer/lazy_background_page_native_handler.cc @@ -0,0 +1,46 @@ +// 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/renderer/lazy_background_page_native_handler.h" + +#include "base/bind.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +LazyBackgroundPageNativeHandler::LazyBackgroundPageNativeHandler( + ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "IncrementKeepaliveCount", + base::Bind(&LazyBackgroundPageNativeHandler::IncrementKeepaliveCount, + base::Unretained(this))); + RouteFunction( + "DecrementKeepaliveCount", + base::Bind(&LazyBackgroundPageNativeHandler::DecrementKeepaliveCount, + base::Unretained(this))); +} + +void LazyBackgroundPageNativeHandler::IncrementKeepaliveCount( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (context() && ExtensionFrameHelper::IsContextForEventPage(context())) { + content::RenderFrame* render_frame = context()->GetRenderFrame(); + render_frame->Send(new ExtensionHostMsg_IncrementLazyKeepaliveCount( + render_frame->GetRoutingID())); + } +} + +void LazyBackgroundPageNativeHandler::DecrementKeepaliveCount( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (context() && ExtensionFrameHelper::IsContextForEventPage(context())) { + content::RenderFrame* render_frame = context()->GetRenderFrame(); + render_frame->Send(new ExtensionHostMsg_DecrementLazyKeepaliveCount( + render_frame->GetRoutingID())); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/lazy_background_page_native_handler.h b/chromium/extensions/renderer/lazy_background_page_native_handler.h new file mode 100644 index 00000000000..88eaf0ffb1f --- /dev/null +++ b/chromium/extensions/renderer/lazy_background_page_native_handler.h @@ -0,0 +1,23 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_LAZY_BACKGROUND_PAGE_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_LAZY_BACKGROUND_PAGE_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class Extension; + +class LazyBackgroundPageNativeHandler : public ObjectBackedNativeHandler { + public: + explicit LazyBackgroundPageNativeHandler(ScriptContext* context); + void IncrementKeepaliveCount(const v8::FunctionCallbackInfo<v8::Value>& args); + void DecrementKeepaliveCount(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_LAZY_BACKGROUND_PAGE_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/logging_native_handler.cc b/chromium/extensions/renderer/logging_native_handler.cc new file mode 100644 index 00000000000..86fbc9c9fbe --- /dev/null +++ b/chromium/extensions/renderer/logging_native_handler.cc @@ -0,0 +1,78 @@ +// 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 "base/logging.h" +#include "base/strings/stringprintf.h" +#include "extensions/renderer/logging_native_handler.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +LoggingNativeHandler::LoggingNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "DCHECK", + base::Bind(&LoggingNativeHandler::Dcheck, base::Unretained(this))); + RouteFunction( + "CHECK", + base::Bind(&LoggingNativeHandler::Check, base::Unretained(this))); + RouteFunction( + "DCHECK_IS_ON", + base::Bind(&LoggingNativeHandler::DcheckIsOn, base::Unretained(this))); + RouteFunction("LOG", + base::Bind(&LoggingNativeHandler::Log, base::Unretained(this))); + RouteFunction( + "WARNING", + base::Bind(&LoggingNativeHandler::Warning, base::Unretained(this))); +} + +LoggingNativeHandler::~LoggingNativeHandler() {} + +void LoggingNativeHandler::Check( + const v8::FunctionCallbackInfo<v8::Value>& args) { + bool check_value; + std::string error_message; + ParseArgs(args, &check_value, &error_message); + CHECK(check_value) << error_message; +} + +void LoggingNativeHandler::Dcheck( + const v8::FunctionCallbackInfo<v8::Value>& args) { + bool check_value; + std::string error_message; + ParseArgs(args, &check_value, &error_message); + DCHECK(check_value) << error_message; +} + +void LoggingNativeHandler::DcheckIsOn( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(DCHECK_IS_ON()); +} + +void LoggingNativeHandler::Log( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + LOG(INFO) << *v8::String::Utf8Value(args[0]); +} + +void LoggingNativeHandler::Warning( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + LOG(WARNING) << *v8::String::Utf8Value(args[0]); +} + +void LoggingNativeHandler::ParseArgs( + const v8::FunctionCallbackInfo<v8::Value>& args, + bool* check_value, + std::string* error_message) { + CHECK_LE(args.Length(), 2); + *check_value = args[0]->BooleanValue(); + if (args.Length() == 2) { + *error_message = "Error: " + std::string(*v8::String::Utf8Value(args[1])); + } + + *error_message += "\n" + context()->GetStackTraceAsString(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/logging_native_handler.h b/chromium/extensions/renderer/logging_native_handler.h new file mode 100644 index 00000000000..ca9938c2760 --- /dev/null +++ b/chromium/extensions/renderer/logging_native_handler.h @@ -0,0 +1,53 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_LOGGING_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_LOGGING_NATIVE_HANDLER_H_ + +#include <string> + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Exposes logging.h macros to JavaScript bindings. +class LoggingNativeHandler : public ObjectBackedNativeHandler { + public: + explicit LoggingNativeHandler(ScriptContext* context); + ~LoggingNativeHandler() override; + + // Equivalent to CHECK(predicate) << message. + // + // void(predicate, message?) + void Check(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Equivalent to DCHECK(predicate) << message. + // + // void(predicate, message?) + void Dcheck(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Equivalent to DCHECK_IS_ON(). + // + // bool() + void DcheckIsOn(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Equivalent to LOG(INFO) << message. + // + // void(message) + void Log(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Equivalent to LOG(WARNING) << message. + // + // void(message) + void Warning(const v8::FunctionCallbackInfo<v8::Value>& args); + + void ParseArgs(const v8::FunctionCallbackInfo<v8::Value>& args, + bool* check_value, + std::string* error_message); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_LOGGING_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/messaging_bindings.cc b/chromium/extensions/renderer/messaging_bindings.cc new file mode 100644 index 00000000000..95bf59a4b67 --- /dev/null +++ b/chromium/extensions/renderer/messaging_bindings.cc @@ -0,0 +1,492 @@ +// 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/renderer/messaging_bindings.h" + +#include <stdint.h> + +#include <map> +#include <string> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/lazy_instance.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/values.h" +#include "components/guest_view/common/guest_view_constants.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/common/child_process_host.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/api/messaging/message.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/manifest_handlers/externally_connectable.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/event_bindings.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/gc_callback.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/v8_helpers.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebScopedWindowFocusAllowedIndicator.h" +#include "third_party/WebKit/public/web/WebUserGestureIndicator.h" +#include "v8/include/v8.h" + +// Message passing API example (in a content script): +// var extension = +// new chrome.Extension('00123456789abcdef0123456789abcdef0123456'); +// var port = runtime.connect(); +// port.postMessage('Can you hear me now?'); +// port.onmessage.addListener(function(msg, port) { +// alert('response=' + msg); +// port.postMessage('I got your reponse'); +// }); + +using content::RenderThread; +using content::V8ValueConverter; + +namespace extensions { + +using v8_helpers::ToV8String; +using v8_helpers::ToV8StringUnsafe; +using v8_helpers::IsEmptyOrUndefied; + +namespace { + +// Tracks every reference between ScriptContexts and Ports, by ID. +class PortTracker { + public: + PortTracker() {} + ~PortTracker() {} + + // Returns true if |context| references |port_id|. + bool HasReference(ScriptContext* context, int port_id) const { + auto ports = contexts_to_ports_.find(context); + return ports != contexts_to_ports_.end() && + ports->second.count(port_id) > 0; + } + + // Marks |context| and |port_id| as referencing each other. + void AddReference(ScriptContext* context, int port_id) { + contexts_to_ports_[context].insert(port_id); + } + + // Removes the references between |context| and |port_id|. + // Returns true if a reference was removed, false if the reference didn't + // exist to be removed. + bool RemoveReference(ScriptContext* context, int port_id) { + auto ports = contexts_to_ports_.find(context); + if (ports == contexts_to_ports_.end() || + ports->second.erase(port_id) == 0) { + return false; + } + if (ports->second.empty()) + contexts_to_ports_.erase(context); + return true; + } + + // Returns true if this tracker has any reference to |port_id|. + bool HasPort(int port_id) const { + for (auto it : contexts_to_ports_) { + if (it.second.count(port_id) > 0) + return true; + } + return false; + } + + // Deletes all references to |port_id|. + void DeletePort(int port_id) { + for (auto it = contexts_to_ports_.begin(); + it != contexts_to_ports_.end();) { + if (it->second.erase(port_id) > 0 && it->second.empty()) + contexts_to_ports_.erase(it++); + else + ++it; + } + } + + // Gets every port ID that has a reference to |context|. + std::set<int> GetPortsForContext(ScriptContext* context) const { + auto ports = contexts_to_ports_.find(context); + return ports == contexts_to_ports_.end() ? std::set<int>() : ports->second; + } + + private: + // Maps ScriptContexts to the port IDs that have a reference to it. + std::map<ScriptContext*, std::set<int>> contexts_to_ports_; + + DISALLOW_COPY_AND_ASSIGN(PortTracker); +}; + +base::LazyInstance<PortTracker> g_port_tracker = LAZY_INSTANCE_INITIALIZER; + +const char kPortClosedError[] = "Attempting to use a disconnected port object"; + +class ExtensionImpl : public ObjectBackedNativeHandler { + public: + ExtensionImpl(Dispatcher* dispatcher, ScriptContext* context) + : ObjectBackedNativeHandler(context), + dispatcher_(dispatcher), + weak_ptr_factory_(this) { + RouteFunction( + "CloseChannel", + base::Bind(&ExtensionImpl::CloseChannel, base::Unretained(this))); + RouteFunction( + "PortAddRef", + base::Bind(&ExtensionImpl::PortAddRef, base::Unretained(this))); + RouteFunction( + "PortRelease", + base::Bind(&ExtensionImpl::PortRelease, base::Unretained(this))); + RouteFunction( + "PostMessage", + base::Bind(&ExtensionImpl::PostMessage, base::Unretained(this))); + // TODO(fsamuel, kalman): Move BindToGC out of messaging natives. + RouteFunction("BindToGC", + base::Bind(&ExtensionImpl::BindToGC, base::Unretained(this))); + + // Observe |context| so that port references to it can be cleared. + context->AddInvalidationObserver(base::Bind( + &ExtensionImpl::OnContextInvalidated, weak_ptr_factory_.GetWeakPtr())); + } + + ~ExtensionImpl() override {} + + private: + void OnContextInvalidated() { + for (int port_id : g_port_tracker.Get().GetPortsForContext(context())) + ReleasePort(port_id); + } + + void ClearPortDataAndNotifyDispatcher(int port_id) { + g_port_tracker.Get().DeletePort(port_id); + dispatcher_->ClearPortData(port_id); + } + + // Sends a message along the given channel. + void PostMessage(const v8::FunctionCallbackInfo<v8::Value>& args) { + content::RenderFrame* render_frame = context()->GetRenderFrame(); + if (!render_frame) + return; + + // Arguments are (int32_t port_id, string message). + CHECK(args.Length() == 2 && args[0]->IsInt32() && args[1]->IsString()); + + int port_id = args[0].As<v8::Int32>()->Value(); + if (!g_port_tracker.Get().HasPort(port_id)) { + v8::Local<v8::String> error_message = + ToV8StringUnsafe(args.GetIsolate(), kPortClosedError); + args.GetIsolate()->ThrowException(v8::Exception::Error(error_message)); + return; + } + + render_frame->Send(new ExtensionHostMsg_PostMessage( + render_frame->GetRoutingID(), port_id, + Message(*v8::String::Utf8Value(args[1]), + blink::WebUserGestureIndicator::isProcessingUserGesture()))); + } + + // Forcefully disconnects a port. + void CloseChannel(const v8::FunctionCallbackInfo<v8::Value>& args) { + // Arguments are (int32_t port_id, boolean notify_browser). + CHECK_EQ(2, args.Length()); + CHECK(args[0]->IsInt32()); + CHECK(args[1]->IsBoolean()); + + int port_id = args[0].As<v8::Int32>()->Value(); + if (!g_port_tracker.Get().HasPort(port_id)) + return; + + // Send via the RenderThread because the RenderFrame might be closing. + bool notify_browser = args[1].As<v8::Boolean>()->Value(); + content::RenderFrame* render_frame = context()->GetRenderFrame(); + if (notify_browser && render_frame) { + render_frame->Send(new ExtensionHostMsg_CloseMessagePort( + render_frame->GetRoutingID(), port_id, true)); + } + + ClearPortDataAndNotifyDispatcher(port_id); + } + + // A new port has been created for a context. This occurs both when script + // opens a connection, and when a connection is opened to this script. + void PortAddRef(const v8::FunctionCallbackInfo<v8::Value>& args) { + // Arguments are (int32_t port_id). + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsInt32()); + + int port_id = args[0].As<v8::Int32>()->Value(); + g_port_tracker.Get().AddReference(context(), port_id); + } + + // The frame a port lived in has been destroyed. When there are no more + // frames with a reference to a given port, we will disconnect it and notify + // the other end of the channel. + // TODO(robwu): Port lifetime management has moved to the browser, this is no + // longer needed. See .destroy_() inmessaging.js for more details. + void PortRelease(const v8::FunctionCallbackInfo<v8::Value>& args) { + // Arguments are (int32_t port_id). + CHECK(args.Length() == 1 && args[0]->IsInt32()); + ReleasePort(args[0].As<v8::Int32>()->Value()); + } + + // Releases the reference to |port_id| for this context, and clears all port + // data if there are no more references. + void ReleasePort(int port_id) { + content::RenderFrame* render_frame = context()->GetRenderFrame(); + if (g_port_tracker.Get().RemoveReference(context(), port_id) && + !g_port_tracker.Get().HasPort(port_id) && render_frame) { + render_frame->Send(new ExtensionHostMsg_CloseMessagePort( + render_frame->GetRoutingID(), port_id, false)); + } + } + + // void BindToGC(object, callback, port_id) + // + // Binds |callback| to be invoked *sometime after* |object| is garbage + // collected. We don't call the method re-entrantly so as to avoid executing + // JS in some bizarro undefined mid-GC state, nor do we then call into the + // script context if it's been invalidated. + // + // If the script context *is* invalidated in the meantime, as a slight hack, + // release the port with ID |port_id| if it's >= 0. + void BindToGC(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(args.Length() == 3 && args[0]->IsObject() && args[1]->IsFunction() && + args[2]->IsInt32()); + int port_id = args[2].As<v8::Int32>()->Value(); + base::Closure fallback = base::Bind(&base::DoNothing); + if (port_id >= 0) { + fallback = base::Bind(&ExtensionImpl::ReleasePort, + weak_ptr_factory_.GetWeakPtr(), port_id); + } + // Destroys itself when the object is GC'd or context is invalidated. + new GCCallback(context(), args[0].As<v8::Object>(), + args[1].As<v8::Function>(), fallback); + } + + // Dispatcher handle. Not owned. + Dispatcher* dispatcher_; + + base::WeakPtrFactory<ExtensionImpl> weak_ptr_factory_; +}; + +void DispatchOnConnectToScriptContext( + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo* source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id, + bool* port_created, + ScriptContext* script_context) { + v8::Isolate* isolate = script_context->isolate(); + v8::HandleScope handle_scope(isolate); + + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + + const std::string& source_url_spec = info.source_url.spec(); + std::string target_extension_id = script_context->GetExtensionID(); + const Extension* extension = script_context->extension(); + + v8::Local<v8::Value> tab = v8::Null(isolate); + v8::Local<v8::Value> tls_channel_id_value = v8::Undefined(isolate); + v8::Local<v8::Value> guest_process_id = v8::Undefined(isolate); + v8::Local<v8::Value> guest_render_frame_routing_id = v8::Undefined(isolate); + + if (extension) { + if (!source->tab.empty() && !extension->is_platform_app()) + tab = converter->ToV8Value(&source->tab, script_context->v8_context()); + + ExternallyConnectableInfo* externally_connectable = + ExternallyConnectableInfo::Get(extension); + if (externally_connectable && + externally_connectable->accepts_tls_channel_id) { + v8::Local<v8::String> v8_tls_channel_id; + if (ToV8String(isolate, tls_channel_id.c_str(), &v8_tls_channel_id)) + tls_channel_id_value = v8_tls_channel_id; + } + + if (info.guest_process_id != content::ChildProcessHost::kInvalidUniqueID) { + guest_process_id = v8::Integer::New(isolate, info.guest_process_id); + guest_render_frame_routing_id = + v8::Integer::New(isolate, info.guest_render_frame_routing_id); + } + } + + v8::Local<v8::String> v8_channel_name; + v8::Local<v8::String> v8_source_id; + v8::Local<v8::String> v8_target_extension_id; + v8::Local<v8::String> v8_source_url_spec; + if (!ToV8String(isolate, channel_name.c_str(), &v8_channel_name) || + !ToV8String(isolate, info.source_id.c_str(), &v8_source_id) || + !ToV8String(isolate, target_extension_id.c_str(), + &v8_target_extension_id) || + !ToV8String(isolate, source_url_spec.c_str(), &v8_source_url_spec)) { + NOTREACHED() << "dispatchOnConnect() passed non-string argument"; + return; + } + + v8::Local<v8::Value> arguments[] = { + // portId + v8::Integer::New(isolate, target_port_id), + // channelName + v8_channel_name, + // sourceTab + tab, + // source_frame_id + v8::Integer::New(isolate, source->frame_id), + // guestProcessId + guest_process_id, + // guestRenderFrameRoutingId + guest_render_frame_routing_id, + // sourceExtensionId + v8_source_id, + // targetExtensionId + v8_target_extension_id, + // sourceUrl + v8_source_url_spec, + // tlsChannelId + tls_channel_id_value, + }; + + v8::Local<v8::Value> retval = + script_context->module_system()->CallModuleMethod( + "messaging", "dispatchOnConnect", arraysize(arguments), arguments); + + if (!IsEmptyOrUndefied(retval)) { + CHECK(retval->IsBoolean()); + *port_created |= retval.As<v8::Boolean>()->Value(); + } else { + LOG(ERROR) << "Empty return value from dispatchOnConnect."; + } +} + +void DeliverMessageToScriptContext(const Message& message, + int target_port_id, + ScriptContext* script_context) { + v8::Isolate* isolate = script_context->isolate(); + v8::HandleScope handle_scope(isolate); + + // Check to see whether the context has this port before bothering to create + // the message. + v8::Local<v8::Value> port_id_handle = + v8::Integer::New(isolate, target_port_id); + v8::Local<v8::Value> has_port = + script_context->module_system()->CallModuleMethod("messaging", "hasPort", + 1, &port_id_handle); + // Could be empty/undefined if an exception was thrown. + // TODO(kalman): Should this be built into CallModuleMethod? + if (IsEmptyOrUndefied(has_port)) + return; + CHECK(has_port->IsBoolean()); + if (!has_port.As<v8::Boolean>()->Value()) + return; + + v8::Local<v8::String> v8_data; + if (!ToV8String(isolate, message.data.c_str(), &v8_data)) + return; + std::vector<v8::Local<v8::Value>> arguments; + arguments.push_back(v8_data); + arguments.push_back(port_id_handle); + + scoped_ptr<blink::WebScopedUserGesture> web_user_gesture; + scoped_ptr<blink::WebScopedWindowFocusAllowedIndicator> allow_window_focus; + if (message.user_gesture) { + web_user_gesture.reset(new blink::WebScopedUserGesture); + + if (script_context->web_frame()) { + blink::WebDocument document = script_context->web_frame()->document(); + allow_window_focus.reset(new blink::WebScopedWindowFocusAllowedIndicator( + &document)); + } + } + + script_context->module_system()->CallModuleMethod( + "messaging", "dispatchOnMessage", &arguments); +} + +void DispatchOnDisconnectToScriptContext(int port_id, + const std::string& error_message, + ScriptContext* script_context) { + v8::Isolate* isolate = script_context->isolate(); + v8::HandleScope handle_scope(isolate); + + std::vector<v8::Local<v8::Value>> arguments; + arguments.push_back(v8::Integer::New(isolate, port_id)); + v8::Local<v8::String> v8_error_message; + if (!error_message.empty()) + ToV8String(isolate, error_message.c_str(), &v8_error_message); + if (!v8_error_message.IsEmpty()) { + arguments.push_back(v8_error_message); + } else { + arguments.push_back(v8::Null(isolate)); + } + + script_context->module_system()->CallModuleMethod( + "messaging", "dispatchOnDisconnect", &arguments); +} + +} // namespace + +ObjectBackedNativeHandler* MessagingBindings::Get(Dispatcher* dispatcher, + ScriptContext* context) { + return new ExtensionImpl(dispatcher, context); +} + +// static +void MessagingBindings::DispatchOnConnect( + const ScriptContextSet& context_set, + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id, + content::RenderFrame* restrict_to_render_frame) { + int routing_id = restrict_to_render_frame + ? restrict_to_render_frame->GetRoutingID() + : MSG_ROUTING_NONE; + bool port_created = false; + context_set.ForEach( + info.target_id, restrict_to_render_frame, + base::Bind(&DispatchOnConnectToScriptContext, target_port_id, + channel_name, &source, info, tls_channel_id, &port_created)); + // Note: |restrict_to_render_frame| may have been deleted at this point! + + if (port_created) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_OpenMessagePort(routing_id, target_port_id)); + } else { + content::RenderThread::Get()->Send(new ExtensionHostMsg_CloseMessagePort( + routing_id, target_port_id, false)); + } +} + +// static +void MessagingBindings::DeliverMessage( + const ScriptContextSet& context_set, + int target_port_id, + const Message& message, + content::RenderFrame* restrict_to_render_frame) { + context_set.ForEach( + restrict_to_render_frame, + base::Bind(&DeliverMessageToScriptContext, message, target_port_id)); +} + +// static +void MessagingBindings::DispatchOnDisconnect( + const ScriptContextSet& context_set, + int port_id, + const std::string& error_message, + content::RenderFrame* restrict_to_render_frame) { + context_set.ForEach( + restrict_to_render_frame, + base::Bind(&DispatchOnDisconnectToScriptContext, port_id, error_message)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/messaging_bindings.h b/chromium/extensions/renderer/messaging_bindings.h new file mode 100644 index 00000000000..a7a1c6fef80 --- /dev/null +++ b/chromium/extensions/renderer/messaging_bindings.h @@ -0,0 +1,73 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_MESSAGING_BINDINGS_H_ +#define EXTENSIONS_RENDERER_MESSAGING_BINDINGS_H_ + +#include <string> + +#include "extensions/renderer/script_context_set.h" + +struct ExtensionMsg_ExternalConnectionInfo; +struct ExtensionMsg_TabConnectionInfo; + +namespace base { +class DictionaryValue; +} + +namespace content { +class RenderFrame; +} + +namespace v8 { +class Extension; +} + +namespace extensions { +class Dispatcher; +struct Message; +class ObjectBackedNativeHandler; +class ScriptContextSet; + +// Manually implements JavaScript bindings for extension messaging. +// +// TODO(aa): This should all get re-implemented using SchemaGeneratedBindings. +// If anything needs to be manual for some reason, it should be implemented in +// its own class. +class MessagingBindings { + public: + // Creates an instance of the extension. + static ObjectBackedNativeHandler* Get(Dispatcher* dispatcher, + ScriptContext* context); + + // Dispatches the onConnect content script messaging event to some contexts + // in |context_set|. If |restrict_to_render_frame| is specified, only contexts + // in that render frame will receive the message. + static void DispatchOnConnect(const ScriptContextSet& context_set, + int target_port_id, + const std::string& channel_name, + const ExtensionMsg_TabConnectionInfo& source, + const ExtensionMsg_ExternalConnectionInfo& info, + const std::string& tls_channel_id, + content::RenderFrame* restrict_to_render_frame); + + // Delivers a message sent using content script messaging to some of the + // contexts in |bindings_context_set|. If |restrict_to_render_frame| is + // specified, only contexts in that render view will receive the message. + static void DeliverMessage(const ScriptContextSet& context_set, + int target_port_id, + const Message& message, + content::RenderFrame* restrict_to_render_frame); + + // Dispatches the onDisconnect event in response to the channel being closed. + static void DispatchOnDisconnect( + const ScriptContextSet& context_set, + int port_id, + const std::string& error_message, + content::RenderFrame* restrict_to_render_frame); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_MESSAGING_BINDINGS_H_ diff --git a/chromium/extensions/renderer/messaging_utils_unittest.cc b/chromium/extensions/renderer/messaging_utils_unittest.cc new file mode 100644 index 00000000000..fbb2dec93c8 --- /dev/null +++ b/chromium/extensions/renderer/messaging_utils_unittest.cc @@ -0,0 +1,196 @@ +// 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 "base/strings/stringprintf.h" +#include "extensions/renderer/module_system_test.h" +#include "grit/extensions_renderer_resources.h" + +namespace extensions { +namespace { + +class MessagingUtilsUnittest : public ModuleSystemTest { + protected: + void RegisterTestModule(const char* code) { + env()->RegisterModule( + "test", + base::StringPrintf( + "var assert = requireNative('assert');\n" + "var AssertTrue = assert.AssertTrue;\n" + "var AssertFalse = assert.AssertFalse;\n" + "var messagingUtils = require('messaging_utils');\n" + "%s", + code)); + } + + private: + void SetUp() override { + ModuleSystemTest::SetUp(); + + env()->RegisterModule("messaging_utils", IDR_MESSAGING_UTILS_JS); + } +}; + +TEST_F(MessagingUtilsUnittest, TestNothing) { + ExpectNoAssertionsMade(); +} + +TEST_F(MessagingUtilsUnittest, NoArguments) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments();\n" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, ZeroArguments) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments([]);" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, TooManyArgumentsNoOptions) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(\n" + " ['a', 'b', 'c', 'd']);\n" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, TooManyArgumentsWithOptions) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(\n" + " ['a', 'b', 'c', 'd', 'e'], true);\n" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, FinalArgumentIsNotAFunctionNoOptions) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(\n" + " ['a', 'b', 'c']);\n" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, FinalArgumentIsNotAFunctionWithOptions) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(\n" + " ['a', 'b', 'c', 'd'], true);\n" + "AssertTrue(args === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, OneStringArgument) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + // Because the request argument is required, a single argument must get + // mapped to it rather than to the optional targetId argument. + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(['a']);\n" + "AssertTrue(args.length == 3);\n" + "AssertTrue(args[0] === null);\n" + "AssertTrue(args[1] == 'a');\n" + "AssertTrue(args[2] === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, OneStringAndOneNullArgument) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + // Explicitly specifying null as the request is allowed. + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments(['a', null]);\n" + "AssertTrue(args.length == 3);\n" + "AssertTrue(args[0] == 'a');\n" + "AssertTrue(args[1] === null);\n" + "AssertTrue(args[2] === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, OneNullAndOneStringArgument) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + RegisterTestModule( + "var args = messagingUtils.alignSendMessageArguments([null, 'a']);\n" + "AssertTrue(args.length == 3);\n" + "AssertTrue(args[0] === null);\n" + "AssertTrue(args[1] == 'a');\n" + "AssertTrue(args[2] === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, OneStringAndOneFunctionArgument) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + // When the arguments are a string and a function, the function is + // unambiguously the responseCallback. Because the request argument is + // required, the remaining argument must get mapped to it rather than to the + // optional targetId argument. + RegisterTestModule( + "var cb = function() {};\n" + "var args = messagingUtils.alignSendMessageArguments(['a', cb]);\n" + "AssertTrue(args.length == 3);\n" + "AssertTrue(args[0] === null);\n" + "AssertTrue(args[1] == 'a');\n" + "AssertTrue(args[2] == cb);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, OneStringAndOneObjectArgument) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + // This tests an ambiguous set of arguments when options are present: + // chrome.runtime.sendMessage('target', {'msg': 'this is a message'}); + // vs. + // chrome.runtime.sendMessage('request', {'includeTlsChannelId': true}); + // + // The question is whether the string should map to the target and the + // dictionary to the message, or whether the string should map to the message + // and the dictionary to the options. Because the target and message arguments + // predate the options argument, we bind the string in this case to the + // targetId. + RegisterTestModule( + "var obj = {'b': true};\n" + "var args = messagingUtils.alignSendMessageArguments(['a', obj], true);\n" + "AssertTrue(args.length == 4);\n" + "AssertTrue(args[0] == 'a');\n" + "AssertTrue(args[1] == obj);\n" + "AssertTrue(args[2] === null);\n" + "AssertTrue(args[3] === null);"); + env()->module_system()->Require("test"); +} + +TEST_F(MessagingUtilsUnittest, TwoObjectArguments) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + // When two non-string arguments are provided and options are present, the + // two arguments must match request and options, respectively, because + // targetId must be a string. + RegisterTestModule( + "var obj1 = {'a': 'foo'};\n" + "var obj2 = {'b': 'bar'};\n" + "var args = messagingUtils.alignSendMessageArguments(\n" + " [obj1, obj2], true);\n" + "AssertTrue(args.length == 4);\n" + "AssertTrue(args[0] === null);\n" + "AssertTrue(args[1] == obj1);\n" + "AssertTrue(args[2] == obj2);\n" + "AssertTrue(args[3] === null);"); + env()->module_system()->Require("test"); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/module_system.cc b/chromium/extensions/renderer/module_system.cc new file mode 100644 index 00000000000..4ff9b5c5f51 --- /dev/null +++ b/chromium/extensions/renderer/module_system.cc @@ -0,0 +1,757 @@ +// 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/renderer/module_system.h" + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/trace_event/trace_event.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_view.h" +#include "extensions/common/extension.h" +#include "extensions/common/extensions_client.h" +#include "extensions/renderer/console.h" +#include "extensions/renderer/safe_builtins.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/v8_helpers.h" +#include "gin/modules/module_registry.h" +#include "third_party/WebKit/public/web/WebFrame.h" + +namespace extensions { + +using namespace v8_helpers; + +namespace { + +const char* kModuleSystem = "module_system"; +const char* kModuleName = "module_name"; +const char* kModuleField = "module_field"; +const char* kModulesField = "modules"; + +// Logs an error for the calling context in preparation for potentially +// crashing the renderer, with some added metadata about the context: +// - Its type (blessed, unblessed, etc). +// - Whether it's valid. +// - The extension ID, if one exists. +// Crashing won't happen in stable/beta releases, but is encouraged to happen +// in the less stable released to catch errors early. +void Fatal(ScriptContext* context, const std::string& message) { + // Prepend some context metadata. + std::string full_message = "("; + if (!context->is_valid()) + full_message += "Invalid "; + full_message += context->GetContextTypeDescription(); + full_message += " context"; + if (context->extension()) { + full_message += " for "; + full_message += context->extension()->id(); + } + full_message += ") "; + full_message += message; + + ExtensionsClient* client = ExtensionsClient::Get(); + if (client->ShouldSuppressFatalErrors()) { + console::Error(context->GetRenderFrame(), full_message); + client->RecordDidSuppressFatalError(); + } else { + console::Fatal(context->GetRenderFrame(), full_message); + } +} + +void Warn(v8::Isolate* isolate, const std::string& message) { + ScriptContext* script_context = + ScriptContextSet::GetContextByV8Context(isolate->GetCurrentContext()); + console::Warn(script_context ? script_context->GetRenderFrame() : nullptr, + message); +} + +// Default exception handler which logs the exception. +class DefaultExceptionHandler : public ModuleSystem::ExceptionHandler { + public: + explicit DefaultExceptionHandler(ScriptContext* context) + : ModuleSystem::ExceptionHandler(context) {} + + // Fatally dumps the debug info from |try_catch| to the console. + // Make sure this is never used for exceptions that originate in external + // code! + void HandleUncaughtException(const v8::TryCatch& try_catch) override { + v8::HandleScope handle_scope(context_->isolate()); + std::string stack_trace = "<stack trace unavailable>"; + v8::Local<v8::Value> v8_stack_trace; + if (try_catch.StackTrace(context_->v8_context()).ToLocal(&v8_stack_trace)) { + v8::String::Utf8Value stack_value(v8_stack_trace); + if (*stack_value) + stack_trace.assign(*stack_value, stack_value.length()); + else + stack_trace = "<could not convert stack trace to string>"; + } + Fatal(context_, CreateExceptionString(try_catch) + "{" + stack_trace + "}"); + } +}; + +// Sets a property on the "exports" object for bindings. Called by JS with +// exports.$set(<key>, <value>). +void SetExportsProperty( + const v8::FunctionCallbackInfo<v8::Value>& args) { + v8::Local<v8::Object> obj = args.This(); + CHECK_EQ(2, args.Length()); + CHECK(args[0]->IsString()); + v8::Maybe<bool> result = + obj->DefineOwnProperty(args.GetIsolate()->GetCurrentContext(), + args[0]->ToString(), args[1], v8::ReadOnly); + if (!result.FromMaybe(false)) + LOG(ERROR) << "Failed to set private property on the export."; +} + +} // namespace + +std::string ModuleSystem::ExceptionHandler::CreateExceptionString( + const v8::TryCatch& try_catch) { + v8::Local<v8::Message> message(try_catch.Message()); + if (message.IsEmpty()) { + return "try_catch has no message"; + } + + std::string resource_name = "<unknown resource>"; + if (!message->GetScriptOrigin().ResourceName().IsEmpty()) { + v8::String::Utf8Value resource_name_v8( + message->GetScriptOrigin().ResourceName()); + resource_name.assign(*resource_name_v8, resource_name_v8.length()); + } + + std::string error_message = "<no error message>"; + if (!message->Get().IsEmpty()) { + v8::String::Utf8Value error_message_v8(message->Get()); + error_message.assign(*error_message_v8, error_message_v8.length()); + } + + auto maybe = message->GetLineNumber(context_->v8_context()); + int line_number = maybe.IsJust() ? maybe.FromJust() : 0; + return base::StringPrintf("%s:%d: %s", + resource_name.c_str(), + line_number, + error_message.c_str()); +} + +ModuleSystem::ModuleSystem(ScriptContext* context, SourceMap* source_map) + : ObjectBackedNativeHandler(context), + context_(context), + source_map_(source_map), + natives_enabled_(0), + exception_handler_(new DefaultExceptionHandler(context)), + weak_factory_(this) { + RouteFunction( + "require", + base::Bind(&ModuleSystem::RequireForJs, base::Unretained(this))); + RouteFunction( + "requireNative", + base::Bind(&ModuleSystem::RequireNative, base::Unretained(this))); + RouteFunction( + "requireAsync", + base::Bind(&ModuleSystem::RequireAsync, base::Unretained(this))); + RouteFunction("privates", + base::Bind(&ModuleSystem::Private, base::Unretained(this))); + + v8::Local<v8::Object> global(context->v8_context()->Global()); + v8::Isolate* isolate = context->isolate(); + SetPrivate(global, kModulesField, v8::Object::New(isolate)); + SetPrivate(global, kModuleSystem, v8::External::New(isolate, this)); + + gin::ModuleRegistry::From(context->v8_context())->AddObserver(this); + if (context_->GetRenderFrame()) { + context_->GetRenderFrame()->EnsureMojoBuiltinsAreAvailable( + context->isolate(), context->v8_context()); + } +} + +ModuleSystem::~ModuleSystem() { +} + +void ModuleSystem::Invalidate() { + // Clear the module system properties from the global context. It's polite, + // and we use this as a signal in lazy handlers that we no longer exist. + { + v8::HandleScope scope(GetIsolate()); + v8::Local<v8::Object> global = context()->v8_context()->Global(); + DeletePrivate(global, kModulesField); + DeletePrivate(global, kModuleSystem); + } + + // Invalidate all active and clobbered NativeHandlers we own. + for (const auto& handler : native_handler_map_) + handler.second->Invalidate(); + for (const auto& clobbered_handler : clobbered_native_handlers_) + clobbered_handler->Invalidate(); + + ObjectBackedNativeHandler::Invalidate(); +} + +ModuleSystem::NativesEnabledScope::NativesEnabledScope( + ModuleSystem* module_system) + : module_system_(module_system) { + module_system_->natives_enabled_++; +} + +ModuleSystem::NativesEnabledScope::~NativesEnabledScope() { + module_system_->natives_enabled_--; + CHECK_GE(module_system_->natives_enabled_, 0); +} + +void ModuleSystem::HandleException(const v8::TryCatch& try_catch) { + exception_handler_->HandleUncaughtException(try_catch); +} + +v8::MaybeLocal<v8::Object> ModuleSystem::Require( + const std::string& module_name) { + v8::Local<v8::String> v8_module_name; + if (!ToV8String(GetIsolate(), module_name, &v8_module_name)) + return v8::MaybeLocal<v8::Object>(); + v8::EscapableHandleScope handle_scope(GetIsolate()); + v8::Local<v8::Value> value = RequireForJsInner( + v8_module_name); + if (value.IsEmpty() || !value->IsObject()) + return v8::MaybeLocal<v8::Object>(); + return handle_scope.Escape(value.As<v8::Object>()); +} + +void ModuleSystem::RequireForJs( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (!args[0]->IsString()) { + NOTREACHED() << "require() called with a non-string argument"; + return; + } + v8::Local<v8::String> module_name = args[0].As<v8::String>(); + args.GetReturnValue().Set(RequireForJsInner(module_name)); +} + +v8::Local<v8::Value> ModuleSystem::RequireForJsInner( + v8::Local<v8::String> module_name) { + v8::EscapableHandleScope handle_scope(GetIsolate()); + v8::Local<v8::Context> v8_context = context()->v8_context(); + v8::Context::Scope context_scope(v8_context); + + v8::Local<v8::Object> global(context()->v8_context()->Global()); + + // The module system might have been deleted. This can happen if a different + // context keeps a reference to us, but our frame is destroyed (e.g. + // background page keeps reference to chrome object in a closed popup). + v8::Local<v8::Value> modules_value; + if (!GetPrivate(global, kModulesField, &modules_value) || + modules_value->IsUndefined()) { + Warn(GetIsolate(), "Extension view no longer exists"); + return v8::Undefined(GetIsolate()); + } + + v8::Local<v8::Object> modules(v8::Local<v8::Object>::Cast(modules_value)); + v8::Local<v8::Value> exports; + if (!GetPrivateProperty(v8_context, modules, module_name, &exports) || + !exports->IsUndefined()) + return handle_scope.Escape(exports); + + exports = LoadModule(*v8::String::Utf8Value(module_name)); + SetPrivateProperty(v8_context, modules, module_name, exports); + return handle_scope.Escape(exports); +} + +v8::Local<v8::Value> ModuleSystem::CallModuleMethod( + const std::string& module_name, + const std::string& method_name) { + v8::EscapableHandleScope handle_scope(GetIsolate()); + v8::Local<v8::Value> no_args; + return handle_scope.Escape( + CallModuleMethod(module_name, method_name, 0, &no_args)); +} + +v8::Local<v8::Value> ModuleSystem::CallModuleMethod( + const std::string& module_name, + const std::string& method_name, + std::vector<v8::Local<v8::Value>>* args) { + return CallModuleMethod(module_name, method_name, args->size(), args->data()); +} + +v8::Local<v8::Value> ModuleSystem::CallModuleMethod( + const std::string& module_name, + const std::string& method_name, + int argc, + v8::Local<v8::Value> argv[]) { + TRACE_EVENT2("v8", + "v8.callModuleMethod", + "module_name", + module_name, + "method_name", + method_name); + + v8::EscapableHandleScope handle_scope(GetIsolate()); + v8::Local<v8::Context> v8_context = context()->v8_context(); + v8::Context::Scope context_scope(v8_context); + + v8::Local<v8::String> v8_module_name; + v8::Local<v8::String> v8_method_name; + if (!ToV8String(GetIsolate(), module_name.c_str(), &v8_module_name) || + !ToV8String(GetIsolate(), method_name.c_str(), &v8_method_name)) { + return handle_scope.Escape(v8::Undefined(GetIsolate())); + } + + v8::Local<v8::Value> module; + { + NativesEnabledScope natives_enabled(this); + module = RequireForJsInner(v8_module_name); + } + + if (module.IsEmpty() || !module->IsObject()) { + Fatal(context_, + "Failed to get module " + module_name + " to call " + method_name); + return handle_scope.Escape(v8::Undefined(GetIsolate())); + } + + v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(module); + v8::Local<v8::Value> value; + if (!GetProperty(v8_context, object, v8_method_name, &value) || + !value->IsFunction()) { + Fatal(context_, module_name + "." + method_name + " is not a function"); + return handle_scope.Escape(v8::Undefined(GetIsolate())); + } + + v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(value); + v8::Local<v8::Value> result; + { + v8::TryCatch try_catch(GetIsolate()); + try_catch.SetCaptureMessage(true); + result = context_->CallFunction(func, argc, argv); + if (try_catch.HasCaught()) { + HandleException(try_catch); + result = v8::Undefined(GetIsolate()); + } + } + return handle_scope.Escape(result); +} + +void ModuleSystem::RegisterNativeHandler( + const std::string& name, + scoped_ptr<NativeHandler> native_handler) { + ClobberExistingNativeHandler(name); + native_handler_map_[name] = std::move(native_handler); +} + +void ModuleSystem::OverrideNativeHandlerForTest(const std::string& name) { + ClobberExistingNativeHandler(name); + overridden_native_handlers_.insert(name); +} + +void ModuleSystem::RunString(const std::string& code, const std::string& name) { + v8::HandleScope handle_scope(GetIsolate()); + v8::Local<v8::String> v8_code; + v8::Local<v8::String> v8_name; + if (!ToV8String(GetIsolate(), code.c_str(), &v8_code) || + !ToV8String(GetIsolate(), name.c_str(), &v8_name)) { + Warn(GetIsolate(), "Too long code or name."); + return; + } + RunString(v8_code, v8_name); +} + +// static +void ModuleSystem::NativeLazyFieldGetter( + v8::Local<v8::Name> property, + const v8::PropertyCallbackInfo<v8::Value>& info) { + LazyFieldGetterInner(property.As<v8::String>(), info, + &ModuleSystem::RequireNativeFromString); +} + +// static +void ModuleSystem::LazyFieldGetter( + v8::Local<v8::Name> property, + const v8::PropertyCallbackInfo<v8::Value>& info) { + LazyFieldGetterInner(property.As<v8::String>(), info, &ModuleSystem::Require); +} + +// static +void ModuleSystem::LazyFieldGetterInner( + v8::Local<v8::String> property, + const v8::PropertyCallbackInfo<v8::Value>& info, + RequireFunction require_function) { + CHECK(!info.Data().IsEmpty()); + CHECK(info.Data()->IsObject()); + v8::Isolate* isolate = info.GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Object> parameters = v8::Local<v8::Object>::Cast(info.Data()); + // This context should be the same as context()->v8_context(). + v8::Local<v8::Context> context = parameters->CreationContext(); + v8::Local<v8::Object> global(context->Global()); + v8::Local<v8::Value> module_system_value; + if (!GetPrivate(context, global, kModuleSystem, &module_system_value) || + !module_system_value->IsExternal()) { + // ModuleSystem has been deleted. + // TODO(kalman): See comment in header file. + Warn(isolate, + "Module system has been deleted, does extension view exist?"); + return; + } + + ModuleSystem* module_system = static_cast<ModuleSystem*>( + v8::Local<v8::External>::Cast(module_system_value)->Value()); + + v8::Local<v8::Value> v8_module_name; + if (!GetPrivateProperty(context, parameters, kModuleName, &v8_module_name)) { + Warn(isolate, "Cannot find module."); + return; + } + std::string name = *v8::String::Utf8Value(v8_module_name); + + // Switch to our v8 context because we need functions created while running + // the require()d module to belong to our context, not the current one. + v8::Context::Scope context_scope(context); + NativesEnabledScope natives_enabled_scope(module_system); + + v8::TryCatch try_catch(isolate); + v8::Local<v8::Value> module_value; + if (!(module_system->*require_function)(name).ToLocal(&module_value)) { + module_system->HandleException(try_catch); + return; + } + + v8::Local<v8::Object> module = v8::Local<v8::Object>::Cast(module_value); + v8::Local<v8::Value> field_value; + if (!GetPrivateProperty(context, parameters, kModuleField, &field_value)) { + module_system->HandleException(try_catch); + return; + } + v8::Local<v8::String> field; + if (!field_value->ToString(context).ToLocal(&field)) { + module_system->HandleException(try_catch); + return; + } + + if (!IsTrue(module->Has(context, field))) { + std::string field_str = *v8::String::Utf8Value(field); + Fatal(module_system->context_, + "Lazy require of " + name + "." + field_str + " did not set the " + + field_str + " field"); + return; + } + + v8::Local<v8::Value> new_field; + if (!GetProperty(context, module, field, &new_field)) { + module_system->HandleException(try_catch); + return; + } + + // Ok for it to be undefined, among other things it's how bindings signify + // that the extension doesn't have permission to use them. + CHECK(!new_field.IsEmpty()); + + // Delete the getter and set this field to |new_field| so the same object is + // returned every time a certain API is accessed. + v8::Local<v8::Value> val = info.This(); + if (val->IsObject()) { + v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(val); + object->Delete(context, property); + SetProperty(context, object, property, new_field); + } else { + NOTREACHED(); + } + info.GetReturnValue().Set(new_field); +} + +void ModuleSystem::SetLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field) { + SetLazyField( + object, field, module_name, module_field, &ModuleSystem::LazyFieldGetter); +} + +void ModuleSystem::SetLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field, + v8::AccessorNameGetterCallback getter) { + CHECK(field.size() < v8::String::kMaxLength); + CHECK(module_name.size() < v8::String::kMaxLength); + CHECK(module_field.size() < v8::String::kMaxLength); + v8::HandleScope handle_scope(GetIsolate()); + v8::Local<v8::Object> parameters = v8::Object::New(GetIsolate()); + v8::Local<v8::Context> context = context_->v8_context(); + SetPrivateProperty(context, parameters, kModuleName, + ToV8StringUnsafe(GetIsolate(), module_name.c_str())); + SetPrivateProperty(context, parameters, kModuleField, + ToV8StringUnsafe(GetIsolate(), module_field.c_str())); + auto maybe = object->SetAccessor( + context, ToV8StringUnsafe(GetIsolate(), field.c_str()), getter, NULL, + parameters); + CHECK(IsTrue(maybe)); +} + +void ModuleSystem::SetNativeLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field) { + SetLazyField(object, + field, + module_name, + module_field, + &ModuleSystem::NativeLazyFieldGetter); +} + +v8::Local<v8::Value> ModuleSystem::RunString(v8::Local<v8::String> code, + v8::Local<v8::String> name) { + return context_->RunScript( + name, code, base::Bind(&ExceptionHandler::HandleUncaughtException, + base::Unretained(exception_handler_.get()))); +} + +v8::Local<v8::Value> ModuleSystem::GetSource(const std::string& module_name) { + v8::EscapableHandleScope handle_scope(GetIsolate()); + if (!source_map_->Contains(module_name)) + return v8::Undefined(GetIsolate()); + return handle_scope.Escape( + v8::Local<v8::Value>(source_map_->GetSource(GetIsolate(), module_name))); +} + +void ModuleSystem::RequireNative( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + std::string native_name = *v8::String::Utf8Value(args[0]); + v8::Local<v8::Object> object; + if (RequireNativeFromString(native_name).ToLocal(&object)) + args.GetReturnValue().Set(object); +} + +v8::MaybeLocal<v8::Object> ModuleSystem::RequireNativeFromString( + const std::string& native_name) { + if (natives_enabled_ == 0) { + // HACK: if in test throw exception so that we can test the natives-disabled + // logic; however, under normal circumstances, this is programmer error so + // we could crash. + if (exception_handler_) { + GetIsolate()->ThrowException( + ToV8StringUnsafe(GetIsolate(), "Natives disabled")); + return v8::MaybeLocal<v8::Object>(); + } + Fatal(context_, "Natives disabled for requireNative(" + native_name + ")"); + return v8::MaybeLocal<v8::Object>(); + } + + if (overridden_native_handlers_.count(native_name) > 0u) { + v8::Local<v8::Value> value = RequireForJsInner( + ToV8StringUnsafe(GetIsolate(), native_name.c_str())); + if (value.IsEmpty() || !value->IsObject()) + return v8::MaybeLocal<v8::Object>(); + return value.As<v8::Object>(); + } + + NativeHandlerMap::iterator i = native_handler_map_.find(native_name); + if (i == native_handler_map_.end()) { + Fatal(context_, + "Couldn't find native for requireNative(" + native_name + ")"); + return v8::MaybeLocal<v8::Object>(); + } + return i->second->NewInstance(); +} + +void ModuleSystem::RequireAsync( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + std::string module_name = *v8::String::Utf8Value(args[0]); + v8::Local<v8::Context> v8_context = context_->v8_context(); + v8::Local<v8::Promise::Resolver> resolver( + v8::Promise::Resolver::New(v8_context).ToLocalChecked()); + args.GetReturnValue().Set(resolver->GetPromise()); + scoped_ptr<v8::Global<v8::Promise::Resolver>> global_resolver( + new v8::Global<v8::Promise::Resolver>(GetIsolate(), resolver)); + gin::ModuleRegistry* module_registry = + gin::ModuleRegistry::From(v8_context); + if (!module_registry) { + Warn(GetIsolate(), "Extension view no longer exists"); + resolver->Reject(v8_context, v8::Exception::Error(ToV8StringUnsafe( + GetIsolate(), "Extension view no longer exists"))); + return; + } + module_registry->LoadModule( + GetIsolate(), module_name, + base::Bind(&ModuleSystem::OnModuleLoaded, weak_factory_.GetWeakPtr(), + base::Passed(&global_resolver))); + if (module_registry->available_modules().count(module_name) == 0) + LoadModule(module_name); +} + +v8::Local<v8::String> ModuleSystem::WrapSource(v8::Local<v8::String> source) { + v8::EscapableHandleScope handle_scope(GetIsolate()); + // Keep in order with the arguments in RequireForJsInner. + v8::Local<v8::String> left = ToV8StringUnsafe( + GetIsolate(), + "(function(define, require, requireNative, requireAsync, exports, " + "console, privates," + "$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {" + "'use strict';"); + v8::Local<v8::String> right = ToV8StringUnsafe(GetIsolate(), "\n})"); + return handle_scope.Escape(v8::Local<v8::String>( + v8::String::Concat(left, v8::String::Concat(source, right)))); +} + +void ModuleSystem::Private(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + if (!args[0]->IsObject() || args[0]->IsNull()) { + GetIsolate()->ThrowException( + v8::Exception::TypeError(ToV8StringUnsafe(GetIsolate(), + args[0]->IsUndefined() + ? "Method called without a valid receiver (this). " + "Did you forget to call .bind()?" + : "Invalid invocation: receiver is not an object!"))); + return; + } + v8::Local<v8::Object> obj = args[0].As<v8::Object>(); + v8::Local<v8::Value> privates; + if (!GetPrivate(obj, "privates", &privates) || !privates->IsObject()) { + privates = v8::Object::New(args.GetIsolate()); + if (privates.IsEmpty()) { + GetIsolate()->ThrowException( + ToV8StringUnsafe(GetIsolate(), "Failed to create privates")); + return; + } + v8::Maybe<bool> maybe = + privates.As<v8::Object>()->SetPrototype(context()->v8_context(), + v8::Null(args.GetIsolate())); + CHECK(maybe.IsJust() && maybe.FromJust()); + SetPrivate(obj, "privates", privates); + } + args.GetReturnValue().Set(privates); +} + +v8::Local<v8::Value> ModuleSystem::LoadModule(const std::string& module_name) { + v8::EscapableHandleScope handle_scope(GetIsolate()); + v8::Local<v8::Context> v8_context = context()->v8_context(); + v8::Context::Scope context_scope(v8_context); + + v8::Local<v8::Value> source(GetSource(module_name)); + if (source.IsEmpty() || source->IsUndefined()) { + Fatal(context_, "No source for require(" + module_name + ")"); + return v8::Undefined(GetIsolate()); + } + v8::Local<v8::String> wrapped_source( + WrapSource(v8::Local<v8::String>::Cast(source))); + v8::Local<v8::String> v8_module_name; + if (!ToV8String(GetIsolate(), module_name.c_str(), &v8_module_name)) { + NOTREACHED() << "module_name is too long"; + return v8::Undefined(GetIsolate()); + } + // Modules are wrapped in (function(){...}) so they always return functions. + v8::Local<v8::Value> func_as_value = + RunString(wrapped_source, v8_module_name); + if (func_as_value.IsEmpty() || func_as_value->IsUndefined()) { + Fatal(context_, "Bad source for require(" + module_name + ")"); + return v8::Undefined(GetIsolate()); + } + + v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(func_as_value); + + v8::Local<v8::Object> define_object = v8::Object::New(GetIsolate()); + gin::ModuleRegistry::InstallGlobals(GetIsolate(), define_object); + + v8::Local<v8::Object> exports = v8::Object::New(GetIsolate()); + + v8::Local<v8::FunctionTemplate> tmpl = v8::FunctionTemplate::New( + GetIsolate(), + &SetExportsProperty); + v8::Local<v8::String> v8_key; + if (!v8_helpers::ToV8String(GetIsolate(), "$set", &v8_key)) { + NOTREACHED(); + return v8::Undefined(GetIsolate()); + } + + v8::Local<v8::Function> function; + if (!tmpl->GetFunction(v8_context).ToLocal(&function)) { + NOTREACHED(); + return v8::Undefined(GetIsolate()); + } + + exports->DefineOwnProperty(v8_context, v8_key, function, v8::ReadOnly) + .FromJust(); + + v8::Local<v8::Object> natives(NewInstance()); + CHECK(!natives.IsEmpty()); // this can fail if v8 has issues + + // These must match the argument order in WrapSource. + v8::Local<v8::Value> args[] = { + // AMD. + GetPropertyUnsafe(v8_context, define_object, "define"), + // CommonJS. + GetPropertyUnsafe(v8_context, natives, "require", + v8::NewStringType::kInternalized), + GetPropertyUnsafe(v8_context, natives, "requireNative", + v8::NewStringType::kInternalized), + GetPropertyUnsafe(v8_context, natives, "requireAsync", + v8::NewStringType::kInternalized), + exports, + // Libraries that we magically expose to every module. + console::AsV8Object(GetIsolate()), + GetPropertyUnsafe(v8_context, natives, "privates", + v8::NewStringType::kInternalized), + // Each safe builtin. Keep in order with the arguments in WrapSource. + context_->safe_builtins()->GetArray(), + context_->safe_builtins()->GetFunction(), + context_->safe_builtins()->GetJSON(), + context_->safe_builtins()->GetObjekt(), + context_->safe_builtins()->GetRegExp(), + context_->safe_builtins()->GetString(), + context_->safe_builtins()->GetError(), + }; + { + v8::TryCatch try_catch(GetIsolate()); + try_catch.SetCaptureMessage(true); + context_->CallFunction(func, arraysize(args), args); + if (try_catch.HasCaught()) { + HandleException(try_catch); + return v8::Undefined(GetIsolate()); + } + } + return handle_scope.Escape(exports); +} + +void ModuleSystem::OnDidAddPendingModule( + const std::string& id, + const std::vector<std::string>& dependencies) { + bool module_system_managed = source_map_->Contains(id); + + gin::ModuleRegistry* registry = + gin::ModuleRegistry::From(context_->v8_context()); + DCHECK(registry); + for (const auto& dependency : dependencies) { + // If a dependency is not available, and either the module or this + // dependency is managed by ModuleSystem, attempt to load it. Other + // gin::ModuleRegistry users (WebUI and users of the mojoPrivate API) are + // responsible for loading their module dependencies when required. + if (registry->available_modules().count(dependency) == 0 && + (module_system_managed || source_map_->Contains(dependency))) { + LoadModule(dependency); + } + } + registry->AttemptToLoadMoreModules(GetIsolate()); +} + +void ModuleSystem::OnModuleLoaded( + scoped_ptr<v8::Global<v8::Promise::Resolver>> resolver, + v8::Local<v8::Value> value) { + if (!is_valid()) + return; + v8::HandleScope handle_scope(GetIsolate()); + v8::Local<v8::Promise::Resolver> resolver_local( + v8::Local<v8::Promise::Resolver>::New(GetIsolate(), *resolver)); + resolver_local->Resolve(context()->v8_context(), value); +} + +void ModuleSystem::ClobberExistingNativeHandler(const std::string& name) { + NativeHandlerMap::iterator existing_handler = native_handler_map_.find(name); + if (existing_handler != native_handler_map_.end()) { + clobbered_native_handlers_.push_back(std::move(existing_handler->second)); + native_handler_map_.erase(existing_handler); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/module_system.h b/chromium/extensions/renderer/module_system.h new file mode 100644 index 00000000000..134d2baa05f --- /dev/null +++ b/chromium/extensions/renderer/module_system.h @@ -0,0 +1,253 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_MODULE_SYSTEM_H_ +#define EXTENSIONS_RENDERER_MODULE_SYSTEM_H_ + +#include <map> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "extensions/renderer/native_handler.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "gin/modules/module_registry_observer.h" +#include "v8/include/v8.h" + +namespace extensions { + +class ScriptContext; + +// A module system for JS similar to node.js' require() function. +// Each module has three variables in the global scope: +// - exports, an object returned to dependencies who require() this +// module. +// - require, a function that takes a module name as an argument and returns +// that module's exports object. +// - requireNative, a function that takes the name of a registered +// NativeHandler and returns an object that contains the functions the +// NativeHandler defines. +// +// Each module in a ModuleSystem is executed at most once and its exports +// object cached. +// +// Note that a ModuleSystem must be used only in conjunction with a single +// v8::Context. +// TODO(koz): Rename this to JavaScriptModuleSystem. +class ModuleSystem : public ObjectBackedNativeHandler, + public gin::ModuleRegistryObserver { + public: + class SourceMap { + public: + virtual ~SourceMap() {} + virtual v8::Local<v8::Value> GetSource(v8::Isolate* isolate, + const std::string& name) = 0; + virtual bool Contains(const std::string& name) = 0; + }; + + class ExceptionHandler { + public: + explicit ExceptionHandler(ScriptContext* context) : context_(context) {} + virtual ~ExceptionHandler() {} + virtual void HandleUncaughtException(const v8::TryCatch& try_catch) = 0; + + protected: + // Formats |try_catch| as a nice string. + std::string CreateExceptionString(const v8::TryCatch& try_catch); + // A script context associated with this handler. Owned by the module + // system. + ScriptContext* context_; + }; + + // Enables native bindings for the duration of its lifetime. + class NativesEnabledScope { + public: + explicit NativesEnabledScope(ModuleSystem* module_system); + ~NativesEnabledScope(); + + private: + ModuleSystem* module_system_; + DISALLOW_COPY_AND_ASSIGN(NativesEnabledScope); + }; + + // |source_map| is a weak pointer. + ModuleSystem(ScriptContext* context, SourceMap* source_map); + ~ModuleSystem() override; + + // Require the specified module. This is the equivalent of calling + // require('module_name') from the loaded JS files. + v8::MaybeLocal<v8::Object> Require(const std::string& module_name); + void Require(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Run |code| in the current context with the name |name| used for stack + // traces. + v8::Local<v8::Value> RunString(v8::Local<v8::String> code, + v8::Local<v8::String> name); + + // Calls the specified method exported by the specified module. This is + // equivalent to calling require('module_name').method_name() from JS. + v8::Local<v8::Value> CallModuleMethod(const std::string& module_name, + const std::string& method_name); + v8::Local<v8::Value> CallModuleMethod( + const std::string& module_name, + const std::string& method_name, + std::vector<v8::Local<v8::Value>>* args); + v8::Local<v8::Value> CallModuleMethod(const std::string& module_name, + const std::string& method_name, + int argc, + v8::Local<v8::Value> argv[]); + + // Register |native_handler| as a potential target for requireNative(), so + // calls to requireNative(|name|) from JS will return a new object created by + // |native_handler|. + void RegisterNativeHandler(const std::string& name, + scoped_ptr<NativeHandler> native_handler); + + // Causes requireNative(|name|) to look for its module in |source_map_| + // instead of using a registered native handler. This can be used in unit + // tests to mock out native modules. + void OverrideNativeHandlerForTest(const std::string& name); + + // Executes |code| in the current context with |name| as the filename. + void RunString(const std::string& code, const std::string& name); + + // Make |object|.|field| lazily evaluate to the result of + // require(|module_name|)[|module_field|]. + // + // TODO(kalman): All targets for this method are ObjectBackedNativeHandlers, + // move this logic into those classes (in fact, the chrome + // object is the only client, only that needs to implement it). + void SetLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field); + + void SetLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field, + v8::AccessorNameGetterCallback getter); + + // Make |object|.|field| lazily evaluate to the result of + // requireNative(|module_name|)[|module_field|]. + // TODO(kalman): Same as above. + void SetNativeLazyField(v8::Local<v8::Object> object, + const std::string& field, + const std::string& module_name, + const std::string& module_field); + + // Passes exceptions to |handler| rather than console::Fatal. + void SetExceptionHandlerForTest(scoped_ptr<ExceptionHandler> handler) { + exception_handler_ = std::move(handler); + } + + protected: + friend class ModuleSystemTestEnvironment; + friend class ScriptContext; + void Invalidate() override; + + private: + typedef std::map<std::string, scoped_ptr<NativeHandler>> NativeHandlerMap; + + // Retrieves the lazily defined field specified by |property|. + static void LazyFieldGetter(v8::Local<v8::Name> property, + const v8::PropertyCallbackInfo<v8::Value>& info); + // Retrieves the lazily defined field specified by |property| on a native + // object. + static void NativeLazyFieldGetter( + v8::Local<v8::Name> property, + const v8::PropertyCallbackInfo<v8::Value>& info); + + // Called when an exception is thrown but not caught. + void HandleException(const v8::TryCatch& try_catch); + + void RequireForJs(const v8::FunctionCallbackInfo<v8::Value>& args); + v8::Local<v8::Value> RequireForJsInner(v8::Local<v8::String> module_name); + + typedef v8::MaybeLocal<v8::Object>(ModuleSystem::*RequireFunction)( + const std::string&); + // Base implementation of a LazyFieldGetter which uses |require_fn| to require + // modules. + static void LazyFieldGetterInner( + v8::Local<v8::String> property, + const v8::PropertyCallbackInfo<v8::Value>& info, + RequireFunction require_function); + + // Return the named source file stored in the source map. + // |args[0]| - the name of a source file in source_map_. + v8::Local<v8::Value> GetSource(const std::string& module_name); + + // Return an object that contains the native methods defined by the named + // NativeHandler. + // |args[0]| - the name of a native handler object. + v8::MaybeLocal<v8::Object> RequireNativeFromString( + const std::string& native_name); + void RequireNative(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Return a promise for a requested module. + // |args[0]| - the name of a module. + void RequireAsync(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Wraps |source| in a (function(define, require, requireNative, ...) {...}). + v8::Local<v8::String> WrapSource(v8::Local<v8::String> source); + + // NativeHandler implementation which returns the private area of an Object. + void Private(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Loads and runs a Javascript module. + v8::Local<v8::Value> LoadModule(const std::string& module_name); + + // Invoked when a module is loaded in response to a requireAsync call. + // Resolves |resolver| with |value|. + void OnModuleLoaded(scoped_ptr<v8::Global<v8::Promise::Resolver>> resolver, + v8::Local<v8::Value> value); + + // gin::ModuleRegistryObserver overrides. + void OnDidAddPendingModule( + const std::string& id, + const std::vector<std::string>& dependencies) override; + + // Marks any existing NativeHandler named |name| as clobbered. + // See |clobbered_native_handlers_|. + void ClobberExistingNativeHandler(const std::string& name); + + ScriptContext* context_; + + // A map from module names to the JS source for that module. GetSource() + // performs a lookup on this map. + SourceMap* source_map_; + + // A map from native handler names to native handlers. + NativeHandlerMap native_handler_map_; + + // When 0, natives are disabled, otherwise indicates how many callers have + // pinned natives as enabled. + int natives_enabled_; + + // Called when an exception is thrown but not caught in JS. Overridable by + // tests. + scoped_ptr<ExceptionHandler> exception_handler_; + + // A set of native handlers that should actually be require()d as non-native + // handlers. This is used for tests to mock out native handlers in JS. + std::set<std::string> overridden_native_handlers_; + + // A list of NativeHandlers that have been clobbered, either due to + // registering a NativeHandler when one was already registered with the same + // name, or due to OverrideNativeHandlerForTest. This is needed so that they + // can be later Invalidated. It should only happen in tests. + std::vector<scoped_ptr<NativeHandler>> clobbered_native_handlers_; + + base::WeakPtrFactory<ModuleSystem> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(ModuleSystem); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_MODULE_SYSTEM_H_ diff --git a/chromium/extensions/renderer/module_system_test.cc b/chromium/extensions/renderer/module_system_test.cc new file mode 100644 index 00000000000..3386ff010c1 --- /dev/null +++ b/chromium/extensions/renderer/module_system_test.cc @@ -0,0 +1,261 @@ +// 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/renderer/module_system_test.h" + +#include <stddef.h> + +#include <map> +#include <string> +#include <utility> + +#include "base/callback.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/lazy_instance.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/strings/string_piece.h" +#include "extensions/common/extension_paths.h" +#include "extensions/renderer/logging_native_handler.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "extensions/renderer/safe_builtins.h" +#include "extensions/renderer/utils_native_handler.h" +#include "ui/base/resource/resource_bundle.h" + +namespace extensions { +namespace { + +class FailsOnException : public ModuleSystem::ExceptionHandler { + public: + FailsOnException() : ModuleSystem::ExceptionHandler(nullptr) {} + void HandleUncaughtException(const v8::TryCatch& try_catch) override { + FAIL() << "Uncaught exception: " << CreateExceptionString(try_catch); + } +}; + +class V8ExtensionConfigurator { + public: + V8ExtensionConfigurator() + : safe_builtins_(SafeBuiltins::CreateV8Extension()), + names_(1, safe_builtins_->name()), + configuration_( + new v8::ExtensionConfiguration(static_cast<int>(names_.size()), + names_.data())) { + v8::RegisterExtension(safe_builtins_.get()); + } + + v8::ExtensionConfiguration* GetConfiguration() { + return configuration_.get(); + } + + private: + scoped_ptr<v8::Extension> safe_builtins_; + std::vector<const char*> names_; + scoped_ptr<v8::ExtensionConfiguration> configuration_; +}; + +base::LazyInstance<V8ExtensionConfigurator>::Leaky g_v8_extension_configurator = + LAZY_INSTANCE_INITIALIZER; + +} // namespace + +// Native JS functions for doing asserts. +class ModuleSystemTestEnvironment::AssertNatives + : public ObjectBackedNativeHandler { + public: + explicit AssertNatives(ScriptContext* context) + : ObjectBackedNativeHandler(context), + assertion_made_(false), + failed_(false) { + RouteFunction( + "AssertTrue", + base::Bind(&AssertNatives::AssertTrue, base::Unretained(this))); + RouteFunction( + "AssertFalse", + base::Bind(&AssertNatives::AssertFalse, base::Unretained(this))); + } + + bool assertion_made() { return assertion_made_; } + bool failed() { return failed_; } + + void AssertTrue(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + assertion_made_ = true; + failed_ = failed_ || !args[0]->ToBoolean(args.GetIsolate())->Value(); + } + + void AssertFalse(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + assertion_made_ = true; + failed_ = failed_ || args[0]->ToBoolean(args.GetIsolate())->Value(); + } + + private: + bool assertion_made_; + bool failed_; +}; + +// Source map that operates on std::strings. +class ModuleSystemTestEnvironment::StringSourceMap + : public ModuleSystem::SourceMap { + public: + StringSourceMap() {} + ~StringSourceMap() override {} + + v8::Local<v8::Value> GetSource(v8::Isolate* isolate, + const std::string& name) override { + if (source_map_.count(name) == 0) + return v8::Undefined(isolate); + return v8::String::NewFromUtf8(isolate, source_map_[name].c_str()); + } + + bool Contains(const std::string& name) override { + return source_map_.count(name); + } + + void RegisterModule(const std::string& name, const std::string& source) { + CHECK_EQ(0u, source_map_.count(name)) << "Module " << name << " not found"; + source_map_[name] = source; + } + + private: + std::map<std::string, std::string> source_map_; +}; + +ModuleSystemTestEnvironment::ModuleSystemTestEnvironment(v8::Isolate* isolate) + : isolate_(isolate), + context_holder_(new gin::ContextHolder(isolate_)), + handle_scope_(isolate_), + source_map_(new StringSourceMap()) { + context_holder_->SetContext(v8::Context::New( + isolate, g_v8_extension_configurator.Get().GetConfiguration())); + context_.reset(new ScriptContext(context_holder_->context(), + nullptr, // WebFrame + nullptr, // Extension + Feature::BLESSED_EXTENSION_CONTEXT, + nullptr, // Effective Extension + Feature::BLESSED_EXTENSION_CONTEXT)); + context_->v8_context()->Enter(); + assert_natives_ = new AssertNatives(context_.get()); + + { + scoped_ptr<ModuleSystem> module_system( + new ModuleSystem(context_.get(), source_map_.get())); + context_->set_module_system(std::move(module_system)); + } + ModuleSystem* module_system = context_->module_system(); + module_system->RegisterNativeHandler( + "assert", scoped_ptr<NativeHandler>(assert_natives_)); + module_system->RegisterNativeHandler( + "logging", + scoped_ptr<NativeHandler>(new LoggingNativeHandler(context_.get()))); + module_system->RegisterNativeHandler( + "utils", + scoped_ptr<NativeHandler>(new UtilsNativeHandler(context_.get()))); + module_system->SetExceptionHandlerForTest( + scoped_ptr<ModuleSystem::ExceptionHandler>(new FailsOnException)); +} + +ModuleSystemTestEnvironment::~ModuleSystemTestEnvironment() { + if (context_->is_valid()) + ShutdownModuleSystem(); +} + +void ModuleSystemTestEnvironment::RegisterModule(const std::string& name, + const std::string& code) { + source_map_->RegisterModule(name, code); +} + +void ModuleSystemTestEnvironment::RegisterModule(const std::string& name, + int resource_id) { + const std::string& code = ResourceBundle::GetSharedInstance() + .GetRawDataResource(resource_id) + .as_string(); + source_map_->RegisterModule(name, code); +} + +void ModuleSystemTestEnvironment::OverrideNativeHandler( + const std::string& name, + const std::string& code) { + RegisterModule(name, code); + context_->module_system()->OverrideNativeHandlerForTest(name); +} + +void ModuleSystemTestEnvironment::RegisterTestFile( + const std::string& module_name, + const std::string& file_name) { + base::FilePath test_js_file_path; + ASSERT_TRUE(PathService::Get(DIR_TEST_DATA, &test_js_file_path)); + test_js_file_path = test_js_file_path.AppendASCII(file_name); + std::string test_js; + ASSERT_TRUE(base::ReadFileToString(test_js_file_path, &test_js)); + source_map_->RegisterModule(module_name, test_js); +} + +void ModuleSystemTestEnvironment::ShutdownGin() { + context_holder_.reset(); +} + +void ModuleSystemTestEnvironment::ShutdownModuleSystem() { + CHECK(context_->is_valid()); + context_->v8_context()->Exit(); + context_->Invalidate(); +} + +v8::Local<v8::Object> ModuleSystemTestEnvironment::CreateGlobal( + const std::string& name) { + v8::EscapableHandleScope handle_scope(isolate_); + v8::Local<v8::Object> object = v8::Object::New(isolate_); + isolate_->GetCurrentContext()->Global()->Set( + v8::String::NewFromUtf8(isolate_, name.c_str()), object); + return handle_scope.Escape(object); +} + +ModuleSystemTest::ModuleSystemTest() + : isolate_(v8::Isolate::GetCurrent()), + should_assertions_be_made_(true) { +} + +ModuleSystemTest::~ModuleSystemTest() { +} + +void ModuleSystemTest::SetUp() { + env_ = CreateEnvironment(); + base::CommandLine::ForCurrentProcess()->AppendSwitch("test-type"); +} + +void ModuleSystemTest::TearDown() { + // All tests must assert at least once unless otherwise specified. + EXPECT_EQ(should_assertions_be_made_, + env_->assert_natives()->assertion_made()); + EXPECT_FALSE(env_->assert_natives()->failed()); + env_.reset(); + v8::HeapStatistics stats; + isolate_->GetHeapStatistics(&stats); + size_t old_heap_size = 0; + // Run the GC until the heap size reaches a steady state to ensure that + // all the garbage is collected. + while (stats.used_heap_size() != old_heap_size) { + old_heap_size = stats.used_heap_size(); + isolate_->RequestGarbageCollectionForTesting( + v8::Isolate::kFullGarbageCollection); + isolate_->GetHeapStatistics(&stats); + } +} + +scoped_ptr<ModuleSystemTestEnvironment> ModuleSystemTest::CreateEnvironment() { + return make_scoped_ptr(new ModuleSystemTestEnvironment(isolate_)); +} + +void ModuleSystemTest::ExpectNoAssertionsMade() { + should_assertions_be_made_ = false; +} + +void ModuleSystemTest::RunResolvedPromises() { + v8::MicrotasksScope::PerformCheckpoint(isolate_); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/module_system_test.h b/chromium/extensions/renderer/module_system_test.h new file mode 100644 index 00000000000..517c3080c00 --- /dev/null +++ b/chromium/extensions/renderer/module_system_test.h @@ -0,0 +1,111 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_MODULE_SYSTEM_TEST_H_ +#define EXTENSIONS_RENDERER_MODULE_SYSTEM_TEST_H_ + +#include "base/macros.h" +#include "extensions/renderer/module_system.h" +#include "extensions/renderer/script_context.h" +#include "gin/public/context_holder.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "v8/include/v8.h" + +namespace extensions { + +class ModuleSystemTestEnvironment { + public: + class AssertNatives; + class StringSourceMap; + + explicit ModuleSystemTestEnvironment(v8::Isolate* isolate); + ~ModuleSystemTestEnvironment(); + + // Register a named JS module in the module system. + void RegisterModule(const std::string& name, const std::string& code); + + // Register a named JS module with source retrieved from a ResourceBundle. + void RegisterModule(const std::string& name, int resource_id); + + // Register a named JS module in the module system and tell the module system + // to use it to handle any requireNative() calls for native modules with that + // name. + void OverrideNativeHandler(const std::string& name, const std::string& code); + + // Registers |file_name| from chrome/test/data/extensions as a module name + // |module_name|. + void RegisterTestFile(const std::string& module_name, + const std::string& file_name); + + // Create an empty object in the global scope with name |name|. + v8::Local<v8::Object> CreateGlobal(const std::string& name); + + void ShutdownGin(); + + void ShutdownModuleSystem(); + + ModuleSystem* module_system() { return context_->module_system(); } + + ScriptContext* context() { return context_.get(); } + + v8::Isolate* isolate() { return isolate_; } + + AssertNatives* assert_natives() { return assert_natives_; } + + private: + v8::Isolate* isolate_; + scoped_ptr<gin::ContextHolder> context_holder_; + v8::HandleScope handle_scope_; + scoped_ptr<ScriptContext> context_; + AssertNatives* assert_natives_; + scoped_ptr<StringSourceMap> source_map_; + + DISALLOW_COPY_AND_ASSIGN(ModuleSystemTestEnvironment); +}; + +// Test fixture for testing JS that makes use of the module system. +// +// Typically tests will look like: +// +// TEST_F(MyModuleSystemTest, TestStuff) { +// ModuleSystem::NativesEnabledScope natives_enabled(module_system_.get()); +// RegisterModule("test", "requireNative('assert').AssertTrue(true);"); +// module_system_->Require("test"); +// } +// +// By default a test will fail if no method in the native module 'assert' is +// called. This behaviour can be overridden by calling ExpectNoAssertionsMade(). +class ModuleSystemTest : public testing::Test { + public: + ModuleSystemTest(); + ~ModuleSystemTest() override; + + void SetUp() override; + void TearDown() override; + + protected: + ModuleSystemTestEnvironment* env() { return env_.get(); } + + scoped_ptr<ModuleSystemTestEnvironment> CreateEnvironment(); + + // Make the test fail if any asserts are called. By default a test will fail + // if no asserts are called. + void ExpectNoAssertionsMade(); + + // Runs promises that have been resolved. Resolved promises will not run + // until this is called. + void RunResolvedPromises(); + + private: + v8::Isolate* isolate_; + scoped_ptr<ModuleSystemTestEnvironment> env_; + bool should_assertions_be_made_; + + private: + DISALLOW_COPY_AND_ASSIGN(ModuleSystemTest); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_MODULE_SYSTEM_TEST_H_ diff --git a/chromium/extensions/renderer/module_system_unittest.cc b/chromium/extensions/renderer/module_system_unittest.cc new file mode 100644 index 00000000000..5803ad0d694 --- /dev/null +++ b/chromium/extensions/renderer/module_system_unittest.cc @@ -0,0 +1,518 @@ +// 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/renderer/module_system.h" + +#include <stdint.h> + +#include <utility> + +#include "base/memory/scoped_ptr.h" +#include "extensions/renderer/module_system_test.h" +#include "gin/modules/module_registry.h" + +namespace extensions { + +class CounterNatives : public ObjectBackedNativeHandler { + public: + explicit CounterNatives(ScriptContext* context) + : ObjectBackedNativeHandler(context), counter_(0) { + RouteFunction("Get", + base::Bind(&CounterNatives::Get, base::Unretained(this))); + RouteFunction( + "Increment", + base::Bind(&CounterNatives::Increment, base::Unretained(this))); + } + + void Get(const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(static_cast<int32_t>(counter_)); + } + + void Increment(const v8::FunctionCallbackInfo<v8::Value>& args) { + counter_++; + } + + private: + int counter_; +}; + +class TestExceptionHandler : public ModuleSystem::ExceptionHandler { + public: + TestExceptionHandler() + : ModuleSystem::ExceptionHandler(nullptr), handled_exception_(false) {} + + void HandleUncaughtException(const v8::TryCatch& try_catch) override { + handled_exception_ = true; + } + + bool handled_exception() const { return handled_exception_; } + + private: + bool handled_exception_; +}; + +TEST_F(ModuleSystemTest, TestExceptionHandling) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + TestExceptionHandler* handler = new TestExceptionHandler; + scoped_ptr<ModuleSystem::ExceptionHandler> scoped_handler(handler); + ASSERT_FALSE(handler->handled_exception()); + env()->module_system()->SetExceptionHandlerForTest(std::move(scoped_handler)); + + env()->RegisterModule("test", "throw 'hi';"); + env()->module_system()->Require("test"); + ASSERT_TRUE(handler->handled_exception()); + + ExpectNoAssertionsMade(); +} + +TEST_F(ModuleSystemTest, TestRequire) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("add", + "exports.$set('Add'," + "function(x, y) { return x + y; });"); + env()->RegisterModule("test", + "var Add = require('add').Add;" + "requireNative('assert').AssertTrue(Add(3, 5) == 8);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestNestedRequire) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("add", + "exports.$set('Add'," + "function(x, y) { return x + y; });"); + env()->RegisterModule("double", + "var Add = require('add').Add;" + "exports.$set('Double'," + "function(x) { return Add(x, x); });"); + env()->RegisterModule("test", + "var Double = require('double').Double;" + "requireNative('assert').AssertTrue(Double(3) == 6);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestModuleInsulation) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("x", + "var x = 10;" + "exports.$set('X', function() { return x; });"); + env()->RegisterModule("y", + "var x = 15;" + "require('x');" + "exports.$set('Y', function() { return x; });"); + env()->RegisterModule("test", + "var Y = require('y').Y;" + "var X = require('x').X;" + "var assert = requireNative('assert');" + "assert.AssertTrue(!this.hasOwnProperty('x'));" + "assert.AssertTrue(Y() == 15);" + "assert.AssertTrue(X() == 10);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestNativesAreDisabledOutsideANativesEnabledScope) { + env()->RegisterModule("test", + "var assert;" + "try {" + " assert = requireNative('assert');" + "} catch (e) {" + " var caught = true;" + "}" + "if (assert) {" + " assert.AssertTrue(true);" + "}"); + env()->module_system()->Require("test"); + ExpectNoAssertionsMade(); +} + +TEST_F(ModuleSystemTest, TestNativesAreEnabledWithinANativesEnabledScope) { + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "assert.AssertTrue(true);"); + + { + ModuleSystem::NativesEnabledScope natives_enabled(env()->module_system()); + { + ModuleSystem::NativesEnabledScope natives_enabled_inner( + env()->module_system()); + } + env()->module_system()->Require("test"); + } +} + +TEST_F(ModuleSystemTest, TestLazyField) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("lazy", "exports.$set('x', 5);"); + + v8::Local<v8::Object> object = env()->CreateGlobal("object"); + + env()->module_system()->SetLazyField(object, "blah", "lazy", "x"); + + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "assert.AssertTrue(object.blah == 5);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestLazyFieldYieldingObject) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "lazy", + "var object = {};" + "object.__defineGetter__('z', function() { return 1; });" + "object.x = 5;" + "object.y = function() { return 10; };" + "exports.$set('object', object);"); + + v8::Local<v8::Object> object = env()->CreateGlobal("object"); + + env()->module_system()->SetLazyField(object, "thing", "lazy", "object"); + + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "assert.AssertTrue(object.thing.x == 5);" + "assert.AssertTrue(object.thing.y() == 10);" + "assert.AssertTrue(object.thing.z == 1);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestLazyFieldIsOnlyEvaledOnce) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->RegisterNativeHandler( + "counter", + scoped_ptr<NativeHandler>(new CounterNatives(env()->context()))); + env()->RegisterModule("lazy", + "requireNative('counter').Increment();" + "exports.$set('x', 5);"); + + v8::Local<v8::Object> object = env()->CreateGlobal("object"); + + env()->module_system()->SetLazyField(object, "x", "lazy", "x"); + + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "var counter = requireNative('counter');" + "assert.AssertTrue(counter.Get() == 0);" + "object.x;" + "assert.AssertTrue(counter.Get() == 1);" + "object.x;" + "assert.AssertTrue(counter.Get() == 1);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestRequireNativesAfterLazyEvaluation) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("lazy", "exports.$set('x', 5);"); + v8::Local<v8::Object> object = env()->CreateGlobal("object"); + + env()->module_system()->SetLazyField(object, "x", "lazy", "x"); + env()->RegisterModule("test", + "object.x;" + "requireNative('assert').AssertTrue(true);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestTransitiveRequire) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("dependency", "exports.$set('x', 5);"); + env()->RegisterModule("lazy", + "exports.$set('output', require('dependency'));"); + + v8::Local<v8::Object> object = env()->CreateGlobal("object"); + + env()->module_system()->SetLazyField(object, "thing", "lazy", "output"); + + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "assert.AssertTrue(object.thing.x == 5);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestModulesOnlyGetEvaledOnce) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->RegisterNativeHandler( + "counter", + scoped_ptr<NativeHandler>(new CounterNatives(env()->context()))); + + env()->RegisterModule("incrementsWhenEvaled", + "requireNative('counter').Increment();"); + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "var counter = requireNative('counter');" + "assert.AssertTrue(counter.Get() == 0);" + "require('incrementsWhenEvaled');" + "assert.AssertTrue(counter.Get() == 1);" + "require('incrementsWhenEvaled');" + "assert.AssertTrue(counter.Get() == 1);"); + + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestOverrideNativeHandler) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->OverrideNativeHandler("assert", + "exports.$set('AssertTrue', function() {});"); + env()->RegisterModule("test", "requireNative('assert').AssertTrue(true);"); + ExpectNoAssertionsMade(); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestOverrideNonExistentNativeHandler) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->OverrideNativeHandler("thing", "exports.$set('x', 5);"); + env()->RegisterModule("test", + "var assert = requireNative('assert');" + "assert.AssertTrue(requireNative('thing').x == 5);"); + env()->module_system()->Require("test"); +} + +TEST_F(ModuleSystemTest, TestRequireAsync) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("add", + "define('add', [], function() {" + " return { Add: function(x, y) { return x + y; } };" + "});"); + env()->RegisterModule("math", + "define('math', ['add'], function(add) {" + " return { Add: add.Add };" + "});"); + env()->RegisterModule( + "test", + "requireAsync('math').then(function(math) {" + " requireNative('assert').AssertTrue(math.Add(3, 5) == 8);" + "});"); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncInParallel) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("add", + "define('add', [], function() {" + " return { Add: function(x, y) { return x + y; } };" + "});"); + env()->RegisterModule( + "subtract", + "define('subtract', [], function() {" + " return { Subtract: function(x, y) { return x - y; } };" + "});"); + env()->RegisterModule( + "math", + "exports.$set('AddAndSubtract', function(x, y, z) {" + " return Promise.all([requireAsync('add')," + " requireAsync('subtract')" + " ]).then(function(modules) {" + " return modules[1].Subtract(modules[0].Add(x, y), z);" + " });" + "});"); + env()->RegisterModule("test", + "var AddAndSubtract = require('math').AddAndSubtract;" + "AddAndSubtract(3, 5, 2).then(function(result) {" + " requireNative('assert').AssertTrue(result == 6);" + "});"); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestNestedRequireAsyncs) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("first", + "define('first', [], function() {" + " return { next: 'second' };" + "});"); + env()->RegisterModule("second", + "define('second', [], function() {" + " return { next: '' };" + "});"); + env()->RegisterModule( + "test", + "requireAsync('first').then(function(module) {" + " return requireAsync(module.next)" + "}).then(function(module) {" + " requireNative('assert').AssertTrue(module.next === '');" + "});"); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireFromAMDModule) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "add", "exports.$set('Add', function(x, y) { return x + y; });"); + env()->RegisterModule("math", + "define('math', [], function() {" + " var add = require('add');" + " return { Add: add.Add };" + "});"); + env()->RegisterModule( + "test", + "requireAsync('math').then(function(math) {" + " requireNative('assert').AssertTrue(math.Add(3, 5) == 8);" + "});"); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncFromAMDModule) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("add", + "define('add', [], function() {" + " return { Add: function(x, y) { return x + y; } };" + "});"); + env()->RegisterModule("math", + "define('math', [], function() {" + " function Add(x, y) {" + " return requireAsync('add').then(function(add) {" + " return add.Add(x, y);" + " });" + " }" + " return { Add: Add };" + "});"); + env()->RegisterModule("test", + "requireAsync('math').then(function(math) {" + " return math.Add(3, 6);" + "}).then(function(result) {" + " requireNative('assert').AssertTrue(result == 9);" + "});"); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncFromAnotherContext) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "requireAsync('natives').then(function(natives) {" + " natives.requireAsync('ping').then(function(ping) {" + " return ping();" + " }).then(function(result) {" + " requireNative('assert').AssertTrue(result == 'pong');" + " });" + "});"); + scoped_ptr<ModuleSystemTestEnvironment> other_env = CreateEnvironment(); + other_env->RegisterModule("ping", + "define('ping', ['natives'], function(natives) {" + " return function() {" + " return 'pong';" + " }" + "});"); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + other_env->module_system()->NewInstance()); + gin::ModuleRegistry::From(other_env->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + env()->module_system()->NewInstance()); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncBetweenContexts) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("pong", + "define('pong', [], function() {" + " return function() { return 'done'; };" + "});"); + env()->RegisterModule( + "test", + "requireAsync('natives').then(function(natives) {" + " natives.requireAsync('ping').then(function(ping) {" + " return ping();" + " }).then(function(pong) {" + " return pong();" + " }).then(function(result) {" + " requireNative('assert').AssertTrue(result == 'done');" + " });" + "});"); + scoped_ptr<ModuleSystemTestEnvironment> other_env = CreateEnvironment(); + other_env->RegisterModule("ping", + "define('ping', ['natives'], function(natives) {" + " return function() {" + " return natives.requireAsync('pong');" + " }" + "});"); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + other_env->module_system()->NewInstance()); + gin::ModuleRegistry::From(other_env->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + env()->module_system()->NewInstance()); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncFromContextWithNoModuleRegistry) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "requireAsync('natives').then(function(natives) {" + " var AssertTrue = requireNative('assert').AssertTrue;" + " natives.requireAsync('foo').then(function() {" + " AssertTrue(false);" + " }).catch(function(error) {" + " AssertTrue(error.message == " + " 'Extension view no longer exists');" + " });" + "});"); + scoped_ptr<ModuleSystemTestEnvironment> other_env = CreateEnvironment(); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + other_env->module_system()->NewInstance()); + other_env->ShutdownGin(); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestRequireAsyncFromContextWithNoModuleSystem) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "requireAsync('natives').then(function(natives) {" + " requireNative('assert').AssertTrue(" + " natives.requireAsync('foo') === undefined);" + "});"); + scoped_ptr<ModuleSystemTestEnvironment> other_env = CreateEnvironment(); + gin::ModuleRegistry::From(env()->context()->v8_context()) + ->AddBuiltinModule( + env()->isolate(), "natives", + other_env->module_system()->NewInstance()); + other_env->ShutdownModuleSystem(); + env()->module_system()->Require("test"); + RunResolvedPromises(); +} + +TEST_F(ModuleSystemTest, TestPrivatesIsPrivate) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var v = privates({});" + "requireNative('assert').AssertFalse(v instanceof Object);"); + env()->module_system()->Require("test"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/mojo/keep_alive_client_unittest.cc b/chromium/extensions/renderer/mojo/keep_alive_client_unittest.cc new file mode 100644 index 00000000000..022d2d2ee54 --- /dev/null +++ b/chromium/extensions/renderer/mojo/keep_alive_client_unittest.cc @@ -0,0 +1,100 @@ +// 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 <utility> + +#include "base/macros.h" +#include "extensions/common/mojo/keep_alive.mojom.h" +#include "extensions/renderer/api_test_base.h" +#include "grit/extensions_renderer_resources.h" +#include "mojo/public/cpp/bindings/strong_binding.h" + +// A test launcher for tests for the stash client defined in +// extensions/test/data/keep_alive_client_unittest.js. + +namespace extensions { +namespace { + +// A KeepAlive implementation that calls provided callbacks on creation and +// destruction. +class TestKeepAlive : public KeepAlive { + public: + TestKeepAlive(const base::Closure& on_destruction, + mojo::InterfaceRequest<KeepAlive> keep_alive) + : on_destruction_(on_destruction), + binding_(this, std::move(keep_alive)) {} + + ~TestKeepAlive() override { on_destruction_.Run(); } + + static void Create(const base::Closure& on_creation, + const base::Closure& on_destruction, + mojo::InterfaceRequest<KeepAlive> keep_alive) { + new TestKeepAlive(on_destruction, std::move(keep_alive)); + on_creation.Run(); + } + + private: + const base::Closure on_destruction_; + mojo::StrongBinding<KeepAlive> binding_; +}; + +} // namespace + +class KeepAliveClientTest : public ApiTestBase { + public: + KeepAliveClientTest() {} + + void SetUp() override { + ApiTestBase::SetUp(); + service_provider()->AddService( + base::Bind(&TestKeepAlive::Create, + base::Bind(&KeepAliveClientTest::KeepAliveCreated, + base::Unretained(this)), + base::Bind(&KeepAliveClientTest::KeepAliveDestroyed, + base::Unretained(this)))); + created_keep_alive_ = false; + destroyed_keep_alive_ = false; + } + + void WaitForKeepAlive() { + // Wait for a keep-alive to be created and destroyed. + while (!created_keep_alive_ || !destroyed_keep_alive_) { + base::RunLoop run_loop; + stop_run_loop_ = run_loop.QuitClosure(); + run_loop.Run(); + } + EXPECT_TRUE(created_keep_alive_); + EXPECT_TRUE(destroyed_keep_alive_); + } + + private: + void KeepAliveCreated() { + created_keep_alive_ = true; + if (!stop_run_loop_.is_null()) + stop_run_loop_.Run(); + } + void KeepAliveDestroyed() { + destroyed_keep_alive_ = true; + if (!stop_run_loop_.is_null()) + stop_run_loop_.Run(); + } + + bool created_keep_alive_; + bool destroyed_keep_alive_; + base::Closure stop_run_loop_; + + DISALLOW_COPY_AND_ASSIGN(KeepAliveClientTest); +}; + +TEST_F(KeepAliveClientTest, KeepAliveWithSuccessfulCall) { + RunTest("keep_alive_client_unittest.js", "testKeepAliveWithSuccessfulCall"); + WaitForKeepAlive(); +} + +TEST_F(KeepAliveClientTest, KeepAliveWithError) { + RunTest("keep_alive_client_unittest.js", "testKeepAliveWithError"); + WaitForKeepAlive(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/mojo/stash_client_unittest.cc b/chromium/extensions/renderer/mojo/stash_client_unittest.cc new file mode 100644 index 00000000000..3f361cc2535 --- /dev/null +++ b/chromium/extensions/renderer/mojo/stash_client_unittest.cc @@ -0,0 +1,55 @@ +// Copyright 2015 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 <vector> + +#include "base/macros.h" +#include "extensions/browser/mojo/stash_backend.h" +#include "extensions/common/mojo/stash.mojom.h" +#include "extensions/renderer/api_test_base.h" +#include "gin/dictionary.h" +#include "grit/extensions_renderer_resources.h" +#include "mojo/public/cpp/bindings/lib/message_builder.h" + +// A test launcher for tests for the stash client defined in +// extensions/test/data/stash_client_unittest.js. + +namespace extensions { +class StashClientTest : public ApiTestBase { + public: + StashClientTest() {} + + void SetUp() override { + ApiTestBase::SetUp(); + stash_backend_.reset(new StashBackend(base::Closure())); + PrepareEnvironment(api_test_env()); + } + + void PrepareEnvironment(ApiTestEnvironment* env) { + env->service_provider()->AddService(base::Bind( + &StashBackend::BindToRequest, base::Unretained(stash_backend_.get()))); + } + + scoped_ptr<StashBackend> stash_backend_; + + private: + DISALLOW_COPY_AND_ASSIGN(StashClientTest); +}; + +// https://crbug.com/599898 +#if defined(LEAK_SANITIZER) +#define MAYBE_StashAndRestore DISABLED_StashAndRestore +#else +#define MAYBE_StashAndRestore StashAndRestore +#endif +// Test that stashing and restoring work correctly. +TEST_F(StashClientTest, MAYBE_StashAndRestore) { + ASSERT_NO_FATAL_FAILURE(RunTest("stash_client_unittest.js", "testStash")); + scoped_ptr<ModuleSystemTestEnvironment> restore_test_env(CreateEnvironment()); + ApiTestEnvironment restore_environment(restore_test_env.get()); + PrepareEnvironment(&restore_environment); + restore_environment.RunTest("stash_client_unittest.js", "testRetrieve"); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/native_handler.cc b/chromium/extensions/renderer/native_handler.cc new file mode 100644 index 00000000000..a8b343ad589 --- /dev/null +++ b/chromium/extensions/renderer/native_handler.cc @@ -0,0 +1,22 @@ +// 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/renderer/native_handler.h" + +#include "base/logging.h" + +namespace extensions { + +NativeHandler::NativeHandler() : is_valid_(true) {} + +NativeHandler::~NativeHandler() { + CHECK(!is_valid_) << "NativeHandlers must be invalidated before destruction"; +} + +void NativeHandler::Invalidate() { + CHECK(is_valid_); + is_valid_ = false; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/native_handler.h b/chromium/extensions/renderer/native_handler.h new file mode 100644 index 00000000000..5b3d3f73f5e --- /dev/null +++ b/chromium/extensions/renderer/native_handler.h @@ -0,0 +1,49 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_NATIVE_HANDLER_H_ + +#include "base/macros.h" +#include "v8/include/v8.h" + +namespace extensions { + +// NativeHandlers are intended to be used with a ModuleSystem. The ModuleSystem +// will assume ownership of the NativeHandler, and as a ModuleSystem is tied to +// a single v8::Context, this implies that NativeHandlers will also be tied to +// a single v8::Context. +// TODO(koz): Rename this to NativeJavaScriptModule. +class NativeHandler { + public: + NativeHandler(); + virtual ~NativeHandler(); + + // Create a new instance of the object this handler specifies. + virtual v8::Local<v8::Object> NewInstance() = 0; + + // Invalidate this object so it cannot be used any more. This is needed + // because it's possible for this to outlive its owner context. Invalidate + // must be called before this happens. + // + // Subclasses should override to invalidate their own V8 state. If they do + // they must call their superclass' Invalidate(). + // + // Invalidate() will be called on destruction, if it hasn't already been. + // Subclasses don't need to do it themselves. + virtual void Invalidate(); + + protected: + // Allow subclasses to query valid state. + bool is_valid() { return is_valid_; } + + private: + bool is_valid_; + + DISALLOW_COPY_AND_ASSIGN(NativeHandler); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/object_backed_native_handler.cc b/chromium/extensions/renderer/object_backed_native_handler.cc new file mode 100644 index 00000000000..1ffc355cc48 --- /dev/null +++ b/chromium/extensions/renderer/object_backed_native_handler.cc @@ -0,0 +1,222 @@ +// 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/renderer/object_backed_native_handler.h" + +#include <stddef.h> + +#include "base/logging.h" +#include "base/memory/linked_ptr.h" +#include "content/public/child/worker_thread.h" +#include "extensions/common/extension_api.h" +#include "extensions/renderer/console.h" +#include "extensions/renderer/module_system.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "extensions/renderer/v8_helpers.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "v8/include/v8.h" + +namespace extensions { + +namespace { +// Key for the base::Bound routed function. +const char* kHandlerFunction = "handler_function"; +const char* kFeatureName = "feature_name"; +} // namespace + +ObjectBackedNativeHandler::ObjectBackedNativeHandler(ScriptContext* context) + : router_data_(context->isolate()), + context_(context), + object_template_(context->isolate(), + v8::ObjectTemplate::New(context->isolate())) { +} + +ObjectBackedNativeHandler::~ObjectBackedNativeHandler() { +} + +v8::Local<v8::Object> ObjectBackedNativeHandler::NewInstance() { + return v8::Local<v8::ObjectTemplate>::New(GetIsolate(), object_template_) + ->NewInstance(); +} + +// static +void ObjectBackedNativeHandler::Router( + const v8::FunctionCallbackInfo<v8::Value>& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Object> data = args.Data().As<v8::Object>(); + v8::Local<v8::Context> context = isolate->GetCurrentContext(); + + v8::Local<v8::Value> handler_function_value; + v8::Local<v8::Value> feature_name_value; + // See comment in header file for why we do this. + if (!GetPrivate(context, data, kHandlerFunction, &handler_function_value) || + handler_function_value->IsUndefined() || + !GetPrivate(context, data, kFeatureName, &feature_name_value) || + !feature_name_value->IsString()) { + ScriptContext* script_context = + ScriptContextSet::GetContextByV8Context(context); + console::Error(script_context ? script_context->GetRenderFrame() : nullptr, + "Extension view no longer exists"); + return; + } + + // We can't access the ScriptContextSet on a worker thread. Luckily, we also + // don't inject many bindings into worker threads. + // TODO(devlin): Figure out a way around this. + if (content::WorkerThread::GetCurrentId() == 0) { + ScriptContext* script_context = + ScriptContextSet::GetContextByV8Context(context); + v8::Local<v8::String> feature_name_string = + feature_name_value->ToString(context).ToLocalChecked(); + std::string feature_name = *v8::String::Utf8Value(feature_name_string); + // TODO(devlin): Eventually, we should fail if either script_context is null + // or feature_name is empty. + if (script_context && + !feature_name.empty() && + !script_context->GetAvailability(feature_name).is_available()) { + return; + } + } + // This CHECK is *important*. Otherwise, we'll go around happily executing + // something random. See crbug.com/548273. + CHECK(handler_function_value->IsExternal()); + static_cast<HandlerFunction*>( + handler_function_value.As<v8::External>()->Value())->Run(args); + + // Verify that the return value, if any, is accessible by the context. + v8::ReturnValue<v8::Value> ret = args.GetReturnValue(); + v8::Local<v8::Value> ret_value = ret.Get(); + if (ret_value->IsObject() && !ret_value->IsNull() && + !ContextCanAccessObject(context, v8::Local<v8::Object>::Cast(ret_value), + true)) { + NOTREACHED() << "Insecure return value"; + ret.SetUndefined(); + } +} + +void ObjectBackedNativeHandler::RouteFunction( + const std::string& name, + const HandlerFunction& handler_function) { + RouteFunction(name, "", handler_function); +} + +void ObjectBackedNativeHandler::RouteFunction( + const std::string& name, + const std::string& feature_name, + const HandlerFunction& handler_function) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context_->v8_context()); + + v8::Local<v8::Object> data = v8::Object::New(isolate); + SetPrivate(data, kHandlerFunction, + v8::External::New(isolate, new HandlerFunction(handler_function))); + DCHECK(feature_name.empty() || + ExtensionAPI::GetSharedInstance()->GetFeatureDependency(feature_name)) + << feature_name; + SetPrivate(data, kFeatureName, + v8_helpers::ToV8StringUnsafe(isolate, feature_name)); + v8::Local<v8::FunctionTemplate> function_template = + v8::FunctionTemplate::New(isolate, Router, data); + v8::Local<v8::ObjectTemplate>::New(isolate, object_template_) + ->Set(isolate, name.c_str(), function_template); + router_data_.Append(data); +} + +v8::Isolate* ObjectBackedNativeHandler::GetIsolate() const { + return context_->isolate(); +} + +void ObjectBackedNativeHandler::Invalidate() { + v8::Isolate* isolate = GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context_->v8_context()); + + for (size_t i = 0; i < router_data_.Size(); i++) { + v8::Local<v8::Object> data = router_data_.Get(i); + v8::Local<v8::Value> handler_function_value; + CHECK(GetPrivate(data, kHandlerFunction, &handler_function_value)); + delete static_cast<HandlerFunction*>( + handler_function_value.As<v8::External>()->Value()); + DeletePrivate(data, kHandlerFunction); + } + + router_data_.Clear(); + object_template_.Reset(); + + NativeHandler::Invalidate(); +} + +// static +bool ObjectBackedNativeHandler::ContextCanAccessObject( + const v8::Local<v8::Context>& context, + const v8::Local<v8::Object>& object, + bool allow_null_context) { + if (object->IsNull()) + return true; + if (context == object->CreationContext()) + return true; + ScriptContext* other_script_context = + ScriptContextSet::GetContextByObject(object); + if (!other_script_context || !other_script_context->web_frame()) + return allow_null_context; + + return blink::WebFrame::scriptCanAccess(other_script_context->web_frame()); +} + +void ObjectBackedNativeHandler::SetPrivate(v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value> value) { + SetPrivate(context_->v8_context(), obj, key, value); +} + +// static +void ObjectBackedNativeHandler::SetPrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value> value) { + obj->SetPrivate(context, v8::Private::ForApi(context->GetIsolate(), + v8::String::NewFromUtf8( + context->GetIsolate(), key)), + value) + .FromJust(); +} + +bool ObjectBackedNativeHandler::GetPrivate(v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value>* result) { + return GetPrivate(context_->v8_context(), obj, key, result); +} + +// static +bool ObjectBackedNativeHandler::GetPrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value>* result) { + return obj->GetPrivate(context, + v8::Private::ForApi(context->GetIsolate(), + v8::String::NewFromUtf8( + context->GetIsolate(), key))) + .ToLocal(result); +} + +void ObjectBackedNativeHandler::DeletePrivate(v8::Local<v8::Object> obj, + const char* key) { + DeletePrivate(context_->v8_context(), obj, key); +} + +// static +void ObjectBackedNativeHandler::DeletePrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key) { + obj->DeletePrivate(context, + v8::Private::ForApi( + context->GetIsolate(), + v8::String::NewFromUtf8(context->GetIsolate(), key))) + .FromJust(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/object_backed_native_handler.h b/chromium/extensions/renderer/object_backed_native_handler.h new file mode 100644 index 00000000000..974abe18600 --- /dev/null +++ b/chromium/extensions/renderer/object_backed_native_handler.h @@ -0,0 +1,123 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_OBJECT_BACKED_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_OBJECT_BACKED_NATIVE_HANDLER_H_ + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/macros.h" +#include "base/memory/linked_ptr.h" +#include "extensions/renderer/native_handler.h" +#include "v8/include/v8-util.h" +#include "v8/include/v8.h" + +namespace extensions { +class ScriptContext; + +// An ObjectBackedNativeHandler is a factory for JS objects with functions on +// them that map to native C++ functions. Subclasses should call RouteFunction() +// in their constructor to define functions on the created JS objects. +class ObjectBackedNativeHandler : public NativeHandler { + public: + explicit ObjectBackedNativeHandler(ScriptContext* context); + ~ObjectBackedNativeHandler() override; + + // Create an object with bindings to the native functions defined through + // RouteFunction(). + v8::Local<v8::Object> NewInstance() override; + + v8::Isolate* GetIsolate() const; + + protected: + typedef base::Callback<void(const v8::FunctionCallbackInfo<v8::Value>&)> + HandlerFunction; + + // Installs a new 'route' from |name| to |handler_function|. This means that + // NewInstance()s of this ObjectBackedNativeHandler will have a property + // |name| which will be handled by |handler_function|. + // + // Routed functions are destroyed along with the destruction of this class, + // and are never called back into, therefore it's safe for |handler_function| + // to bind to base::Unretained. + // + // |feature_name| corresponds to the api feature the native handler is used + // for. If the associated ScriptContext does not have access to that feature, + // the |handler_function| is not invoked. + // TODO(devlin): Deprecate the version that doesn't take a |feature_name|. + void RouteFunction(const std::string& name, + const HandlerFunction& handler_function); + void RouteFunction(const std::string& name, + const std::string& feature_name, + const HandlerFunction& handler_function); + + ScriptContext* context() const { return context_; } + + void Invalidate() override; + + // Returns true if the given |context| is allowed to access the given + // |object|. This should be checked before returning any objects from another + // context. + // |allow_null_context| indicates that if there is no ScriptContext associated + // with the |object|, it should be allowed. + // TODO(devlin): It'd be nice to track down when when there's no ScriptContext + // and remove |allow_null_context|. + static bool ContextCanAccessObject(const v8::Local<v8::Context>& context, + const v8::Local<v8::Object>& object, + bool allow_null_context); + + // The following methods are convenience wrappers for methods on v8::Object + // with the corresponding names. + void SetPrivate(v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value> value); + static void SetPrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value> value); + bool GetPrivate(v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value>* result); + static bool GetPrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key, + v8::Local<v8::Value>* result); + void DeletePrivate(v8::Local<v8::Object> obj, const char* key); + static void DeletePrivate(v8::Local<v8::Context> context, + v8::Local<v8::Object> obj, + const char* key); + + private: + // Callback for RouteFunction which routes the V8 call to the correct + // base::Bound callback. + static void Router(const v8::FunctionCallbackInfo<v8::Value>& args); + + // When RouteFunction is called we create a v8::Object to hold the data we + // need when handling it in Router() - this is the base::Bound function to + // route to. + // + // We need a v8::Object because it's possible for v8 to outlive the + // base::Bound function; the lifetime of an ObjectBackedNativeHandler is the + // lifetime of webkit's involvement with it, not the life of the v8 context. + // A scenario when v8 will outlive us is if a frame holds onto the + // contentWindow of an iframe after it's removed. + // + // So, we use v8::Objects here to hold that data, effectively refcounting + // the data. When |this| is destroyed we remove the base::Bound function from + // the object to indicate that it shoudn't be called. + typedef v8::PersistentValueVector<v8::Object> RouterData; + RouterData router_data_; + + ScriptContext* context_; + + v8::Global<v8::ObjectTemplate> object_template_; + + DISALLOW_COPY_AND_ASSIGN(ObjectBackedNativeHandler); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_OBJECT_BACKED_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/print_native_handler.cc b/chromium/extensions/renderer/print_native_handler.cc new file mode 100644 index 00000000000..b6ee63a7e59 --- /dev/null +++ b/chromium/extensions/renderer/print_native_handler.cc @@ -0,0 +1,32 @@ +// 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/renderer/print_native_handler.h" + +#include <string> + +#include "base/bind.h" +#include "base/strings/string_util.h" + +namespace extensions { + +PrintNativeHandler::PrintNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("Print", + base::Bind(&PrintNativeHandler::Print, base::Unretained(this))); +} + +void PrintNativeHandler::Print( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() < 1) + return; + + std::vector<std::string> components; + for (int i = 0; i < args.Length(); ++i) + components.push_back(*v8::String::Utf8Value(args[i])); + + LOG(ERROR) << base::JoinString(components, ","); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/print_native_handler.h b/chromium/extensions/renderer/print_native_handler.h new file mode 100644 index 00000000000..b2e5d375d7f --- /dev/null +++ b/chromium/extensions/renderer/print_native_handler.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_PRINT_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_PRINT_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class PrintNativeHandler : public ObjectBackedNativeHandler { + public: + explicit PrintNativeHandler(ScriptContext* context); + + void Print(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_PRINT_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/process_info_native_handler.cc b/chromium/extensions/renderer/process_info_native_handler.cc new file mode 100644 index 00000000000..545a21274dc --- /dev/null +++ b/chromium/extensions/renderer/process_info_native_handler.cc @@ -0,0 +1,98 @@ +// 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/renderer/process_info_native_handler.h" + +#include <stdint.h> + +#include "base/bind.h" +#include "base/command_line.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +ProcessInfoNativeHandler::ProcessInfoNativeHandler( + ScriptContext* context, + const std::string& extension_id, + const std::string& context_type, + bool is_incognito_context, + bool is_component_extension, + int manifest_version, + bool send_request_disabled) + : ObjectBackedNativeHandler(context), + extension_id_(extension_id), + context_type_(context_type), + is_incognito_context_(is_incognito_context), + is_component_extension_(is_component_extension), + manifest_version_(manifest_version), + send_request_disabled_(send_request_disabled) { + RouteFunction("GetExtensionId", + base::Bind(&ProcessInfoNativeHandler::GetExtensionId, + base::Unretained(this))); + RouteFunction("GetContextType", + base::Bind(&ProcessInfoNativeHandler::GetContextType, + base::Unretained(this))); + RouteFunction("InIncognitoContext", + base::Bind(&ProcessInfoNativeHandler::InIncognitoContext, + base::Unretained(this))); + RouteFunction("IsComponentExtension", + base::Bind(&ProcessInfoNativeHandler::IsComponentExtension, + base::Unretained(this))); + RouteFunction("GetManifestVersion", + base::Bind(&ProcessInfoNativeHandler::GetManifestVersion, + base::Unretained(this))); + RouteFunction("IsSendRequestDisabled", + base::Bind(&ProcessInfoNativeHandler::IsSendRequestDisabled, + base::Unretained(this))); + RouteFunction( + "HasSwitch", + base::Bind(&ProcessInfoNativeHandler::HasSwitch, base::Unretained(this))); +} + +void ProcessInfoNativeHandler::GetExtensionId( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set( + v8::String::NewFromUtf8(args.GetIsolate(), extension_id_.c_str())); +} + +void ProcessInfoNativeHandler::GetContextType( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set( + v8::String::NewFromUtf8(args.GetIsolate(), context_type_.c_str())); +} + +void ProcessInfoNativeHandler::InIncognitoContext( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(is_incognito_context_); +} + +void ProcessInfoNativeHandler::IsComponentExtension( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(is_component_extension_); +} + +void ProcessInfoNativeHandler::GetManifestVersion( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(static_cast<int32_t>(manifest_version_)); +} + +void ProcessInfoNativeHandler::IsSendRequestDisabled( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (send_request_disabled_) { + args.GetReturnValue().Set(v8::String::NewFromUtf8( + args.GetIsolate(), + "sendRequest and onRequest are obsolete." + " Please use sendMessage and onMessage instead.")); + } +} + +void ProcessInfoNativeHandler::HasSwitch( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(args.Length() == 1 && args[0]->IsString()); + bool has_switch = base::CommandLine::ForCurrentProcess()->HasSwitch( + *v8::String::Utf8Value(args[0])); + args.GetReturnValue().Set(v8::Boolean::New(args.GetIsolate(), has_switch)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/process_info_native_handler.h b/chromium/extensions/renderer/process_info_native_handler.h new file mode 100644 index 00000000000..dcde4300807 --- /dev/null +++ b/chromium/extensions/renderer/process_info_native_handler.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_PROCESS_INFO_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_PROCESS_INFO_NATIVE_HANDLER_H_ + +#include <string> + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class ProcessInfoNativeHandler : public ObjectBackedNativeHandler { + public: + ProcessInfoNativeHandler(ScriptContext* context, + const std::string& extension_id, + const std::string& context_type, + bool is_incognito_context, + bool is_component_extension, + int manifest_version, + bool send_request_disabled); + + private: + void GetExtensionId(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetContextType(const v8::FunctionCallbackInfo<v8::Value>& args); + void InIncognitoContext(const v8::FunctionCallbackInfo<v8::Value>& args); + void IsComponentExtension(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetManifestVersion(const v8::FunctionCallbackInfo<v8::Value>& args); + void IsSendRequestDisabled(const v8::FunctionCallbackInfo<v8::Value>& args); + void HasSwitch(const v8::FunctionCallbackInfo<v8::Value>& args); + + std::string extension_id_; + std::string context_type_; + bool is_incognito_context_; + bool is_component_extension_; + int manifest_version_; + bool send_request_disabled_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_PROCESS_INFO_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/programmatic_script_injector.cc b/chromium/extensions/renderer/programmatic_script_injector.cc new file mode 100644 index 00000000000..eea394b0aa8 --- /dev/null +++ b/chromium/extensions/renderer/programmatic_script_injector.cc @@ -0,0 +1,185 @@ +// 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/renderer/programmatic_script_injector.h" + +#include <utility> +#include <vector> + +#include "base/values.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/error_utils.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/manifest_constants.h" +#include "extensions/common/permissions/api_permission.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/renderer/injection_host.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScriptSource.h" + +namespace extensions { + +ProgrammaticScriptInjector::ProgrammaticScriptInjector( + const ExtensionMsg_ExecuteCode_Params& params, + content::RenderFrame* render_frame) + : params_(new ExtensionMsg_ExecuteCode_Params(params)), + url_( + ScriptContext::GetDataSourceURLForFrame(render_frame->GetWebFrame())), + finished_(false) { + if (url_.SchemeIs(url::kAboutScheme)) { + origin_for_about_error_ = + render_frame->GetWebFrame()->getSecurityOrigin().toString().utf8(); + } +} + +ProgrammaticScriptInjector::~ProgrammaticScriptInjector() { +} + +UserScript::InjectionType ProgrammaticScriptInjector::script_type() + const { + return UserScript::PROGRAMMATIC_SCRIPT; +} + +bool ProgrammaticScriptInjector::ShouldExecuteInMainWorld() const { + return params_->in_main_world; +} + +bool ProgrammaticScriptInjector::IsUserGesture() const { + return params_->user_gesture; +} + +bool ProgrammaticScriptInjector::ExpectsResults() const { + return params_->wants_result; +} + +bool ProgrammaticScriptInjector::ShouldInjectJs( + UserScript::RunLocation run_location) const { + return GetRunLocation() == run_location && params_->is_javascript; +} + +bool ProgrammaticScriptInjector::ShouldInjectCss( + UserScript::RunLocation run_location) const { + return GetRunLocation() == run_location && !params_->is_javascript; +} + +PermissionsData::AccessType ProgrammaticScriptInjector::CanExecuteOnFrame( + const InjectionHost* injection_host, + blink::WebLocalFrame* frame, + int tab_id) const { + GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL( + frame, frame->document().url(), params_->match_about_blank); + if (params_->is_web_view) { + if (frame->parent()) { + // This is a subframe inside <webview>, so allow it. + return PermissionsData::ACCESS_ALLOWED; + } + + return effective_document_url == params_->webview_src + ? PermissionsData::ACCESS_ALLOWED + : PermissionsData::ACCESS_DENIED; + } + DCHECK_EQ(injection_host->id().type(), HostID::EXTENSIONS); + + return injection_host->CanExecuteOnFrame( + effective_document_url, + content::RenderFrame::FromWebFrame(frame), + tab_id, + true /* is_declarative */); +} + +std::vector<blink::WebScriptSource> ProgrammaticScriptInjector::GetJsSources( + UserScript::RunLocation run_location) const { + DCHECK_EQ(GetRunLocation(), run_location); + DCHECK(params_->is_javascript); + + return std::vector<blink::WebScriptSource>( + 1, + blink::WebScriptSource( + blink::WebString::fromUTF8(params_->code), params_->file_url)); +} + +std::vector<std::string> ProgrammaticScriptInjector::GetCssSources( + UserScript::RunLocation run_location) const { + DCHECK_EQ(GetRunLocation(), run_location); + DCHECK(!params_->is_javascript); + + return std::vector<std::string>(1, params_->code); +} + +void ProgrammaticScriptInjector::GetRunInfo( + ScriptsRunInfo* scripts_run_info, + UserScript::RunLocation run_location) const { +} + +void ProgrammaticScriptInjector::OnInjectionComplete( + scoped_ptr<base::Value> execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) { + DCHECK(results_.empty()); + if (execution_result) + results_.Append(std::move(execution_result)); + Finish(std::string(), render_frame); +} + +void ProgrammaticScriptInjector::OnWillNotInject( + InjectFailureReason reason, + content::RenderFrame* render_frame) { + std::string error; + switch (reason) { + case NOT_ALLOWED: + if (!CanShowUrlInError()) { + error = manifest_errors::kCannotAccessPage; + } else if (!origin_for_about_error_.empty()) { + error = ErrorUtils::FormatErrorMessage( + manifest_errors::kCannotAccessAboutUrl, url_.spec(), + origin_for_about_error_); + } else { + error = ErrorUtils::FormatErrorMessage( + manifest_errors::kCannotAccessPageWithUrl, url_.spec()); + } + break; + case EXTENSION_REMOVED: // no special error here. + case WONT_INJECT: + break; + } + Finish(error, render_frame); +} + +bool ProgrammaticScriptInjector::CanShowUrlInError() const { + if (params_->host_id.type() != HostID::EXTENSIONS) + return false; + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(params_->host_id.id()); + if (!extension) + return false; + return extension->permissions_data()->active_permissions().HasAPIPermission( + APIPermission::kTab); +} + +UserScript::RunLocation ProgrammaticScriptInjector::GetRunLocation() const { + return static_cast<UserScript::RunLocation>(params_->run_at); +} + +void ProgrammaticScriptInjector::Finish(const std::string& error, + content::RenderFrame* render_frame) { + DCHECK(!finished_); + finished_ = true; + + // It's possible that the render frame was destroyed in the course of + // injecting scripts. Don't respond if it was (the browser side watches for + // frame deletions so nothing is left hanging). + if (render_frame) { + render_frame->Send( + new ExtensionHostMsg_ExecuteCodeFinished( + render_frame->GetRoutingID(), params_->request_id, + error, url_, results_)); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/programmatic_script_injector.h b/chromium/extensions/renderer/programmatic_script_injector.h new file mode 100644 index 00000000000..a7af9ff3818 --- /dev/null +++ b/chromium/extensions/renderer/programmatic_script_injector.h @@ -0,0 +1,84 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_PROGRAMMATIC_SCRIPT_INJECTOR_H_ +#define EXTENSIONS_RENDERER_PROGRAMMATIC_SCRIPT_INJECTOR_H_ + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "extensions/renderer/script_injection.h" +#include "url/gurl.h" + +struct ExtensionMsg_ExecuteCode_Params; + +namespace content { +class RenderFrame; +} + +namespace extensions { + +// A ScriptInjector to handle tabs.executeScript(). +class ProgrammaticScriptInjector : public ScriptInjector { + public: + ProgrammaticScriptInjector(const ExtensionMsg_ExecuteCode_Params& params, + content::RenderFrame* render_frame); + ~ProgrammaticScriptInjector() override; + + private: + // ScriptInjector implementation. + UserScript::InjectionType script_type() const override; + bool ShouldExecuteInMainWorld() const override; + bool IsUserGesture() const override; + bool ExpectsResults() const override; + bool ShouldInjectJs(UserScript::RunLocation run_location) const override; + bool ShouldInjectCss(UserScript::RunLocation run_location) const override; + PermissionsData::AccessType CanExecuteOnFrame( + const InjectionHost* injection_host, + blink::WebLocalFrame* web_frame, + int tab_id) const override; + std::vector<blink::WebScriptSource> GetJsSources( + UserScript::RunLocation run_location) const override; + std::vector<std::string> GetCssSources( + UserScript::RunLocation run_location) const override; + void GetRunInfo(ScriptsRunInfo* scripts_run_info, + UserScript::RunLocation run_location) const override; + void OnInjectionComplete(scoped_ptr<base::Value> execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) override; + void OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) override; + + // Whether it is safe to include information about the URL in error messages. + bool CanShowUrlInError() const; + + // Return the run location for this injector. + UserScript::RunLocation GetRunLocation() const; + + // Notify the browser that the script was injected (or never will be), and + // send along any results or errors. + void Finish(const std::string& error, content::RenderFrame* render_frame); + + // The parameters for injecting the script. + scoped_ptr<ExtensionMsg_ExecuteCode_Params> params_; + + // The url of the frame into which we are injecting. + GURL url_; + + // The serialization of the frame's origin if the frame is an about:-URL. This + // is used to provide user-friendly messages. + std::string origin_for_about_error_; + + // The results of the script execution. + base::ListValue results_; + + // Whether or not this script injection has finished. + bool finished_; + + DISALLOW_COPY_AND_ASSIGN(ProgrammaticScriptInjector); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_PROGRAMMATIC_SCRIPT_INJECTOR_H_ diff --git a/chromium/extensions/renderer/render_frame_observer_natives.cc b/chromium/extensions/renderer/render_frame_observer_natives.cc new file mode 100644 index 00000000000..7c507b37993 --- /dev/null +++ b/chromium/extensions/renderer/render_frame_observer_natives.cc @@ -0,0 +1,107 @@ +// 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/renderer/render_frame_observer_natives.h" + +#include "base/bind.h" +#include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +namespace { + +// Deletes itself when done. +class LoadWatcher : public content::RenderFrameObserver { + public: + LoadWatcher(content::RenderFrame* frame, + const base::Callback<void(bool)>& callback) + : content::RenderFrameObserver(frame), callback_(callback) {} + + void DidCreateDocumentElement() override { + // Defer the callback instead of running it now to avoid re-entrancy caused + // by the JavaScript callback. + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentStart(base::Bind(callback_, true)); + delete this; + } + + void DidFailProvisionalLoad(const blink::WebURLError& error) override { + // Use PostTask to avoid running user scripts while handling this + // DidFailProvisionalLoad notification. + base::MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(callback_, false)); + delete this; + } + + private: + base::Callback<void(bool)> callback_; + + DISALLOW_COPY_AND_ASSIGN(LoadWatcher); +}; + +} // namespace + +RenderFrameObserverNatives::RenderFrameObserverNatives(ScriptContext* context) + : ObjectBackedNativeHandler(context), weak_ptr_factory_(this) { + RouteFunction( + "OnDocumentElementCreated", + base::Bind(&RenderFrameObserverNatives::OnDocumentElementCreated, + base::Unretained(this))); +} + +RenderFrameObserverNatives::~RenderFrameObserverNatives() {} + +void RenderFrameObserverNatives::Invalidate() { + weak_ptr_factory_.InvalidateWeakPtrs(); + ObjectBackedNativeHandler::Invalidate(); +} + +void RenderFrameObserverNatives::OnDocumentElementCreated( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(args.Length() == 2); + CHECK(args[0]->IsInt32()); + CHECK(args[1]->IsFunction()); + + int frame_id = args[0]->Int32Value(); + + content::RenderFrame* frame = content::RenderFrame::FromRoutingID(frame_id); + if (!frame) { + LOG(WARNING) << "No render frame found to register LoadWatcher."; + return; + } + + v8::Global<v8::Function> v8_callback(context()->isolate(), + args[1].As<v8::Function>()); + base::Callback<void(bool)> callback( + base::Bind(&RenderFrameObserverNatives::InvokeCallback, + weak_ptr_factory_.GetWeakPtr(), base::Passed(&v8_callback))); + if (ExtensionFrameHelper::Get(frame)->did_create_current_document_element()) { + // If the document element is already created, then we can call the callback + // immediately (though use PostTask to ensure that the callback is called + // asynchronously). + base::MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(callback, true)); + } else { + new LoadWatcher(frame, callback); + } + + args.GetReturnValue().Set(true); +} + +void RenderFrameObserverNatives::InvokeCallback( + v8::Global<v8::Function> callback, + bool succeeded) { + v8::Isolate* isolate = context()->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Value> args[] = {v8::Boolean::New(isolate, succeeded)}; + context()->CallFunction(v8::Local<v8::Function>::New(isolate, callback), + arraysize(args), args); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/render_frame_observer_natives.h b/chromium/extensions/renderer/render_frame_observer_natives.h new file mode 100644 index 00000000000..4e81e3bc8b7 --- /dev/null +++ b/chromium/extensions/renderer/render_frame_observer_natives.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_RENDER_FRAME_OBSERVER_NATIVES_H_ +#define EXTENSIONS_RENDERER_RENDER_FRAME_OBSERVER_NATIVES_H_ + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +// Native functions for JS to run callbacks upon RenderFrame events. +class RenderFrameObserverNatives : public ObjectBackedNativeHandler { + public: + explicit RenderFrameObserverNatives(ScriptContext* context); + ~RenderFrameObserverNatives() override; + + private: + void Invalidate() override; + + // Runs a callback upon creation of new document element inside a render frame + // (document.documentElement). + void OnDocumentElementCreated( + const v8::FunctionCallbackInfo<v8::Value>& args); + + void InvokeCallback(v8::Global<v8::Function> callback, bool succeeded); + + base::WeakPtrFactory<RenderFrameObserverNatives> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(RenderFrameObserverNatives); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_RENDER_FRAME_OBSERVER_NATIVES_H_ diff --git a/chromium/extensions/renderer/renderer_extension_registry.cc b/chromium/extensions/renderer/renderer_extension_registry.cc new file mode 100644 index 00000000000..ddc3a1b89f3 --- /dev/null +++ b/chromium/extensions/renderer/renderer_extension_registry.cc @@ -0,0 +1,105 @@ +// Copyright 2015 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/renderer_extension_registry.h" + +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "content/public/renderer/render_thread.h" + +namespace extensions { + +namespace { + +base::LazyInstance<RendererExtensionRegistry> g_renderer_extension_registry = + LAZY_INSTANCE_INITIALIZER; + +} // namespace + +RendererExtensionRegistry::RendererExtensionRegistry() {} + +RendererExtensionRegistry::~RendererExtensionRegistry() {} + +// static +RendererExtensionRegistry* RendererExtensionRegistry::Get() { + return g_renderer_extension_registry.Pointer(); +} + +const ExtensionSet* RendererExtensionRegistry::GetMainThreadExtensionSet() + const { + // This can only be modified on the RenderThread, because + // GetMainThreadExtensionSet is inherently thread unsafe. + // Enforcing single-thread modification at least mitigates this. + // TODO(annekao): Remove this restriction once GetMainThreadExtensionSet is + // fixed. + DCHECK(content::RenderThread::Get()); + base::AutoLock lock(lock_); + return &extensions_; +} + +size_t RendererExtensionRegistry::size() const { + base::AutoLock lock(lock_); + return extensions_.size(); +} + +bool RendererExtensionRegistry::is_empty() const { + base::AutoLock lock(lock_); + return extensions_.is_empty(); +} + +bool RendererExtensionRegistry::Contains( + const std::string& extension_id) const { + base::AutoLock lock(lock_); + return extensions_.Contains(extension_id); +} + +bool RendererExtensionRegistry::Insert( + const scoped_refptr<const Extension>& extension) { + DCHECK(content::RenderThread::Get()); + base::AutoLock lock(lock_); + return extensions_.Insert(extension); +} + +bool RendererExtensionRegistry::Remove(const std::string& id) { + DCHECK(content::RenderThread::Get()); + base::AutoLock lock(lock_); + return extensions_.Remove(id); +} + +std::string RendererExtensionRegistry::GetExtensionOrAppIDByURL( + const GURL& url) const { + base::AutoLock lock(lock_); + return extensions_.GetExtensionOrAppIDByURL(url); +} + +const Extension* RendererExtensionRegistry::GetExtensionOrAppByURL( + const GURL& url) const { + base::AutoLock lock(lock_); + return extensions_.GetExtensionOrAppByURL(url); +} + +const Extension* RendererExtensionRegistry::GetHostedAppByURL( + const GURL& url) const { + base::AutoLock lock(lock_); + return extensions_.GetHostedAppByURL(url); +} + +const Extension* RendererExtensionRegistry::GetByID( + const std::string& id) const { + base::AutoLock lock(lock_); + return extensions_.GetByID(id); +} + +ExtensionIdSet RendererExtensionRegistry::GetIDs() const { + base::AutoLock lock(lock_); + return extensions_.GetIDs(); +} + +bool RendererExtensionRegistry::ExtensionBindingsAllowed( + const GURL& url) const { + base::AutoLock lock(lock_); + return extensions_.ExtensionBindingsAllowed(url); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/renderer_extension_registry.h b/chromium/extensions/renderer/renderer_extension_registry.h new file mode 100644 index 00000000000..97c24bbf535 --- /dev/null +++ b/chromium/extensions/renderer/renderer_extension_registry.h @@ -0,0 +1,62 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_RENDERER_EXTENSION_REGISTRY_H_ +#define EXTENSIONS_RENDERER_RENDERER_EXTENSION_REGISTRY_H_ + +#include <stddef.h> + +#include <string> + +#include "base/macros.h" +#include "base/synchronization/lock.h" +#include "extensions/common/extension_set.h" + +class GURL; + +namespace extensions { + +// Thread safe container for all loaded extensions in this process, +// essentially the renderer counterpart to ExtensionRegistry. +class RendererExtensionRegistry { + public: + RendererExtensionRegistry(); + ~RendererExtensionRegistry(); + + static RendererExtensionRegistry* Get(); + + // Returns the ExtensionSet that underlies this RenderExtensionRegistry. + // + // This is not thread-safe and must only be called on the RenderThread, but + // even so, it's not thread safe because other threads may decide to + // modify this. Don't persist a reference to this. + // + // TODO(annekao): remove or make thread-safe and callback-based. + const ExtensionSet* GetMainThreadExtensionSet() const; + + size_t size() const; + bool is_empty() const; + + // Forwards to the ExtensionSet methods by the same name. + bool Contains(const std::string& id) const; + bool Insert(const scoped_refptr<const Extension>& extension); + bool Remove(const std::string& id); + std::string GetExtensionOrAppIDByURL(const GURL& url) const; + const Extension* GetExtensionOrAppByURL(const GURL& url) const; + const Extension* GetHostedAppByURL(const GURL& url) const; + const Extension* GetByID(const std::string& id) const; + ExtensionIdSet GetIDs() const; + bool ExtensionBindingsAllowed(const GURL& url) const; + + private: + ExtensionSet extensions_; + + mutable base::Lock lock_; + + DISALLOW_COPY_AND_ASSIGN(RendererExtensionRegistry); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_RENDERER_EXTENSION_REGISTRY_H_ diff --git a/chromium/extensions/renderer/request_sender.cc b/chromium/extensions/renderer/request_sender.cc new file mode 100644 index 00000000000..6f3cf51a8f1 --- /dev/null +++ b/chromium/extensions/renderer/request_sender.cc @@ -0,0 +1,143 @@ +// 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/renderer/request_sender.h" + +#include "base/values.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebUserGestureIndicator.h" +#include "third_party/WebKit/public/web/WebUserGestureToken.h" + +namespace extensions { + +// Contains info relevant to a pending API request. +struct PendingRequest { + public: + PendingRequest(const std::string& name, + RequestSender::Source* source, + blink::WebUserGestureToken token) + : name(name), source(source), token(token) {} + + std::string name; + RequestSender::Source* source; + blink::WebUserGestureToken token; +}; + +RequestSender::ScopedTabID::ScopedTabID(RequestSender* request_sender, + int tab_id) + : request_sender_(request_sender), + tab_id_(tab_id), + previous_tab_id_(request_sender->source_tab_id_) { + request_sender_->source_tab_id_ = tab_id; +} + +RequestSender::ScopedTabID::~ScopedTabID() { + DCHECK_EQ(tab_id_, request_sender_->source_tab_id_); + request_sender_->source_tab_id_ = previous_tab_id_; +} + +RequestSender::RequestSender(Dispatcher* dispatcher) + : dispatcher_(dispatcher), source_tab_id_(-1) {} + +RequestSender::~RequestSender() {} + +void RequestSender::InsertRequest(int request_id, + PendingRequest* pending_request) { + DCHECK_EQ(0u, pending_requests_.count(request_id)); + pending_requests_[request_id].reset(pending_request); +} + +linked_ptr<PendingRequest> RequestSender::RemoveRequest(int request_id) { + PendingRequestMap::iterator i = pending_requests_.find(request_id); + if (i == pending_requests_.end()) + return linked_ptr<PendingRequest>(); + linked_ptr<PendingRequest> result = i->second; + pending_requests_.erase(i); + return result; +} + +int RequestSender::GetNextRequestId() const { + static int next_request_id = 0; + return next_request_id++; +} + +void RequestSender::StartRequest(Source* source, + const std::string& name, + int request_id, + bool has_callback, + bool for_io_thread, + base::ListValue* value_args) { + ScriptContext* context = source->GetContext(); + if (!context) + return; + + // Get the current RenderFrame so that we can send a routed IPC message from + // the correct source. + content::RenderFrame* render_frame = context->GetRenderFrame(); + if (!render_frame) + return; + + // TODO(koz): See if we can make this a CHECK. + if (!context->HasAccessOrThrowError(name)) + return; + + GURL source_url; + if (blink::WebLocalFrame* webframe = context->web_frame()) + source_url = webframe->document().url(); + + InsertRequest(request_id, new PendingRequest(name, source, + blink::WebUserGestureIndicator::currentUserGestureToken())); + + ExtensionHostMsg_Request_Params params; + params.name = name; + params.arguments.Swap(value_args); + params.extension_id = context->GetExtensionID(); + params.source_url = source_url; + params.source_tab_id = source_tab_id_; + params.request_id = request_id; + params.has_callback = has_callback; + params.user_gesture = + blink::WebUserGestureIndicator::isProcessingUserGesture(); + if (for_io_thread) { + render_frame->Send(new ExtensionHostMsg_RequestForIOThread( + render_frame->GetRoutingID(), params)); + } else { + render_frame->Send( + new ExtensionHostMsg_Request(render_frame->GetRoutingID(), params)); + } +} + +void RequestSender::HandleResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error) { + linked_ptr<PendingRequest> request = RemoveRequest(request_id); + + if (!request.get()) { + // This can happen if a context is destroyed while a request is in flight. + return; + } + + blink::WebScopedUserGesture gesture(request->token); + request->source->OnResponseReceived( + request->name, request_id, success, response, error); +} + +void RequestSender::InvalidateSource(Source* source) { + for (PendingRequestMap::iterator it = pending_requests_.begin(); + it != pending_requests_.end();) { + if (it->second->source == source) + pending_requests_.erase(it++); + else + ++it; + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/request_sender.h b/chromium/extensions/renderer/request_sender.h new file mode 100644 index 00000000000..245c0feb67e --- /dev/null +++ b/chromium/extensions/renderer/request_sender.h @@ -0,0 +1,107 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_REQUEST_SENDER_H_ +#define EXTENSIONS_RENDERER_REQUEST_SENDER_H_ + +#include <map> +#include <string> + +#include "base/macros.h" +#include "base/memory/linked_ptr.h" +#include "v8/include/v8.h" + +namespace base { +class ListValue; +} + +namespace extensions { +class Dispatcher; +class ScriptContext; + +struct PendingRequest; + +// Responsible for sending requests for named extension API functions to the +// extension host and routing the responses back to the caller. +class RequestSender { + public: + // Source represents a user of RequestSender. Every request is associated with + // a Source object, which will be notified when the corresponding response + // arrives. When a Source object is going away and there are pending requests, + // it should call InvalidateSource() to make sure no notifications are sent to + // it later. + class Source { + public: + virtual ~Source() {} + + virtual ScriptContext* GetContext() = 0; + virtual void OnResponseReceived(const std::string& name, + int request_id, + bool success, + const base::ListValue& response, + const std::string& error) = 0; + }; + + // Helper class to (re)set the |source_tab_id_| below. + class ScopedTabID { + public: + ScopedTabID(RequestSender* request_sender, int tab_id); + ~ScopedTabID(); + + private: + RequestSender* const request_sender_; + const int tab_id_; + const int previous_tab_id_; + + DISALLOW_COPY_AND_ASSIGN(ScopedTabID); + }; + + explicit RequestSender(Dispatcher* dispatcher); + ~RequestSender(); + + // In order to avoid collision, all |request_id|s passed into StartRequest() + // should be generated by this method. + int GetNextRequestId() const; + + // Makes a call to the API function |name| that is to be handled by the + // extension host. The response to this request will be received in + // HandleResponse(). + // TODO(koz): Remove |request_id| and generate that internally. + // There are multiple of these per render view though, so we'll + // need to vend the IDs centrally. + void StartRequest(Source* source, + const std::string& name, + int request_id, + bool has_callback, + bool for_io_thread, + base::ListValue* value_args); + + // Handles responses from the extension host to calls made by StartRequest(). + void HandleResponse(int request_id, + bool success, + const base::ListValue& response, + const std::string& error); + + // Notifies this that a request source is no longer valid. + // TODO(kalman): Do this in a generic/safe way. + void InvalidateSource(Source* source); + + private: + friend class ScopedTabID; + typedef std::map<int, linked_ptr<PendingRequest> > PendingRequestMap; + + void InsertRequest(int request_id, PendingRequest* pending_request); + linked_ptr<PendingRequest> RemoveRequest(int request_id); + + Dispatcher* dispatcher_; + PendingRequestMap pending_requests_; + + int source_tab_id_; // Id of the tab sending the request, or -1 if no tab. + + DISALLOW_COPY_AND_ASSIGN(RequestSender); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_REQUEST_SENDER_H_ diff --git a/chromium/extensions/renderer/resource_bundle_source_map.cc b/chromium/extensions/renderer/resource_bundle_source_map.cc new file mode 100644 index 00000000000..88fc5372185 --- /dev/null +++ b/chromium/extensions/renderer/resource_bundle_source_map.cc @@ -0,0 +1,55 @@ +// 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/renderer/resource_bundle_source_map.h" + +#include "base/logging.h" +#include "ui/base/resource/resource_bundle.h" + +namespace extensions { + +ResourceBundleSourceMap::ResourceBundleSourceMap( + const ui::ResourceBundle* resource_bundle) + : resource_bundle_(resource_bundle) { +} + +ResourceBundleSourceMap::~ResourceBundleSourceMap() { +} + +void ResourceBundleSourceMap::RegisterSource(const std::string& name, + int resource_id) { + resource_id_map_[name] = resource_id; +} + +v8::Local<v8::Value> ResourceBundleSourceMap::GetSource( + v8::Isolate* isolate, + const std::string& name) { + if (!Contains(name)) { + NOTREACHED() << "No module is registered with name \"" << name << "\""; + return v8::Undefined(isolate); + } + base::StringPiece resource = + resource_bundle_->GetRawDataResource(resource_id_map_[name]); + if (resource.empty()) { + NOTREACHED() + << "Module resource registered as \"" << name << "\" not found"; + return v8::Undefined(isolate); + } + return ConvertString(isolate, resource); +} + +bool ResourceBundleSourceMap::Contains(const std::string& name) { + return !!resource_id_map_.count(name); +} + +v8::Local<v8::String> ResourceBundleSourceMap::ConvertString( + v8::Isolate* isolate, + const base::StringPiece& string) { + // v8 takes ownership of the StaticV8ExternalOneByteStringResource (see + // v8::String::NewExternal()). + return v8::String::NewExternal( + isolate, new StaticV8ExternalOneByteStringResource(string)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/resource_bundle_source_map.h b/chromium/extensions/renderer/resource_bundle_source_map.h new file mode 100644 index 00000000000..13f8f216697 --- /dev/null +++ b/chromium/extensions/renderer/resource_bundle_source_map.h @@ -0,0 +1,45 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_RESOURCE_BUNDLE_SOURCE_MAP_H_ +#define EXTENSIONS_RENDERER_RESOURCE_BUNDLE_SOURCE_MAP_H_ + +#include <map> +#include <string> + +#include "base/compiler_specific.h" +#include "base/memory/linked_ptr.h" +#include "base/strings/string_piece.h" +#include "extensions/renderer/module_system.h" +#include "extensions/renderer/static_v8_external_one_byte_string_resource.h" +#include "v8/include/v8.h" + +namespace ui { +class ResourceBundle; +} + +namespace extensions { + +class ResourceBundleSourceMap : public extensions::ModuleSystem::SourceMap { + public: + explicit ResourceBundleSourceMap(const ui::ResourceBundle* resource_bundle); + ~ResourceBundleSourceMap() override; + + v8::Local<v8::Value> GetSource(v8::Isolate* isolate, + const std::string& name) override; + bool Contains(const std::string& name) override; + + void RegisterSource(const std::string& name, int resource_id); + + private: + v8::Local<v8::String> ConvertString(v8::Isolate* isolate, + const base::StringPiece& string); + + const ui::ResourceBundle* resource_bundle_; + std::map<std::string, int> resource_id_map_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_RESOURCE_BUNDLE_SOURCE_MAP_H_ diff --git a/chromium/extensions/renderer/resources/OWNERS b/chromium/extensions/renderer/resources/OWNERS new file mode 100644 index 00000000000..33637287bcf --- /dev/null +++ b/chromium/extensions/renderer/resources/OWNERS @@ -0,0 +1,4 @@ +per-file media_router_bindings.js=imcheng@chromium.org +per-file media_router_bindings.js=kmarshall@chromium.org +per-file media_router_bindings.js=mfoltz@chromium.org + diff --git a/chromium/extensions/renderer/resources/app_runtime_custom_bindings.js b/chromium/extensions/renderer/resources/app_runtime_custom_bindings.js new file mode 100644 index 00000000000..3f0dbd27272 --- /dev/null +++ b/chromium/extensions/renderer/resources/app_runtime_custom_bindings.js @@ -0,0 +1,83 @@ +// 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. + +// Custom binding for the chrome.app.runtime API. + +var binding = require('binding').Binding.create('app.runtime'); + +var AppViewGuestInternal = + require('binding').Binding.create('appViewGuestInternal').generate(); +var eventBindings = require('event_bindings'); +var fileSystemHelpers = requireNative('file_system_natives'); +var GetIsolatedFileSystem = fileSystemHelpers.GetIsolatedFileSystem; +var entryIdManager = require('entryIdManager'); + +eventBindings.registerArgumentMassager('app.runtime.onEmbedRequested', + function(args, dispatch) { + var appEmbeddingRequest = args[0]; + var id = appEmbeddingRequest.guestInstanceId; + delete appEmbeddingRequest.guestInstanceId; + appEmbeddingRequest.allow = function(url) { + AppViewGuestInternal.attachFrame(url, id); + }; + + appEmbeddingRequest.deny = function() { + AppViewGuestInternal.denyRequest(id); + }; + + dispatch([appEmbeddingRequest]); +}); + +eventBindings.registerArgumentMassager('app.runtime.onLaunched', + function(args, dispatch) { + var launchData = args[0]; + if (launchData.items) { + // An onLaunched corresponding to file_handlers in the app's manifest. + var items = []; + var numItems = launchData.items.length; + var itemLoaded = function(err, item) { + if (err) { + console.error('Error getting fileEntry, code: ' + err.code); + } else { + $Array.push(items, item); + } + if (--numItems === 0) { + var data = { + isKioskSession: launchData.isKioskSession, + isPublicSession: launchData.isPublicSession, + source: launchData.source + }; + if (items.length !== 0) { + data.id = launchData.id; + data.items = items; + } + dispatch([data]); + } + }; + $Array.forEach(launchData.items, function(item) { + var fs = GetIsolatedFileSystem(item.fileSystemId); + if (item.isDirectory) { + fs.root.getDirectory(item.baseName, {}, function(dirEntry) { + entryIdManager.registerEntry(item.entryId, dirEntry); + itemLoaded(null, {entry: dirEntry}); + }, function(fileError) { + itemLoaded(fileError); + }); + } else { + fs.root.getFile(item.baseName, {}, function(fileEntry) { + entryIdManager.registerEntry(item.entryId, fileEntry); + itemLoaded(null, {entry: fileEntry, type: item.mimeType}); + }, function(fileError) { + itemLoaded(fileError); + }); + } + }); + } else { + // Default case. This currently covers an onLaunched corresponding to + // url_handlers in the app's manifest. + dispatch([launchData]); + } +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/app_window_custom_bindings.js b/chromium/extensions/renderer/resources/app_window_custom_bindings.js new file mode 100644 index 00000000000..b0ae54f4a7b --- /dev/null +++ b/chromium/extensions/renderer/resources/app_window_custom_bindings.js @@ -0,0 +1,410 @@ +// Copyright (c) 2012 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. + +// Custom binding for the app_window API. + +var appWindowNatives = requireNative('app_window_natives'); +var runtimeNatives = requireNative('runtime'); +var Binding = require('binding').Binding; +var Event = require('event_bindings').Event; +var forEach = require('utils').forEach; +var renderFrameObserverNatives = requireNative('renderFrameObserverNatives'); + +var appWindowData = null; +var currentAppWindow = null; +var currentWindowInternal = null; + +var kSetBoundsFunction = 'setBounds'; +var kSetSizeConstraintsFunction = 'setSizeConstraints'; + +// Bounds class definition. +var Bounds = function(boundsKey) { + privates(this).boundsKey_ = boundsKey; +}; +Object.defineProperty(Bounds.prototype, 'left', { + get: function() { + return appWindowData[privates(this).boundsKey_].left; + }, + set: function(left) { + this.setPosition(left, null); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'top', { + get: function() { + return appWindowData[privates(this).boundsKey_].top; + }, + set: function(top) { + this.setPosition(null, top); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'width', { + get: function() { + return appWindowData[privates(this).boundsKey_].width; + }, + set: function(width) { + this.setSize(width, null); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'height', { + get: function() { + return appWindowData[privates(this).boundsKey_].height; + }, + set: function(height) { + this.setSize(null, height); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'minWidth', { + get: function() { + return appWindowData[privates(this).boundsKey_].minWidth; + }, + set: function(minWidth) { + updateSizeConstraints(privates(this).boundsKey_, { minWidth: minWidth }); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'maxWidth', { + get: function() { + return appWindowData[privates(this).boundsKey_].maxWidth; + }, + set: function(maxWidth) { + updateSizeConstraints(privates(this).boundsKey_, { maxWidth: maxWidth }); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'minHeight', { + get: function() { + return appWindowData[privates(this).boundsKey_].minHeight; + }, + set: function(minHeight) { + updateSizeConstraints(privates(this).boundsKey_, { minHeight: minHeight }); + }, + enumerable: true +}); +Object.defineProperty(Bounds.prototype, 'maxHeight', { + get: function() { + return appWindowData[privates(this).boundsKey_].maxHeight; + }, + set: function(maxHeight) { + updateSizeConstraints(privates(this).boundsKey_, { maxHeight: maxHeight }); + }, + enumerable: true +}); +Bounds.prototype.setPosition = function(left, top) { + updateBounds(privates(this).boundsKey_, { left: left, top: top }); +}; +Bounds.prototype.setSize = function(width, height) { + updateBounds(privates(this).boundsKey_, { width: width, height: height }); +}; +Bounds.prototype.setMinimumSize = function(minWidth, minHeight) { + updateSizeConstraints(privates(this).boundsKey_, + { minWidth: minWidth, minHeight: minHeight }); +}; +Bounds.prototype.setMaximumSize = function(maxWidth, maxHeight) { + updateSizeConstraints(privates(this).boundsKey_, + { maxWidth: maxWidth, maxHeight: maxHeight }); +}; + +var appWindow = Binding.create('app.window'); +appWindow.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setCustomCallback('create', + function(name, request, callback, windowParams) { + var view = null; + + // When window creation fails, |windowParams| will be undefined. + if (windowParams && windowParams.frameId) { + view = appWindowNatives.GetFrame( + windowParams.frameId, true /* notifyBrowser */); + } + + if (!view) { + // No route to created window. If given a callback, trigger it with an + // undefined object. + if (callback) + callback(); + return; + } + + if (windowParams.existingWindow) { + // Not creating a new window, but activating an existing one, so trigger + // callback with existing window and don't do anything else. + if (callback) + callback(view.chrome.app.window.current()); + return; + } + + // Initialize appWindowData in the newly created JS context + if (view.chrome.app) { + view.chrome.app.window.initializeAppWindow(windowParams); + } else { + var sandbox_window_message = 'Creating sandboxed window, it doesn\'t ' + + 'have access to the chrome.app API.'; + if (callback) { + sandbox_window_message = sandbox_window_message + + ' The chrome.app.window.create callback will be called, but ' + + 'there will be no object provided for the sandboxed window.'; + } + console.warn(sandbox_window_message); + } + + if (callback) { + if (!view || !view.chrome.app /* sandboxed window */) { + callback(undefined); + return; + } + + var willCallback = + renderFrameObserverNatives.OnDocumentElementCreated( + windowParams.frameId, + function(success) { + if (success) { + callback(view.chrome.app.window.current()); + } else { + callback(undefined); + } + }); + if (!willCallback) { + callback(undefined); + } + } + }); + + apiFunctions.setHandleRequest('current', function() { + if (!currentAppWindow) { + console.error('The JavaScript context calling ' + + 'chrome.app.window.current() has no associated AppWindow.'); + return null; + } + return currentAppWindow; + }); + + apiFunctions.setHandleRequest('getAll', function() { + var views = runtimeNatives.GetExtensionViews(-1, 'APP_WINDOW'); + return $Array.map(views, function(win) { + return win.chrome.app.window.current(); + }); + }); + + apiFunctions.setHandleRequest('get', function(id) { + var windows = $Array.filter(chrome.app.window.getAll(), function(win) { + return win.id == id; + }); + return windows.length > 0 ? windows[0] : null; + }); + + apiFunctions.setHandleRequest('canSetVisibleOnAllWorkspaces', function() { + return /Mac/.test(navigator.platform) || /Linux/.test(navigator.userAgent); + }); + + // This is an internal function, but needs to be bound into a closure + // so the correct JS context is used for global variables such as + // currentWindowInternal, appWindowData, etc. + apiFunctions.setHandleRequest('initializeAppWindow', function(params) { + currentWindowInternal = + Binding.create('app.currentWindowInternal').generate(); + var AppWindow = function() { + this.innerBounds = new Bounds('innerBounds'); + this.outerBounds = new Bounds('outerBounds'); + }; + forEach(currentWindowInternal, function(key, value) { + // Do not add internal functions that should not appear in the AppWindow + // interface. They are called by Bounds mutators. + if (key !== kSetBoundsFunction && key !== kSetSizeConstraintsFunction) + AppWindow.prototype[key] = value; + }); + AppWindow.prototype.moveTo = $Function.bind(window.moveTo, window); + AppWindow.prototype.resizeTo = $Function.bind(window.resizeTo, window); + AppWindow.prototype.contentWindow = window; + AppWindow.prototype.onClosed = new Event(); + AppWindow.prototype.onWindowFirstShownForTests = new Event(); + AppWindow.prototype.close = function() { + this.contentWindow.close(); + }; + AppWindow.prototype.getBounds = function() { + // This is to maintain backcompatibility with a bug on Windows and + // ChromeOS, which returns the position of the window but the size of + // the content. + var innerBounds = appWindowData.innerBounds; + var outerBounds = appWindowData.outerBounds; + return { left: outerBounds.left, top: outerBounds.top, + width: innerBounds.width, height: innerBounds.height }; + }; + AppWindow.prototype.setBounds = function(bounds) { + updateBounds('bounds', bounds); + }; + AppWindow.prototype.isFullscreen = function() { + return appWindowData.fullscreen; + }; + AppWindow.prototype.isMinimized = function() { + return appWindowData.minimized; + }; + AppWindow.prototype.isMaximized = function() { + return appWindowData.maximized; + }; + AppWindow.prototype.isAlwaysOnTop = function() { + return appWindowData.alwaysOnTop; + }; + AppWindow.prototype.alphaEnabled = function() { + return appWindowData.alphaEnabled; + }; + AppWindow.prototype.handleWindowFirstShownForTests = function(callback) { + // This allows test apps to get have their callback run even if they + // call this after the first show has happened. + if (this.firstShowHasHappened) { + callback(); + return; + } + this.onWindowFirstShownForTests.addListener(callback); + } + + Object.defineProperty(AppWindow.prototype, 'id', {get: function() { + return appWindowData.id; + }}); + + // These properties are for testing. + Object.defineProperty( + AppWindow.prototype, 'hasFrameColor', {get: function() { + return appWindowData.hasFrameColor; + }}); + + Object.defineProperty(AppWindow.prototype, 'activeFrameColor', + {get: function() { + return appWindowData.activeFrameColor; + }}); + + Object.defineProperty(AppWindow.prototype, 'inactiveFrameColor', + {get: function() { + return appWindowData.inactiveFrameColor; + }}); + + appWindowData = { + id: params.id || '', + innerBounds: { + left: params.innerBounds.left, + top: params.innerBounds.top, + width: params.innerBounds.width, + height: params.innerBounds.height, + + minWidth: params.innerBounds.minWidth, + minHeight: params.innerBounds.minHeight, + maxWidth: params.innerBounds.maxWidth, + maxHeight: params.innerBounds.maxHeight + }, + outerBounds: { + left: params.outerBounds.left, + top: params.outerBounds.top, + width: params.outerBounds.width, + height: params.outerBounds.height, + + minWidth: params.outerBounds.minWidth, + minHeight: params.outerBounds.minHeight, + maxWidth: params.outerBounds.maxWidth, + maxHeight: params.outerBounds.maxHeight + }, + fullscreen: params.fullscreen, + minimized: params.minimized, + maximized: params.maximized, + alwaysOnTop: params.alwaysOnTop, + hasFrameColor: params.hasFrameColor, + activeFrameColor: params.activeFrameColor, + inactiveFrameColor: params.inactiveFrameColor, + alphaEnabled: params.alphaEnabled + }; + currentAppWindow = new AppWindow; + }); +}); + +function boundsEqual(bounds1, bounds2) { + if (!bounds1 || !bounds2) + return false; + return (bounds1.left == bounds2.left && bounds1.top == bounds2.top && + bounds1.width == bounds2.width && bounds1.height == bounds2.height); +} + +function dispatchEventIfExists(target, name) { + // Sometimes apps like to put their own properties on the window which + // break our assumptions. + var event = target[name]; + if (event && (typeof event.dispatch == 'function')) + event.dispatch(); + else + console.warn('Could not dispatch ' + name + ', event has been clobbered'); +} + +function updateAppWindowProperties(update) { + if (!appWindowData) + return; + + var oldData = appWindowData; + update.id = oldData.id; + appWindowData = update; + + var currentWindow = currentAppWindow; + + if (!boundsEqual(oldData.innerBounds, update.innerBounds)) + dispatchEventIfExists(currentWindow, "onBoundsChanged"); + + if (!oldData.fullscreen && update.fullscreen) + dispatchEventIfExists(currentWindow, "onFullscreened"); + if (!oldData.minimized && update.minimized) + dispatchEventIfExists(currentWindow, "onMinimized"); + if (!oldData.maximized && update.maximized) + dispatchEventIfExists(currentWindow, "onMaximized"); + + if ((oldData.fullscreen && !update.fullscreen) || + (oldData.minimized && !update.minimized) || + (oldData.maximized && !update.maximized)) + dispatchEventIfExists(currentWindow, "onRestored"); + + if (oldData.alphaEnabled !== update.alphaEnabled) + dispatchEventIfExists(currentWindow, "onAlphaEnabledChanged"); +}; + +function onAppWindowShownForTests() { + if (!currentAppWindow) + return; + + if (!currentAppWindow.firstShowHasHappened) + dispatchEventIfExists(currentAppWindow, "onWindowFirstShownForTests"); + + currentAppWindow.firstShowHasHappened = true; +} + +function onAppWindowClosed() { + if (!currentAppWindow) + return; + dispatchEventIfExists(currentAppWindow, "onClosed"); +} + +function updateBounds(boundsType, bounds) { + if (!currentWindowInternal) + return; + + currentWindowInternal.setBounds(boundsType, bounds); +} + +function updateSizeConstraints(boundsType, constraints) { + if (!currentWindowInternal) + return; + + forEach(constraints, function(key, value) { + // From the perspective of the API, null is used to reset constraints. + // We need to convert this to 0 because a value of null is interpreted + // the same as undefined in the browser and leaves the constraint unchanged. + if (value === null) + constraints[key] = 0; + }); + + currentWindowInternal.setSizeConstraints(boundsType, constraints); +} + +exports.$set('binding', appWindow.generate()); +exports.$set('onAppWindowClosed', onAppWindowClosed); +exports.$set('updateAppWindowProperties', updateAppWindowProperties); +exports.$set('appWindowShownForTests', onAppWindowShownForTests); diff --git a/chromium/extensions/renderer/resources/async_waiter.js b/chromium/extensions/renderer/resources/async_waiter.js new file mode 100644 index 00000000000..6470f64b4d5 --- /dev/null +++ b/chromium/extensions/renderer/resources/async_waiter.js @@ -0,0 +1,93 @@ +// 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. + +define('async_waiter', [ + 'mojo/public/js/support', +], function(supportModule) { + /** + * @module async_waiter + */ + + /** + * @callback module:async_waiter.AsyncWaiter.Callback + * @param {number} result The result of waiting. + */ + + /** + * A waiter that waits for a handle to be ready for either reading or writing. + * @param {!MojoHandle} handle The handle to wait on. + * @param {number} signals The signals to wait for handle to be ready for. + * @param {module:async_waiter.AsyncWaiter.Callback} callback The callback to + * call when handle is ready. + * @constructor + * @alias module:async_waiter.AsyncWaiter + */ + function AsyncWaiter(handle, signals, callback) { + /** + * The handle to wait on. + * @type {!MojoHandle} + * @private + */ + this.handle_ = handle; + + /** + * The signals to wait for. + * @type {number} + * @private + */ + this.signals_ = signals; + + /** + * The callback to invoke when + * |[handle_]{@link module:async_waiter.AsyncWaiter#handle_}| is ready. + * @type {module:async_waiter.AsyncWaiter.Callback} + * @private + */ + this.callback_ = callback; + this.id_ = null; + } + + /** + * Start waiting for the handle to be ready. + * @throws Will throw if this is already waiting. + */ + AsyncWaiter.prototype.start = function() { + if (this.id_) + throw new Error('Already started'); + this.id_ = supportModule.asyncWait( + this.handle_, this.signals_, this.onHandleReady_.bind(this)); + }; + + /** + * Stop waiting for the handle to be ready. + */ + AsyncWaiter.prototype.stop = function() { + if (!this.id_) + return; + + supportModule.cancelWait(this.id_); + this.id_ = null; + }; + + /** + * Returns whether this {@link AsyncWaiter} is waiting. + * @return {boolean} Whether this AsyncWaiter is waiting. + */ + AsyncWaiter.prototype.isWaiting = function() { + return !!this.id_; + }; + + /** + * Invoked when |[handle_]{@link module:async_waiter.AsyncWaiter#handle_}| is + * ready. + * @param {number} result The result of the wait. + * @private + */ + AsyncWaiter.prototype.onHandleReady_ = function(result) { + this.id_ = null; + this.callback_(result); + }; + + return {AsyncWaiter: AsyncWaiter}; +}); diff --git a/chromium/extensions/renderer/resources/binding.js b/chromium/extensions/renderer/resources/binding.js new file mode 100644 index 00000000000..c5690907f40 --- /dev/null +++ b/chromium/extensions/renderer/resources/binding.js @@ -0,0 +1,574 @@ +// 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. + +var Event = require('event_bindings').Event; +var forEach = require('utils').forEach; +// Note: Beware sneaky getters/setters when using GetAvailbility(). Use safe/raw +// variables as arguments. +var GetAvailability = requireNative('v8_context').GetAvailability; +var exceptionHandler = require('uncaught_exception_handler'); +var lastError = require('lastError'); +var logActivity = requireNative('activityLogger'); +var logging = requireNative('logging'); +var process = requireNative('process'); +var schemaRegistry = requireNative('schema_registry'); +var schemaUtils = require('schemaUtils'); +var utils = require('utils'); +var sendRequestHandler = require('sendRequest'); + +var contextType = process.GetContextType(); +var extensionId = process.GetExtensionId(); +var manifestVersion = process.GetManifestVersion(); +var sendRequest = sendRequestHandler.sendRequest; + +// Stores the name and definition of each API function, with methods to +// modify their behaviour (such as a custom way to handle requests to the +// API, a custom callback, etc). +function APIFunctions(namespace) { + this.apiFunctions_ = {}; + this.unavailableApiFunctions_ = {}; + this.namespace = namespace; +} + +APIFunctions.prototype.register = function(apiName, apiFunction) { + this.apiFunctions_[apiName] = apiFunction; +}; + +// Registers a function as existing but not available, meaning that calls to +// the set* methods that reference this function should be ignored rather +// than throwing Errors. +APIFunctions.prototype.registerUnavailable = function(apiName) { + this.unavailableApiFunctions_[apiName] = apiName; +}; + +APIFunctions.prototype.setHook_ = + function(apiName, propertyName, customizedFunction) { + if ($Object.hasOwnProperty(this.unavailableApiFunctions_, apiName)) + return; + if (!$Object.hasOwnProperty(this.apiFunctions_, apiName)) + throw new Error('Tried to set hook for unknown API "' + apiName + '"'); + this.apiFunctions_[apiName][propertyName] = customizedFunction; +}; + +APIFunctions.prototype.setHandleRequest = + function(apiName, customizedFunction) { + var prefix = this.namespace; + return this.setHook_(apiName, 'handleRequest', + function() { + var ret = $Function.apply(customizedFunction, this, arguments); + // Logs API calls to the Activity Log if it doesn't go through an + // ExtensionFunction. + if (!sendRequestHandler.getCalledSendRequest()) + logActivity.LogAPICall(extensionId, prefix + "." + apiName, + $Array.slice(arguments)); + return ret; + }); +}; + +APIFunctions.prototype.setHandleRequestWithPromise = + function(apiName, customizedFunction) { + var prefix = this.namespace; + return this.setHook_(apiName, 'handleRequest', function() { + var name = prefix + '.' + apiName; + logActivity.LogAPICall(extensionId, name, $Array.slice(arguments)); + var stack = exceptionHandler.getExtensionStackTrace(); + var callback = arguments[arguments.length - 1]; + var args = $Array.slice(arguments, 0, arguments.length - 1); + var keepAlivePromise = requireAsync('keep_alive').then(function(module) { + return module.createKeepAlive(); + }); + $Function.apply(customizedFunction, this, args).then(function(result) { + if (callback) { + sendRequestHandler.safeCallbackApply(name, {'stack': stack}, callback, + [result]); + } + }).catch(function(error) { + if (callback) { + var message = exceptionHandler.safeErrorToString(error, true); + lastError.run(name, message, stack, callback); + } + }).then(function() { + keepAlivePromise.then(function(keepAlive) { + keepAlive.close(); + }); + }); + }); +}; + +APIFunctions.prototype.setUpdateArgumentsPostValidate = + function(apiName, customizedFunction) { + return this.setHook_( + apiName, 'updateArgumentsPostValidate', customizedFunction); +}; + +APIFunctions.prototype.setUpdateArgumentsPreValidate = + function(apiName, customizedFunction) { + return this.setHook_( + apiName, 'updateArgumentsPreValidate', customizedFunction); +}; + +APIFunctions.prototype.setCustomCallback = + function(apiName, customizedFunction) { + return this.setHook_(apiName, 'customCallback', customizedFunction); +}; + +function CustomBindingsObject() { +} + +CustomBindingsObject.prototype.setSchema = function(schema) { + // The functions in the schema are in list form, so we move them into a + // dictionary for easier access. + var self = this; + self.functionSchemas = {}; + $Array.forEach(schema.functions, function(f) { + self.functionSchemas[f.name] = { + name: f.name, + definition: f + } + }); +}; + +// Get the platform from navigator.appVersion. +function getPlatform() { + var platforms = [ + [/CrOS Touch/, "chromeos touch"], + [/CrOS/, "chromeos"], + [/Linux/, "linux"], + [/Mac/, "mac"], + [/Win/, "win"], + ]; + + for (var i = 0; i < platforms.length; i++) { + if ($RegExp.exec(platforms[i][0], navigator.appVersion)) { + return platforms[i][1]; + } + } + return "unknown"; +} + +function isPlatformSupported(schemaNode, platform) { + return !schemaNode.platforms || + $Array.indexOf(schemaNode.platforms, platform) > -1; +} + +function isManifestVersionSupported(schemaNode, manifestVersion) { + return !schemaNode.maximumManifestVersion || + manifestVersion <= schemaNode.maximumManifestVersion; +} + +function isSchemaNodeSupported(schemaNode, platform, manifestVersion) { + return isPlatformSupported(schemaNode, platform) && + isManifestVersionSupported(schemaNode, manifestVersion); +} + +function createCustomType(type) { + var jsModuleName = type.js_module; + logging.CHECK(jsModuleName, 'Custom type ' + type.id + + ' has no "js_module" property.'); + // This list contains all types that has a js_module property. It is ugly to + // hard-code them here, but the number of APIs that use js_module has not + // changed since the introduction of js_modules in crbug.com/222156. + // This whitelist serves as an extra line of defence to avoid exposing + // arbitrary extension modules when the |type| definition is poisoned. + var whitelistedModules = [ + 'ChromeDirectSetting', + 'ChromeSetting', + 'ContentSetting', + 'StorageArea', + ]; + logging.CHECK($Array.indexOf(whitelistedModules, jsModuleName) !== -1, + 'Module ' + jsModuleName + ' does not define a custom type.'); + var jsModule = require(jsModuleName); + logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' + + type.id + '.'); + var customType = jsModule[jsModuleName]; + logging.CHECK(customType, jsModuleName + ' must export itself.'); + customType.prototype = new CustomBindingsObject(); + customType.prototype.setSchema(type); + return customType; +} + +var platform = getPlatform(); + +function Binding(apiName) { + this.apiName_ = apiName; + this.apiFunctions_ = new APIFunctions(apiName); + this.customEvent_ = null; + this.customHooks_ = []; +}; + +Binding.create = function(apiName) { + return new Binding(apiName); +}; + +Binding.prototype = { + // The API through which the ${api_name}_custom_bindings.js files customize + // their API bindings beyond what can be generated. + // + // There are 2 types of customizations available: those which are required in + // order to do the schema generation (registerCustomEvent and + // registerCustomType), and those which can only run after the bindings have + // been generated (registerCustomHook). + + // Registers a custom event type for the API identified by |namespace|. + // |event| is the event's constructor. + registerCustomEvent: function(event) { + this.customEvent_ = event; + }, + + // Registers a function |hook| to run after the schema for all APIs has been + // generated. The hook is passed as its first argument an "API" object to + // interact with, and second the current extension ID. See where + // |customHooks| is used. + registerCustomHook: function(fn) { + $Array.push(this.customHooks_, fn); + }, + + // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed. + runHooks_: function(api, schema) { + $Array.forEach(this.customHooks_, function(hook) { + if (!isSchemaNodeSupported(schema, platform, manifestVersion)) + return; + + if (!hook) + return; + + hook({ + apiFunctions: this.apiFunctions_, + schema: schema, + compiledApi: api + }, extensionId, contextType); + }, this); + }, + + // Generates the bindings from the schema for |this.apiName_| and integrates + // any custom bindings that might be present. + generate: function() { + // NB: It's important to load the schema during generation rather than + // setting it beforehand so that we're more confident the schema we're + // loading is real, and not one that was injected by a page intercepting + // Binding.generate. + // Additionally, since the schema is an object returned from a native + // handler, its properties don't have the custom getters/setters that a page + // may have put on Object.prototype, and the object is frozen by v8. + var schema = schemaRegistry.GetSchema(this.apiName_); + + function shouldCheckUnprivileged() { + var shouldCheck = 'unprivileged' in schema; + if (shouldCheck) + return shouldCheck; + + $Array.forEach(['functions', 'events'], function(type) { + if ($Object.hasOwnProperty(schema, type)) { + $Array.forEach(schema[type], function(node) { + if ('unprivileged' in node) + shouldCheck = true; + }); + } + }); + if (shouldCheck) + return shouldCheck; + + for (var property in schema.properties) { + if ($Object.hasOwnProperty(schema, property) && + 'unprivileged' in schema.properties[property]) { + shouldCheck = true; + break; + } + } + return shouldCheck; + } + var checkUnprivileged = shouldCheckUnprivileged(); + + // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the + // supporting code. + if (!isSchemaNodeSupported(schema, platform, manifestVersion)) { + console.error('chrome.' + schema.namespace + ' is not supported on ' + + 'this platform or manifest version'); + return undefined; + } + + var mod = {}; + + var namespaces = $String.split(schema.namespace, '.'); + for (var index = 0, name; name = namespaces[index]; index++) { + mod[name] = mod[name] || {}; + mod = mod[name]; + } + + if (schema.types) { + $Array.forEach(schema.types, function(t) { + if (!isSchemaNodeSupported(t, platform, manifestVersion)) + return; + + // Add types to global schemaValidator; the types we depend on from + // other namespaces will be added as needed. + schemaUtils.schemaValidator.addTypes(t); + + // Generate symbols for enums. + var enumValues = t['enum']; + if (enumValues) { + // Type IDs are qualified with the namespace during compilation, + // unfortunately, so remove it here. + logging.DCHECK($String.substr(t.id, 0, schema.namespace.length) == + schema.namespace); + // Note: + 1 because it ends in a '.', e.g., 'fooApi.Type'. + var id = $String.substr(t.id, schema.namespace.length + 1); + mod[id] = {}; + $Array.forEach(enumValues, function(enumValue) { + // Note: enums can be declared either as a list of strings + // ['foo', 'bar'] or as a list of objects + // [{'name': 'foo'}, {'name': 'bar'}]. + enumValue = $Object.hasOwnProperty(enumValue, 'name') ? + enumValue.name : enumValue; + if (enumValue) { // Avoid setting any empty enums. + // Make all properties in ALL_CAPS_STYLE. + // + // The built-in versions of $String.replace call other built-ins, + // which may be clobbered. Instead, manually build the property + // name. + // + // If the first character is a digit (we know it must be one of + // a digit, a letter, or an underscore), precede it with an + // underscore. + var propertyName = ($RegExp.exec(/\d/, enumValue[0])) ? '_' : ''; + for (var i = 0; i < enumValue.length; ++i) { + var next; + if (i > 0 && $RegExp.exec(/[a-z]/, enumValue[i-1]) && + $RegExp.exec(/[A-Z]/, enumValue[i])) { + // Replace myEnum-Foo with my_Enum-Foo: + next = '_' + enumValue[i]; + } else if ($RegExp.exec(/\W/, enumValue[i])) { + // Replace my_Enum-Foo with my_Enum_Foo: + next = '_'; + } else { + next = enumValue[i]; + } + propertyName += next; + } + // Uppercase (replace my_Enum_Foo with MY_ENUM_FOO): + propertyName = $String.toUpperCase(propertyName); + mod[id][propertyName] = enumValue; + } + }); + } + }, this); + } + + // TODO(cduvall): Take out when all APIs have been converted to features. + // Returns whether access to the content of a schema should be denied, + // based on the presence of "unprivileged" and whether this is an + // extension process (versus e.g. a content script). + function isSchemaAccessAllowed(itemSchema) { + return (contextType == 'BLESSED_EXTENSION') || + schema.unprivileged || + itemSchema.unprivileged; + }; + + // Setup Functions. + if (schema.functions) { + $Array.forEach(schema.functions, function(functionDef) { + if (functionDef.name in mod) { + throw new Error('Function ' + functionDef.name + + ' already defined in ' + schema.namespace); + } + + if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) { + this.apiFunctions_.registerUnavailable(functionDef.name); + return; + } + + var apiFunction = {}; + apiFunction.definition = functionDef; + var apiFunctionName = schema.namespace + '.' + functionDef.name; + apiFunction.name = apiFunctionName; + + if (!GetAvailability(apiFunctionName).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) { + this.apiFunctions_.registerUnavailable(functionDef.name); + return; + } + + // TODO(aa): It would be best to run this in a unit test, but in order + // to do that we would need to better factor this code so that it + // doesn't depend on so much v8::Extension machinery. + if (logging.DCHECK_IS_ON() && + schemaUtils.isFunctionSignatureAmbiguous(apiFunction.definition)) { + throw new Error( + apiFunction.name + ' has ambiguous optional arguments. ' + + 'To implement custom disambiguation logic, add ' + + '"allowAmbiguousOptionalArguments" to the function\'s schema.'); + } + + this.apiFunctions_.register(functionDef.name, apiFunction); + + mod[functionDef.name] = $Function.bind(function() { + var args = $Array.slice(arguments); + if (this.updateArgumentsPreValidate) + args = $Function.apply(this.updateArgumentsPreValidate, this, args); + + args = schemaUtils.normalizeArgumentsAndValidate(args, this); + if (this.updateArgumentsPostValidate) { + args = $Function.apply(this.updateArgumentsPostValidate, + this, + args); + } + + sendRequestHandler.clearCalledSendRequest(); + + var retval; + if (this.handleRequest) { + retval = $Function.apply(this.handleRequest, this, args); + } else { + var optArgs = { + customCallback: this.customCallback + }; + retval = sendRequest(this.name, args, + this.definition.parameters, + optArgs); + } + sendRequestHandler.clearCalledSendRequest(); + + // Validate return value if in sanity check mode. + if (logging.DCHECK_IS_ON() && this.definition.returns) + schemaUtils.validate([retval], [this.definition.returns]); + return retval; + }, apiFunction); + }, this); + } + + // Setup Events + if (schema.events) { + $Array.forEach(schema.events, function(eventDef) { + if (eventDef.name in mod) { + throw new Error('Event ' + eventDef.name + + ' already defined in ' + schema.namespace); + } + if (!isSchemaNodeSupported(eventDef, platform, manifestVersion)) + return; + + var eventName = schema.namespace + "." + eventDef.name; + if (!GetAvailability(eventName).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) { + return; + } + + var options = eventDef.options || {}; + if (eventDef.filters && eventDef.filters.length > 0) + options.supportsFilters = true; + + var parameters = eventDef.parameters; + if (this.customEvent_) { + mod[eventDef.name] = new this.customEvent_( + eventName, parameters, eventDef.extraParameters, options); + } else { + mod[eventDef.name] = new Event(eventName, parameters, options); + } + }, this); + } + + function addProperties(m, parentDef) { + var properties = parentDef.properties; + if (!properties) + return; + + forEach(properties, function(propertyName, propertyDef) { + if (propertyName in m) + return; // TODO(kalman): be strict like functions/events somehow. + if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion)) + return; + if (!GetAvailability(schema.namespace + "." + + propertyName).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) { + return; + } + + // |value| is eventually added to |m|, the exposed API. Make copies + // of everything from the schema. (The schema is also frozen, so as long + // as we don't make any modifications, shallow copies are fine.) + var value; + if ($Array.isArray(propertyDef.value)) + value = $Array.slice(propertyDef.value); + else if (typeof propertyDef.value === 'object') + value = $Object.assign({}, propertyDef.value); + else + value = propertyDef.value; + + if (value) { + // Values may just have raw types as defined in the JSON, such + // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here. + // TODO(kalman): enforce that things with a "value" property can't + // define their own types. + var type = propertyDef.type || typeof(value); + if (type === 'integer' || type === 'number') { + value = parseInt(value); + } else if (type === 'boolean') { + value = value === 'true'; + } else if (propertyDef['$ref']) { + var ref = propertyDef['$ref']; + var type = utils.loadTypeSchema(propertyDef['$ref'], schema); + logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found'); + var constructor = createCustomType(type); + var args = value; + // For an object propertyDef, |value| is an array of constructor + // arguments, but we want to pass the arguments directly (i.e. + // not as an array), so we have to fake calling |new| on the + // constructor. + value = { __proto__: constructor.prototype }; + $Function.apply(constructor, value, args); + // Recursively add properties. + addProperties(value, propertyDef); + } else if (type === 'object') { + // Recursively add properties. + addProperties(value, propertyDef); + } else if (type !== 'string') { + throw new Error('NOT IMPLEMENTED (extension_api.json error): ' + + 'Cannot parse values for type "' + type + '"'); + } + m[propertyName] = value; + } + }); + }; + + addProperties(mod, schema); + + // This generate() call is considered successful if any functions, + // properties, or events were created. + var success = ($Object.keys(mod).length > 0); + + // Special case: webViewRequest is a vacuous API which just copies its + // implementation from declarativeWebRequest. + // + // TODO(kalman): This would be unnecessary if we did these checks after the + // hooks (i.e. this.runHooks_(mod)). The reason we don't is to be very + // conservative with running any JS which might actually be for an API + // which isn't available, but this is probably overly cautious given the + // C++ is only giving us APIs which are available. FIXME. + if (schema.namespace == 'webViewRequest') { + success = true; + } + + // Special case: runtime.lastError is only occasionally set, so + // specifically check its availability. + if (schema.namespace == 'runtime' && + GetAvailability('runtime.lastError').is_available) { + success = true; + } + + if (!success) { + var availability = GetAvailability(schema.namespace); + // If an API was available it should have been successfully generated. + logging.DCHECK(!availability.is_available, + schema.namespace + ' was available but not generated'); + console.error('chrome.' + schema.namespace + ' is not available: ' + + availability.message); + return; + } + + this.runHooks_(mod, schema); + return mod; + } +}; + +exports.$set('Binding', Binding); diff --git a/chromium/extensions/renderer/resources/browser_test_environment_specific_bindings.js b/chromium/extensions/renderer/resources/browser_test_environment_specific_bindings.js new file mode 100644 index 00000000000..8574068656d --- /dev/null +++ b/chromium/extensions/renderer/resources/browser_test_environment_specific_bindings.js @@ -0,0 +1,15 @@ +// 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. + +function registerHooks(api) { +} + +function testDone(runNextTest) { + // Use setTimeout here to allow previous test contexts to be + // eligible for garbage collection. + setTimeout(runNextTest, 0); +} + +exports.$set('registerHooks', registerHooks); +exports.$set('testDone', testDone); diff --git a/chromium/extensions/renderer/resources/context_menus_custom_bindings.js b/chromium/extensions/renderer/resources/context_menus_custom_bindings.js new file mode 100644 index 00000000000..ec8080f5294 --- /dev/null +++ b/chromium/extensions/renderer/resources/context_menus_custom_bindings.js @@ -0,0 +1,26 @@ +// 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. + +// Custom binding for the contextMenus API. + +var binding = require('binding').Binding.create('contextMenus'); +var contextMenusHandlers = require('contextMenusHandlers'); + +binding.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + + var handlers = contextMenusHandlers.create(false /* isWebview */); + + apiFunctions.setHandleRequest('create', handlers.requestHandlers.create); + + apiFunctions.setCustomCallback('create', handlers.callbacks.create); + + apiFunctions.setCustomCallback('remove', handlers.callbacks.remove); + + apiFunctions.setCustomCallback('update', handlers.callbacks.update); + + apiFunctions.setCustomCallback('removeAll', handlers.callbacks.removeAll); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/context_menus_handlers.js b/chromium/extensions/renderer/resources/context_menus_handlers.js new file mode 100644 index 00000000000..aef6889d7a3 --- /dev/null +++ b/chromium/extensions/renderer/resources/context_menus_handlers.js @@ -0,0 +1,141 @@ +// Copyright 2015 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. + +// Implementation of custom bindings for the contextMenus API. +// This is used to implement the contextMenus API for extensions and for the +// <webview> tag (see chrome_web_view_experimental.js). + +var contextMenuNatives = requireNative('context_menus'); +var sendRequest = require('sendRequest').sendRequest; +var Event = require('event_bindings').Event; +var lastError = require('lastError'); + +// Add the bindings to the contextMenus API. +function createContextMenusHandlers(isWebview) { + var eventName = isWebview ? 'webViewInternal.contextMenus' : 'contextMenus'; + // Some dummy value for chrome.contextMenus instances. + // Webviews use positive integers, and 0 to denote an invalid webview ID. + // The following constant is -1 to avoid any conflicts between webview IDs and + // extensions. + var INSTANCEID_NON_WEBVIEW = -1; + + // Generates a customCallback for a given method. |handleCallback| will be + // invoked with |request.args| as parameters. + function createCustomCallback(handleCallback) { + return function(name, request, callback) { + if (lastError.hasError(chrome)) { + if (callback) + callback(); + return; + } + var args = request.args; + if (!isWebview) { + // <webview>s have an extra item in front of the parameter list, which + // specifies the viewInstanceId of the webview. This is used to hide + // context menu events in one webview from another. + // The non-webview chrome.contextMenus API is not called with such an + // ID, so we prepend an ID to match the function signature. + args = $Array.concat([INSTANCEID_NON_WEBVIEW], args); + } + $Function.apply(handleCallback, null, args); + if (callback) + callback(); + }; + } + + var contextMenus = {}; + contextMenus.handlers = {}; + contextMenus.event = new Event(eventName); + + contextMenus.getIdFromCreateProperties = function(createProperties) { + if (typeof createProperties.id !== 'undefined') + return createProperties.id; + return createProperties.generatedId; + }; + + contextMenus.handlersForId = function(instanceId, id) { + if (!contextMenus.handlers[instanceId]) { + contextMenus.handlers[instanceId] = { + generated: {}, + string: {} + }; + } + if (typeof id === 'number') + return contextMenus.handlers[instanceId].generated; + return contextMenus.handlers[instanceId].string; + }; + + contextMenus.ensureListenerSetup = function() { + if (contextMenus.listening) { + return; + } + contextMenus.listening = true; + contextMenus.event.addListener(function(info) { + var instanceId = INSTANCEID_NON_WEBVIEW; + if (isWebview) { + instanceId = info.webviewInstanceId; + // Don't expose |webviewInstanceId| via the public API. + delete info.webviewInstanceId; + } + + var id = info.menuItemId; + var onclick = contextMenus.handlersForId(instanceId, id)[id]; + if (onclick) { + $Function.apply(onclick, null, arguments); + } + }); + }; + + // To be used with apiFunctions.setHandleRequest + var requestHandlers = {}; + // To be used with apiFunctions.setCustomCallback + var callbacks = {}; + + requestHandlers.create = function() { + var createProperties = isWebview ? arguments[1] : arguments[0]; + createProperties.generatedId = contextMenuNatives.GetNextContextMenuId(); + var optArgs = { + customCallback: this.customCallback, + }; + sendRequest(this.name, arguments, this.definition.parameters, optArgs); + return contextMenus.getIdFromCreateProperties(createProperties); + }; + + callbacks.create = + createCustomCallback(function(instanceId, createProperties) { + var id = contextMenus.getIdFromCreateProperties(createProperties); + var onclick = createProperties.onclick; + if (onclick) { + contextMenus.ensureListenerSetup(); + contextMenus.handlersForId(instanceId, id)[id] = onclick; + } + }); + + callbacks.remove = createCustomCallback(function(instanceId, id) { + delete contextMenus.handlersForId(instanceId, id)[id]; + }); + + callbacks.update = + createCustomCallback(function(instanceId, id, updateProperties) { + var onclick = updateProperties.onclick; + if (onclick) { + contextMenus.ensureListenerSetup(); + contextMenus.handlersForId(instanceId, id)[id] = onclick; + } else if (onclick === null) { + // When onclick is explicitly set to null, remove the event listener. + delete contextMenus.handlersForId(instanceId, id)[id]; + } + }); + + callbacks.removeAll = createCustomCallback(function(instanceId) { + delete contextMenus.handlers[instanceId]; + }); + + return { + requestHandlers: requestHandlers, + callbacks: callbacks + }; +} + +exports.$set('create', createContextMenusHandlers); diff --git a/chromium/extensions/renderer/resources/data_receiver.js b/chromium/extensions/renderer/resources/data_receiver.js new file mode 100644 index 00000000000..a248ffbf4be --- /dev/null +++ b/chromium/extensions/renderer/resources/data_receiver.js @@ -0,0 +1,336 @@ +// 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. + +define('data_receiver', [ + 'device/serial/data_stream.mojom', + 'device/serial/data_stream_serialization.mojom', + 'mojo/public/js/core', + 'mojo/public/js/router', +], function(dataStream, serialization, core, router) { + /** + * @module data_receiver + */ + + /** + * A pending receive operation. + * @constructor + * @alias module:data_receiver~PendingReceive + * @private + */ + function PendingReceive() { + /** + * The promise that will be resolved or rejected when this receive completes + * or fails, respectively. + * @type {!Promise<ArrayBuffer>} + * @private + */ + this.promise_ = new Promise(function(resolve, reject) { + /** + * The callback to call with the data received on success. + * @type {Function} + * @private + */ + this.dataCallback_ = resolve; + /** + * The callback to call with the error on failure. + * @type {Function} + * @private + */ + this.errorCallback_ = reject; + }.bind(this)); + } + + /** + * Returns the promise that will be resolved when this operation completes or + * rejected if an error occurs. + * @return {Promise<ArrayBuffer>} A promise to the data received. + */ + PendingReceive.prototype.getPromise = function() { + return this.promise_; + }; + + /** + * Dispatches received data to the promise returned by + * [getPromise]{@link module:data_receiver.PendingReceive#getPromise}. + * @param {!ArrayBuffer} data The data to dispatch. + */ + PendingReceive.prototype.dispatchData = function(data) { + this.dataCallback_(data); + }; + + /** + * Dispatches an error if the offset of the error has been reached. + * @param {!PendingReceiveError} error The error to dispatch. + * @param {number} bytesReceived The number of bytes that have been received. + */ + PendingReceive.prototype.dispatchError = function(error) { + if (error.queuePosition > 0) + return false; + + var e = new Error(); + e.error = error.error; + this.errorCallback_(e); + return true; + }; + + /** + * Unconditionally dispatches an error. + * @param {number} error The error to dispatch. + */ + PendingReceive.prototype.dispatchFatalError = function(error) { + var e = new Error(); + e.error = error; + this.errorCallback_(e); + }; + + /** + * A DataReceiver that receives data from a DataSource. + * @param {!MojoHandle} source The handle to the DataSource. + * @param {!MojoHandle} client The handle to the DataSourceClient. + * @param {number} bufferSize How large a buffer to use. + * @param {number} fatalErrorValue The receive error value to report in the + * event of a fatal error. + * @constructor + * @alias module:data_receiver.DataReceiver + */ + function DataReceiver(source, client, bufferSize, fatalErrorValue) { + this.init_(source, client, fatalErrorValue, 0, null, [], false); + this.source_.init(bufferSize); + } + + DataReceiver.prototype = + $Object.create(dataStream.DataSourceClient.stubClass.prototype); + + /** + * Closes this DataReceiver. + */ + DataReceiver.prototype.close = function() { + if (this.shutDown_) + return; + this.shutDown_ = true; + this.router_.close(); + this.clientRouter_.close(); + if (this.receive_) { + this.receive_.dispatchFatalError(this.fatalErrorValue_); + this.receive_ = null; + } + }; + + /** + * Initialize this DataReceiver. + * @param {!MojoHandle} source A handle to the DataSource. + * @param {!MojoHandle} client A handle to the DataSourceClient. + * @param {number} fatalErrorValue The error to dispatch in the event of a + * fatal error. + * @param {number} bytesReceived The number of bytes already received. + * @param {PendingReceiveError} pendingError The pending error if there is + * one. + * @param {!Array<!ArrayBuffer>} pendingData Data received from the + * DataSource not yet requested by the client. + * @param {boolean} paused Whether the DataSource is paused. + * @private + */ + DataReceiver.prototype.init_ = function(source, client, fatalErrorValue, + bytesReceived, pendingError, + pendingData, paused) { + /** + * The [Router]{@link module:mojo/public/js/router.Router} for the + * connection to the DataSource. + * @private + */ + this.router_ = new router.Router(source); + /** + * The [Router]{@link module:mojo/public/js/router.Router} for the + * connection to the DataSource. + * @private + */ + this.clientRouter_ = new router.Router(client); + /** + * The connection to the DataSource. + * @private + */ + this.source_ = new dataStream.DataSource.proxyClass(this.router_); + this.client_ = new dataStream.DataSourceClient.stubClass(this); + this.clientRouter_.setIncomingReceiver(this.client_); + /** + * The current receive operation. + * @type {module:data_receiver~PendingReceive} + * @private + */ + this.receive_ = null; + /** + * The error to be dispatched in the event of a fatal error. + * @const {number} + * @private + */ + this.fatalErrorValue_ = fatalErrorValue; + /** + * The pending error if there is one. + * @type {PendingReceiveError} + * @private + */ + this.pendingError_ = pendingError; + /** + * Whether the DataSource is paused. + * @type {boolean} + * @private + */ + this.paused_ = paused; + /** + * A queue of data that has been received from the DataSource, but not + * consumed by the client. + * @type {module:data_receiver~PendingData[]} + * @private + */ + this.pendingDataBuffers_ = pendingData; + /** + * Whether this DataReceiver has shut down. + * @type {boolean} + * @private + */ + this.shutDown_ = false; + }; + + /** + * Serializes this DataReceiver. + * This will cancel a receive if one is in progress. + * @return {!Promise<SerializedDataReceiver>} A promise that will resolve to + * the serialization of this DataReceiver. If this DataReceiver has shut + * down, the promise will resolve to null. + */ + DataReceiver.prototype.serialize = function() { + if (this.shutDown_) + return Promise.resolve(null); + + if (this.receive_) { + this.receive_.dispatchFatalError(this.fatalErrorValue_); + this.receive_ = null; + } + var serialized = new serialization.SerializedDataReceiver(); + serialized.source = this.router_.connector_.handle_; + serialized.client = this.clientRouter_.connector_.handle_; + serialized.fatal_error_value = this.fatalErrorValue_; + serialized.paused = this.paused_; + serialized.pending_error = this.pendingError_; + serialized.pending_data = []; + $Array.forEach(this.pendingDataBuffers_, function(buffer) { + serialized.pending_data.push(new Uint8Array(buffer)); + }); + this.router_.connector_.handle_ = null; + this.router_.close(); + this.clientRouter_.connector_.handle_ = null; + this.clientRouter_.close(); + this.shutDown_ = true; + return Promise.resolve(serialized); + }; + + /** + * Deserializes a SerializedDataReceiver. + * @param {SerializedDataReceiver} serialized The serialized DataReceiver. + * @return {!DataReceiver} The deserialized DataReceiver. + */ + DataReceiver.deserialize = function(serialized) { + var receiver = $Object.create(DataReceiver.prototype); + receiver.deserialize_(serialized); + return receiver; + }; + + /** + * Deserializes a SerializedDataReceiver into this DataReceiver. + * @param {SerializedDataReceiver} serialized The serialized DataReceiver. + * @private + */ + DataReceiver.prototype.deserialize_ = function(serialized) { + if (!serialized) { + this.shutDown_ = true; + return; + } + var pendingData = []; + $Array.forEach(serialized.pending_data, function(data) { + var buffer = new Uint8Array(data.length); + buffer.set(data); + pendingData.push(buffer.buffer); + }); + this.init_(serialized.source, serialized.client, + serialized.fatal_error_value, serialized.bytes_received, + serialized.pending_error, pendingData, serialized.paused); + }; + + /** + * Receive data from the DataSource. + * @return {Promise<ArrayBuffer>} A promise to the received data. If an error + * occurs, the promise will reject with an Error object with a property + * error containing the error code. + * @throws Will throw if this has encountered a fatal error or another receive + * is in progress. + */ + DataReceiver.prototype.receive = function() { + if (this.shutDown_) + throw new Error('DataReceiver has been closed'); + if (this.receive_) + throw new Error('Receive already in progress.'); + var receive = new PendingReceive(); + var promise = receive.getPromise(); + if (this.pendingError_ && + receive.dispatchError(this.pendingError_)) { + this.pendingError_ = null; + this.paused_ = true; + return promise; + } + if (this.paused_) { + this.source_.resume(); + this.paused_ = false; + } + this.receive_ = receive; + this.dispatchData_(); + return promise; + }; + + DataReceiver.prototype.dispatchData_ = function() { + if (!this.receive_) { + this.close(); + return; + } + if (this.pendingDataBuffers_.length) { + this.receive_.dispatchData(this.pendingDataBuffers_[0]); + this.source_.reportBytesReceived(this.pendingDataBuffers_[0].byteLength); + this.receive_ = null; + this.pendingDataBuffers_.shift(); + if (this.pendingError_) + this.pendingError_.queuePosition--; + } + }; + + /** + * Invoked by the DataSource when an error is encountered. + * @param {number} offset The location at which the error occurred. + * @param {number} error The error that occurred. + * @private + */ + DataReceiver.prototype.onError = function(error) { + if (this.shutDown_) + return; + + var pendingError = new serialization.PendingReceiveError(); + pendingError.error = error; + pendingError.queuePosition = this.pendingDataBuffers_.length; + if (this.receive_ && this.receive_.dispatchError(pendingError)) { + this.receive_ = null; + this.paused_ = true; + return; + } + this.pendingError_ = pendingError; + }; + + DataReceiver.prototype.onData = function(data) { + var buffer = new ArrayBuffer(data.length); + var uintView = new Uint8Array(buffer); + uintView.set(data); + this.pendingDataBuffers_.push(buffer); + if (this.receive_) + this.dispatchData_(); + }; + + return {DataReceiver: DataReceiver}; +}); diff --git a/chromium/extensions/renderer/resources/data_sender.js b/chromium/extensions/renderer/resources/data_sender.js new file mode 100644 index 00000000000..5fa8708ba33 --- /dev/null +++ b/chromium/extensions/renderer/resources/data_sender.js @@ -0,0 +1,335 @@ +// 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. + +define('data_sender', [ + 'device/serial/data_stream.mojom', + 'device/serial/data_stream_serialization.mojom', + 'mojo/public/js/core', + 'mojo/public/js/router', +], function(dataStreamMojom, serialization, core, routerModule) { + /** + * @module data_sender + */ + + /** + * A pending send operation. + * @param {!ArrayBuffer} data The data to be sent. + * @constructor + * @alias module:data_sender~PendingSend + * @private + */ + function PendingSend(data) { + /** + * The data to be sent. + * @type {ArrayBuffer} + * @private + */ + this.data_ = data; + /** + * The total length of data to be sent. + * @type {number} + * @private + */ + this.length_ = data.byteLength; + /** + * The promise that will be resolved or rejected when this send completes + * or fails, respectively. + * @type {!Promise<number>} + * @private + */ + this.promise_ = new Promise(function(resolve, reject) { + /** + * The callback to call on success. + * @type {Function} + * @private + */ + this.successCallback_ = resolve; + /** + * The callback to call with the error on failure. + * @type {Function} + * @private + */ + this.errorCallback_ = reject; + }.bind(this)); + } + + /** + * Returns the promise that will be resolved when this operation completes or + * rejected if an error occurs. + * @return {!Promise<number>} A promise to the number of bytes sent. + */ + PendingSend.prototype.getPromise = function() { + return this.promise_; + }; + + /** + * Invoked when the DataSink reports that bytes have been sent. Resolves the + * promise returned by + * [getPromise()]{@link module:data_sender~PendingSend#getPromise} once all + * bytes have been reported as sent. + */ + PendingSend.prototype.reportBytesSent = function() { + this.successCallback_(this.length_); + }; + + /** + * Invoked when the DataSink reports an error. Rejects the promise returned by + * [getPromise()]{@link module:data_sender~PendingSend#getPromise} unless the + * error occurred after this send, that is, unless numBytes is greater than + * the nubmer of outstanding bytes. + * @param {number} numBytes The number of bytes sent. + * @param {number} error The error reported by the DataSink. + */ + PendingSend.prototype.reportBytesSentAndError = function(numBytes, error) { + var e = new Error(); + e.error = error; + e.bytesSent = numBytes; + this.errorCallback_(e); + }; + + /** + * Writes pending data into the data pipe. + * @param {!DataSink} sink The DataSink to receive the data. + * @return {!Object} result The send result. + * @return {boolean} result.completed Whether all of the pending data was + * sent. + */ + PendingSend.prototype.sendData = function(sink) { + var dataSent = sink.onData(new Uint8Array(this.data_)); + this.data_ = null; + return dataSent; + }; + + /** + * A DataSender that sends data to a DataSink. + * @param {!MojoHandle} sink The handle to the DataSink. + * @param {number} bufferSize How large a buffer to use for data. + * @param {number} fatalErrorValue The send error value to report in the + * event of a fatal error. + * @constructor + * @alias module:data_sender.DataSender + */ + function DataSender(sink, bufferSize, fatalErrorValue) { + this.init_(sink, fatalErrorValue); + } + + /** + * Closes this DataSender. + */ + DataSender.prototype.close = function() { + if (this.shutDown_) + return; + this.shutDown_ = true; + this.router_.close(); + while (this.sendsAwaitingAck_.length) { + this.sendsAwaitingAck_.pop().reportBytesSentAndError( + 0, this.fatalErrorValue_); + } + this.callCancelCallback_(); + }; + + /** + * Initialize this DataSender. + * @param {!MojoHandle} sink A handle to the DataSink. + * @param {number} fatalErrorValue The error to dispatch in the event of a + * fatal error. + * @private + */ + DataSender.prototype.init_ = function(sink, fatalErrorValue) { + /** + * The error to be dispatched in the event of a fatal error. + * @const {number} + * @private + */ + this.fatalErrorValue_ = fatalErrorValue; + /** + * Whether this DataSender has shut down. + * @type {boolean} + * @private + */ + this.shutDown_ = false; + /** + * The [Router]{@link module:mojo/public/js/router.Router} for the + * connection to the DataSink. + * @private + */ + this.router_ = new routerModule.Router(sink); + /** + * The connection to the DataSink. + * @private + */ + this.sink_ = new dataStreamMojom.DataSink.proxyClass(this.router_); + /** + * A queue of sends that have sent their data to the DataSink, but have not + * been received by the DataSink. + * @type {!module:data_sender~PendingSend[]} + * @private + */ + this.sendsAwaitingAck_ = []; + + /** + * The callback that will resolve a pending cancel if one is in progress. + * @type {?Function} + * @private + */ + this.pendingCancel_ = null; + + /** + * The promise that will be resolved when a pending cancel completes if one + * is in progress. + * @type {Promise} + * @private + */ + this.cancelPromise_ = null; + }; + + /** + * Serializes this DataSender. + * This will cancel any sends in progress before the returned promise + * resolves. + * @return {!Promise<SerializedDataSender>} A promise that will resolve to + * the serialization of this DataSender. If this DataSender has shut down, + * the promise will resolve to null. + */ + DataSender.prototype.serialize = function() { + if (this.shutDown_) + return Promise.resolve(null); + + var readyToSerialize = Promise.resolve(); + if (this.sendsAwaitingAck_.length) { + if (this.pendingCancel_) + readyToSerialize = this.cancelPromise_; + else + readyToSerialize = this.cancel(this.fatalErrorValue_); + } + return readyToSerialize.then(function() { + var serialized = new serialization.SerializedDataSender(); + serialized.sink = this.router_.connector_.handle_; + serialized.fatal_error_value = this.fatalErrorValue_; + this.router_.connector_.handle_ = null; + this.router_.close(); + this.shutDown_ = true; + return serialized; + }.bind(this)); + }; + + /** + * Deserializes a SerializedDataSender. + * @param {SerializedDataSender} serialized The serialized DataSender. + * @return {!DataSender} The deserialized DataSender. + */ + DataSender.deserialize = function(serialized) { + var sender = $Object.create(DataSender.prototype); + sender.deserialize_(serialized); + return sender; + }; + + /** + * Deserializes a SerializedDataSender into this DataSender. + * @param {SerializedDataSender} serialized The serialized DataSender. + * @private + */ + DataSender.prototype.deserialize_ = function(serialized) { + if (!serialized) { + this.shutDown_ = true; + return; + } + this.init_(serialized.sink, serialized.fatal_error_value, + serialized.buffer_size); + }; + + /** + * Sends data to the DataSink. + * @return {!Promise<number>} A promise to the number of bytes sent. If an + * error occurs, the promise will reject with an Error object with a + * property error containing the error code. + * @throws Will throw if this has encountered a fatal error or a cancel is in + * progress. + */ + DataSender.prototype.send = function(data) { + if (this.shutDown_) + throw new Error('DataSender has been closed'); + if (this.pendingCancel_) + throw new Error('Cancel in progress'); + var send = new PendingSend(data); + this.sendsAwaitingAck_.push(send); + send.sendData(this.sink_).then(this.reportBytesSentAndError.bind(this)); + return send.getPromise(); + }; + + /** + * Requests the cancellation of any in-progress sends. Calls to + * [send()]{@link module:data_sender.DataSender#send} will fail until the + * cancel has completed. + * @param {number} error The error to report for cancelled sends. + * @return {!Promise} A promise that will resolve when the cancel completes. + * @throws Will throw if this has encountered a fatal error or another cancel + * is in progress. + */ + DataSender.prototype.cancel = function(error) { + if (this.shutDown_) + throw new Error('DataSender has been closed'); + if (this.pendingCancel_) + throw new Error('Cancel already in progress'); + if (this.sendsAwaitingAck_.length == 0) + return Promise.resolve(); + + this.sink_.cancel(error); + this.cancelPromise_ = new Promise(function(resolve) { + this.pendingCancel_ = resolve; + }.bind(this)); + return this.cancelPromise_; + }; + + /** + * Calls and clears the pending cancel callback if one is pending. + * @private + */ + DataSender.prototype.callCancelCallback_ = function() { + if (this.pendingCancel_) { + this.cancelPromise_ = null; + this.pendingCancel_(); + this.pendingCancel_ = null; + } + }; + + /** + * Invoked by the DataSink to report that data has been successfully sent. + * @private + */ + DataSender.prototype.reportBytesSent = function() { + var result = this.sendsAwaitingAck_[0].reportBytesSent(); + this.sendsAwaitingAck_.shift(); + + // A cancel is completed when all of the sends that were in progress have + // completed or failed. This is the case where all sends complete + // successfully. + if (this.sendsAwaitingAck_.length == 0) + this.callCancelCallback_(); + }; + + /** + * Invoked by the DataSink to report an error in sending data. + * @param {number} numBytes The number of bytes sent. + * @param {number} error The error reported by the DataSink. + * @private + */ + DataSender.prototype.reportBytesSentAndError = function(result) { + var numBytes = result.bytes_sent; + var error = result.error; + if (!error) { + this.reportBytesSent(); + return; + } + var result = + this.sendsAwaitingAck_[0].reportBytesSentAndError(numBytes, error); + this.sendsAwaitingAck_.shift(); + if (this.sendsAwaitingAck_.length) + return; + this.callCancelCallback_(); + this.sink_.clearError(); + }; + + return {DataSender: DataSender}; +}); diff --git a/chromium/extensions/renderer/resources/declarative_webrequest_custom_bindings.js b/chromium/extensions/renderer/resources/declarative_webrequest_custom_bindings.js new file mode 100644 index 00000000000..e9da12b7077 --- /dev/null +++ b/chromium/extensions/renderer/resources/declarative_webrequest_custom_bindings.js @@ -0,0 +1,96 @@ +// Copyright (c) 2012 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. + +// Custom binding for the declarativeWebRequest API. + +var binding = require('binding').Binding.create('declarativeWebRequest'); + +var utils = require('utils'); +var validate = require('schemaUtils').validate; + +binding.registerCustomHook(function(api) { + var declarativeWebRequest = api.compiledApi; + + // Returns the schema definition of type |typeId| defined in |namespace|. + function getSchema(typeId) { + return utils.lookup(api.schema.types, + 'id', + 'declarativeWebRequest.' + typeId); + } + + // Helper function for the constructor of concrete datatypes of the + // declarative webRequest API. + // Makes sure that |this| contains the union of parameters and + // {'instanceType': 'declarativeWebRequest.' + typeId} and validates the + // generated union dictionary against the schema for |typeId|. + function setupInstance(instance, parameters, typeId) { + for (var key in parameters) { + if ($Object.hasOwnProperty(parameters, key)) { + instance[key] = parameters[key]; + } + } + instance.instanceType = 'declarativeWebRequest.' + typeId; + var schema = getSchema(typeId); + validate([instance], [schema]); + } + + // Setup all data types for the declarative webRequest API. + declarativeWebRequest.RequestMatcher = function(parameters) { + setupInstance(this, parameters, 'RequestMatcher'); + }; + declarativeWebRequest.CancelRequest = function(parameters) { + setupInstance(this, parameters, 'CancelRequest'); + }; + declarativeWebRequest.RedirectRequest = function(parameters) { + setupInstance(this, parameters, 'RedirectRequest'); + }; + declarativeWebRequest.SetRequestHeader = function(parameters) { + setupInstance(this, parameters, 'SetRequestHeader'); + }; + declarativeWebRequest.RemoveRequestHeader = function(parameters) { + setupInstance(this, parameters, 'RemoveRequestHeader'); + }; + declarativeWebRequest.AddResponseHeader = function(parameters) { + setupInstance(this, parameters, 'AddResponseHeader'); + }; + declarativeWebRequest.RemoveResponseHeader = function(parameters) { + setupInstance(this, parameters, 'RemoveResponseHeader'); + }; + declarativeWebRequest.RedirectToTransparentImage = + function(parameters) { + setupInstance(this, parameters, 'RedirectToTransparentImage'); + }; + declarativeWebRequest.RedirectToEmptyDocument = function(parameters) { + setupInstance(this, parameters, 'RedirectToEmptyDocument'); + }; + declarativeWebRequest.RedirectByRegEx = function(parameters) { + setupInstance(this, parameters, 'RedirectByRegEx'); + }; + declarativeWebRequest.IgnoreRules = function(parameters) { + setupInstance(this, parameters, 'IgnoreRules'); + }; + declarativeWebRequest.AddRequestCookie = function(parameters) { + setupInstance(this, parameters, 'AddRequestCookie'); + }; + declarativeWebRequest.AddResponseCookie = function(parameters) { + setupInstance(this, parameters, 'AddResponseCookie'); + }; + declarativeWebRequest.EditRequestCookie = function(parameters) { + setupInstance(this, parameters, 'EditRequestCookie'); + }; + declarativeWebRequest.EditResponseCookie = function(parameters) { + setupInstance(this, parameters, 'EditResponseCookie'); + }; + declarativeWebRequest.RemoveRequestCookie = function(parameters) { + setupInstance(this, parameters, 'RemoveRequestCookie'); + }; + declarativeWebRequest.RemoveResponseCookie = function(parameters) { + setupInstance(this, parameters, 'RemoveResponseCookie'); + }; + declarativeWebRequest.SendMessageToExtension = function(parameters) { + setupInstance(this, parameters, 'SendMessageToExtension'); + }; +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/display_source_custom_bindings.js b/chromium/extensions/renderer/resources/display_source_custom_bindings.js new file mode 100644 index 00000000000..38d7e3c791e --- /dev/null +++ b/chromium/extensions/renderer/resources/display_source_custom_bindings.js @@ -0,0 +1,69 @@ +// Copyright 2015 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. + +// Custom binding for the Display Source API. + +var binding = require('binding').Binding.create('displaySource'); +var chrome = requireNative('chrome').GetChrome(); +var lastError = require('lastError'); +var natives = requireNative('display_source'); +var logging = requireNative('logging'); + +var callbacksInfo = {}; + +function callbackWrapper(callback, method, message) { + if (callback == undefined) + return; + + try { + if (message !== null) + lastError.set(method, message, null, chrome); + callback(); + } finally { + lastError.clear(chrome); + } +} + +function callCompletionCallback(callbackId, error_message) { + try { + var callbackInfo = callbacksInfo[callbackId]; + logging.DCHECK(callbackInfo != null); + callbackWrapper(callbackInfo.callback, callbackInfo.method, error_message); + } finally { + delete callbacksInfo[callbackId]; + } +} + +binding.registerCustomHook(function(bindingsAPI, extensionId) { + var apiFunctions = bindingsAPI.apiFunctions; + apiFunctions.setHandleRequest( + 'startSession', function(sessionInfo, callback) { + try { + var callId = natives.StartSession(sessionInfo); + callbacksInfo[callId] = { + callback: callback, + method: 'displaySource.startSession' + }; + } catch (e) { + callbackWrapper(callback, 'displaySource.startSession', e.message); + } + }); + apiFunctions.setHandleRequest( + 'terminateSession', function(sink_id, callback) { + try { + var callId = natives.TerminateSession(sink_id); + callbacksInfo[callId] = { + callback: callback, + method: 'displaySource.terminateSession' + }; + } catch (e) { + callbackWrapper( + callback, 'displaySource.terminateSession', e.message); + } + }); +}); + +exports.$set('binding', binding.generate()); +// Called by C++. +exports.$set('callCompletionCallback', callCompletionCallback); diff --git a/chromium/extensions/renderer/resources/entry_id_manager.js b/chromium/extensions/renderer/resources/entry_id_manager.js new file mode 100644 index 00000000000..8e23942f93a --- /dev/null +++ b/chromium/extensions/renderer/resources/entry_id_manager.js @@ -0,0 +1,52 @@ +// 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. + +var fileSystemNatives = requireNative('file_system_natives'); + +var nameToIds = {}; +var idsToEntries = {}; + +function computeName(entry) { + return entry.filesystem.name + ':' + entry.fullPath; +} + +function computeId(entry) { + var fileSystemId = fileSystemNatives.CrackIsolatedFileSystemName( + entry.filesystem.name); + if (!fileSystemId) + return null; + // Strip the leading '/' from the path. + return fileSystemId + ':' + $String.slice(entry.fullPath, 1); +} + +function registerEntry(id, entry) { + var name = computeName(entry); + nameToIds[name] = id; + idsToEntries[id] = entry; +} + +function getEntryId(entry) { + var name = null; + try { + name = computeName(entry); + } catch(e) { + return null; + } + var id = nameToIds[name]; + if (id != null) + return id; + + // If an entry has not been registered, compute its id and register it. + id = computeId(entry); + registerEntry(id, entry); + return id; +} + +function getEntryById(id) { + return idsToEntries[id]; +} + +exports.$set('registerEntry', registerEntry); +exports.$set('getEntryId', getEntryId); +exports.$set('getEntryById', getEntryById); diff --git a/chromium/extensions/renderer/resources/event.js b/chromium/extensions/renderer/resources/event.js new file mode 100644 index 00000000000..2f9aa3c7a55 --- /dev/null +++ b/chromium/extensions/renderer/resources/event.js @@ -0,0 +1,520 @@ +// 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. + +// TODO(robwu): Fix indentation. + + var exceptionHandler = require('uncaught_exception_handler'); + var eventNatives = requireNative('event_natives'); + var logging = requireNative('logging'); + var schemaRegistry = requireNative('schema_registry'); + var sendRequest = require('sendRequest').sendRequest; + var utils = require('utils'); + var validate = require('schemaUtils').validate; + + // Schemas for the rule-style functions on the events API that + // only need to be generated occasionally, so populate them lazily. + var ruleFunctionSchemas = { + __proto__: null, + // These values are set lazily: + // addRules: {}, + // getRules: {}, + // removeRules: {} + }; + + // This function ensures that |ruleFunctionSchemas| is populated. + function ensureRuleSchemasLoaded() { + if (ruleFunctionSchemas.addRules) + return; + var eventsSchema = schemaRegistry.GetSchema("events"); + var eventType = utils.lookup(eventsSchema.types, 'id', 'events.Event'); + + ruleFunctionSchemas.addRules = + utils.lookup(eventType.functions, 'name', 'addRules'); + ruleFunctionSchemas.getRules = + utils.lookup(eventType.functions, 'name', 'getRules'); + ruleFunctionSchemas.removeRules = + utils.lookup(eventType.functions, 'name', 'removeRules'); + } + + // A map of event names to the event object that is registered to that name. + var attachedNamedEvents = {__proto__: null}; + + // A map of functions that massage event arguments before they are dispatched. + // Key is event name, value is function. + var eventArgumentMassagers = {__proto__: null}; + + // An attachment strategy for events that aren't attached to the browser. + // This applies to events with the "unmanaged" option and events without + // names. + function NullAttachmentStrategy(event) { + this.event_ = event; + } + $Object.setPrototypeOf(NullAttachmentStrategy.prototype, null); + + NullAttachmentStrategy.prototype.onAddedListener = + function(listener) { + }; + NullAttachmentStrategy.prototype.onRemovedListener = + function(listener) { + }; + NullAttachmentStrategy.prototype.detach = function(manual) { + }; + NullAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + // |ids| is for filtered events only. + return this.event_.listeners; + }; + + // Handles adding/removing/dispatching listeners for unfiltered events. + function UnfilteredAttachmentStrategy(event) { + this.event_ = event; + } + $Object.setPrototypeOf(UnfilteredAttachmentStrategy.prototype, null); + + UnfilteredAttachmentStrategy.prototype.onAddedListener = + function(listener) { + // Only attach / detach on the first / last listener removed. + if (this.event_.listeners.length == 0) + eventNatives.AttachEvent(this.event_.eventName); + }; + + UnfilteredAttachmentStrategy.prototype.onRemovedListener = + function(listener) { + if (this.event_.listeners.length == 0) + this.detach(true); + }; + + UnfilteredAttachmentStrategy.prototype.detach = function(manual) { + eventNatives.DetachEvent(this.event_.eventName, manual); + }; + + UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + // |ids| is for filtered events only. + return this.event_.listeners; + }; + + function FilteredAttachmentStrategy(event) { + this.event_ = event; + this.listenerMap_ = {__proto__: null}; + } + $Object.setPrototypeOf(FilteredAttachmentStrategy.prototype, null); + + utils.defineProperty(FilteredAttachmentStrategy, 'idToEventMap', + {__proto__: null}); + + FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { + var id = eventNatives.AttachFilteredEvent(this.event_.eventName, + listener.filters || {}); + if (id == -1) + throw new Error("Can't add listener"); + listener.id = id; + this.listenerMap_[id] = listener; + FilteredAttachmentStrategy.idToEventMap[id] = this.event_; + }; + + FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { + this.detachListener(listener, true); + }; + + FilteredAttachmentStrategy.prototype.detachListener = + function(listener, manual) { + if (listener.id == undefined) + throw new Error("listener.id undefined - '" + listener + "'"); + var id = listener.id; + delete this.listenerMap_[id]; + delete FilteredAttachmentStrategy.idToEventMap[id]; + eventNatives.DetachFilteredEvent(id, manual); + }; + + FilteredAttachmentStrategy.prototype.detach = function(manual) { + for (var i in this.listenerMap_) + this.detachListener(this.listenerMap_[i], manual); + }; + + FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + var result = []; + for (var i = 0; i < ids.length; i++) + $Array.push(result, this.listenerMap_[ids[i]]); + return result; + }; + + function parseEventOptions(opt_eventOptions) { + return $Object.assign({ + __proto__: null, + }, { + // Event supports adding listeners with filters ("filtered events"), for + // example as used in the webNavigation API. + // + // event.addListener(listener, [filter1, filter2]); + supportsFilters: false, + + // Events supports vanilla events. Most APIs use these. + // + // event.addListener(listener); + supportsListeners: true, + + // Event supports adding rules ("declarative events") rather than + // listeners, for example as used in the declarativeWebRequest API. + // + // event.addRules([rule1, rule2]); + supportsRules: false, + + // Event is unmanaged in that the browser has no knowledge of its + // existence; it's never invoked, doesn't keep the renderer alive, and + // the bindings system has no knowledge of it. + // + // Both events created by user code (new chrome.Event()) and messaging + // events are unmanaged, though in the latter case the browser *does* + // interact indirectly with them via IPCs written by hand. + unmanaged: false, + }, opt_eventOptions); + } + + // Event object. If opt_eventName is provided, this object represents + // the unique instance of that named event, and dispatching an event + // with that name will route through this object's listeners. Note that + // opt_eventName is required for events that support rules. + // + // Example: + // var Event = require('event_bindings').Event; + // chrome.tabs.onChanged = new Event("tab-changed"); + // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); + // Event.dispatch("tab-changed", "hi"); + // will result in an alert dialog that says 'hi'. + // + // If opt_eventOptions exists, it is a dictionary that contains the boolean + // entries "supportsListeners" and "supportsRules". + // If opt_webViewInstanceId exists, it is an integer uniquely identifying a + // <webview> tag within the embedder. If it does not exist, then this is an + // extension event rather than a <webview> event. + function EventImpl(opt_eventName, opt_argSchemas, opt_eventOptions, + opt_webViewInstanceId) { + this.eventName = opt_eventName; + this.argSchemas = opt_argSchemas; + this.listeners = []; + this.eventOptions = parseEventOptions(opt_eventOptions); + this.webViewInstanceId = opt_webViewInstanceId || 0; + + if (!this.eventName) { + if (this.eventOptions.supportsRules) + throw new Error("Events that support rules require an event name."); + // Events without names cannot be managed by the browser by definition + // (the browser has no way of identifying them). + this.eventOptions.unmanaged = true; + } + + // Track whether the event has been destroyed to help track down the cause + // of http://crbug.com/258526. + // This variable will eventually hold the stack trace of the destroy call. + // TODO(kalman): Delete this and replace with more sound logic that catches + // when events are used without being *attached*. + this.destroyed = null; + + if (this.eventOptions.unmanaged) + this.attachmentStrategy = new NullAttachmentStrategy(this); + else if (this.eventOptions.supportsFilters) + this.attachmentStrategy = new FilteredAttachmentStrategy(this); + else + this.attachmentStrategy = new UnfilteredAttachmentStrategy(this); + } + $Object.setPrototypeOf(EventImpl.prototype, null); + + // callback is a function(args, dispatch). args are the args we receive from + // dispatchEvent(), and dispatch is a function(args) that dispatches args to + // its listeners. + function registerArgumentMassager(name, callback) { + if (eventArgumentMassagers[name]) + throw new Error("Massager already registered for event: " + name); + eventArgumentMassagers[name] = callback; + } + + // Dispatches a named event with the given argument array. The args array is + // the list of arguments that will be sent to the event callback. + function dispatchEvent(name, args, filteringInfo) { + var listenerIDs = []; + + if (filteringInfo) + listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo); + + var event = attachedNamedEvents[name]; + if (!event) + return; + + var dispatchArgs = function(args) { + var result = event.dispatch_(args, listenerIDs); + if (result) + logging.DCHECK(!result.validationErrors, result.validationErrors); + return result; + }; + + if (eventArgumentMassagers[name]) + eventArgumentMassagers[name](args, dispatchArgs); + else + dispatchArgs(args); + } + + // Registers a callback to be called when this event is dispatched. + EventImpl.prototype.addListener = function(cb, filters) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + if (this.eventOptions.maxListeners && + this.getListenerCount_() >= this.eventOptions.maxListeners) { + throw new Error("Too many listeners for " + this.eventName); + } + if (filters) { + if (!this.eventOptions.supportsFilters) + throw new Error("This event does not support filters."); + if (filters.url && !(filters.url instanceof Array)) + throw new Error("filters.url should be an array."); + if (filters.serviceType && + !(typeof filters.serviceType === 'string')) { + throw new Error("filters.serviceType should be a string.") + } + } + var listener = {callback: cb, filters: filters}; + this.attach_(listener); + $Array.push(this.listeners, listener); + }; + + EventImpl.prototype.attach_ = function(listener) { + this.attachmentStrategy.onAddedListener(listener); + + if (this.listeners.length == 0) { + if (this.eventName) { + if (attachedNamedEvents[this.eventName]) { + throw new Error("Event '" + this.eventName + + "' is already attached."); + } + attachedNamedEvents[this.eventName] = this; + } + } + }; + + // Unregisters a callback. + EventImpl.prototype.removeListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + + var idx = this.findListener_(cb); + if (idx == -1) + return; + + var removedListener = $Array.splice(this.listeners, idx, 1)[0]; + this.attachmentStrategy.onRemovedListener(removedListener); + + if (this.listeners.length == 0) { + if (this.eventName) { + if (!attachedNamedEvents[this.eventName]) { + throw new Error( + "Event '" + this.eventName + "' is not attached."); + } + delete attachedNamedEvents[this.eventName]; + } + } + }; + + // Test if the given callback is registered for this event. + EventImpl.prototype.hasListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + return this.findListener_(cb) > -1; + }; + + // Test if any callbacks are registered for this event. + EventImpl.prototype.hasListeners = function() { + return this.getListenerCount_() > 0; + }; + + // Returns the number of listeners on this event. + EventImpl.prototype.getListenerCount_ = function() { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + return this.listeners.length; + }; + + // Returns the index of the given callback if registered, or -1 if not + // found. + EventImpl.prototype.findListener_ = function(cb) { + for (var i = 0; i < this.listeners.length; i++) { + if (this.listeners[i].callback == cb) { + return i; + } + } + + return -1; + }; + + EventImpl.prototype.dispatch_ = function(args, listenerIDs) { + if (this.destroyed) { + throw new Error(this.eventName + ' was already destroyed at: ' + + this.destroyed); + } + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + + if (this.argSchemas && logging.DCHECK_IS_ON()) { + try { + validate(args, this.argSchemas); + } catch (e) { + e.message += ' in ' + this.eventName; + throw e; + } + } + + // Make a copy of the listeners in case the listener list is modified + // while dispatching the event. + var listeners = $Array.slice( + this.attachmentStrategy.getListenersByIDs(listenerIDs)); + + var results = []; + for (var i = 0; i < listeners.length; i++) { + try { + var result = this.wrapper.dispatchToListener(listeners[i].callback, + args); + if (result !== undefined) + $Array.push(results, result); + } catch (e) { + exceptionHandler.handle('Error in event handler for ' + + (this.eventName ? this.eventName : '(unknown)'), + e); + } + } + if (results.length) + return {results: results}; + } + + // Can be overridden to support custom dispatching. + EventImpl.prototype.dispatchToListener = function(callback, args) { + return $Function.apply(callback, null, args); + } + + // Dispatches this event object to all listeners, passing all supplied + // arguments to this function each listener. + EventImpl.prototype.dispatch = function(varargs) { + return this.dispatch_($Array.slice(arguments), undefined); + }; + + // Detaches this event object from its name. + EventImpl.prototype.detach_ = function() { + this.attachmentStrategy.detach(false); + }; + + EventImpl.prototype.destroy_ = function() { + this.listeners.length = 0; + this.detach_(); + this.destroyed = exceptionHandler.getStackTrace(); + }; + + EventImpl.prototype.addRules = function(rules, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + + // Takes a list of JSON datatype identifiers and returns a schema fragment + // that verifies that a JSON object corresponds to an array of only these + // data types. + function buildArrayOfChoicesSchema(typesList) { + return { + __proto__: null, + 'type': 'array', + 'items': { + __proto__: null, + 'choices': $Array.map(typesList, function(el) { + return { + __proto__: null, + '$ref': el, + }; + }), + } + }; + } + + // Validate conditions and actions against specific schemas of this + // event object type. + // |rules| is an array of JSON objects that follow the Rule type of the + // declarative extension APIs. |conditions| is an array of JSON type + // identifiers that are allowed to occur in the conditions attribute of each + // rule. Likewise, |actions| is an array of JSON type identifiers that are + // allowed to occur in the actions attribute of each rule. + function validateRules(rules, conditions, actions) { + var conditionsSchema = buildArrayOfChoicesSchema(conditions); + var actionsSchema = buildArrayOfChoicesSchema(actions); + $Array.forEach(rules, function(rule) { + validate([rule.conditions], [conditionsSchema]); + validate([rule.actions], [actionsSchema]); + }); + }; + + if (!this.eventOptions.conditions || !this.eventOptions.actions) { + throw new Error('Event ' + this.eventName + ' misses ' + + 'conditions or actions in the API specification.'); + } + + validateRules(rules, + this.eventOptions.conditions, + this.eventOptions.actions); + + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, rules, opt_cb], + $Array.slice(ruleFunctionSchemas.addRules.parameters, 1)); + sendRequest( + "events.addRules", + [this.eventName, this.webViewInstanceId, rules, opt_cb], + ruleFunctionSchemas.addRules.parameters); + } + + EventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, ruleIdentifiers, opt_cb], + $Array.slice(ruleFunctionSchemas.removeRules.parameters, 1)); + sendRequest("events.removeRules", + [this.eventName, + this.webViewInstanceId, + ruleIdentifiers, + opt_cb], + ruleFunctionSchemas.removeRules.parameters); + } + + EventImpl.prototype.getRules = function(ruleIdentifiers, cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, ruleIdentifiers, cb], + $Array.slice(ruleFunctionSchemas.getRules.parameters, 1)); + + sendRequest( + "events.getRules", + [this.eventName, this.webViewInstanceId, ruleIdentifiers, cb], + ruleFunctionSchemas.getRules.parameters); + } + + function Event() { + privates(Event).constructPrivate(this, arguments); + } + utils.expose(Event, EventImpl, { + functions: [ + 'addListener', + 'removeListener', + 'hasListener', + 'hasListeners', + 'dispatchToListener', + 'dispatch', + 'addRules', + 'removeRules', + 'getRules', + ], + }); + + // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc. + exports.$set('Event', Event); + + exports.$set('dispatchEvent', dispatchEvent); + exports.$set('parseEventOptions', parseEventOptions); + exports.$set('registerArgumentMassager', registerArgumentMassager); diff --git a/chromium/extensions/renderer/resources/extension.css b/chromium/extensions/renderer/resources/extension.css new file mode 100644 index 00000000000..808a08ce151 --- /dev/null +++ b/chromium/extensions/renderer/resources/extension.css @@ -0,0 +1,352 @@ +/* + * 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. + * + * This stylesheet is used to apply Chrome styles to extension pages that opt in + * to using them. + * + * These styles have been copied from ui/webui/resources/css/chrome_shared.css + * and ui/webui/resources/css/widgets.css *with CSS class logic removed*, so + * that it's as close to a user-agent stylesheet as possible. + * + * For example, extensions shouldn't be able to set a .link-button class and + * have it do anything. + * + * Other than that, keep this file and chrome_shared.css/widgets.cc in sync as + * much as possible. + */ + +body { + color: #333; + cursor: default; + /* Note that the correct font-family and font-size are set in + * extension_fonts.css. */ + /* This top margin of 14px matches the top padding on the h1 element on + * overlays (see the ".overlay .page h1" selector in overlay.css), which + * every dialogue has. + * + * Similarly, the bottom 14px margin matches the bottom padding of the area + * which hosts the buttons (see the ".overlay .page * .action-area" selector + * in overlay.css). + * + * Both have a padding left/right of 17px. + * + * Note that we're putting this here in the Extension content, rather than + * the WebUI element which contains the content, so that scrollbars in the + * Extension content don't get a 6px margin, which looks quite odd. + */ + margin: 14px 17px; +} + +p { + line-height: 1.8em; +} + +h1, +h2, +h3 { + -webkit-user-select: none; + font-weight: normal; + /* Makes the vertical size of the text the same for all fonts. */ + line-height: 1; +} + +h1 { + font-size: 1.5em; +} + +h2 { + font-size: 1.3em; + margin-bottom: 0.4em; +} + +h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; +} + +a { + color: rgb(17, 85, 204); + text-decoration: underline; +} + +a:active { + color: rgb(5, 37, 119); +} + +/* Default state **************************************************************/ + +:-webkit-any(button, + input[type='button'], + input[type='submit']), +select, +input[type='checkbox'], +input[type='radio'] { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + outline: none; + text-shadow: 0 1px 0 rgb(240, 240, 240); +} + +:-webkit-any(button, + input[type='button'], + input[type='submit']), +select { + min-height: 2em; + min-width: 4em; +<if expr="is_win"> + /* The following platform-specific rule is necessary to get adjacent + * buttons, text inputs, and so forth to align on their borders while also + * aligning on the text's baselines. */ + padding-bottom: 1px; +</if> +} + +:-webkit-any(button, + input[type='button'], + input[type='submit']) { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; +} + +select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(../../../ui/webui/resources/images/select.png), + linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; +} + +html[dir='rtl'] select { + background-position: center left; +} + +input[type='checkbox'] { + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; +} + +input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; +} + +/* TODO(estade): add more types here? */ +input[type='number'], +input[type='password'], +input[type='search'], +input[type='text'], +input[type='url'], +input:not([type]), +textarea { + border: 1px solid #bfbfbf; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + /* Use min-height to accommodate addditional padding for touch as needed. */ + min-height: 2em; + padding: 3px; + outline: none; +<if expr="is_win or is_macosx or is_ios"> + /* For better alignment between adjacent buttons and inputs. */ + padding-bottom: 4px; +</if> +} + +input[type='search'] { + -webkit-appearance: textfield; + /* NOTE: Keep a relatively high min-width for this so we don't obscure the end + * of the default text in relatively spacious languages (i.e. German). */ + min-width: 160px; +} + +/* Remove when https://bugs.webkit.org/show_bug.cgi?id=51499 is fixed. + * TODO(dbeam): are there more types that would benefit from this? */ +input[type='search']::-webkit-textfield-decoration-container { + direction: inherit; +} + +/* Checked ********************************************************************/ + +input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url(../../../ui/webui/resources/images/check.png); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; +} + +input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; +} + +/* Hover **********************************************************************/ + +:enabled:hover:-webkit-any( + select, + input[type='checkbox'], + input[type='radio'], + :-webkit-any( + button, + input[type='button'], + input[type='submit'])) { + background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), + inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; +} + +:enabled:hover:-webkit-any(select) { + /* OVERRIDE */ + background-image: url(../../../ui/webui/resources/images/select.png), + linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +} + +/* Active *********************************************************************/ + +:enabled:active:-webkit-any( + select, + input[type='checkbox'], + input[type='radio'], + :-webkit-any( + button, + input[type='button'], + input[type='submit'])) { + background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; +} + +:enabled:active:-webkit-any(select) { + /* OVERRIDE */ + background-image: url(../../../ui/webui/resources/images/select.png), + linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +} + +/* Disabled *******************************************************************/ + +:disabled:-webkit-any( + button, + input[type='button'], + input[type='submit']), +select:disabled { + background-image: linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; +} + +select:disabled { + /* OVERRIDE */ + background-image: url(../../../ui/webui/resources/images/disabled_select.png), + linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); +} + +input:disabled:-webkit-any([type='checkbox'], + [type='radio']) { + opacity: .75; +} + +input:disabled:-webkit-any([type='password'], + [type='search'], + [type='text'], + [type='url'], + :not([type])) { + color: #999; +} + +/* Focus **********************************************************************/ + +:enabled:focus:-webkit-any( + select, + input[type='checkbox'], + input[type='number'], + input[type='password'], + input[type='radio'], + input[type='search'], + input[type='text'], + input[type='url'], + input:not([type]), + :-webkit-any( + button, + input[type='button'], + input[type='submit'])) { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: rgb(77, 144, 254); + outline: none; +} + +/* Checkbox/radio helpers ****************************************************** + * + * .checkbox and .radio classes wrap labels. Checkboxes and radios should use + * these classes with the markup structure: + * + * <div class="checkbox"> + * <label> + * <input type="checkbox"></input> + * <span> + * </label> + * </div> + */ + +:-webkit-any(.checkbox, .radio) label { + /* Don't expand horizontally: <http://crbug.com/112091>. */ + align-items: center; + display: inline-flex; + padding-bottom: 7px; + padding-top: 7px; +} + +:-webkit-any(.checkbox, .radio) label input { + flex-shrink: 0; +} + +:-webkit-any(.checkbox, .radio) label input ~ span { + -webkit-margin-start: 0.6em; + /* Make sure long spans wrap at the same horizontal position they start. */ + display: block; +} + +:-webkit-any(.checkbox, .radio) label:hover { + color: black; +} + +label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span { + color: #999; +} diff --git a/chromium/extensions/renderer/resources/extension_custom_bindings.js b/chromium/extensions/renderer/resources/extension_custom_bindings.js new file mode 100644 index 00000000000..68450d56b3a --- /dev/null +++ b/chromium/extensions/renderer/resources/extension_custom_bindings.js @@ -0,0 +1,111 @@ +// 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. + +// Custom binding for the extension API. + +var binding = require('binding').Binding.create('extension'); + +var messaging = require('messaging'); +var runtimeNatives = requireNative('runtime'); +var GetExtensionViews = runtimeNatives.GetExtensionViews; +var chrome = requireNative('chrome').GetChrome(); + +var inIncognitoContext = requireNative('process').InIncognitoContext(); +var sendRequestIsDisabled = requireNative('process').IsSendRequestDisabled(); +var contextType = requireNative('process').GetContextType(); +var manifestVersion = requireNative('process').GetManifestVersion(); + +// This should match chrome.windows.WINDOW_ID_NONE. +// +// We can't use chrome.windows.WINDOW_ID_NONE directly because the +// chrome.windows API won't exist unless this extension has permission for it; +// which may not be the case. +var WINDOW_ID_NONE = -1; + +binding.registerCustomHook(function(bindingsAPI, extensionId) { + var extension = bindingsAPI.compiledApi; + if (manifestVersion < 2) { + chrome.self = extension; + extension.inIncognitoTab = inIncognitoContext; + } + extension.inIncognitoContext = inIncognitoContext; + + var apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setHandleRequest('getViews', function(properties) { + var windowId = WINDOW_ID_NONE; + var type = 'ALL'; + if (properties) { + if (properties.type != null) { + type = properties.type; + } + if (properties.windowId != null) { + windowId = properties.windowId; + } + } + return GetExtensionViews(windowId, type); + }); + + apiFunctions.setHandleRequest('getBackgroundPage', function() { + return GetExtensionViews(-1, 'BACKGROUND')[0] || null; + }); + + apiFunctions.setHandleRequest('getExtensionTabs', function(windowId) { + if (windowId == null) + windowId = WINDOW_ID_NONE; + return GetExtensionViews(windowId, 'TAB'); + }); + + apiFunctions.setHandleRequest('getURL', function(path) { + path = String(path); + if (!path.length || path[0] != '/') + path = '/' + path; + return 'chrome-extension://' + extensionId + path; + }); + + // Alias several messaging deprecated APIs to their runtime counterparts. + var mayNeedAlias = [ + // Types + 'Port', + // Functions + 'connect', 'sendMessage', 'connectNative', 'sendNativeMessage', + // Events + 'onConnect', 'onConnectExternal', 'onMessage', 'onMessageExternal' + ]; + $Array.forEach(mayNeedAlias, function(alias) { + // Checking existence isn't enough since some functions are disabled via + // getters that throw exceptions. Assume that any getter is such a function. + if (chrome.runtime && + $Object.hasOwnProperty(chrome.runtime, alias) && + chrome.runtime.__lookupGetter__(alias) === undefined) { + extension[alias] = chrome.runtime[alias]; + } + }); + + apiFunctions.setUpdateArgumentsPreValidate('sendRequest', + $Function.bind(messaging.sendMessageUpdateArguments, + null, 'sendRequest', false /* hasOptionsArgument */)); + + apiFunctions.setHandleRequest('sendRequest', + function(targetId, request, responseCallback) { + if (sendRequestIsDisabled) + throw new Error(sendRequestIsDisabled); + var port = chrome.runtime.connect(targetId || extensionId, + {name: messaging.kRequestChannel}); + messaging.sendMessageImpl(port, request, responseCallback); + }); + + if (sendRequestIsDisabled) { + extension.onRequest.addListener = function() { + throw new Error(sendRequestIsDisabled); + }; + if (contextType == 'BLESSED_EXTENSION') { + extension.onRequestExternal.addListener = function() { + throw new Error(sendRequestIsDisabled); + }; + } + } +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/extension_fonts.css b/chromium/extensions/renderer/resources/extension_fonts.css new file mode 100644 index 00000000000..464f8ebfc99 --- /dev/null +++ b/chromium/extensions/renderer/resources/extension_fonts.css @@ -0,0 +1,12 @@ +/* + * 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. + * + * This stylesheet is used to apply Chrome system fonts to all extension pages. + */ + +body { + font-family: $FONTFAMILY; + font-size: $FONTSIZE; +} diff --git a/chromium/extensions/renderer/resources/extensions_renderer_resources.grd b/chromium/extensions/renderer/resources/extensions_renderer_resources.grd new file mode 100644 index 00000000000..808b931d542 --- /dev/null +++ b/chromium/extensions/renderer/resources/extensions_renderer_resources.grd @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<grit latest_public_release="0" current_release="1"> + <outputs> + <output filename="grit/extensions_renderer_resources.h" type="rc_header"> + <emit emit_type='prepend'></emit> + </output> + <output filename="extensions_renderer_resources.pak" type="data_package" /> + </outputs> + <release seq="1"> + <includes> + <!-- Extension libraries. --> + <include name="IDR_APP_VIEW_JS" file="guest_view/app_view/app_view.js" type="BINDATA" /> + <include name="IDR_ASYNC_WAITER_JS" file="async_waiter.js" type="BINDATA" /> + <include name="IDR_BROWSER_TEST_ENVIRONMENT_SPECIFIC_BINDINGS_JS" file="browser_test_environment_specific_bindings.js" type="BINDATA" /> + <include name="IDR_DATA_RECEIVER_JS" file="data_receiver.js" type="BINDATA" /> + <include name="IDR_DATA_SENDER_JS" file="data_sender.js" type="BINDATA" /> + <include name="IDR_DATA_STREAM_MOJOM_JS" file="${mojom_root}\device\serial\data_stream.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_DATA_STREAM_SERIALIZATION_MOJOM_JS" file="${mojom_root}\device\serial\data_stream_serialization.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_ENTRY_ID_MANAGER" file="entry_id_manager.js" type="BINDATA" /> + <include name="IDR_EVENT_BINDINGS_JS" file="event.js" type="BINDATA" /> + <include name="IDR_EXTENSION_OPTIONS_JS" file="guest_view/extension_options/extension_options.js" type="BINDATA"/> + <include name="IDR_EXTENSION_OPTIONS_ATTRIBUTES_JS" file="guest_view/extension_options/extension_options_attributes.js" type="BINDATA"/> + <include name="IDR_EXTENSION_OPTIONS_CONSTANTS_JS" file="guest_view/extension_options/extension_options_constants.js" type="BINDATA"/> + <include name="IDR_EXTENSION_OPTIONS_EVENTS_JS" file="guest_view/extension_options/extension_options_events.js" type="BINDATA"/> + <include name="IDR_EXTENSION_VIEW_JS" file="guest_view/extension_view/extension_view.js" type="BINDATA" /> + <include name="IDR_EXTENSION_VIEW_API_METHODS_JS" file="guest_view/extension_view/extension_view_api_methods.js" type="BINDATA" /> + <include name="IDR_EXTENSION_VIEW_ATTRIBUTES_JS" file="guest_view/extension_view/extension_view_attributes.js" type="BINDATA" /> + <include name="IDR_EXTENSION_VIEW_CONSTANTS_JS" file="guest_view/extension_view/extension_view_constants.js" type="BINDATA" /> + <include name="IDR_EXTENSION_VIEW_EVENTS_JS" file="guest_view/extension_view/extension_view_events.js" type="BINDATA" /> + <include name="IDR_EXTENSION_VIEW_INTERNAL_CUSTOM_BINDINGS_JS" file="guest_view/extension_view/extension_view_internal.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_ATTRIBUTES_JS" file="guest_view/guest_view_attributes.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_CONTAINER_JS" file="guest_view/guest_view_container.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_DENY_JS" file="guest_view/guest_view_deny.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_EVENTS_JS" file="guest_view/guest_view_events.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_IFRAME_CONTAINER_JS" file="guest_view/guest_view_iframe_container.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_IFRAME_JS" file="guest_view/guest_view_iframe.js" type="BINDATA" /> + <include name="IDR_GUEST_VIEW_JS" file="guest_view/guest_view.js" type="BINDATA" /> + <include name="IDR_IMAGE_UTIL_JS" file="image_util.js" type="BINDATA" /> + <include name="IDR_JSON_SCHEMA_JS" file="json_schema.js" type="BINDATA" /> + <include name="IDR_KEEP_ALIVE_JS" file="keep_alive.js" type="BINDATA" /> + <include name="IDR_KEEP_ALIVE_MOJOM_JS" file="${mojom_root}\extensions\common\mojo\keep_alive.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_LAST_ERROR_JS" file="last_error.js" type="BINDATA" /> + <include name="IDR_MESSAGING_JS" file="messaging.js" type="BINDATA" /> + <include name="IDR_MESSAGING_UTILS_JS" file="messaging_utils.js" type="BINDATA" /> + <include name="IDR_MIME_HANDLER_PRIVATE_CUSTOM_BINDINGS_JS" file="mime_handler_private_custom_bindings.js" type="BINDATA" /> + <include name="IDR_MIME_HANDLER_MOJOM_JS" file="${mojom_root}\extensions\common\api\mime_handler.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_SCHEMA_UTILS_JS" file="schema_utils.js" type="BINDATA" /> + <include name="IDR_SEND_REQUEST_JS" file="send_request.js" type="BINDATA" /> + <include name="IDR_SERIAL_CUSTOM_BINDINGS_JS" file="serial_custom_bindings.js" type="BINDATA" /> + <include name="IDR_SERIAL_MOJOM_JS" file="${mojom_root}\device\serial\serial.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_SERIAL_SERIALIZATION_MOJOM_JS" file="${mojom_root}\device\serial\serial_serialization.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_SERIAL_SERVICE_JS" file="serial_service.js" type="BINDATA" /> + <include name="IDR_SET_ICON_JS" file="set_icon.js" type="BINDATA" /> + <include name="IDR_STASH_CLIENT_JS" file="stash_client.js" type="BINDATA" /> + <include name="IDR_STASH_MOJOM_JS" file="${mojom_root}\extensions\common\mojo\stash.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_TEST_CUSTOM_BINDINGS_JS" file="test_custom_bindings.js" type="BINDATA" /> + <include name="IDR_UNCAUGHT_EXCEPTION_HANDLER_JS" file="uncaught_exception_handler.js" type="BINDATA" /> + <include name="IDR_UTILS_JS" file="utils.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_ACTION_REQUESTS_JS" file="guest_view/web_view/web_view_action_requests.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_API_METHODS_JS" file="guest_view/web_view/web_view_api_methods.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_ATTRIBUTES_JS" file="guest_view/web_view/web_view_attributes.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_CONSTANTS_JS" file="guest_view/web_view/web_view_constants.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_EVENTS_JS" file="guest_view/web_view/web_view_events.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_EXPERIMENTAL_JS" file="guest_view/web_view/web_view_experimental.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS" file="guest_view/web_view/web_view_internal.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_JS" file="guest_view/web_view/web_view.js" type="BINDATA" /> + + <!-- Custom bindings for APIs. --> + <include name="IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS" file="app_runtime_custom_bindings.js" type="BINDATA" /> + <include name="IDR_APP_WINDOW_CUSTOM_BINDINGS_JS" file="app_window_custom_bindings.js" type="BINDATA" /> + <include name="IDR_BINDING_JS" file="binding.js" type="BINDATA" /> + <include name="IDR_CONTEXT_MENUS_CUSTOM_BINDINGS_JS" file="context_menus_custom_bindings.js" type="BINDATA" /> + <include name="IDR_CONTEXT_MENUS_HANDLERS_JS" file="context_menus_handlers.js" type="BINDATA" /> + <include name="IDR_DECLARATIVE_WEBREQUEST_CUSTOM_BINDINGS_JS" file="declarative_webrequest_custom_bindings.js" type="BINDATA" /> + <include name="IDR_DISPLAY_SOURCE_CUSTOM_BINDINGS_JS" file="display_source_custom_bindings.js" type="BINDATA" /> + <include name="IDR_EXTENSION_CUSTOM_BINDINGS_JS" file="extension_custom_bindings.js" type="BINDATA" /> + <include name="IDR_GREASEMONKEY_API_JS" file="greasemonkey_api.js" type="BINDATA" /> + <include name="IDR_I18N_CUSTOM_BINDINGS_JS" file="i18n_custom_bindings.js" type="BINDATA" /> + <include name="IDR_MOJO_PRIVATE_CUSTOM_BINDINGS_JS" file="mojo_private_custom_bindings.js" type="BINDATA" /> + <include name="IDR_PERMISSIONS_CUSTOM_BINDINGS_JS" file="permissions_custom_bindings.js" type="BINDATA" /> + <include name="IDR_PRINTER_PROVIDER_CUSTOM_BINDINGS_JS" file="printer_provider_custom_bindings.js" type="BINDATA" /> + <include name="IDR_RUNTIME_CUSTOM_BINDINGS_JS" file="runtime_custom_bindings.js" type="BINDATA" /> + <include name="IDR_SERVICE_WORKER_BINDINGS_JS" file="service_worker_bindings.js" type="BINDATA" /> + <include name="IDR_WEB_REQUEST_CUSTOM_BINDINGS_JS" file="web_request_custom_bindings.js" type="BINDATA" /> + <include name="IDR_WEB_REQUEST_INTERNAL_CUSTOM_BINDINGS_JS" file="web_request_internal_custom_bindings.js" type="BINDATA" /> + <include name="IDR_WINDOW_CONTROLS_JS" file="window_controls.js" type="BINDATA" /> + <include name="IDR_WINDOW_CONTROLS_TEMPLATE_HTML" file="window_controls_template.html" type="BINDATA" /> + <include name="IDR_WEB_VIEW_REQUEST_CUSTOM_BINDINGS_JS" file="guest_view/web_view/web_view_request_custom_bindings.js" type="BINDATA" /> + + <!-- Custom types for APIs. --> + <include name="IDR_STORAGE_AREA_JS" file="storage_area.js" type="BINDATA" /> + + <!-- Platform app support. --> + <include name="IDR_PLATFORM_APP_CSS" file="platform_app.css" type="BINDATA" /> + <include name="IDR_PLATFORM_APP_JS" file="platform_app.js" type="BINDATA" /> + + <!-- Extension styles. --> + <include name="IDR_EXTENSION_FONTS_CSS" file="extension_fonts.css" type="BINDATA"/> + + <!-- Media Router Mojo service and bindings. --> + <if expr="enable_media_router"> + <include name="IDR_MEDIA_ROUTER_MOJOM_JS" file="${mojom_root}\chrome\browser\media\router\mojo\media_router.mojom.js" use_base_dir="false" type="BINDATA" /> + <include name="IDR_MEDIA_ROUTER_BINDINGS_JS" file="media_router_bindings.js" type="BINDATA" /> + </if> + </includes> + <structures> + <!-- Extension styles. --> + <structure name="IDR_EXTENSION_CSS" file="extension.css" type="chrome_html" flattenhtml="true" /> + </structures> + </release> +</grit> diff --git a/chromium/extensions/renderer/resources/greasemonkey_api.js b/chromium/extensions/renderer/resources/greasemonkey_api.js new file mode 100644 index 00000000000..bc09911571a --- /dev/null +++ b/chromium/extensions/renderer/resources/greasemonkey_api.js @@ -0,0 +1,82 @@ +// 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. + +// ----------------------------------------------------------------------------- +// NOTE: If you change this file you need to touch renderer_resources.grd to +// have your change take effect. +// ----------------------------------------------------------------------------- + +// Partial implementation of the Greasemonkey API, see: +// http://wiki.greasespot.net/Greasemonkey_Manual:APIs + +function GM_addStyle(css) { + var parent = document.getElementsByTagName("head")[0]; + if (!parent) { + parent = document.documentElement; + } + var style = document.createElement("style"); + style.type = "text/css"; + var textNode = document.createTextNode(css); + style.appendChild(textNode); + parent.appendChild(style); +} + +function GM_xmlhttpRequest(details) { + function setupEvent(xhr, url, eventName, callback) { + xhr[eventName] = function () { + var isComplete = xhr.readyState == 4; + var responseState = { + responseText: xhr.responseText, + readyState: xhr.readyState, + responseHeaders: isComplete ? xhr.getAllResponseHeaders() : "", + status: isComplete ? xhr.status : 0, + statusText: isComplete ? xhr.statusText : "", + finalUrl: isComplete ? url : "" + }; + callback(responseState); + }; + } + + var xhr = new XMLHttpRequest(); + var eventNames = ["onload", "onerror", "onreadystatechange"]; + for (var i = 0; i < eventNames.length; i++ ) { + var eventName = eventNames[i]; + if (eventName in details) { + setupEvent(xhr, details.url, eventName, details[eventName]); + } + } + + xhr.open(details.method, details.url); + + if (details.overrideMimeType) { + xhr.overrideMimeType(details.overrideMimeType); + } + if (details.headers) { + for (var header in details.headers) { + xhr.setRequestHeader(header, details.headers[header]); + } + } + xhr.send(details.data ? details.data : null); +} + +function GM_openInTab(url) { + window.open(url, ""); +} + +function GM_log(message) { + window.console.log(message); +} + +(function() { + function generateGreasemonkeyStub(name) { + return function() { + console.log("%s is not supported.", name); + }; + } + + var apis = ["GM_getValue", "GM_setValue", "GM_registerMenuCommand"]; + for (var i = 0, api; api = apis[i]; i++) { + window[api] = generateGreasemonkeyStub(api); + } +})(); diff --git a/chromium/extensions/renderer/resources/guest_view/OWNERS b/chromium/extensions/renderer/resources/guest_view/OWNERS new file mode 100644 index 00000000000..c7f0051d52f --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/OWNERS @@ -0,0 +1,4 @@ +paulmeyer@chromium.org +fsamuel@chromium.org +lazyboy@chromium.org +wjmaclean@chromium.org diff --git a/chromium/extensions/renderer/resources/guest_view/app_view/app_view.js b/chromium/extensions/renderer/resources/guest_view/app_view/app_view.js new file mode 100644 index 00000000000..a164fab1865 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/app_view/app_view.js @@ -0,0 +1,80 @@ +// 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. + +var DocumentNatives = requireNative('document_natives'); +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; +var IdGenerator = requireNative('id_generator'); + +function AppViewImpl(appviewElement) { + GuestViewContainer.call(this, appviewElement, 'appview'); + + this.app = ''; + this.data = ''; +} + +AppViewImpl.prototype.__proto__ = GuestViewContainer.prototype; + +AppViewImpl.VIEW_TYPE = 'AppView'; + +// Add extra functionality to |this.element|. +AppViewImpl.setupElement = function(proto) { + var apiMethods = [ + 'connect' + ]; + + // Forward proto.foo* method calls to AppViewImpl.foo*. + GuestViewContainer.forwardApiMethods(proto, apiMethods); +} + +AppViewImpl.prototype.getErrorNode = function() { + if (!this.errorNode) { + this.errorNode = document.createElement('div'); + this.errorNode.innerText = 'Unable to connect to app.'; + this.errorNode.style.position = 'absolute'; + this.errorNode.style.left = '0px'; + this.errorNode.style.top = '0px'; + this.errorNode.style.width = '100%'; + this.errorNode.style.height = '100%'; + this.element.shadowRoot.appendChild(this.errorNode); + } + return this.errorNode; +}; + +AppViewImpl.prototype.buildContainerParams = function() { + return { + 'appId': this.app, + 'data': this.data || {} + }; +}; + +AppViewImpl.prototype.connect = function(app, data, callback) { + if (!this.elementAttached) { + if (callback) { + callback(false); + } + return; + } + + this.app = app; + this.data = data; + + this.guest.destroy(); + this.guest.create(this.buildParams(), function() { + if (!this.guest.getId()) { + var errorMsg = 'Unable to connect to app "' + app + '".'; + window.console.warn(errorMsg); + this.getErrorNode().innerText = errorMsg; + if (callback) { + callback(false); + } + return; + } + this.attachWindow$(); + if (callback) { + callback(true); + } + }.bind(this)); +}; + +GuestViewContainer.registerElement(AppViewImpl); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options.js b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options.js new file mode 100644 index 00000000000..70e1158d403 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options.js @@ -0,0 +1,52 @@ +// 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. + +var ExtensionOptionsConstants = + require('extensionOptionsConstants').ExtensionOptionsConstants; +var ExtensionOptionsEvents = + require('extensionOptionsEvents').ExtensionOptionsEvents; +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; + +function ExtensionOptionsImpl(extensionoptionsElement) { + GuestViewContainer.call(this, extensionoptionsElement, 'extensionoptions'); + + new ExtensionOptionsEvents(this); +}; + +ExtensionOptionsImpl.prototype.__proto__ = GuestViewContainer.prototype; + +ExtensionOptionsImpl.VIEW_TYPE = 'ExtensionOptions'; + +ExtensionOptionsImpl.prototype.onElementAttached = function() { + this.createGuest(); +} + +ExtensionOptionsImpl.prototype.buildContainerParams = function() { + var params = {}; + for (var i in this.attributes) { + params[i] = this.attributes[i].getValue(); + } + return params; +}; + +ExtensionOptionsImpl.prototype.createGuest = function() { + // Destroy the old guest if one exists. + this.guest.destroy(); + + this.guest.create(this.buildParams(), function() { + if (!this.guest.getId()) { + // Fire a createfailed event here rather than in ExtensionOptionsGuest + // because the guest will not be created, and cannot fire an event. + var createFailedEvent = new Event('createfailed', { bubbles: true }); + this.dispatchEvent(createFailedEvent); + } else { + this.attachWindow$(); + } + }.bind(this)); +}; + +GuestViewContainer.registerElement(ExtensionOptionsImpl); + +// Exports. +exports.$set('ExtensionOptionsImpl', ExtensionOptionsImpl); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_attributes.js b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_attributes.js new file mode 100644 index 00000000000..5206f1dc713 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_attributes.js @@ -0,0 +1,43 @@ +// Copyright 2015 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. + +// This module implements the attributes of the <extensionoptions> tag. + +var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; +var ExtensionOptionsConstants = + require('extensionOptionsConstants').ExtensionOptionsConstants; +var ExtensionOptionsImpl = require('extensionOptions').ExtensionOptionsImpl; + +// ----------------------------------------------------------------------------- +// ExtensionAttribute object. + +// Attribute that handles extension binded to the extensionoptions. +function ExtensionAttribute(view) { + GuestViewAttributes.Attribute.call( + this, ExtensionOptionsConstants.ATTRIBUTE_EXTENSION, view); +} + +ExtensionAttribute.prototype.__proto__ = + GuestViewAttributes.Attribute.prototype; + +ExtensionAttribute.prototype.handleMutation = function(oldValue, newValue) { + // Once this attribute has been set, it cannot be unset. + if (!newValue && oldValue) { + this.setValueIgnoreMutation(oldValue); + return; + } + + if (!newValue || !this.elementAttached) + return; + + this.view.createGuest(); +}; + +// ----------------------------------------------------------------------------- + +// Sets up all of the extensionoptions attributes. +ExtensionOptionsImpl.prototype.setupAttributes = function() { + this.attributes[ExtensionOptionsConstants.ATTRIBUTE_EXTENSION] = + new ExtensionAttribute(this); +}; diff --git a/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_constants.js b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_constants.js new file mode 100644 index 00000000000..c4d9692aff2 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_constants.js @@ -0,0 +1,14 @@ +// Copyright 2015 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. + +// This module contains constants used in extensionoptions. + +// Container for the extensionview constants. +var ExtensionOptionsConstants = { + // Attributes. + ATTRIBUTE_EXTENSION: 'extension' +}; + +exports.$set('ExtensionOptionsConstants', + $Object.freeze(ExtensionOptionsConstants)); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_events.js b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_events.js new file mode 100644 index 00000000000..20a2f1f8a6e --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_options/extension_options_events.js @@ -0,0 +1,40 @@ +// 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. + +var CreateEvent = require('guestViewEvents').CreateEvent; +var GuestViewEvents = require('guestViewEvents').GuestViewEvents; + +function ExtensionOptionsEvents(extensionOptionsImpl) { + GuestViewEvents.call(this, extensionOptionsImpl); + + // |setupEventProperty| is normally called automatically, but the + // 'createfailed' event is registered here because the event is fired from + // ExtensionOptionsImpl instead of in response to an extension event. + this.setupEventProperty('createfailed'); +} + +ExtensionOptionsEvents.prototype.__proto__ = GuestViewEvents.prototype; + +// A dictionary of <extensionoptions> extension events to be listened for. This +// dictionary augments |GuestViewEvents.EVENTS| in guest_view_events.js. See the +// documentation there for details. +ExtensionOptionsEvents.EVENTS = { + 'close': { + evt: CreateEvent('extensionOptionsInternal.onClose') + }, + 'load': { + evt: CreateEvent('extensionOptionsInternal.onLoad') + }, + 'preferredsizechanged': { + evt: CreateEvent('extensionOptionsInternal.onPreferredSizeChanged'), + fields:['width', 'height'] + } +} + +ExtensionOptionsEvents.prototype.getEvents = function() { + return ExtensionOptionsEvents.EVENTS; +}; + +// Exports. +exports.$set('ExtensionOptionsEvents', ExtensionOptionsEvents); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view.js new file mode 100644 index 00000000000..763b5541446 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view.js @@ -0,0 +1,139 @@ +// Copyright 2015 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. + +// This module implements the ExtensionView <extensionview>. + +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; +var ExtensionViewConstants = + require('extensionViewConstants').ExtensionViewConstants; +var ExtensionViewEvents = require('extensionViewEvents').ExtensionViewEvents; +var ExtensionViewInternal = + require('extensionViewInternal').ExtensionViewInternal; + +function ExtensionViewImpl(extensionviewElement) { + GuestViewContainer.call(this, extensionviewElement, 'extensionview'); + + // A queue of objects in the order they should be loaded. + // Every load call will add the given src, as well as the resolve and reject + // functions. Each src will be loaded in the order they were called. + this.loadQueue = []; + + // The current src that is loading. + // @type {Object<!string, function, function>} + this.pendingLoad = null; + + new ExtensionViewEvents(this, this.viewInstanceId); +} + +ExtensionViewImpl.prototype.__proto__ = GuestViewContainer.prototype; + +ExtensionViewImpl.VIEW_TYPE = 'ExtensionView'; + +ExtensionViewImpl.setupElement = function(proto) { + var apiMethods = ExtensionViewImpl.getApiMethods(); + + GuestViewContainer.forwardApiMethods(proto, apiMethods); +}; + +ExtensionViewImpl.prototype.createGuest = function(callback) { + this.guest.create(this.buildParams(), function() { + this.attachWindow$(); + callback(); + }.bind(this)); +}; + +ExtensionViewImpl.prototype.buildContainerParams = function() { + var params = {}; + for (var i in this.attributes) { + params[i] = this.attributes[i].getValue(); + } + return params; +}; + +ExtensionViewImpl.prototype.onElementDetached = function() { + this.guest.destroy(); + + // Reset all attributes. + for (var i in this.attributes) { + this.attributes[i].setValueIgnoreMutation(); + } +}; + +// Updates src upon loadcommit. +ExtensionViewImpl.prototype.onLoadCommit = function(url) { + this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC]. + setValueIgnoreMutation(url); +}; + +// Loads the next pending src from |loadQueue| to the extensionview. +ExtensionViewImpl.prototype.loadNextSrc = function() { + // If extensionview isn't currently loading a src, load the next src + // in |loadQueue|. Otherwise, do nothing. + if (!this.pendingLoad && this.loadQueue.length) { + this.pendingLoad = this.loadQueue.shift(); + var src = this.pendingLoad.src; + var resolve = this.pendingLoad.resolve; + var reject = this.pendingLoad.reject; + + // The extensionview validates the |src| twice, once in |parseSrc| and then + // in |loadSrc|. The |src| isn't checked directly in |loadNextSrc| for + // validity since the sending renderer (WebUI) is trusted. + ExtensionViewInternal.parseSrc(src, function(isSrcValid, extensionId) { + // Check if the src is valid. + if (!isSrcValid) { + reject('Failed to load: src is not valid.'); + return; + } + + // Destroy the current guest and create a new one if extension ID + // is different. + // + // This may happen if the extensionview is loads an extension page, and + // is then intended to load a page served from a different extension in + // the same part of the WebUI. + // + // The two calls may look like the following: + // extensionview.load('chrome-extension://firstId/page.html'); + // extensionview.load('chrome-extension://secondId/page.html'); + // The second time load is called, we destroy the current guest since + // we will be loading content from a different extension. + if (extensionId != + this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] + .getValue()) { + this.guest.destroy(); + + // Update the extension and src attributes. + this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] + .setValueIgnoreMutation(extensionId); + this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] + .setValueIgnoreMutation(src); + + this.createGuest(function() { + if (this.guest.getId() <= 0) { + reject('Failed to load: guest creation failed.'); + } else { + resolve('Successful load.'); + } + }.bind(this)); + } else { + ExtensionViewInternal.loadSrc(this.guest.getId(), src, + function(hasLoadSucceeded) { + if (!hasLoadSucceeded) { + reject('Failed to load.'); + } else { + // Update the src attribute. + this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] + .setValueIgnoreMutation(src); + resolve('Successful load.'); + } + }.bind(this)); + } + }.bind(this)); + } +}; + +GuestViewContainer.registerElement(ExtensionViewImpl); + +// Exports. +exports.$set('ExtensionViewImpl', ExtensionViewImpl); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_api_methods.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_api_methods.js new file mode 100644 index 00000000000..d495973072a --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_api_methods.js @@ -0,0 +1,45 @@ +// Copyright 2015 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. + +// This module implements the public-facing API functions for the +// <extensionview> tag. + +var ExtensionViewInternal = + require('extensionViewInternal').ExtensionViewInternal; +var ExtensionViewImpl = require('extensionView').ExtensionViewImpl; +var ExtensionViewConstants = + require('extensionViewConstants').ExtensionViewConstants; + +// An array of <extensionview>'s public-facing API methods. +var EXTENSION_VIEW_API_METHODS = [ + // Loads the given src into extensionview. Must be called every time the + // the extensionview should load a new page. This is the only way to set + // the extension and src attributes. Returns a promise indicating whether + // or not load was successful. + 'load' +]; + +// ----------------------------------------------------------------------------- +// Custom API method implementations. + +ExtensionViewImpl.prototype.load = function(src) { + return new Promise(function(resolve, reject) { + this.loadQueue.push({src: src, resolve: resolve, reject: reject}); + this.loadNextSrc(); + }.bind(this)) + .then(function onLoadResolved() { + this.pendingLoad = null; + this.loadNextSrc(); + }.bind(this), function onLoadRejected() { + this.pendingLoad = null; + this.loadNextSrc(); + reject('Failed to load.'); + }.bind(this)); +}; + +// ----------------------------------------------------------------------------- + +ExtensionViewImpl.getApiMethods = function() { + return EXTENSION_VIEW_API_METHODS; +}; diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_attributes.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_attributes.js new file mode 100644 index 00000000000..550fc0f5f3f --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_attributes.js @@ -0,0 +1,55 @@ +// Copyright 2015 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. + +// This module implements the attributes of the <extensionview> tag. + +var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; +var ExtensionViewConstants = + require('extensionViewConstants').ExtensionViewConstants; +var ExtensionViewImpl = require('extensionView').ExtensionViewImpl; +var ExtensionViewInternal = + require('extensionViewInternal').ExtensionViewInternal; + +// ----------------------------------------------------------------------------- +// ExtensionAttribute object. + +// Attribute that handles the extension associated with the extensionview. +function ExtensionAttribute(view) { + GuestViewAttributes.ReadOnlyAttribute.call( + this, ExtensionViewConstants.ATTRIBUTE_EXTENSION, view); +} + +ExtensionAttribute.prototype.__proto__ = + GuestViewAttributes.ReadOnlyAttribute.prototype; + +// ----------------------------------------------------------------------------- +// SrcAttribute object. + +// Attribute that handles the location and navigation of the extensionview. +// This is read only because we only want to be able to navigate to a src +// through the load API call, which checks for URL validity and the extension +// ID of the new src. +function SrcAttribute(view) { + GuestViewAttributes.ReadOnlyAttribute.call( + this, ExtensionViewConstants.ATTRIBUTE_SRC, view); +} + +SrcAttribute.prototype.__proto__ = + GuestViewAttributes.ReadOnlyAttribute.prototype; + +SrcAttribute.prototype.handleMutation = function(oldValue, newValue) { + console.log('src is read only. Use .load(url) to navigate to a new ' + + 'extension page.'); + this.setValueIgnoreMutation(oldValue); +} + +// ----------------------------------------------------------------------------- + +// Sets up all of the extensionview attributes. +ExtensionViewImpl.prototype.setupAttributes = function() { + this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] = + new ExtensionAttribute(this); + this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] = + new SrcAttribute(this); +}; diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_constants.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_constants.js new file mode 100644 index 00000000000..80934cf31e7 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_constants.js @@ -0,0 +1,14 @@ +// Copyright 2015 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. + +// This module contains constants used in extensionview. + +// Container for the extensionview constants. +var ExtensionViewConstants = { + // Attributes. + ATTRIBUTE_EXTENSION: 'extension', + ATTRIBUTE_SRC: 'src', +}; + +exports.$set('ExtensionViewConstants', $Object.freeze(ExtensionViewConstants)); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_events.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_events.js new file mode 100644 index 00000000000..db674180166 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_events.js @@ -0,0 +1,32 @@ +// Copyright 2015 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. + +// Event management for ExtensionView. + +var CreateEvent = require('guestViewEvents').CreateEvent; +var GuestViewEvents = require('guestViewEvents').GuestViewEvents; + +function ExtensionViewEvents(extensionViewImpl) { + GuestViewEvents.call(this, extensionViewImpl); +} + +ExtensionViewEvents.prototype.__proto__ = GuestViewEvents.prototype; + +ExtensionViewEvents.EVENTS = { + 'loadcommit': { + evt: CreateEvent('extensionViewInternal.onLoadCommit'), + handler: 'handleLoadCommitEvent', + internal: true + } +}; + +ExtensionViewEvents.prototype.getEvents = function() { + return ExtensionViewEvents.EVENTS; +}; + +ExtensionViewEvents.prototype.handleLoadCommitEvent = function(event) { + this.view.onLoadCommit(event.url); +}; + +exports.$set('ExtensionViewEvents', ExtensionViewEvents); diff --git a/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_internal.js b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_internal.js new file mode 100644 index 00000000000..07c6dde8c9b --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/extension_view/extension_view_internal.js @@ -0,0 +1,7 @@ +// Copyright 2015 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. + +exports.$set( + 'ExtensionViewInternal', + require('binding').Binding.create('extensionViewInternal').generate()); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view.js b/chromium/extensions/renderer/resources/guest_view/guest_view.js new file mode 100644 index 00000000000..1f887a746b3 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view.js @@ -0,0 +1,355 @@ +// 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. + +// This module implements a wrapper for a guestview that manages its +// creation, attaching, and destruction. + +var CreateEvent = require('guestViewEvents').CreateEvent; +var EventBindings = require('event_bindings'); +var GuestViewInternal = + require('binding').Binding.create('guestViewInternal').generate(); +var GuestViewInternalNatives = requireNative('guest_view_internal'); + +// Events. +var ResizeEvent = CreateEvent('guestViewInternal.onResize'); + +// Error messages. +var ERROR_MSG_ALREADY_ATTACHED = 'The guest has already been attached.'; +var ERROR_MSG_ALREADY_CREATED = 'The guest has already been created.'; +var ERROR_MSG_INVALID_STATE = 'The guest is in an invalid state.'; +var ERROR_MSG_NOT_ATTACHED = 'The guest is not attached.'; +var ERROR_MSG_NOT_CREATED = 'The guest has not been created.'; + +// Properties. +var PROPERTY_ON_RESIZE = 'onresize'; + +// Contains and hides the internal implementation details of |GuestView|, +// including maintaining its state and enforcing the proper usage of its API +// fucntions. +function GuestViewImpl(guestView, viewType, guestInstanceId) { + if (guestInstanceId) { + this.id = guestInstanceId; + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; + } else { + this.id = 0; + this.state = GuestViewImpl.GuestState.GUEST_STATE_START; + } + this.actionQueue = []; + this.contentWindow = null; + this.guestView = guestView; + this.pendingAction = null; + this.viewType = viewType; + this.internalInstanceId = 0; + + this.setupOnResize(); +} + +// Possible states. +GuestViewImpl.GuestState = { + GUEST_STATE_START: 0, + GUEST_STATE_CREATED: 1, + GUEST_STATE_ATTACHED: 2 +}; + +// Sets up the onResize property on the GuestView. +GuestViewImpl.prototype.setupOnResize = function() { + $Object.defineProperty(this.guestView, PROPERTY_ON_RESIZE, { + get: function() { + return this[PROPERTY_ON_RESIZE]; + }.bind(this), + set: function(value) { + this[PROPERTY_ON_RESIZE] = value; + }.bind(this), + enumerable: true + }); + + this.callOnResize = function(e) { + if (!this[PROPERTY_ON_RESIZE]) { + return; + } + this[PROPERTY_ON_RESIZE](e); + }.bind(this); +}; + +// Callback wrapper that is used to call the callback of the pending action (if +// one exists), and then performs the next action in the queue. +GuestViewImpl.prototype.handleCallback = function(callback) { + if (callback) { + callback(); + } + this.pendingAction = null; + this.performNextAction(); +}; + +// Perform the next action in the queue, if one exists. +GuestViewImpl.prototype.performNextAction = function() { + // Make sure that there is not already an action in progress, and that there + // exists a queued action to perform. + if (!this.pendingAction && this.actionQueue.length) { + this.pendingAction = this.actionQueue.shift(); + this.pendingAction(); + } +}; + +// Check the current state to see if the proposed action is valid. Returns false +// if invalid. +GuestViewImpl.prototype.checkState = function(action) { + // Create an error prefix based on the proposed action. + var errorPrefix = 'Error calling ' + action + ': '; + + // Check that the current state is valid. + if (!(this.state >= 0 && this.state <= 2)) { + window.console.error(errorPrefix + ERROR_MSG_INVALID_STATE); + return false; + } + + // Map of possible errors for each action. For each action, the errors are + // listed for states in the order: GUEST_STATE_START, GUEST_STATE_CREATED, + // GUEST_STATE_ATTACHED. + var errors = { + 'attach': [ERROR_MSG_NOT_CREATED, null, ERROR_MSG_ALREADY_ATTACHED], + 'create': [null, ERROR_MSG_ALREADY_CREATED, ERROR_MSG_ALREADY_CREATED], + 'destroy': [null, null, null], + 'detach': [ERROR_MSG_NOT_ATTACHED, ERROR_MSG_NOT_ATTACHED, null], + 'setSize': [ERROR_MSG_NOT_CREATED, null, null] + }; + + // Check that the proposed action is a real action. + if (errors[action] == undefined) { + window.console.error(errorPrefix + ERROR_MSG_INVALID_ACTION); + return false; + } + + // Report the error if the proposed action is found to be invalid for the + // current state. + var error; + if (error = errors[action][this.state]) { + window.console.error(errorPrefix + error); + return false; + } + + return true; +}; + +// Returns a wrapper function for |func| with a weak reference to |this|. This +// implementation of weakWrapper() requires a provided |viewInstanceId| since +// GuestViewImpl does not store this ID. +GuestViewImpl.prototype.weakWrapper = function(func, viewInstanceId) { + return function() { + var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); + if (view && view.guest) { + return $Function.apply(func, + privates(view.guest).internal, + $Array.slice(arguments)); + } + }; +}; + +// Internal implementation of attach(). +GuestViewImpl.prototype.attachImpl$ = function( + internalInstanceId, viewInstanceId, attachParams, callback) { + // Check the current state. + if (!this.checkState('attach')) { + this.handleCallback(callback); + return; + } + + // Callback wrapper function to store the contentWindow from the attachGuest() + // callback, handle potential attaching failure, register an automatic detach, + // and advance the queue. + var callbackWrapper = function(callback, contentWindow) { + // Check if attaching failed. + if (!contentWindow) { + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; + this.internalInstanceId = 0; + } else { + // Only update the contentWindow if attaching is successful. + this.contentWindow = contentWindow; + } + + this.handleCallback(callback); + }; + + attachParams['instanceId'] = viewInstanceId; + GuestViewInternalNatives.AttachGuest(internalInstanceId, + this.id, + attachParams, + callbackWrapper.bind(this, callback)); + + this.internalInstanceId = internalInstanceId; + this.state = GuestViewImpl.GuestState.GUEST_STATE_ATTACHED; + + // Detach automatically when the container is destroyed. + GuestViewInternalNatives.RegisterDestructionCallback( + internalInstanceId, this.weakWrapper(function() { + if (this.state != GuestViewImpl.GuestState.GUEST_STATE_ATTACHED || + this.internalInstanceId != internalInstanceId) { + return; + } + + this.internalInstanceId = 0; + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; + }, viewInstanceId)); +}; + +// Internal implementation of create(). +GuestViewImpl.prototype.createImpl$ = function(createParams, callback) { + // Check the current state. + if (!this.checkState('create')) { + this.handleCallback(callback); + return; + } + + // Callback wrapper function to store the guestInstanceId from the + // createGuest() callback, handle potential creation failure, and advance the + // queue. + var callbackWrapper = function(callback, guestInfo) { + this.id = guestInfo.id; + this.contentWindow = + GuestViewInternalNatives.GetContentWindow(guestInfo.contentWindowId); + + // Check if creation failed. + if (this.id === 0) { + this.state = GuestViewImpl.GuestState.GUEST_STATE_START; + this.contentWindow = null; + } + + ResizeEvent.addListener(this.callOnResize, {instanceId: this.id}); + this.handleCallback(callback); + }; + + this.sendCreateRequest(createParams, callbackWrapper.bind(this, callback)); + + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; +}; + +GuestViewImpl.prototype.sendCreateRequest = function( + createParams, boundCallback) { + GuestViewInternal.createGuest(this.viewType, createParams, boundCallback); +}; + +// Internal implementation of destroy(). +GuestViewImpl.prototype.destroyImpl = function(callback) { + // Check the current state. + if (!this.checkState('destroy')) { + this.handleCallback(callback); + return; + } + + if (this.state == GuestViewImpl.GuestState.GUEST_STATE_START) { + // destroy() does nothing in this case. + this.handleCallback(callback); + return; + } + + // If this guest is attached, then detach it first. + if (!!this.internalInstanceId) { + GuestViewInternalNatives.DetachGuest(this.internalInstanceId); + } + + GuestViewInternal.destroyGuest(this.id, + this.handleCallback.bind(this, callback)); + + // Reset the state of the destroyed guest; + this.contentWindow = null; + this.id = 0; + this.internalInstanceId = 0; + this.state = GuestViewImpl.GuestState.GUEST_STATE_START; + if (ResizeEvent.hasListener(this.callOnResize)) { + ResizeEvent.removeListener(this.callOnResize); + } +}; + +// Internal implementation of detach(). +GuestViewImpl.prototype.detachImpl = function(callback) { + // Check the current state. + if (!this.checkState('detach')) { + this.handleCallback(callback); + return; + } + + GuestViewInternalNatives.DetachGuest( + this.internalInstanceId, + this.handleCallback.bind(this, callback)); + + this.internalInstanceId = 0; + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; +}; + +// Internal implementation of setSize(). +GuestViewImpl.prototype.setSizeImpl = function(sizeParams, callback) { + // Check the current state. + if (!this.checkState('setSize')) { + this.handleCallback(callback); + return; + } + + GuestViewInternal.setSize(this.id, sizeParams, + this.handleCallback.bind(this, callback)); +}; + +// The exposed interface to a guestview. Exposes in its API the functions +// attach(), create(), destroy(), and getId(). All other implementation details +// are hidden. +function GuestView(viewType, guestInstanceId) { + privates(this).internal = new GuestViewImpl(this, viewType, guestInstanceId); +} + +// Attaches the guestview to the container with ID |internalInstanceId|. +GuestView.prototype.attach = function( + internalInstanceId, viewInstanceId, attachParams, callback) { + var internal = privates(this).internal; + internal.actionQueue.push(internal.attachImpl$.bind( + internal, internalInstanceId, viewInstanceId, attachParams, callback)); + internal.performNextAction(); +}; + +// Creates the guestview. +GuestView.prototype.create = function(createParams, callback) { + var internal = privates(this).internal; + internal.actionQueue.push(internal.createImpl$.bind( + internal, createParams, callback)); + internal.performNextAction(); +}; + +// Destroys the guestview. Nothing can be done with the guestview after it has +// been destroyed. +GuestView.prototype.destroy = function(callback) { + var internal = privates(this).internal; + internal.actionQueue.push(internal.destroyImpl.bind(internal, callback)); + internal.performNextAction(); +}; + +// Detaches the guestview from its container. +// Note: This is not currently used. +GuestView.prototype.detach = function(callback) { + var internal = privates(this).internal; + internal.actionQueue.push(internal.detachImpl.bind(internal, callback)); + internal.performNextAction(); +}; + +// Adjusts the guestview's sizing parameters. +GuestView.prototype.setSize = function(sizeParams, callback) { + var internal = privates(this).internal; + internal.actionQueue.push(internal.setSizeImpl.bind( + internal, sizeParams, callback)); + internal.performNextAction(); +}; + +// Returns the contentWindow for this guestview. +GuestView.prototype.getContentWindow = function() { + var internal = privates(this).internal; + return internal.contentWindow; +}; + +// Returns the ID for this guestview. +GuestView.prototype.getId = function() { + var internal = privates(this).internal; + return internal.id; +}; + +// Exports +exports.$set('GuestView', GuestView); +exports.$set('GuestViewImpl', GuestViewImpl); +exports.$set('ResizeEvent', ResizeEvent); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_attributes.js b/chromium/extensions/renderer/resources/guest_view/guest_view_attributes.js new file mode 100644 index 00000000000..6c7f711ef6c --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_attributes.js @@ -0,0 +1,142 @@ +// Copyright 2015 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. + +// This module implements the base attributes of the GuestView tags. + +// ----------------------------------------------------------------------------- +// Attribute object. + +// Default implementation of a GuestView attribute. +function Attribute(name, view) { + this.dirty = false; + this.ignoreMutation = false; + this.name = name; + this.view = view; + + this.defineProperty(); +} + +// Retrieves and returns the attribute's value. +Attribute.prototype.getValue = function() { + return this.view.element.getAttribute(this.name) || ''; +}; + +// Retrieves and returns the attribute's value if it has been dirtied since +// the last time this method was called. Returns null otherwise. +Attribute.prototype.getValueIfDirty = function() { + if (!this.dirty) + return null; + this.dirty = false; + return this.getValue(); +}; + +// Sets the attribute's value. +Attribute.prototype.setValue = function(value) { + this.view.element.setAttribute(this.name, value || ''); +}; + +// Changes the attribute's value without triggering its mutation handler. +Attribute.prototype.setValueIgnoreMutation = function(value) { + this.ignoreMutation = true; + this.setValue(value); + this.ignoreMutation = false; +}; + +// Defines this attribute as a property on the view's element. +Attribute.prototype.defineProperty = function() { + $Object.defineProperty(this.view.element, this.name, { + get: function() { + return this.getValue(); + }.bind(this), + set: function(value) { + this.setValue(value); + }.bind(this), + enumerable: true + }); +}; + +// Called when the attribute's value changes. +Attribute.prototype.maybeHandleMutation = function(oldValue, newValue) { + if (this.ignoreMutation) + return; + + this.dirty = true; + this.handleMutation(oldValue, newValue); +}; + +// Called when a change that isn't ignored occurs to the attribute's value. +Attribute.prototype.handleMutation = function(oldValue, newValue) {}; + +// Called when the view's element is attached to the DOM tree. +Attribute.prototype.attach = function() {}; + +// Called when the view's element is detached from the DOM tree. +Attribute.prototype.detach = function() {}; + +// ----------------------------------------------------------------------------- +// BooleanAttribute object. + +// An attribute that is treated as a Boolean. +function BooleanAttribute(name, view) { + Attribute.call(this, name, view); +} + +BooleanAttribute.prototype.__proto__ = Attribute.prototype; + +BooleanAttribute.prototype.getValue = function() { + return this.view.element.hasAttribute(this.name); +}; + +BooleanAttribute.prototype.setValue = function(value) { + if (!value) { + this.view.element.removeAttribute(this.name); + } else { + this.view.element.setAttribute(this.name, ''); + } +}; + +// ----------------------------------------------------------------------------- +// IntegerAttribute object. + +// An attribute that is treated as an integer. +function IntegerAttribute(name, view) { + Attribute.call(this, name, view); +} + +IntegerAttribute.prototype.__proto__ = Attribute.prototype; + +IntegerAttribute.prototype.getValue = function() { + return parseInt(this.view.element.getAttribute(this.name)) || 0; +}; + +IntegerAttribute.prototype.setValue = function(value) { + this.view.element.setAttribute(this.name, parseInt(value) || 0); +}; + +// ----------------------------------------------------------------------------- +// ReadOnlyAttribute object. + +// An attribute that cannot be changed (externally). The only way to set it +// internally is via |setValueIgnoreMutation|. +function ReadOnlyAttribute(name, view) { + Attribute.call(this, name, view); +} + +ReadOnlyAttribute.prototype.__proto__ = Attribute.prototype; + +ReadOnlyAttribute.prototype.handleMutation = function(oldValue, newValue) { + this.setValueIgnoreMutation(oldValue); +} + +// ----------------------------------------------------------------------------- + +var GuestViewAttributes = { + Attribute: Attribute, + BooleanAttribute: BooleanAttribute, + IntegerAttribute: IntegerAttribute, + ReadOnlyAttribute: ReadOnlyAttribute +}; + +// Exports. +exports.$set('GuestViewAttributes', GuestViewAttributes); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_container.js b/chromium/extensions/renderer/resources/guest_view/guest_view_container.js new file mode 100644 index 00000000000..acba9fb7b0f --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_container.js @@ -0,0 +1,311 @@ +// 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. + +// This module implements the shared functionality for different guestview +// containers, such as web_view, app_view, etc. + +var DocumentNatives = requireNative('document_natives'); +var GuestView = require('guestView').GuestView; +var GuestViewInternalNatives = requireNative('guest_view_internal'); +var IdGenerator = requireNative('id_generator'); +var MessagingNatives = requireNative('messaging_natives'); + +function GuestViewContainer(element, viewType) { + privates(element).internal = this; + this.attributes = {}; + this.element = element; + this.elementAttached = false; + this.viewInstanceId = IdGenerator.GetNextId(); + this.viewType = viewType; + + this.setupGuestProperty(); + this.guest = new GuestView(viewType); + this.setupAttributes(); + + privates(this).internalElement = this.createInternalElement$(); + this.setupFocusPropagation(); + var shadowRoot = this.element.createShadowRoot(); + shadowRoot.appendChild(privates(this).internalElement); + + GuestViewInternalNatives.RegisterView(this.viewInstanceId, this, viewType); +} + +// Forward public API methods from |proto| to their internal implementations. +GuestViewContainer.forwardApiMethods = function(proto, apiMethods) { + var createProtoHandler = function(m) { + return function(var_args) { + var internal = privates(this).internal; + return $Function.apply(internal[m], internal, arguments); + }; + }; + for (var i = 0; apiMethods[i]; ++i) { + proto[apiMethods[i]] = createProtoHandler(apiMethods[i]); + } +}; + +// Registers the browserplugin and guestview as custom elements once the +// document has loaded. +GuestViewContainer.registerElement = function(guestViewContainerType) { + var useCapture = true; + window.addEventListener('readystatechange', function listener(event) { + if (document.readyState == 'loading') + return; + + registerInternalElement(guestViewContainerType.VIEW_TYPE.toLowerCase()); + registerGuestViewElement(guestViewContainerType); + window.removeEventListener(event.type, listener, useCapture); + }, useCapture); +}; + +// Create the 'guest' property to track new GuestViews and always listen for +// their resizes. +GuestViewContainer.prototype.setupGuestProperty = function() { + $Object.defineProperty(this, 'guest', { + get: function() { + return privates(this).guest; + }.bind(this), + set: function(value) { + privates(this).guest = value; + if (!value) { + return; + } + privates(this).guest.onresize = function(e) { + // Dispatch the 'contentresize' event. + var contentResizeEvent = new Event('contentresize', { bubbles: true }); + contentResizeEvent.oldWidth = e.oldWidth; + contentResizeEvent.oldHeight = e.oldHeight; + contentResizeEvent.newWidth = e.newWidth; + contentResizeEvent.newHeight = e.newHeight; + this.dispatchEvent(contentResizeEvent); + }.bind(this); + }.bind(this), + enumerable: true + }); +}; + +GuestViewContainer.prototype.createInternalElement$ = function() { + // We create BrowserPlugin as a custom element in order to observe changes + // to attributes synchronously. + var browserPluginElement = + new GuestViewContainer[this.viewType + 'BrowserPlugin'](); + privates(browserPluginElement).internal = this; + return browserPluginElement; +}; + +GuestViewContainer.prototype.setupFocusPropagation = function() { + if (!this.element.hasAttribute('tabIndex')) { + // GuestViewContainer needs a tabIndex in order to be focusable. + // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute + // to allow GuestViewContainer to be focusable. + // See http://crbug.com/231664. + this.element.setAttribute('tabIndex', -1); + } + this.element.addEventListener('focus', this.weakWrapper(function(e) { + // Focus the BrowserPlugin when the GuestViewContainer takes focus. + privates(this).internalElement.focus(); + })); + this.element.addEventListener('blur', this.weakWrapper(function(e) { + // Blur the BrowserPlugin when the GuestViewContainer loses focus. + privates(this).internalElement.blur(); + })); +}; + +GuestViewContainer.prototype.focus = function() { + // Focus the internal element when focus() is called on the GuestView element. + privates(this).internalElement.focus(); +} + +GuestViewContainer.prototype.attachWindow$ = function() { + if (!this.internalInstanceId) { + return true; + } + + this.guest.attach(this.internalInstanceId, + this.viewInstanceId, + this.buildParams()); + return true; +}; + +GuestViewContainer.prototype.makeGCOwnContainer = function(internalInstanceId) { + MessagingNatives.BindToGC(this, function() { + GuestViewInternalNatives.DestroyContainer(internalInstanceId); + }, -1); +}; + +GuestViewContainer.prototype.onInternalInstanceId = function( + internalInstanceId) { + this.internalInstanceId = internalInstanceId; + this.makeGCOwnContainer(this.internalInstanceId); + + // Track when the element resizes using the element resize callback. + GuestViewInternalNatives.RegisterElementResizeCallback( + this.internalInstanceId, this.weakWrapper(this.onElementResize)); + + if (!this.guest.getId()) { + return; + } + this.guest.attach(this.internalInstanceId, + this.viewInstanceId, + this.buildParams()); +}; + +GuestViewContainer.prototype.handleInternalElementAttributeMutation = + function(name, oldValue, newValue) { + if (name == 'internalinstanceid' && !oldValue && !!newValue) { + privates(this).internalElement.removeAttribute('internalinstanceid'); + this.onInternalInstanceId(parseInt(newValue)); + } +}; + +GuestViewContainer.prototype.onElementResize = function(newWidth, newHeight) { + if (!this.guest.getId()) + return; + this.guest.setSize({normal: {width: newWidth, height: newHeight}}); +}; + +GuestViewContainer.prototype.buildParams = function() { + var params = this.buildContainerParams(); + params['instanceId'] = this.viewInstanceId; + // When the GuestViewContainer is not participating in layout (display:none) + // then getBoundingClientRect() would report a width and height of 0. + // However, in the case where the GuestViewContainer has a fixed size we can + // use that value to initially size the guest so as to avoid a relayout of the + // on display:block. + var css = window.getComputedStyle(this.element, null); + var elementRect = this.element.getBoundingClientRect(); + params['elementWidth'] = parseInt(elementRect.width) || + parseInt(css.getPropertyValue('width')); + params['elementHeight'] = parseInt(elementRect.height) || + parseInt(css.getPropertyValue('height')); + return params; +}; + +GuestViewContainer.prototype.dispatchEvent = function(event) { + return this.element.dispatchEvent(event); +} + +// Returns a wrapper function for |func| with a weak reference to |this|. +GuestViewContainer.prototype.weakWrapper = function(func) { + var viewInstanceId = this.viewInstanceId; + return function() { + var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); + if (view) { + return $Function.apply(func, view, $Array.slice(arguments)); + } + }; +}; + +// Implemented by the specific view type, if needed. +GuestViewContainer.prototype.buildContainerParams = function() { return {}; }; +GuestViewContainer.prototype.willAttachElement = function() {}; +GuestViewContainer.prototype.onElementAttached = function() {}; +GuestViewContainer.prototype.onElementDetached = function() {}; +GuestViewContainer.prototype.setupAttributes = function() {}; + +// Registers the browser plugin <object> custom element. |viewType| is the +// name of the specific guestview container (e.g. 'webview'). +function registerInternalElement(viewType) { + var proto = $Object.create(HTMLElement.prototype); + + proto.createdCallback = function() { + this.setAttribute('type', 'application/browser-plugin'); + this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId()); + this.style.width = '100%'; + this.style.height = '100%'; + }; + + proto.attachedCallback = function() { + // Load the plugin immediately. + var unused = this.nonExistentAttribute; + }; + + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.handleInternalElementAttributeMutation(name, oldValue, newValue); + }; + + GuestViewContainer[viewType + 'BrowserPlugin'] = + DocumentNatives.RegisterElement(viewType + 'browserplugin', + {extends: 'object', prototype: proto}); + + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + delete proto.attributeChangedCallback; +}; + +// Registers the guestview container as a custom element. +// |guestViewContainerType| is the type of guestview container +// (e.g. WebViewImpl). +function registerGuestViewElement(guestViewContainerType) { + var proto = $Object.create(HTMLElement.prototype); + + proto.createdCallback = function() { + new guestViewContainerType(this); + }; + + proto.attachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.elementAttached = true; + internal.willAttachElement(); + internal.onElementAttached(); + }; + + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal = privates(this).internal; + if (!internal || !internal.attributes[name]) { + return; + } + + // Let the changed attribute handle its own mutation. + internal.attributes[name].maybeHandleMutation(oldValue, newValue); + }; + + proto.detachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.elementAttached = false; + internal.internalInstanceId = 0; + internal.guest.destroy(); + internal.onElementDetached(); + }; + + // Override |focus| to let |internal| handle it. + proto.focus = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.focus(); + }; + + // Let the specific view type add extra functionality to its custom element + // through |proto|. + if (guestViewContainerType.setupElement) { + guestViewContainerType.setupElement(proto); + } + + window[guestViewContainerType.VIEW_TYPE] = + DocumentNatives.RegisterElement( + guestViewContainerType.VIEW_TYPE.toLowerCase(), + {prototype: proto}); + + // Delete the callbacks so developers cannot call them and produce unexpected + // behavior. + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + delete proto.attributeChangedCallback; +} + +// Exports. +exports.$set('GuestViewContainer', GuestViewContainer); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_deny.js b/chromium/extensions/renderer/resources/guest_view/guest_view_deny.js new file mode 100644 index 00000000000..395594e77ae --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_deny.js @@ -0,0 +1,58 @@ +// Copyright 2015 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. + +// This module implements the registration of guestview elements when +// permissions are not available. These elements exist only to provide a useful +// error message when developers attempt to use them. + +var DocumentNatives = requireNative('document_natives'); +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; + +var ERROR_MESSAGE = 'You do not have permission to use the %1 element.' + + ' Be sure to declare the "%1" permission in your manifest file.'; + +// A list of view types that will have custom elements registered if they are +// not already registered by the time this module is loaded. +var VIEW_TYPES = [ + 'AppView', + 'ExtensionOptions', + 'ExtensionView', + 'WebView' +]; + +// Registers a GuestView custom element. +function registerGuestViewElement(viewType) { + var proto = Object.create(HTMLElement.prototype); + + proto.createdCallback = function() { + window.console.error(ERROR_MESSAGE.replace(/%1/g, viewType.toLowerCase())); + }; + + window[viewType] = DocumentNatives.RegisterElement(viewType.toLowerCase(), + {prototype: proto}); + + // Delete the callbacks so developers cannot call them and produce unexpected + // behavior. + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + delete proto.attributeChangedCallback; +} + +var useCapture = true; +window.addEventListener('readystatechange', function listener(event) { + if (document.readyState == 'loading') + return; + + for (var i = 0; i != VIEW_TYPES.length; ++i) { + // Register the error-providing custom element only for those view types + // that have not already been registered. Since this module is always loaded + // last, all the view types that are available (i.e. have the proper + // permissions) will have already been registered on |window|. + if (!window[VIEW_TYPES[i]]) + registerGuestViewElement(VIEW_TYPES[i]); + } + + window.removeEventListener(event.type, listener, useCapture); +}, useCapture); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_events.js b/chromium/extensions/renderer/resources/guest_view/guest_view_events.js new file mode 100644 index 00000000000..e3ccde1a6c6 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_events.js @@ -0,0 +1,178 @@ +// Copyright 2015 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. + +// Event management for GuestViewContainers. + +var EventBindings = require('event_bindings'); +var GuestViewInternalNatives = requireNative('guest_view_internal'); +var MessagingNatives = requireNative('messaging_natives'); + +var CreateEvent = function(name) { + var eventOpts = {supportsListeners: true, supportsFilters: true}; + return new EventBindings.Event(name, undefined, eventOpts); +}; + +function GuestViewEvents(view) { + view.events = this; + + this.view = view; + this.on = {}; + + // |setupEventProperty| is normally called automatically, but these events are + // are registered here because they are dispatched from GuestViewContainer + // instead of in response to extension events. + this.setupEventProperty('contentresize'); + this.setupEventProperty('resize'); + this.setupEvents(); +} + +// |GuestViewEvents.EVENTS| is a dictionary of extension events to be listened +// for, which specifies how each event should be handled. The events are +// organized by name, and by default will be dispatched as DOM events with +// the same name. +// |cancelable| (default: false) specifies whether the DOM event's default +// behavior can be canceled. If the default action associated with the event +// is prevented, then its dispatch function will return false in its event +// handler. The event must have a specified |handler| for this to be +// meaningful. +// |evt| specifies a descriptor object for the extension event. An event +// listener will be attached to this descriptor. +// |fields| (default: none) specifies the public-facing fields in the DOM event +// that are accessible to developers. +// |handler| specifies the name of a handler function to be called each time +// that extension event is caught by its event listener. The DOM event +// should be dispatched within this handler function (if desired). With no +// handler function, the DOM event will be dispatched by default each time +// the extension event is caught. +// |internal| (default: false) specifies that the event will not be dispatched +// as a DOM event, and will also not appear as an on* property on the view’s +// element. A |handler| should be specified for all internal events, and +// |fields| and |cancelable| should be left unspecified (as they are only +// meaningful for DOM events). +GuestViewEvents.EVENTS = {}; + +// Attaches |listener| onto the event descriptor object |evt|, and registers it +// to be removed once this GuestViewEvents object is garbage collected. +GuestViewEvents.prototype.addScopedListener = function( + evt, listener, listenerOpts) { + this.listenersToBeRemoved.push({ 'evt': evt, 'listener': listener }); + evt.addListener(listener, listenerOpts); +}; + +// Sets up the handling of events. +GuestViewEvents.prototype.setupEvents = function() { + // An array of registerd event listeners that should be removed when this + // GuestViewEvents is garbage collected. + this.listenersToBeRemoved = []; + MessagingNatives.BindToGC(this, function(listenersToBeRemoved) { + for (var i = 0; i != listenersToBeRemoved.length; ++i) { + listenersToBeRemoved[i].evt.removeListener( + listenersToBeRemoved[i].listener); + listenersToBeRemoved[i] = null; + } + }.bind(undefined, this.listenersToBeRemoved), -1 /* portId */); + + // Set up the GuestView events. + for (var eventName in GuestViewEvents.EVENTS) { + this.setupEvent(eventName, GuestViewEvents.EVENTS[eventName]); + } + + // Set up the derived view's events. + var events = this.getEvents(); + for (var eventName in events) { + this.setupEvent(eventName, events[eventName]); + } +}; + +// Sets up the handling of the |eventName| event. +GuestViewEvents.prototype.setupEvent = function(eventName, eventInfo) { + if (!eventInfo.internal) { + this.setupEventProperty(eventName); + } + + var listenerOpts = { instanceId: this.view.viewInstanceId }; + if (eventInfo.handler) { + this.addScopedListener(eventInfo.evt, this.weakWrapper(function(e) { + this[eventInfo.handler](e, eventName); + }), listenerOpts); + return; + } + + // Internal events are not dispatched as DOM events. + if (eventInfo.internal) { + return; + } + + this.addScopedListener(eventInfo.evt, this.weakWrapper(function(e) { + var domEvent = this.makeDomEvent(e, eventName); + this.view.dispatchEvent(domEvent); + }), listenerOpts); +}; + +// Constructs a DOM event based on the info for the |eventName| event provided +// in either |GuestViewEvents.EVENTS| or getEvents(). +GuestViewEvents.prototype.makeDomEvent = function(event, eventName) { + var eventInfo = + GuestViewEvents.EVENTS[eventName] || this.getEvents()[eventName]; + + // Internal events are not dispatched as DOM events. + if (eventInfo.internal) { + return null; + } + + var details = { bubbles: true }; + if (eventInfo.cancelable) { + details.cancelable = true; + } + var domEvent = new Event(eventName, details); + if (eventInfo.fields) { + $Array.forEach(eventInfo.fields, function(field) { + if (event[field] !== undefined) { + domEvent[field] = event[field]; + } + }.bind(this)); + } + + return domEvent; +}; + +// Adds an 'on<event>' property on the view, which can be used to set/unset +// an event handler. +GuestViewEvents.prototype.setupEventProperty = function(eventName) { + var propertyName = 'on' + eventName.toLowerCase(); + $Object.defineProperty(this.view.element, propertyName, { + get: function() { + return this.on[propertyName]; + }.bind(this), + set: function(value) { + if (this.on[propertyName]) { + this.view.element.removeEventListener(eventName, this.on[propertyName]); + } + this.on[propertyName] = value; + if (value) { + this.view.element.addEventListener(eventName, value); + } + }.bind(this), + enumerable: true + }); +}; + +// returns a wrapper for |func| with a weak reference to |this|. +GuestViewEvents.prototype.weakWrapper = function(func) { + var viewInstanceId = this.view.viewInstanceId; + return function() { + var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); + if (!view) { + return; + } + return $Function.apply(func, view.events, $Array.slice(arguments)); + }; +}; + +// Implemented by the derived event manager, if one exists. +GuestViewEvents.prototype.getEvents = function() { return {}; }; + +// Exports. +exports.$set('GuestViewEvents', GuestViewEvents); +exports.$set('CreateEvent', CreateEvent); diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_iframe.js b/chromium/extensions/renderer/resources/guest_view/guest_view_iframe.js new file mode 100644 index 00000000000..a73de791a98 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_iframe.js @@ -0,0 +1,108 @@ +// Copyright 2015 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. + +// --site-per-process overrides for guest_view.js. + +var GuestView = require('guestView').GuestView; +var GuestViewImpl = require('guestView').GuestViewImpl; +var GuestViewInternalNatives = requireNative('guest_view_internal'); +var ResizeEvent = require('guestView').ResizeEvent; + +var getIframeContentWindow = function(viewInstanceId) { + var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); + if (!view) + return null; + + var internalIframeElement = privates(view).internalElement; + if (internalIframeElement) + return internalIframeElement.contentWindow; + + return null; +}; + +// Internal implementation of attach(). +GuestViewImpl.prototype.attachImpl$ = function( + internalInstanceId, viewInstanceId, attachParams, callback) { + var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); + if (!view.elementAttached) { + // Defer the attachment until the <webview> element is attached. + view.deferredAttachCallback = this.attachImpl$.bind( + this, internalInstanceId, viewInstanceId, attachParams, callback); + return; + }; + + // Check the current state. + if (!this.checkState('attach')) { + this.handleCallback(callback); + return; + } + + // Callback wrapper function to store the contentWindow from the attachGuest() + // callback, handle potential attaching failure, register an automatic detach, + // and advance the queue. + var callbackWrapper = function(callback, contentWindow) { + // Check if attaching failed. + contentWindow = getIframeContentWindow(viewInstanceId); + if (!contentWindow) { + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; + this.internalInstanceId = 0; + } else { + // Only update the contentWindow if attaching is successful. + this.contentWindow = contentWindow; + } + + this.handleCallback(callback); + }; + + attachParams['instanceId'] = viewInstanceId; + var contentWindow = getIframeContentWindow(viewInstanceId); + // |contentWindow| is used to retrieve the RenderFrame in cpp. + GuestViewInternalNatives.AttachIframeGuest( + internalInstanceId, this.id, attachParams, contentWindow, + callbackWrapper.bind(this, callback)); + + this.internalInstanceId = internalInstanceId; + this.state = GuestViewImpl.GuestState.GUEST_STATE_ATTACHED; + + // Detach automatically when the container is destroyed. + GuestViewInternalNatives.RegisterDestructionCallback( + internalInstanceId, this.weakWrapper(function() { + if (this.state != GuestViewImpl.GuestState.GUEST_STATE_ATTACHED || + this.internalInstanceId != internalInstanceId) { + return; + } + + this.internalInstanceId = 0; + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; + }, viewInstanceId)); +}; + +// Internal implementation of create(). +GuestViewImpl.prototype.createImpl$ = function(createParams, callback) { + // Check the current state. + if (!this.checkState('create')) { + this.handleCallback(callback); + return; + } + + // Callback wrapper function to store the guestInstanceId from the + // createGuest() callback, handle potential creation failure, and advance the + // queue. + var callbackWrapper = function(callback, guestInfo) { + this.id = guestInfo.id; + + // Check if creation failed. + if (this.id === 0) { + this.state = GuestViewImpl.GuestState.GUEST_STATE_START; + this.contentWindow = null; + } + + ResizeEvent.addListener(this.callOnResize, {instanceId: this.id}); + this.handleCallback(callback); + }; + + this.sendCreateRequest(createParams, callbackWrapper.bind(this, callback)); + + this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; +}; diff --git a/chromium/extensions/renderer/resources/guest_view/guest_view_iframe_container.js b/chromium/extensions/renderer/resources/guest_view/guest_view_iframe_container.js new file mode 100644 index 00000000000..4cd628be8f7 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/guest_view_iframe_container.js @@ -0,0 +1,30 @@ +// Copyright 2015 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. + +// --site-per-process overrides for guest_view_container.js + +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; +var IdGenerator = requireNative('id_generator'); + +GuestViewContainer.prototype.createInternalElement$ = function() { + var iframeElement = document.createElement('iframe'); + iframeElement.style.width = '100%'; + iframeElement.style.height = '100%'; + privates(iframeElement).internal = this; + return iframeElement; +}; + +GuestViewContainer.prototype.attachWindow$ = function() { + var generatedId = IdGenerator.GetNextId(); + // Generate an instance id for the container. + this.onInternalInstanceId(generatedId); + return true; +}; + +GuestViewContainer.prototype.willAttachElement = function () { + if (this.deferredAttachCallback) { + this.deferredAttachCallback(); + this.deferredAttachCallback = null; + } +}; diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view.js new file mode 100644 index 00000000000..b5d08c19ad5 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view.js @@ -0,0 +1,235 @@ +// Copyright (c) 2012 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. + +// This module implements WebView (<webview>) as a custom element that wraps a +// BrowserPlugin object element. The object element is hidden within +// the shadow DOM of the WebView element. + +var DocumentNatives = requireNative('document_natives'); +var GuestView = require('guestView').GuestView; +var GuestViewContainer = require('guestViewContainer').GuestViewContainer; +var GuestViewInternalNatives = requireNative('guest_view_internal'); +var WebViewConstants = require('webViewConstants').WebViewConstants; +var WebViewEvents = require('webViewEvents').WebViewEvents; +var WebViewInternal = require('webViewInternal').WebViewInternal; + +// Represents the internal state of <webview>. +function WebViewImpl(webviewElement) { + GuestViewContainer.call(this, webviewElement, 'webview'); + this.cachedZoom = 1; + this.setupElementProperties(); + new WebViewEvents(this, this.viewInstanceId); +} + +WebViewImpl.prototype.__proto__ = GuestViewContainer.prototype; + +WebViewImpl.VIEW_TYPE = 'WebView'; + +// Add extra functionality to |this.element|. +WebViewImpl.setupElement = function(proto) { + // Public-facing API methods. + var apiMethods = WebViewImpl.getApiMethods(); + + // Add the experimental API methods, if available. + var experimentalApiMethods = WebViewImpl.maybeGetExperimentalApiMethods(); + apiMethods = $Array.concat(apiMethods, experimentalApiMethods); + + // Create default implementations for undefined API methods. + var createDefaultApiMethod = function(m) { + return function(var_args) { + if (!this.guest.getId()) { + return false; + } + var args = $Array.concat([this.guest.getId()], $Array.slice(arguments)); + $Function.apply(WebViewInternal[m], null, args); + return true; + }; + }; + for (var i = 0; i != apiMethods.length; ++i) { + if (WebViewImpl.prototype[apiMethods[i]] == undefined) { + WebViewImpl.prototype[apiMethods[i]] = + createDefaultApiMethod(apiMethods[i]); + } + } + + // Forward proto.foo* method calls to WebViewImpl.foo*. + GuestViewContainer.forwardApiMethods(proto, apiMethods); +}; + +// Initiates navigation once the <webview> element is attached to the DOM. +WebViewImpl.prototype.onElementAttached = function() { + // Mark all attributes as dirty on attachment. + for (var i in this.attributes) { + this.attributes[i].dirty = true; + } + for (var i in this.attributes) { + this.attributes[i].attach(); + } +}; + +// Resets some state upon detaching <webview> element from the DOM. +WebViewImpl.prototype.onElementDetached = function() { + this.guest.destroy(); + for (var i in this.attributes) { + this.attributes[i].dirty = false; + } + for (var i in this.attributes) { + this.attributes[i].detach(); + } +}; + +// Sets the <webview>.request property. +WebViewImpl.prototype.setRequestPropertyOnWebViewElement = function(request) { + Object.defineProperty( + this.element, + 'request', + { + value: request, + enumerable: true + } + ); +}; + +WebViewImpl.prototype.setupElementProperties = function() { + // We cannot use {writable: true} property descriptor because we want a + // dynamic getter value. + Object.defineProperty(this.element, 'contentWindow', { + get: function() { + return this.guest.getContentWindow(); + }.bind(this), + // No setter. + enumerable: true + }); +}; + +WebViewImpl.prototype.onSizeChanged = function(webViewEvent) { + var newWidth = webViewEvent.newWidth; + var newHeight = webViewEvent.newHeight; + + var element = this.element; + + var width = element.offsetWidth; + var height = element.offsetHeight; + + // Check the current bounds to make sure we do not resize <webview> + // outside of current constraints. + var maxWidth = this.attributes[ + WebViewConstants.ATTRIBUTE_MAXWIDTH].getValue() || width; + var minWidth = this.attributes[ + WebViewConstants.ATTRIBUTE_MINWIDTH].getValue() || width; + var maxHeight = this.attributes[ + WebViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() || height; + var minHeight = this.attributes[ + WebViewConstants.ATTRIBUTE_MINHEIGHT].getValue() || height; + + minWidth = Math.min(minWidth, maxWidth); + minHeight = Math.min(minHeight, maxHeight); + + if (!this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || + (newWidth >= minWidth && + newWidth <= maxWidth && + newHeight >= minHeight && + newHeight <= maxHeight)) { + element.style.width = newWidth + 'px'; + element.style.height = newHeight + 'px'; + // Only fire the DOM event if the size of the <webview> has actually + // changed. + this.dispatchEvent(webViewEvent); + } +}; + +WebViewImpl.prototype.createGuest = function() { + this.guest.create(this.buildParams(), function() { + this.attachWindow$(); + }.bind(this)); +}; + +WebViewImpl.prototype.onFrameNameChanged = function(name) { + this.attributes[WebViewConstants.ATTRIBUTE_NAME].setValueIgnoreMutation(name); +}; + +// Updates state upon loadcommit. +WebViewImpl.prototype.onLoadCommit = function( + baseUrlForDataUrl, currentEntryIndex, entryCount, + processId, url, isTopLevel) { + this.baseUrlForDataUrl = baseUrlForDataUrl; + this.currentEntryIndex = currentEntryIndex; + this.entryCount = entryCount; + this.processId = processId; + if (isTopLevel) { + // Touching the src attribute triggers a navigation. To avoid + // triggering a page reload on every guest-initiated navigation, + // we do not handle this mutation. + this.attributes[ + WebViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(url); + } +}; + +WebViewImpl.prototype.onAttach = function(storagePartitionId) { + this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].setValueIgnoreMutation( + storagePartitionId); +}; + +WebViewImpl.prototype.buildContainerParams = function() { + var params = { 'initialZoomFactor': this.cachedZoomFactor, + 'userAgentOverride': this.userAgentOverride }; + for (var i in this.attributes) { + var value = this.attributes[i].getValueIfDirty(); + if (value) + params[i] = value; + } + return params; +}; + +WebViewImpl.prototype.attachWindow$ = function(opt_guestInstanceId) { + // If |opt_guestInstanceId| was provided, then a different existing guest is + // being attached to this webview, and the current one will get destroyed. + if (opt_guestInstanceId) { + if (this.guest.getId() == opt_guestInstanceId) { + return true; + } + this.guest.destroy(); + this.guest = new GuestView('webview', opt_guestInstanceId); + } + + return GuestViewContainer.prototype.attachWindow$.call(this); +}; + +// Shared implementation of executeScript() and insertCSS(). +WebViewImpl.prototype.executeCode = function(func, args) { + if (!this.guest.getId()) { + window.console.error(WebViewConstants.ERROR_MSG_CANNOT_INJECT_SCRIPT); + return false; + } + + var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue(); + if (this.baseUrlForDataUrl) { + webviewSrc = this.baseUrlForDataUrl; + } + + args = $Array.concat([this.guest.getId(), webviewSrc], + $Array.slice(args)); + $Function.apply(func, null, args); + return true; +} + +// Requests the <webview> element wihtin the embedder to enter fullscreen. +WebViewImpl.prototype.makeElementFullscreen = function() { + GuestViewInternalNatives.RunWithGesture(function() { + this.element.webkitRequestFullScreen(); + }.bind(this)); +}; + +// Implemented when the ChromeWebView API is available. +WebViewImpl.prototype.maybeSetupContextMenus = function() {}; + +// Implemented when the experimental WebView API is available. +WebViewImpl.maybeGetExperimentalApiMethods = function() { + return []; +}; + +GuestViewContainer.registerElement(WebViewImpl); + +// Exports. +exports.$set('WebViewImpl', WebViewImpl); diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_action_requests.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_action_requests.js new file mode 100644 index 00000000000..5f4bf2c0607 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_action_requests.js @@ -0,0 +1,296 @@ +// 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. + +// This module implements helper objects for the dialog, newwindow, and +// permissionrequest <webview> events. + +var MessagingNatives = requireNative('messaging_natives'); +var WebViewConstants = require('webViewConstants').WebViewConstants; +var WebViewInternal = require('webViewInternal').WebViewInternal; + +var PERMISSION_TYPES = ['media', + 'geolocation', + 'pointerLock', + 'download', + 'loadplugin', + 'filesystem', + 'fullscreen']; + +// ----------------------------------------------------------------------------- +// WebViewActionRequest object. + +// Default partial implementation of a webview action request. +function WebViewActionRequest(webViewImpl, event, webViewEvent, interfaceName) { + this.webViewImpl = webViewImpl; + this.event = event; + this.webViewEvent = webViewEvent; + this.interfaceName = interfaceName; + this.guestInstanceId = this.webViewImpl.guest.getId(); + this.requestId = event.requestId; + this.actionTaken = false; + + // Add on the request information specific to the request type. + for (var infoName in this.event.requestInfo) { + this.event[infoName] = this.event.requestInfo[infoName]; + this.webViewEvent[infoName] = this.event.requestInfo[infoName]; + } +} + +// Performs the default action for the request. +WebViewActionRequest.prototype.defaultAction = function() { + // Do nothing if the action has already been taken or the requester is + // already gone (in which case its guestInstanceId will be stale). + if (this.actionTaken || + this.guestInstanceId != this.webViewImpl.guest.getId()) { + return; + } + + this.actionTaken = true; + WebViewInternal.setPermission(this.guestInstanceId, this.requestId, + 'default', '', function(allowed) { + if (allowed) { + return; + } + this.showWarningMessage(); + }.bind(this)); +}; + +// Called to handle the action request's event. +WebViewActionRequest.prototype.handleActionRequestEvent = function() { + // Construct the interface object and attach it to |webViewEvent|. + var request = this.getInterfaceObject(); + this.webViewEvent[this.interfaceName] = request; + + var defaultPrevented = !this.webViewImpl.dispatchEvent(this.webViewEvent); + // Set |webViewEvent| to null to break the circular reference to |request| so + // that the garbage collector can eventually collect it. + this.webViewEvent = null; + if (this.actionTaken) { + return; + } + + if (defaultPrevented) { + // Track the lifetime of |request| with the garbage collector. + var portId = -1; // (hack) there is no Extension Port to release + MessagingNatives.BindToGC(request, this.defaultAction.bind(this), portId); + } else { + this.defaultAction(); + } +}; + +// Displays a warning message when an action request is blocked by default. +WebViewActionRequest.prototype.showWarningMessage = function() { + window.console.warn(this.WARNING_MSG_REQUEST_BLOCKED); +}; + +// This function ensures that each action is taken at most once. +WebViewActionRequest.prototype.validateCall = function() { + if (this.actionTaken) { + throw new Error(this.ERROR_MSG_ACTION_ALREADY_TAKEN); + } + this.actionTaken = true; +}; + +// The following are implemented by the specific action request. + +// Returns the interface object for this action request. +WebViewActionRequest.prototype.getInterfaceObject = undefined; + +// Error/warning messages. +WebViewActionRequest.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = undefined; +WebViewActionRequest.prototype.WARNING_MSG_REQUEST_BLOCKED = undefined; + +// ----------------------------------------------------------------------------- +// Dialog object. + +// Represents a dialog box request (e.g. alert()). +function Dialog(webViewImpl, event, webViewEvent) { + WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'dialog'); + + this.handleActionRequestEvent(); +} + +Dialog.prototype.__proto__ = WebViewActionRequest.prototype; + +Dialog.prototype.getInterfaceObject = function() { + return { + ok: function(user_input) { + this.validateCall(); + user_input = user_input || ''; + WebViewInternal.setPermission( + this.guestInstanceId, this.requestId, 'allow', user_input); + }.bind(this), + cancel: function() { + this.validateCall(); + WebViewInternal.setPermission( + this.guestInstanceId, this.requestId, 'deny'); + }.bind(this) + }; +}; + +Dialog.prototype.showWarningMessage = function() { + var VOWELS = ['a', 'e', 'i', 'o', 'u']; + var dialogType = this.event.messageType; + var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A'; + this.WARNING_MSG_REQUEST_BLOCKED = this.WARNING_MSG_REQUEST_BLOCKED. + replace('%1', article).replace('%2', dialogType); + window.console.warn(this.WARNING_MSG_REQUEST_BLOCKED); +}; + +Dialog.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = + WebViewConstants.ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN; +Dialog.prototype.WARNING_MSG_REQUEST_BLOCKED = + WebViewConstants.WARNING_MSG_DIALOG_REQUEST_BLOCKED; + +// ----------------------------------------------------------------------------- +// NewWindow object. + +// Represents a new window request. +function NewWindow(webViewImpl, event, webViewEvent) { + WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'window'); + + this.handleActionRequestEvent(); +} + +NewWindow.prototype.__proto__ = WebViewActionRequest.prototype; + +NewWindow.prototype.getInterfaceObject = function() { + return { + attach: function(webview) { + this.validateCall(); + if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW') { + throw new Error(ERROR_MSG_WEBVIEW_EXPECTED); + } + + var webViewImpl = privates(webview).internal; + // Update the partition. + if (this.event.partition) { + webViewImpl.onAttach(this.event.partition); + } + + var attached = webViewImpl.attachWindow$(this.event.windowId); + if (!attached) { + window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); + } + + if (this.guestInstanceId != this.webViewImpl.guest.getId()) { + // If the opener is already gone, then its guestInstanceId will be + // stale. + return; + } + + // If the object being passed into attach is not a valid <webview> + // then we will fail and it will be treated as if the new window + // was rejected. The permission API plumbing is used here to clean + // up the state created for the new window if attaching fails. + WebViewInternal.setPermission(this.guestInstanceId, this.requestId, + attached ? 'allow' : 'deny'); + }.bind(this), + discard: function() { + this.validateCall(); + if (!this.guestInstanceId) { + // If the opener is already gone, then we won't have its + // guestInstanceId. + return; + } + WebViewInternal.setPermission( + this.guestInstanceId, this.requestId, 'deny'); + }.bind(this) + }; +}; + +NewWindow.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = + WebViewConstants.ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN; +NewWindow.prototype.WARNING_MSG_REQUEST_BLOCKED = + WebViewConstants.WARNING_MSG_NEWWINDOW_REQUEST_BLOCKED; + +// ----------------------------------------------------------------------------- +// PermissionRequest object. + +// Represents a permission request (e.g. to access the filesystem). +function PermissionRequest(webViewImpl, event, webViewEvent) { + WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'request'); + + if (!this.validPermissionCheck()) { + return; + } + + this.handleActionRequestEvent(); +} + +PermissionRequest.prototype.__proto__ = WebViewActionRequest.prototype; + +PermissionRequest.prototype.allow = function() { + this.validateCall(); + WebViewInternal.setPermission(this.guestInstanceId, this.requestId, 'allow'); +}; + +PermissionRequest.prototype.deny = function() { + this.validateCall(); + WebViewInternal.setPermission(this.guestInstanceId, this.requestId, 'deny'); +}; + +PermissionRequest.prototype.getInterfaceObject = function() { + var request = { + allow: this.allow.bind(this), + deny: this.deny.bind(this) + }; + + // Add on the request information specific to the request type. + for (var infoName in this.event.requestInfo) { + request[infoName] = this.event.requestInfo[infoName]; + } + + return $Object.freeze(request); +}; + +PermissionRequest.prototype.showWarningMessage = function() { + window.console.warn( + this.WARNING_MSG_REQUEST_BLOCKED.replace('%1', this.event.permission)); +}; + +// Checks that the requested permission is valid. Returns true if valid. +PermissionRequest.prototype.validPermissionCheck = function() { + if (PERMISSION_TYPES.indexOf(this.event.permission) < 0) { + // The permission type is not allowed. Trigger the default response. + this.defaultAction(); + return false; + } + return true; +}; + +PermissionRequest.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = + WebViewConstants.ERROR_MSG_PERMISSION_ACTION_ALREADY_TAKEN; +PermissionRequest.prototype.WARNING_MSG_REQUEST_BLOCKED = + WebViewConstants.WARNING_MSG_PERMISSION_REQUEST_BLOCKED; + +// ----------------------------------------------------------------------------- + +// FullscreenPermissionRequest object. + +// Represents a fullscreen permission request. +function FullscreenPermissionRequest(webViewImpl, event, webViewEvent) { + PermissionRequest.call(this, webViewImpl, event, webViewEvent); +} + +FullscreenPermissionRequest.prototype.__proto__ = PermissionRequest.prototype; + +FullscreenPermissionRequest.prototype.allow = function() { + PermissionRequest.prototype.allow.call(this); + // Now make the <webview> element go fullscreen. + this.webViewImpl.makeElementFullscreen(); +}; + +// ----------------------------------------------------------------------------- + +var WebViewActionRequests = { + WebViewActionRequest: WebViewActionRequest, + Dialog: Dialog, + NewWindow: NewWindow, + PermissionRequest: PermissionRequest, + FullscreenPermissionRequest: FullscreenPermissionRequest +}; + +// Exports. +exports.$set('WebViewActionRequests', WebViewActionRequests); diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_api_methods.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_api_methods.js new file mode 100644 index 00000000000..a28741b7430 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_api_methods.js @@ -0,0 +1,204 @@ +// 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. + +// This module implements the public-facing API functions for the <webview> tag. + +var WebViewInternal = require('webViewInternal').WebViewInternal; +var WebViewImpl = require('webView').WebViewImpl; + +// An array of <webview>'s public-facing API methods. Methods without custom +// implementations will be given default implementations that call into the +// internal API method with the same name in |WebViewInternal|. For example, a +// method called 'someApiMethod' would be given the following default +// implementation: +// +// WebViewImpl.prototype.someApiMethod = function(var_args) { +// if (!this.guest.getId()) { +// return false; +// } +// var args = $Array.concat([this.guest.getId()], $Array.slice(arguments)); +// $Function.apply(WebViewInternal.someApiMethod, null, args); +// return true; +// }; +// +// These default implementations come from createDefaultApiMethod() in +// web_view.js. +var WEB_VIEW_API_METHODS = [ + // Add content scripts for the guest page. + 'addContentScripts', + + // Navigates to the previous history entry. + 'back', + + // Returns whether there is a previous history entry to navigate to. + 'canGoBack', + + // Returns whether there is a subsequent history entry to navigate to. + 'canGoForward', + + // Clears browsing data for the WebView partition. + 'clearData', + + // Injects JavaScript code into the guest page. + 'executeScript', + + // Initiates a find-in-page request. + 'find', + + // Navigates to the subsequent history entry. + 'forward', + + // Returns Chrome's internal process ID for the guest web page's current + // process. + 'getProcessId', + + // Returns the user agent string used by the webview for guest page requests. + 'getUserAgent', + + // Gets the current zoom factor. + 'getZoom', + + // Gets the current zoom mode of the webview. + 'getZoomMode', + + // Navigates to a history entry using a history index relative to the current + // navigation. + 'go', + + // Injects CSS into the guest page. + 'insertCSS', + + // Indicates whether or not the webview's user agent string has been + // overridden. + 'isUserAgentOverridden', + + // Loads a data URL with a specified base URL used for relative links. + // Optionally, a virtual URL can be provided to be shown to the user instead + // of the data URL. + 'loadDataWithBaseUrl', + + // Prints the contents of the webview. + 'print', + + // Removes content scripts for the guest page. + 'removeContentScripts', + + // Reloads the current top-level page. + 'reload', + + // Override the user agent string used by the webview for guest page requests. + 'setUserAgentOverride', + + // Changes the zoom factor of the page. + 'setZoom', + + // Changes the zoom mode of the webview. + 'setZoomMode', + + // Stops loading the current navigation if one is in progress. + 'stop', + + // Ends the current find session. + 'stopFinding', + + // Forcibly kills the guest web page's renderer process. + 'terminate' +]; + +// ----------------------------------------------------------------------------- +// Custom API method implementations. + +WebViewImpl.prototype.addContentScripts = function(rules) { + return WebViewInternal.addContentScripts(this.viewInstanceId, rules); +}; + +WebViewImpl.prototype.back = function(callback) { + return this.go(-1, callback); +}; + +WebViewImpl.prototype.canGoBack = function() { + return this.entryCount > 1 && this.currentEntryIndex > 0; +}; + +WebViewImpl.prototype.canGoForward = function() { + return this.currentEntryIndex >= 0 && + this.currentEntryIndex < (this.entryCount - 1); +}; + +WebViewImpl.prototype.executeScript = function(var_args) { + return this.executeCode(WebViewInternal.executeScript, + $Array.slice(arguments)); +}; + +WebViewImpl.prototype.forward = function(callback) { + return this.go(1, callback); +}; + +WebViewImpl.prototype.getProcessId = function() { + return this.processId; +}; + +WebViewImpl.prototype.getUserAgent = function() { + return this.userAgentOverride || navigator.userAgent; +}; + +WebViewImpl.prototype.insertCSS = function(var_args) { + return this.executeCode(WebViewInternal.insertCSS, $Array.slice(arguments)); +}; + +WebViewImpl.prototype.isUserAgentOverridden = function() { + return !!this.userAgentOverride && + this.userAgentOverride != navigator.userAgent; +}; + +WebViewImpl.prototype.loadDataWithBaseUrl = function( + dataUrl, baseUrl, virtualUrl) { + if (!this.guest.getId()) { + return; + } + WebViewInternal.loadDataWithBaseUrl( + this.guest.getId(), dataUrl, baseUrl, virtualUrl, function() { + // Report any errors. + if (chrome.runtime.lastError != undefined) { + window.console.error( + 'Error while running webview.loadDataWithBaseUrl: ' + + chrome.runtime.lastError.message); + } + }); +}; + +WebViewImpl.prototype.print = function() { + return this.executeScript({code: 'window.print();'}); +}; + +WebViewImpl.prototype.removeContentScripts = function(names) { + return WebViewInternal.removeContentScripts(this.viewInstanceId, names); +}; + +WebViewImpl.prototype.setUserAgentOverride = function(userAgentOverride) { + this.userAgentOverride = userAgentOverride; + if (!this.guest.getId()) { + // If we are not attached yet, then we will pick up the user agent on + // attachment. + return false; + } + WebViewInternal.overrideUserAgent(this.guest.getId(), userAgentOverride); + return true; +}; + +WebViewImpl.prototype.setZoom = function(zoomFactor, callback) { + if (!this.guest.getId()) { + this.cachedZoomFactor = zoomFactor; + return false; + } + this.cachedZoomFactor = 1; + WebViewInternal.setZoom(this.guest.getId(), zoomFactor, callback); + return true; +}; + +// ----------------------------------------------------------------------------- + +WebViewImpl.getApiMethods = function() { + return WEB_VIEW_API_METHODS; +}; diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_attributes.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_attributes.js new file mode 100644 index 00000000000..ce538fdc3df --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_attributes.js @@ -0,0 +1,277 @@ +// 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. + +// This module implements the attributes of the <webview> tag. + +var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; +var WebViewConstants = require('webViewConstants').WebViewConstants; +var WebViewImpl = require('webView').WebViewImpl; +var WebViewInternal = require('webViewInternal').WebViewInternal; + +// ----------------------------------------------------------------------------- +// AllowScalingAttribute object. + +// Attribute that specifies whether scaling is allowed in the webview. +function AllowScalingAttribute(view) { + GuestViewAttributes.BooleanAttribute.call( + this, WebViewConstants.ATTRIBUTE_ALLOWSCALING, view); +} + +AllowScalingAttribute.prototype.__proto__ = + GuestViewAttributes.BooleanAttribute.prototype; + +AllowScalingAttribute.prototype.handleMutation = function(oldValue, newValue) { + if (!this.view.guest.getId()) + return; + + WebViewInternal.setAllowScaling(this.view.guest.getId(), this.getValue()); +}; + +// ----------------------------------------------------------------------------- +// AllowTransparencyAttribute object. + +// Attribute that specifies whether transparency is allowed in the webview. +function AllowTransparencyAttribute(view) { + GuestViewAttributes.BooleanAttribute.call( + this, WebViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY, view); +} + +AllowTransparencyAttribute.prototype.__proto__ = + GuestViewAttributes.BooleanAttribute.prototype; + +AllowTransparencyAttribute.prototype.handleMutation = function(oldValue, + newValue) { + if (!this.view.guest.getId()) + return; + + WebViewInternal.setAllowTransparency(this.view.guest.getId(), + this.getValue()); +}; + +// ----------------------------------------------------------------------------- +// AutosizeDimensionAttribute object. + +// Attribute used to define the demension limits of autosizing. +function AutosizeDimensionAttribute(name, view) { + GuestViewAttributes.IntegerAttribute.call(this, name, view); +} + +AutosizeDimensionAttribute.prototype.__proto__ = + GuestViewAttributes.IntegerAttribute.prototype; + +AutosizeDimensionAttribute.prototype.handleMutation = function( + oldValue, newValue) { + if (!this.view.guest.getId()) + return; + + this.view.guest.setSize({ + 'enableAutoSize': this.view.attributes[ + WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue(), + 'min': { + 'width': this.view.attributes[ + WebViewConstants.ATTRIBUTE_MINWIDTH].getValue(), + 'height': this.view.attributes[ + WebViewConstants.ATTRIBUTE_MINHEIGHT].getValue() + }, + 'max': { + 'width': this.view.attributes[ + WebViewConstants.ATTRIBUTE_MAXWIDTH].getValue(), + 'height': this.view.attributes[ + WebViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() + } + }); + return; +}; + +// ----------------------------------------------------------------------------- +// AutosizeAttribute object. + +// Attribute that specifies whether the webview should be autosized. +function AutosizeAttribute(view) { + GuestViewAttributes.BooleanAttribute.call( + this, WebViewConstants.ATTRIBUTE_AUTOSIZE, view); +} + +AutosizeAttribute.prototype.__proto__ = + GuestViewAttributes.BooleanAttribute.prototype; + +AutosizeAttribute.prototype.handleMutation = + AutosizeDimensionAttribute.prototype.handleMutation; + +// ----------------------------------------------------------------------------- +// NameAttribute object. + +// Attribute that sets the guest content's window.name object. +function NameAttribute(view) { + GuestViewAttributes.Attribute.call( + this, WebViewConstants.ATTRIBUTE_NAME, view); +} + +NameAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype + +NameAttribute.prototype.handleMutation = function(oldValue, newValue) { + oldValue = oldValue || ''; + newValue = newValue || ''; + if (oldValue === newValue || !this.view.guest.getId()) + return; + + WebViewInternal.setName(this.view.guest.getId(), newValue); +}; + +NameAttribute.prototype.setValue = function(value) { + value = value || ''; + if (value === '') + this.view.element.removeAttribute(this.name); + else + this.view.element.setAttribute(this.name, value); +}; + +// ----------------------------------------------------------------------------- +// PartitionAttribute object. + +// Attribute representing the state of the storage partition. +function PartitionAttribute(view) { + GuestViewAttributes.Attribute.call( + this, WebViewConstants.ATTRIBUTE_PARTITION, view); + this.validPartitionId = true; +} + +PartitionAttribute.prototype.__proto__ = + GuestViewAttributes.Attribute.prototype; + +PartitionAttribute.prototype.handleMutation = function(oldValue, newValue) { + newValue = newValue || ''; + + // The partition cannot change if the webview has already navigated. + if (!this.view.attributes[ + WebViewConstants.ATTRIBUTE_SRC].beforeFirstNavigation) { + window.console.error(WebViewConstants.ERROR_MSG_ALREADY_NAVIGATED); + this.setValueIgnoreMutation(oldValue); + return; + } + if (newValue == 'persist:') { + this.validPartitionId = false; + window.console.error( + WebViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE); + } +}; + +PartitionAttribute.prototype.detach = function() { + this.validPartitionId = true; +}; + +// ----------------------------------------------------------------------------- +// SrcAttribute object. + +// Attribute that handles the location and navigation of the webview. +function SrcAttribute(view) { + GuestViewAttributes.Attribute.call( + this, WebViewConstants.ATTRIBUTE_SRC, view); + this.setupMutationObserver(); + this.beforeFirstNavigation = true; +} + +SrcAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype; + +SrcAttribute.prototype.setValueIgnoreMutation = function(value) { + GuestViewAttributes.Attribute.prototype.setValueIgnoreMutation.call( + this, value); + // takeRecords() is needed to clear queued up src mutations. Without it, it is + // possible for this change to get picked up asyncronously by src's mutation + // observer |observer|, and then get handled even though we do not want to + // handle this mutation. + this.observer.takeRecords(); +} + +SrcAttribute.prototype.handleMutation = function(oldValue, newValue) { + // Once we have navigated, we don't allow clearing the src attribute. + // Once <webview> enters a navigated state, it cannot return to a + // placeholder state. + if (!newValue && oldValue) { + // src attribute changes normally initiate a navigation. We suppress + // the next src attribute handler call to avoid reloading the page + // on every guest-initiated navigation. + this.setValueIgnoreMutation(oldValue); + return; + } + this.parse(); +}; + +SrcAttribute.prototype.attach = function() { + this.parse(); +}; + +SrcAttribute.prototype.detach = function() { + this.beforeFirstNavigation = true; +}; + +// The purpose of this mutation observer is to catch assignment to the src +// attribute without any changes to its value. This is useful in the case +// where the webview guest has crashed and navigating to the same address +// spawns off a new process. +SrcAttribute.prototype.setupMutationObserver = + function() { + this.observer = new MutationObserver(function(mutations) { + $Array.forEach(mutations, function(mutation) { + var oldValue = mutation.oldValue; + var newValue = this.getValue(); + if (oldValue != newValue) { + return; + } + this.handleMutation(oldValue, newValue); + }.bind(this)); + }.bind(this)); + var params = { + attributes: true, + attributeOldValue: true, + attributeFilter: [this.name] + }; + this.observer.observe(this.view.element, params); +}; + +SrcAttribute.prototype.parse = function() { + if (!this.view.elementAttached || + !this.view.attributes[ + WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId || + !this.getValue()) { + return; + } + + if (!this.view.guest.getId()) { + if (this.beforeFirstNavigation) { + this.beforeFirstNavigation = false; + this.view.createGuest(); + } + return; + } + + WebViewInternal.navigate(this.view.guest.getId(), this.getValue()); +}; + +// ----------------------------------------------------------------------------- + +// Sets up all of the webview attributes. +WebViewImpl.prototype.setupAttributes = function() { + this.attributes[WebViewConstants.ATTRIBUTE_ALLOWSCALING] = + new AllowScalingAttribute(this); + this.attributes[WebViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY] = + new AllowTransparencyAttribute(this); + this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE] = + new AutosizeAttribute(this); + this.attributes[WebViewConstants.ATTRIBUTE_NAME] = + new NameAttribute(this); + this.attributes[WebViewConstants.ATTRIBUTE_PARTITION] = + new PartitionAttribute(this); + this.attributes[WebViewConstants.ATTRIBUTE_SRC] = + new SrcAttribute(this); + + var autosizeAttributes = [WebViewConstants.ATTRIBUTE_MAXHEIGHT, + WebViewConstants.ATTRIBUTE_MAXWIDTH, + WebViewConstants.ATTRIBUTE_MINHEIGHT, + WebViewConstants.ATTRIBUTE_MINWIDTH]; + for (var i = 0; autosizeAttributes[i]; ++i) { + this.attributes[autosizeAttributes[i]] = + new AutosizeDimensionAttribute(autosizeAttributes[i], this); + } +}; diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_constants.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_constants.js new file mode 100644 index 00000000000..09110e14749 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_constants.js @@ -0,0 +1,40 @@ +// Copyright (c) 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. + +// This module contains constants used in webview. + +// Container for the webview constants. +var WebViewConstants = { + // Attributes. + ATTRIBUTE_ALLOWTRANSPARENCY: 'allowtransparency', + ATTRIBUTE_ALLOWSCALING: 'allowscaling', + ATTRIBUTE_AUTOSIZE: 'autosize', + ATTRIBUTE_MAXHEIGHT: 'maxheight', + ATTRIBUTE_MAXWIDTH: 'maxwidth', + ATTRIBUTE_MINHEIGHT: 'minheight', + ATTRIBUTE_MINWIDTH: 'minwidth', + ATTRIBUTE_NAME: 'name', + ATTRIBUTE_PARTITION: 'partition', + ATTRIBUTE_SRC: 'src', + + // Error/warning messages. + ERROR_MSG_ALREADY_NAVIGATED: '<webview>: ' + + 'The object has already navigated, so its partition cannot be changed.', + ERROR_MSG_CANNOT_INJECT_SCRIPT: '<webview>: ' + + 'Script cannot be injected into content until the page has loaded.', + ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN: '<webview>: ' + + 'An action has already been taken for this "dialog" event.', + ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN: '<webview>: ' + + 'An action has already been taken for this "newwindow" event.', + ERROR_MSG_PERMISSION_ACTION_ALREADY_TAKEN: '<webview>: ' + + 'Permission has already been decided for this "permissionrequest" event.', + ERROR_MSG_INVALID_PARTITION_ATTRIBUTE: '<webview>: ' + + 'Invalid partition attribute.', + WARNING_MSG_DIALOG_REQUEST_BLOCKED: '<webview>: %1 %2 dialog was blocked.', + WARNING_MSG_NEWWINDOW_REQUEST_BLOCKED: '<webview>: A new window was blocked.', + WARNING_MSG_PERMISSION_REQUEST_BLOCKED: '<webview>: ' + + 'The permission request for "%1" has been denied.' +}; + +exports.$set('WebViewConstants', $Object.freeze(WebViewConstants)); diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_events.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_events.js new file mode 100644 index 00000000000..1077684de00 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_events.js @@ -0,0 +1,301 @@ +// 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. + +// Event management for WebView. + +var CreateEvent = require('guestViewEvents').CreateEvent; +var DeclarativeWebRequestSchema = + requireNative('schema_registry').GetSchema('declarativeWebRequest'); +var EventBindings = require('event_bindings'); +var GuestViewEvents = require('guestViewEvents').GuestViewEvents; +var GuestViewInternalNatives = requireNative('guest_view_internal'); +var IdGenerator = requireNative('id_generator'); +var WebRequestEvent = require('webRequestInternal').WebRequestEvent; +var WebRequestSchema = + requireNative('schema_registry').GetSchema('webRequest'); +var WebViewActionRequests = + require('webViewActionRequests').WebViewActionRequests; + +var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage'); + +function WebViewEvents(webViewImpl) { + GuestViewEvents.call(this, webViewImpl); + + this.setupWebRequestEvents(); + this.view.maybeSetupContextMenus(); +} + +WebViewEvents.prototype.__proto__ = GuestViewEvents.prototype; + +// A dictionary of <webview> extension events to be listened for. This +// dictionary augments |GuestViewEvents.EVENTS| in guest_view_events.js. See the +// documentation there for details. +WebViewEvents.EVENTS = { + 'close': { + evt: CreateEvent('webViewInternal.onClose') + }, + 'consolemessage': { + evt: CreateEvent('webViewInternal.onConsoleMessage'), + fields: ['level', 'message', 'line', 'sourceId'] + }, + 'contentload': { + evt: CreateEvent('webViewInternal.onContentLoad') + }, + 'dialog': { + cancelable: true, + evt: CreateEvent('webViewInternal.onDialog'), + fields: ['defaultPromptText', 'messageText', 'messageType', 'url'], + handler: 'handleDialogEvent' + }, + 'droplink': { + evt: CreateEvent('webViewInternal.onDropLink'), + fields: ['url'] + }, + 'exit': { + evt: CreateEvent('webViewInternal.onExit'), + fields: ['processId', 'reason'] + }, + 'exitfullscreen': { + evt: CreateEvent('webViewInternal.onExitFullscreen'), + fields: ['url'], + handler: 'handleFullscreenExitEvent', + internal: true + }, + 'findupdate': { + evt: CreateEvent('webViewInternal.onFindReply'), + fields: [ + 'searchText', + 'numberOfMatches', + 'activeMatchOrdinal', + 'selectionRect', + 'canceled', + 'finalUpdate' + ] + }, + 'framenamechanged': { + evt: CreateEvent('webViewInternal.onFrameNameChanged'), + handler: 'handleFrameNameChangedEvent', + internal: true + }, + 'loadabort': { + cancelable: true, + evt: CreateEvent('webViewInternal.onLoadAbort'), + fields: ['url', 'isTopLevel', 'code', 'reason'], + handler: 'handleLoadAbortEvent' + }, + 'loadcommit': { + evt: CreateEvent('webViewInternal.onLoadCommit'), + fields: ['url', 'isTopLevel'], + handler: 'handleLoadCommitEvent' + }, + 'loadprogress': { + evt: CreateEvent('webViewInternal.onLoadProgress'), + fields: ['url', 'progress'] + }, + 'loadredirect': { + evt: CreateEvent('webViewInternal.onLoadRedirect'), + fields: ['isTopLevel', 'oldUrl', 'newUrl'] + }, + 'loadstart': { + evt: CreateEvent('webViewInternal.onLoadStart'), + fields: ['url', 'isTopLevel'] + }, + 'loadstop': { + evt: CreateEvent('webViewInternal.onLoadStop') + }, + 'newwindow': { + cancelable: true, + evt: CreateEvent('webViewInternal.onNewWindow'), + fields: [ + 'initialHeight', + 'initialWidth', + 'targetUrl', + 'windowOpenDisposition', + 'name' + ], + handler: 'handleNewWindowEvent' + }, + 'permissionrequest': { + cancelable: true, + evt: CreateEvent('webViewInternal.onPermissionRequest'), + fields: [ + 'identifier', + 'lastUnlockedBySelf', + 'name', + 'permission', + 'requestMethod', + 'url', + 'userGesture' + ], + handler: 'handlePermissionEvent' + }, + 'responsive': { + evt: CreateEvent('webViewInternal.onResponsive'), + fields: ['processId'] + }, + 'sizechanged': { + evt: CreateEvent('webViewInternal.onSizeChanged'), + fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'], + handler: 'handleSizeChangedEvent' + }, + 'unresponsive': { + evt: CreateEvent('webViewInternal.onUnresponsive'), + fields: ['processId'] + }, + 'zoomchange': { + evt: CreateEvent('webViewInternal.onZoomChange'), + fields: ['oldZoomFactor', 'newZoomFactor'] + } +}; + +WebViewEvents.prototype.setupWebRequestEvents = function() { + var request = {}; + var createWebRequestEvent = function(webRequestEvent) { + return this.weakWrapper(function() { + if (!this[webRequestEvent.name]) { + this[webRequestEvent.name] = + new WebRequestEvent( + 'webViewInternal.' + webRequestEvent.name, + webRequestEvent.parameters, + webRequestEvent.extraParameters, webRequestEvent.options, + this.view.viewInstanceId); + } + return this[webRequestEvent.name]; + }); + }.bind(this); + + var createDeclarativeWebRequestEvent = function(webRequestEvent) { + return this.weakWrapper(function() { + if (!this[webRequestEvent.name]) { + // The onMessage event gets a special event type because we want + // the listener to fire only for messages targeted for this particular + // <webview>. + var EventClass = webRequestEvent.name === 'onMessage' ? + DeclarativeWebRequestEvent : EventBindings.Event; + this[webRequestEvent.name] = + new EventClass( + 'webViewInternal.declarativeWebRequest.' + webRequestEvent.name, + webRequestEvent.parameters, + webRequestEvent.options, + this.view.viewInstanceId); + } + return this[webRequestEvent.name]; + }); + }.bind(this); + + for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { + var eventSchema = DeclarativeWebRequestSchema.events[i]; + var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema); + Object.defineProperty( + request, + eventSchema.name, + { + get: webRequestEvent, + enumerable: true + } + ); + } + + // Populate the WebRequest events from the API definition. + for (var i = 0; i < WebRequestSchema.events.length; ++i) { + var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]); + Object.defineProperty( + request, + WebRequestSchema.events[i].name, + { + get: webRequestEvent, + enumerable: true + } + ); + } + + this.view.setRequestPropertyOnWebViewElement(request); +}; + +WebViewEvents.prototype.getEvents = function() { + return WebViewEvents.EVENTS; +}; + +WebViewEvents.prototype.handleDialogEvent = function(event, eventName) { + var webViewEvent = this.makeDomEvent(event, eventName); + new WebViewActionRequests.Dialog(this.view, event, webViewEvent); +}; + +WebViewEvents.prototype.handleFrameNameChangedEvent = function(event) { + this.view.onFrameNameChanged(event.name); +}; + +WebViewEvents.prototype.handleFullscreenExitEvent = function(event, eventName) { + document.webkitCancelFullScreen(); +}; + +WebViewEvents.prototype.handleLoadAbortEvent = function(event, eventName) { + var showWarningMessage = function(code, reason) { + var WARNING_MSG_LOAD_ABORTED = '<webview>: ' + + 'The load has aborted with error %1: %2.'; + window.console.warn( + WARNING_MSG_LOAD_ABORTED.replace('%1', code).replace('%2', reason)); + }; + var webViewEvent = this.makeDomEvent(event, eventName); + if (this.view.dispatchEvent(webViewEvent)) { + showWarningMessage(event.code, event.reason); + } +}; + +WebViewEvents.prototype.handleLoadCommitEvent = function(event, eventName) { + this.view.onLoadCommit(event.baseUrlForDataUrl, + event.currentEntryIndex, + event.entryCount, + event.processId, + event.url, + event.isTopLevel); + var webViewEvent = this.makeDomEvent(event, eventName); + this.view.dispatchEvent(webViewEvent); +}; + +WebViewEvents.prototype.handleNewWindowEvent = function(event, eventName) { + var webViewEvent = this.makeDomEvent(event, eventName); + new WebViewActionRequests.NewWindow(this.view, event, webViewEvent); +}; + +WebViewEvents.prototype.handlePermissionEvent = function(event, eventName) { + var webViewEvent = this.makeDomEvent(event, eventName); + if (event.permission === 'fullscreen') { + new WebViewActionRequests.FullscreenPermissionRequest( + this.view, event, webViewEvent); + } else { + new WebViewActionRequests.PermissionRequest(this.view, event, webViewEvent); + } +}; + +WebViewEvents.prototype.handleSizeChangedEvent = function(event, eventName) { + var webViewEvent = this.makeDomEvent(event, eventName); + this.view.onSizeChanged(webViewEvent); +}; + +function DeclarativeWebRequestEvent(opt_eventName, + opt_argSchemas, + opt_eventOptions, + opt_webViewInstanceId) { + var subEventName = opt_eventName + '/' + IdGenerator.GetNextId(); + EventBindings.Event.call(this, + subEventName, + opt_argSchemas, + opt_eventOptions, + opt_webViewInstanceId); + + var view = GuestViewInternalNatives.GetViewFromID(opt_webViewInstanceId || 0); + if (!view) { + return; + } + view.events.addScopedListener(WebRequestMessageEvent, function() { + // Re-dispatch to subEvent's listeners. + $Function.apply(this.dispatch, this, $Array.slice(arguments)); + }.bind(this), {instanceId: opt_webViewInstanceId || 0}); +} + +DeclarativeWebRequestEvent.prototype.__proto__ = EventBindings.Event.prototype; + +// Exports. +exports.$set('WebViewEvents', WebViewEvents); diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_experimental.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_experimental.js new file mode 100644 index 00000000000..96331df2a89 --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_experimental.js @@ -0,0 +1,24 @@ +// Copyright 2015 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. + +// This module implements experimental API for <webview>. +// See web_view.js and web_view_api_methods.js for details. +// +// <webview> Experimental API is only available on canary and channels of +// Chrome. + +var WebViewImpl = require('webView').WebViewImpl; +var WebViewInternal = require('webViewInternal').WebViewInternal; + +// An array of <webview>'s experimental API methods. See |WEB_VIEW_API_METHODS| +// in web_view_api_methods.js for more details. +var WEB_VIEW_EXPERIMENTAL_API_METHODS = [ + // Captures the visible region of the WebView contents into a bitmap. + 'captureVisibleRegion' +]; + +// Registers the experimantal WebVIew API when available. +WebViewImpl.maybeGetExperimentalApiMethods = function() { + return WEB_VIEW_EXPERIMENTAL_API_METHODS; +}; diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_internal.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_internal.js new file mode 100644 index 00000000000..4d2c36c2dad --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_internal.js @@ -0,0 +1,7 @@ +// 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. + +exports.$set( + 'WebViewInternal', + require('binding').Binding.create('webViewInternal').generate()); diff --git a/chromium/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js new file mode 100644 index 00000000000..61b553b693f --- /dev/null +++ b/chromium/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js @@ -0,0 +1,55 @@ +// 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. + +// Custom binding for the webViewRequest API. + +var binding = require('binding').Binding.create('webViewRequest'); + +var declarativeWebRequestSchema = + requireNative('schema_registry').GetSchema('declarativeWebRequest'); +var utils = require('utils'); +var validate = require('schemaUtils').validate; + +binding.registerCustomHook(function(api) { + var webViewRequest = api.compiledApi; + + // Returns the schema definition of type |typeId| defined in + // |declarativeWebRequestScheme.types|. + function getSchema(typeId) { + return utils.lookup(declarativeWebRequestSchema.types, + 'id', + 'declarativeWebRequest.' + typeId); + } + + // Helper function for the constructor of concrete datatypes of the + // declarative webRequest API. + // Makes sure that |this| contains the union of parameters and + // {'instanceType': 'declarativeWebRequest.' + typeId} and validates the + // generated union dictionary against the schema for |typeId|. + function setupInstance(instance, parameters, typeId) { + for (var key in parameters) { + if ($Object.hasOwnProperty(parameters, key)) { + instance[key] = parameters[key]; + } + } + + instance.instanceType = 'declarativeWebRequest.' + typeId; + var schema = getSchema(typeId); + validate([instance], [schema]); + } + + // Setup all data types for the declarative webRequest API from the schema. + for (var i = 0; i < declarativeWebRequestSchema.types.length; ++i) { + var typeSchema = declarativeWebRequestSchema.types[i]; + var typeId = typeSchema.id.replace('declarativeWebRequest.', ''); + var action = function(typeId) { + return function(parameters) { + setupInstance(this, parameters, typeId); + }; + }(typeId); + webViewRequest[typeId] = action; + } +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/i18n_custom_bindings.js b/chromium/extensions/renderer/resources/i18n_custom_bindings.js new file mode 100644 index 00000000000..de69fa2ebe0 --- /dev/null +++ b/chromium/extensions/renderer/resources/i18n_custom_bindings.js @@ -0,0 +1,49 @@ +// 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. + +// Custom binding for the i18n API. + +var binding = require('binding').Binding.create('i18n'); + +var i18nNatives = requireNative('i18n'); +var GetL10nMessage = i18nNatives.GetL10nMessage; +var GetL10nUILanguage = i18nNatives.GetL10nUILanguage; +var DetectTextLanguage = i18nNatives.DetectTextLanguage; + +binding.registerCustomHook(function(bindingsAPI, extensionId) { + var apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setUpdateArgumentsPreValidate('getMessage', function() { + var args = $Array.slice(arguments); + + // The first argument is the message, and should be a string. + var message = args[0]; + if (typeof(message) !== 'string') { + console.warn(extensionId + ': the first argument to getMessage should ' + + 'be type "string", was ' + message + + ' (type "' + typeof(message) + '")'); + args[0] = String(message); + } + + return args; + }); + + apiFunctions.setHandleRequest('getMessage', + function(messageName, substitutions) { + return GetL10nMessage(messageName, substitutions, extensionId); + }); + + apiFunctions.setHandleRequest('getUILanguage', function() { + return GetL10nUILanguage(); + }); + + apiFunctions.setHandleRequest('detectLanguage', function(text, callback) { + window.setTimeout(function() { + var response = DetectTextLanguage(text); + callback(response); + }, 0); + }); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/image_util.js b/chromium/extensions/renderer/resources/image_util.js new file mode 100644 index 00000000000..a7a27a64a66 --- /dev/null +++ b/chromium/extensions/renderer/resources/image_util.js @@ -0,0 +1,82 @@ +// 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. + +// This function takes an object |imageSpec| with the key |path| - +// corresponding to the internet URL to be translated - and optionally +// |width| and |height| which are the maximum dimensions to be used when +// converting the image. +function loadImageData(imageSpec, callbacks) { + var path = imageSpec.path; + var img = new Image(); + if (typeof callbacks.onerror === 'function') { + img.onerror = function() { + callbacks.onerror({ problem: 'could_not_load', path: path }); + }; + } + img.onload = function() { + var canvas = document.createElement('canvas'); + + if (img.width <= 0 || img.height <= 0) { + callbacks.onerror({ problem: 'image_size_invalid', path: path}); + return; + } + + var scaleFactor = 1; + if (imageSpec.width && imageSpec.width < img.width) + scaleFactor = imageSpec.width / img.width; + + if (imageSpec.height && imageSpec.height < img.height) { + var heightScale = imageSpec.height / img.height; + if (heightScale < scaleFactor) + scaleFactor = heightScale; + } + + canvas.width = img.width * scaleFactor; + canvas.height = img.height * scaleFactor; + + var canvas_context = canvas.getContext('2d'); + canvas_context.clearRect(0, 0, canvas.width, canvas.height); + canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); + try { + var imageData = canvas_context.getImageData( + 0, 0, canvas.width, canvas.height); + if (typeof callbacks.oncomplete === 'function') { + callbacks.oncomplete( + imageData.width, imageData.height, imageData.data.buffer); + } + } catch (e) { + if (typeof callbacks.onerror === 'function') { + callbacks.onerror({ problem: 'data_url_unavailable', path: path }); + } + } + } + img.src = path; +} + +function on_complete_index(index, err, loading, finished, callbacks) { + return function(width, height, imageData) { + delete loading[index]; + finished[index] = { width: width, height: height, data: imageData }; + if (err) + callbacks.onerror(index); + if ($Object.keys(loading).length == 0) + callbacks.oncomplete(finished); + } +} + +function loadAllImages(imageSpecs, callbacks) { + var loading = {}, finished = [], + index, pathname; + + for (var index = 0; index < imageSpecs.length; index++) { + loading[index] = imageSpecs[index]; + loadImageData(imageSpecs[index], { + oncomplete: on_complete_index(index, false, loading, finished, callbacks), + onerror: on_complete_index(index, true, loading, finished, callbacks) + }); + } +} + +exports.$set('loadImageData', loadImageData); +exports.$set('loadAllImages', loadAllImages); diff --git a/chromium/extensions/renderer/resources/json_schema.js b/chromium/extensions/renderer/resources/json_schema.js new file mode 100644 index 00000000000..447d1ea21c2 --- /dev/null +++ b/chromium/extensions/renderer/resources/json_schema.js @@ -0,0 +1,525 @@ +// 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. + +// ----------------------------------------------------------------------------- +// NOTE: If you change this file you need to touch +// extension_renderer_resources.grd to have your change take effect. +// ----------------------------------------------------------------------------- + +//============================================================================== +// This file contains a class that implements a subset of JSON Schema. +// See: http://www.json.com/json-schema-proposal/ for more details. +// +// The following features of JSON Schema are not implemented: +// - requires +// - unique +// - disallow +// - union types (but replaced with 'choices') +// +// The following properties are not applicable to the interface exposed by +// this class: +// - options +// - readonly +// - title +// - description +// - format +// - default +// - transient +// - hidden +// +// There are also these departures from the JSON Schema proposal: +// - function and undefined types are supported +// - null counts as 'unspecified' for optional values +// - added the 'choices' property, to allow specifying a list of possible types +// for a value +// - by default an "object" typed schema does not allow additional properties. +// if present, "additionalProperties" is to be a schema against which all +// additional properties will be validated. +//============================================================================== + +var loadTypeSchema = require('utils').loadTypeSchema; +var CHECK = requireNative('logging').CHECK; + +function isInstanceOfClass(instance, className) { + while ((instance = instance.__proto__)) { + if (instance.constructor.name == className) + return true; + } + return false; +} + +function isOptionalValue(value) { + return typeof(value) === 'undefined' || value === null; +} + +function enumToString(enumValue) { + if (enumValue.name === undefined) + return enumValue; + + return enumValue.name; +} + +/** + * Validates an instance against a schema and accumulates errors. Usage: + * + * var validator = new JSONSchemaValidator(); + * validator.validate(inst, schema); + * if (validator.errors.length == 0) + * console.log("Valid!"); + * else + * console.log(validator.errors); + * + * The errors property contains a list of objects. Each object has two + * properties: "path" and "message". The "path" property contains the path to + * the key that had the problem, and the "message" property contains a sentence + * describing the error. + */ +function JSONSchemaValidator() { + this.errors = []; + this.types = []; +} + +JSONSchemaValidator.messages = { + invalidEnum: "Value must be one of: [*].", + propertyRequired: "Property is required.", + unexpectedProperty: "Unexpected property.", + arrayMinItems: "Array must have at least * items.", + arrayMaxItems: "Array must not have more than * items.", + itemRequired: "Item is required.", + stringMinLength: "String must be at least * characters long.", + stringMaxLength: "String must not be more than * characters long.", + stringPattern: "String must match the pattern: *.", + numberFiniteNotNan: "Value must not be *.", + numberMinValue: "Value must not be less than *.", + numberMaxValue: "Value must not be greater than *.", + numberIntValue: "Value must fit in a 32-bit signed integer.", + numberMaxDecimal: "Value must not have more than * decimal places.", + invalidType: "Expected '*' but got '*'.", + invalidTypeIntegerNumber: + "Expected 'integer' but got 'number', consider using Math.round().", + invalidChoice: "Value does not match any valid type choices.", + invalidPropertyType: "Missing property type.", + schemaRequired: "Schema value required.", + unknownSchemaReference: "Unknown schema reference: *.", + notInstance: "Object must be an instance of *." +}; + +/** + * Builds an error message. Key is the property in the |errors| object, and + * |opt_replacements| is an array of values to replace "*" characters with. + */ +JSONSchemaValidator.formatError = function(key, opt_replacements) { + var message = this.messages[key]; + if (opt_replacements) { + for (var i = 0; i < opt_replacements.length; i++) { + message = message.replace("*", opt_replacements[i]); + } + } + return message; +}; + +/** + * Classifies a value as one of the JSON schema primitive types. Note that we + * don't explicitly disallow 'function', because we want to allow functions in + * the input values. + */ +JSONSchemaValidator.getType = function(value) { + var s = typeof value; + + if (s == "object") { + if (value === null) { + return "null"; + } else if (Object.prototype.toString.call(value) == "[object Array]") { + return "array"; + } else if (Object.prototype.toString.call(value) == + "[object ArrayBuffer]") { + return "binary"; + } + } else if (s == "number") { + if (value % 1 == 0) { + return "integer"; + } + } + + return s; +}; + +/** + * Add types that may be referenced by validated schemas that reference them + * with "$ref": <typeId>. Each type must be a valid schema and define an + * "id" property. + */ +JSONSchemaValidator.prototype.addTypes = function(typeOrTypeList) { + function addType(validator, type) { + if (!type.id) + throw new Error("Attempt to addType with missing 'id' property"); + validator.types[type.id] = type; + } + + if (typeOrTypeList instanceof Array) { + for (var i = 0; i < typeOrTypeList.length; i++) { + addType(this, typeOrTypeList[i]); + } + } else { + addType(this, typeOrTypeList); + } +} + +/** + * Returns a list of strings of the types that this schema accepts. + */ +JSONSchemaValidator.prototype.getAllTypesForSchema = function(schema) { + var schemaTypes = []; + if (schema.type) + $Array.push(schemaTypes, schema.type); + if (schema.choices) { + for (var i = 0; i < schema.choices.length; i++) { + var choiceTypes = this.getAllTypesForSchema(schema.choices[i]); + schemaTypes = $Array.concat(schemaTypes, choiceTypes); + } + } + var ref = schema['$ref']; + if (ref) { + var type = this.getOrAddType(ref); + CHECK(type, 'Could not find type ' + ref); + schemaTypes = $Array.concat(schemaTypes, this.getAllTypesForSchema(type)); + } + return schemaTypes; +}; + +JSONSchemaValidator.prototype.getOrAddType = function(typeName) { + if (!this.types[typeName]) + this.types[typeName] = loadTypeSchema(typeName); + return this.types[typeName]; +}; + +/** + * Returns true if |schema| would accept an argument of type |type|. + */ +JSONSchemaValidator.prototype.isValidSchemaType = function(type, schema) { + if (type == 'any') + return true; + + // TODO(kalman): I don't understand this code. How can type be "null"? + if (schema.optional && (type == "null" || type == "undefined")) + return true; + + var schemaTypes = this.getAllTypesForSchema(schema); + for (var i = 0; i < schemaTypes.length; i++) { + if (schemaTypes[i] == "any" || type == schemaTypes[i] || + (type == "integer" && schemaTypes[i] == "number")) + return true; + } + + return false; +}; + +/** + * Returns true if there is a non-null argument that both |schema1| and + * |schema2| would accept. + */ +JSONSchemaValidator.prototype.checkSchemaOverlap = function(schema1, schema2) { + var schema1Types = this.getAllTypesForSchema(schema1); + for (var i = 0; i < schema1Types.length; i++) { + if (this.isValidSchemaType(schema1Types[i], schema2)) + return true; + } + return false; +}; + +/** + * Validates an instance against a schema. The instance can be any JavaScript + * value and will be validated recursively. When this method returns, the + * |errors| property will contain a list of errors, if any. + */ +JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) { + var path = opt_path || ""; + + if (!schema) { + this.addError(path, "schemaRequired"); + return; + } + + // If this schema defines itself as reference type, save it in this.types. + if (schema.id) + this.types[schema.id] = schema; + + // If the schema has an extends property, the instance must validate against + // that schema too. + if (schema.extends) + this.validate(instance, schema.extends, path); + + // If the schema has a $ref property, the instance must validate against + // that schema too. It must be present in this.types to be referenced. + var ref = schema["$ref"]; + if (ref) { + if (!this.getOrAddType(ref)) + this.addError(path, "unknownSchemaReference", [ ref ]); + else + this.validate(instance, this.getOrAddType(ref), path) + } + + // If the schema has a choices property, the instance must validate against at + // least one of the items in that array. + if (schema.choices) { + this.validateChoices(instance, schema, path); + return; + } + + // If the schema has an enum property, the instance must be one of those + // values. + if (schema.enum) { + if (!this.validateEnum(instance, schema, path)) + return; + } + + if (schema.type && schema.type != "any") { + if (!this.validateType(instance, schema, path)) + return; + + // Type-specific validation. + switch (schema.type) { + case "object": + this.validateObject(instance, schema, path); + break; + case "array": + this.validateArray(instance, schema, path); + break; + case "string": + this.validateString(instance, schema, path); + break; + case "number": + case "integer": + this.validateNumber(instance, schema, path); + break; + } + } +}; + +/** + * Validates an instance against a choices schema. The instance must match at + * least one of the provided choices. + */ +JSONSchemaValidator.prototype.validateChoices = + function(instance, schema, path) { + var originalErrors = this.errors; + + for (var i = 0; i < schema.choices.length; i++) { + this.errors = []; + this.validate(instance, schema.choices[i], path); + if (this.errors.length == 0) { + this.errors = originalErrors; + return; + } + } + + this.errors = originalErrors; + this.addError(path, "invalidChoice"); +}; + +/** + * Validates an instance against a schema with an enum type. Populates the + * |errors| property, and returns a boolean indicating whether the instance + * validates. + */ +JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) { + for (var i = 0; i < schema.enum.length; i++) { + if (instance === enumToString(schema.enum[i])) + return true; + } + + this.addError(path, "invalidEnum", + [$Array.join($Array.map(schema.enum, enumToString), ", ")]); + return false; +}; + +/** + * Validates an instance against an object schema and populates the errors + * property. + */ +JSONSchemaValidator.prototype.validateObject = + function(instance, schema, path) { + if (schema.properties) { + for (var prop in schema.properties) { + // It is common in JavaScript to add properties to Object.prototype. This + // check prevents such additions from being interpreted as required + // schema properties. + // TODO(aa): If it ever turns out that we actually want this to work, + // there are other checks we could put here, like requiring that schema + // properties be objects that have a 'type' property. + if (!$Object.hasOwnProperty(schema.properties, prop)) + continue; + + var propPath = path ? path + "." + prop : prop; + if (schema.properties[prop] == undefined) { + this.addError(propPath, "invalidPropertyType"); + } else if (prop in instance && !isOptionalValue(instance[prop])) { + this.validate(instance[prop], schema.properties[prop], propPath); + } else if (!schema.properties[prop].optional) { + this.addError(propPath, "propertyRequired"); + } + } + } + + // If "instanceof" property is set, check that this object inherits from + // the specified constructor (function). + if (schema.isInstanceOf) { + if (!isInstanceOfClass(instance, schema.isInstanceOf)) + this.addError(propPath, "notInstance", [schema.isInstanceOf]); + } + + // Exit early from additional property check if "type":"any" is defined. + if (schema.additionalProperties && + schema.additionalProperties.type && + schema.additionalProperties.type == "any") { + return; + } + + // By default, additional properties are not allowed on instance objects. This + // can be overridden by setting the additionalProperties property to a schema + // which any additional properties must validate against. + for (var prop in instance) { + if (schema.properties && prop in schema.properties) + continue; + + // Any properties inherited through the prototype are ignored. + if (!$Object.hasOwnProperty(instance, prop)) + continue; + + var propPath = path ? path + "." + prop : prop; + if (schema.additionalProperties) + this.validate(instance[prop], schema.additionalProperties, propPath); + else + this.addError(propPath, "unexpectedProperty"); + } +}; + +/** + * Validates an instance against an array schema and populates the errors + * property. + */ +JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) { + var typeOfItems = JSONSchemaValidator.getType(schema.items); + + if (typeOfItems == 'object') { + if (schema.minItems && instance.length < schema.minItems) { + this.addError(path, "arrayMinItems", [schema.minItems]); + } + + if (typeof schema.maxItems != "undefined" && + instance.length > schema.maxItems) { + this.addError(path, "arrayMaxItems", [schema.maxItems]); + } + + // If the items property is a single schema, each item in the array must + // have that schema. + for (var i = 0; i < instance.length; i++) { + this.validate(instance[i], schema.items, path + "." + i); + } + } else if (typeOfItems == 'array') { + // If the items property is an array of schemas, each item in the array must + // validate against the corresponding schema. + for (var i = 0; i < schema.items.length; i++) { + var itemPath = path ? path + "." + i : String(i); + if (i in instance && !isOptionalValue(instance[i])) { + this.validate(instance[i], schema.items[i], itemPath); + } else if (!schema.items[i].optional) { + this.addError(itemPath, "itemRequired"); + } + } + + if (schema.additionalProperties) { + for (var i = schema.items.length; i < instance.length; i++) { + var itemPath = path ? path + "." + i : String(i); + this.validate(instance[i], schema.additionalProperties, itemPath); + } + } else { + if (instance.length > schema.items.length) { + this.addError(path, "arrayMaxItems", [schema.items.length]); + } + } + } +}; + +/** + * Validates a string and populates the errors property. + */ +JSONSchemaValidator.prototype.validateString = + function(instance, schema, path) { + if (schema.minLength && instance.length < schema.minLength) + this.addError(path, "stringMinLength", [schema.minLength]); + + if (schema.maxLength && instance.length > schema.maxLength) + this.addError(path, "stringMaxLength", [schema.maxLength]); + + if (schema.pattern && !schema.pattern.test(instance)) + this.addError(path, "stringPattern", [schema.pattern]); +}; + +/** + * Validates a number and populates the errors property. The instance is + * assumed to be a number. + */ +JSONSchemaValidator.prototype.validateNumber = + function(instance, schema, path) { + // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and + // JSON serialization encodes them as 'null'. Re-evaluate supporting + // them if we add an API that could reasonably take them as a parameter. + if (isNaN(instance) || + instance == Number.POSITIVE_INFINITY || + instance == Number.NEGATIVE_INFINITY ) + this.addError(path, "numberFiniteNotNan", [instance]); + + if (schema.minimum !== undefined && instance < schema.minimum) + this.addError(path, "numberMinValue", [schema.minimum]); + + if (schema.maximum !== undefined && instance > schema.maximum) + this.addError(path, "numberMaxValue", [schema.maximum]); + + // Check for integer values outside of -2^31..2^31-1. + if (schema.type === "integer" && (instance | 0) !== instance) + this.addError(path, "numberIntValue", []); + + if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) + this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); +}; + +/** + * Validates the primitive type of an instance and populates the errors + * property. Returns true if the instance validates, false otherwise. + */ +JSONSchemaValidator.prototype.validateType = function(instance, schema, path) { + var actualType = JSONSchemaValidator.getType(instance); + if (schema.type == actualType || + (schema.type == "number" && actualType == "integer")) { + return true; + } else if (schema.type == "integer" && actualType == "number") { + this.addError(path, "invalidTypeIntegerNumber"); + return false; + } else { + this.addError(path, "invalidType", [schema.type, actualType]); + return false; + } +}; + +/** + * Adds an error message. |key| is an index into the |messages| object. + * |replacements| is an array of values to replace '*' characters in the + * message. + */ +JSONSchemaValidator.prototype.addError = function(path, key, replacements) { + $Array.push(this.errors, { + path: path, + message: JSONSchemaValidator.formatError(key, replacements) + }); +}; + +/** + * Resets errors to an empty list so you can call 'validate' again. + */ +JSONSchemaValidator.prototype.resetErrors = function() { + this.errors = []; +}; + +exports.$set('JSONSchemaValidator', JSONSchemaValidator); diff --git a/chromium/extensions/renderer/resources/keep_alive.js b/chromium/extensions/renderer/resources/keep_alive.js new file mode 100644 index 00000000000..5269c30285f --- /dev/null +++ b/chromium/extensions/renderer/resources/keep_alive.js @@ -0,0 +1,41 @@ +// 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. + +define('keep_alive', [ + 'content/public/renderer/frame_service_registry', + 'extensions/common/mojo/keep_alive.mojom', + 'mojo/public/js/core', +], function(serviceProvider, mojom, core) { + + /** + * An object that keeps the background page alive until closed. + * @constructor + * @alias module:keep_alive~KeepAlive + */ + function KeepAlive() { + /** + * The handle to the keep-alive object in the browser. + * @type {!MojoHandle} + * @private + */ + this.handle_ = serviceProvider.connectToService(mojom.KeepAlive.name); + } + + /** + * Removes this keep-alive. + */ + KeepAlive.prototype.close = function() { + core.close(this.handle_); + }; + + var exports = {}; + + return { + /** + * Creates a keep-alive. + * @return {!module:keep_alive~KeepAlive} A new keep-alive. + */ + createKeepAlive: function() { return new KeepAlive(); } + }; +}); diff --git a/chromium/extensions/renderer/resources/last_error.js b/chromium/extensions/renderer/resources/last_error.js new file mode 100644 index 00000000000..fc0c7b5e4d3 --- /dev/null +++ b/chromium/extensions/renderer/resources/last_error.js @@ -0,0 +1,143 @@ +// 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. + +var GetAvailability = requireNative('v8_context').GetAvailability; +var GetGlobal = requireNative('sendRequest').GetGlobal; + +// Utility for setting chrome.*.lastError. +// +// A utility here is useful for two reasons: +// 1. For backwards compatibility we need to set chrome.extension.lastError, +// but not all contexts actually have access to the extension namespace. +// 2. When calling across contexts, the global object that gets lastError set +// needs to be that of the caller. We force callers to explicitly specify +// the chrome object to try to prevent bugs here. + +/** + * Sets the last error for |name| on |targetChrome| to |message| with an + * optional |stack|. + */ +function set(name, message, stack, targetChrome) { + if (!targetChrome) { + var errorMessage = name + ': ' + message; + if (stack != null && stack != '') + errorMessage += '\n' + stack; + throw new Error('No chrome object to set error: ' + errorMessage); + } + clear(targetChrome); // in case somebody has set a sneaky getter/setter + + var errorObject = { message: message }; + if (GetAvailability('extension.lastError').is_available) + targetChrome.extension.lastError = errorObject; + + assertRuntimeIsAvailable(); + + // We check to see if developers access runtime.lastError in order to decide + // whether or not to log it in the (error) console. + privates(targetChrome.runtime).accessedLastError = false; + $Object.defineProperty(targetChrome.runtime, 'lastError', { + configurable: true, + get: function() { + privates(targetChrome.runtime).accessedLastError = true; + return errorObject; + }, + set: function(error) { + errorObject = errorObject; + }}); +}; + +/** + * Check if anyone has checked chrome.runtime.lastError since it was set. + * @param {Object} targetChrome the Chrome object to check. + * @return boolean True if the lastError property was set. + */ +function hasAccessed(targetChrome) { + assertRuntimeIsAvailable(); + return privates(targetChrome.runtime).accessedLastError === true; +} + +/** + * Check whether there is an error set on |targetChrome| without setting + * |accessedLastError|. + * @param {Object} targetChrome the Chrome object to check. + * @return boolean Whether lastError has been set. + */ +function hasError(targetChrome) { + if (!targetChrome) + throw new Error('No target chrome to check'); + + assertRuntimeIsAvailable(); + if ('lastError' in targetChrome.runtime) + return true; + + return false; +}; + +/** + * Clears the last error on |targetChrome|. + */ +function clear(targetChrome) { + if (!targetChrome) + throw new Error('No target chrome to clear error'); + + if (GetAvailability('extension.lastError').is_available) + delete targetChrome.extension.lastError; + + assertRuntimeIsAvailable(); + delete targetChrome.runtime.lastError; + delete privates(targetChrome.runtime).accessedLastError; +}; + +function assertRuntimeIsAvailable() { + // chrome.runtime should always be available, but maybe it's disappeared for + // some reason? Add debugging for http://crbug.com/258526. + var runtimeAvailability = GetAvailability('runtime.lastError'); + if (!runtimeAvailability.is_available) { + throw new Error('runtime.lastError is not available: ' + + runtimeAvailability.message); + } + if (!chrome.runtime) + throw new Error('runtime namespace is null or undefined'); +} + +/** + * Runs |callback(args)| with last error args as in set(). + * + * The target chrome object is the global object's of the callback, so this + * method won't work if the real callback has been wrapped (etc). + */ +function run(name, message, stack, callback, args) { + var global = GetGlobal(callback); + var targetChrome = global && global.chrome; + set(name, message, stack, targetChrome); + try { + $Function.apply(callback, undefined, args); + } finally { + reportIfUnchecked(name, targetChrome, stack); + clear(targetChrome); + } +} + +/** + * Checks whether chrome.runtime.lastError has been accessed if set. + * If it was set but not accessed, the error is reported to the console. + * + * @param {string=} name - name of API. + * @param {Object} targetChrome - the Chrome object to check. + * @param {string=} stack - Stack trace of the call up to the error. + */ +function reportIfUnchecked(name, targetChrome, stack) { + if (hasAccessed(targetChrome) || !hasError(targetChrome)) + return; + var message = targetChrome.runtime.lastError.message; + console.error("Unchecked runtime.lastError while running " + + (name || "unknown") + ": " + message + (stack ? "\n" + stack : "")); +} + +exports.$set('clear', clear); +exports.$set('hasAccessed', hasAccessed); +exports.$set('hasError', hasError); +exports.$set('set', set); +exports.$set('run', run); +exports.$set('reportIfUnchecked', reportIfUnchecked); diff --git a/chromium/extensions/renderer/resources/media_router_bindings.js b/chromium/extensions/renderer/resources/media_router_bindings.js new file mode 100644 index 00000000000..4eaee9f0286 --- /dev/null +++ b/chromium/extensions/renderer/resources/media_router_bindings.js @@ -0,0 +1,777 @@ +// Copyright 2015 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. + +var mediaRouter; + +define('media_router_bindings', [ + 'mojo/public/js/bindings', + 'mojo/public/js/core', + 'content/public/renderer/frame_service_registry', + 'chrome/browser/media/router/mojo/media_router.mojom', + 'extensions/common/mojo/keep_alive.mojom', + 'mojo/public/js/connection', + 'mojo/public/js/router', +], function(bindings, + core, + serviceProvider, + mediaRouterMojom, + keepAliveMojom, + connector, + routerModule) { + 'use strict'; + + /** + * Converts a media sink to a MediaSink Mojo object. + * @param {!MediaSink} sink A media sink. + * @return {!mediaRouterMojom.MediaSink} A Mojo MediaSink object. + */ + function sinkToMojo_(sink) { + return new mediaRouterMojom.MediaSink({ + 'name': sink.friendlyName, + 'description': sink.description, + 'domain': sink.domain, + 'sink_id': sink.id, + 'icon_type': sinkIconTypeToMojo(sink.iconType), + }); + } + + /** + * Converts a media sink's icon type to a MediaSink.IconType Mojo object. + * @param {!MediaSink.IconType} type A media sink's icon type. + * @return {!mediaRouterMojom.MediaSink.IconType} A Mojo MediaSink.IconType + * object. + */ + function sinkIconTypeToMojo(type) { + switch (type) { + case 'cast': + return mediaRouterMojom.MediaSink.IconType.CAST; + case 'cast_audio': + return mediaRouterMojom.MediaSink.IconType.CAST_AUDIO; + case 'cast_audio_group': + return mediaRouterMojom.MediaSink.IconType.CAST_AUDIO_GROUP; + case 'generic': + return mediaRouterMojom.MediaSink.IconType.GENERIC; + case 'hangout': + return mediaRouterMojom.MediaSink.IconType.HANGOUT; + default: + console.error('Unknown sink icon type : ' + type); + return mediaRouterMojom.MediaSink.IconType.GENERIC; + } + } + + /** + * Returns a Mojo MediaRoute object given a MediaRoute and a + * media sink name. + * @param {!MediaRoute} route + * @return {!mojo.MediaRoute} + */ + function routeToMojo_(route) { + return new mediaRouterMojom.MediaRoute({ + 'media_route_id': route.id, + 'media_source': route.mediaSource, + 'media_sink_id': route.sinkId, + 'description': route.description, + 'icon_url': route.iconUrl, + 'is_local': route.isLocal, + 'custom_controller_path': route.customControllerPath, + // Begin newly added properties, followed by the milestone they were + // added. The guard should be safe to remove N+2 milestones later. + 'for_display': route.forDisplay, // M47 + 'off_the_record': !!route.offTheRecord // M50 + }); + } + + /** + * Converts a route message to a RouteMessage Mojo object. + * @param {!RouteMessage} message + * @return {!mediaRouterMojom.RouteMessage} A Mojo RouteMessage object. + */ + function messageToMojo_(message) { + if ("string" == typeof message.message) { + return new mediaRouterMojom.RouteMessage({ + 'type': mediaRouterMojom.RouteMessage.Type.TEXT, + 'message': message.message, + }); + } else { + return new mediaRouterMojom.RouteMessage({ + 'type': mediaRouterMojom.RouteMessage.Type.BINARY, + 'data': message.message, + }); + } + } + + /** + * Converts presentation connection state to Mojo enum value. + * @param {!string} state + * @return {!mediaRouterMojom.MediaRouter.PresentationConnectionState} + */ + function presentationConnectionStateToMojo_(state) { + var PresentationConnectionState = + mediaRouterMojom.MediaRouter.PresentationConnectionState; + switch (state) { + case 'connecting': + return PresentationConnectionState.CONNECTING; + case 'connected': + return PresentationConnectionState.CONNECTED; + case 'closed': + return PresentationConnectionState.CLOSED; + case 'terminated': + return PresentationConnectionState.TERMINATED; + default: + console.error('Unknown presentation connection state: ' + state); + return PresentationConnectionState.TERMINATED; + } + } + + /** + * Converts presentation connection close reason to Mojo enum value. + * @param {!string} reason + * @return {!mediaRouterMojom.MediaRouter.PresentationConnectionCloseReason} + */ + function presentationConnectionCloseReasonToMojo_(reason) { + var PresentationConnectionCloseReason = + mediaRouterMojom.MediaRouter.PresentationConnectionCloseReason; + switch (reason) { + case 'error': + return PresentationConnectionCloseReason.CONNECTION_ERROR; + case 'closed': + return PresentationConnectionCloseReason.CLOSED; + case 'went_away': + return PresentationConnectionCloseReason.WENT_AWAY; + default: + console.error('Unknown presentation connection close reason : ' + + reason); + return PresentationConnectionCloseReason.CONNECTION_ERROR; + } + } + + /** + * Parses the given route request Error object and converts it to the + * corresponding result code. + * @param {!Error} error + * @return {!mediaRouterMojom.RouteRequestResultCode} + */ + function getRouteRequestResultCode_(error) { + if (error.message.startsWith('timeout')) + return mediaRouterMojom.RouteRequestResultCode.TIMED_OUT; + else + return mediaRouterMojom.RouteRequestResultCode.UNKNOWN_ERROR; + } + + /** + * Creates and returns a successful route response from given route. + * @param {!MediaRoute} route + * @return {!Object} + */ + function toSuccessRouteResponse_(route) { + return { + route: routeToMojo_(route), + result_code: mediaRouterMojom.RouteRequestResultCode.OK + }; + } + + /** + * Creates and returns a error route response from given Error object + * @param {!Error} error + * @return {!Object} + */ + function toErrorRouteResponse_(error) { + return { + error_text: 'Error creating route: ' + error.message, + result_code: getRouteRequestResultCode_(error) + }; + } + + /** + * Creates a new MediaRouter. + * Converts a route struct to its Mojo form. + * @param {!MediaRouterService} service + * @constructor + */ + function MediaRouter(service) { + /** + * The Mojo service proxy. Allows extension code to call methods that reside + * in the browser. + * @type {!MediaRouterService} + */ + this.service_ = service; + + /** + * The provider manager service delegate. Its methods are called by the + * browser-resident Mojo service. + * @type {!MediaRouter} + */ + this.mrpm_ = new MediaRouteProvider(this); + + /** + * The message pipe that connects the Media Router to mrpm_ across + * browser/renderer IPC boundaries. Object must remain in scope for the + * lifetime of the connection to prevent the connection from closing + * automatically. + * @type {!mojo.MessagePipe} + */ + this.pipe_ = core.createMessagePipe(); + + /** + * Handle to a KeepAlive service object, which prevents the extension from + * being suspended as long as it remains in scope. + * @type {boolean} + */ + this.keepAlive_ = null; + + /** + * The stub used to bind the service delegate to the Mojo interface. + * Object must remain in scope for the lifetime of the connection to + * prevent the connection from closing automatically. + * @type {!mojom.MediaRouter} + */ + this.mediaRouteProviderStub_ = connector.bindHandleToStub( + this.pipe_.handle0, mediaRouterMojom.MediaRouteProvider); + + // Link mediaRouteProviderStub_ to the provider manager delegate. + bindings.StubBindings(this.mediaRouteProviderStub_).delegate = this.mrpm_; + } + + /** + * Registers the Media Router Provider Manager with the Media Router. + * @return {!Promise<string>} Instance ID for the Media Router. + */ + MediaRouter.prototype.start = function() { + return this.service_.registerMediaRouteProvider(this.pipe_.handle1).then( + function(result) { + return result.instance_id; + }.bind(this)); + } + + /** + * Sets the service delegate methods. + * @param {Object} handlers + */ + MediaRouter.prototype.setHandlers = function(handlers) { + this.mrpm_.setHandlers(handlers); + } + + /** + * The keep alive status. + * @return {boolean} + */ + MediaRouter.prototype.getKeepAlive = function() { + return this.keepAlive_ != null; + }; + + /** + * Called by the provider manager when a sink list for a given source is + * updated. + * @param {!string} sourceUrn + * @param {!Array<!MediaSink>} sinks + * @param {Array<string>=} opt_origins + */ + MediaRouter.prototype.onSinksReceived = function(sourceUrn, sinks, + opt_origins) { + // TODO(imcheng): Make origins required in M52+. + this.service_.onSinksReceived(sourceUrn, sinks.map(sinkToMojo_), + opt_origins || []); + }; + + /** + * Called by the provider manager to keep the extension from suspending + * if it enters a state where suspension is undesirable (e.g. there is an + * active MediaRoute.) + * If keepAlive is true, the extension is kept alive. + * If keepAlive is false, the extension is allowed to suspend. + * @param {boolean} keepAlive + */ + MediaRouter.prototype.setKeepAlive = function(keepAlive) { + if (keepAlive === false && this.keepAlive_) { + this.keepAlive_.close(); + this.keepAlive_ = null; + } else if (keepAlive === true && !this.keepAlive_) { + this.keepAlive_ = new routerModule.Router( + serviceProvider.connectToService( + keepAliveMojom.KeepAlive.name)); + } + }; + + /** + * Called by the provider manager to send an issue from a media route + * provider to the Media Router, to show the user. + * @param {!Object} issue The issue object. + */ + MediaRouter.prototype.onIssue = function(issue) { + function issueSeverityToMojo_(severity) { + switch (severity) { + case 'fatal': + return mediaRouterMojom.Issue.Severity.FATAL; + case 'warning': + return mediaRouterMojom.Issue.Severity.WARNING; + case 'notification': + return mediaRouterMojom.Issue.Severity.NOTIFICATION; + default: + console.error('Unknown issue severity: ' + severity); + return mediaRouterMojom.Issue.Severity.NOTIFICATION; + } + } + + function issueActionToMojo_(action) { + switch (action) { + case 'ok': + return mediaRouterMojom.Issue.ActionType.OK; + case 'cancel': + return mediaRouterMojom.Issue.ActionType.CANCEL; + case 'dismiss': + return mediaRouterMojom.Issue.ActionType.DISMISS; + case 'learn_more': + return mediaRouterMojom.Issue.ActionType.LEARN_MORE; + default: + console.error('Unknown issue action type : ' + action); + return mediaRouterMojom.Issue.ActionType.OK; + } + } + + var secondaryActions = (issue.secondaryActions || []).map(function(e) { + return issueActionToMojo_(e); + }); + this.service_.onIssue(new mediaRouterMojom.Issue({ + 'route_id': issue.routeId, + 'severity': issueSeverityToMojo_(issue.severity), + 'title': issue.title, + 'message': issue.message, + 'default_action': issueActionToMojo_(issue.defaultAction), + 'secondary_actions': secondaryActions, + 'help_url': issue.helpUrl, + 'is_blocking': issue.isBlocking + })); + }; + + /** + * Called by the provider manager when the set of active routes + * has been updated. + * @param {!Array<MediaRoute>} routes The active set of media routes. + * @param {string=} opt_sourceUrn The sourceUrn associated with this route + * query. This parameter is optional and can be empty. + * @param {Array<string>=} opt_joinableRouteIds The active set of joinable + * media routes. This parameter is optional and can be empty. + */ + MediaRouter.prototype.onRoutesUpdated = + function(routes, opt_sourceUrn, opt_joinableRouteIds) { + // TODO(boetger): This check allows backward compatibility with the Cast SDK + // and can be removed when the Cast SDK is updated. + if (typeof(opt_sourceUrn) != 'string') { + opt_sourceUrn = ''; + } + + this.service_.onRoutesUpdated( + routes.map(routeToMojo_), + opt_sourceUrn || '', + opt_joinableRouteIds || []); + }; + + /** + * Called by the provider manager when sink availability has been updated. + * @param {!mediaRouterMojom.MediaRouter.SinkAvailability} availability + * The new sink availability. + */ + MediaRouter.prototype.onSinkAvailabilityUpdated = function(availability) { + this.service_.onSinkAvailabilityUpdated(availability); + }; + + /** + * Called by the provider manager when the state of a presentation connected + * to a route has changed. + * @param {string} routeId + * @param {string} state + */ + MediaRouter.prototype.onPresentationConnectionStateChanged = + function(routeId, state) { + this.service_.onPresentationConnectionStateChanged( + routeId, presentationConnectionStateToMojo_(state)); + }; + + /** + * Called by the provider manager when the state of a presentation connected + * to a route has closed. + * @param {string} routeId + * @param {string} reason + * @param {string} message + */ + MediaRouter.prototype.onPresentationConnectionClosed = + function(routeId, reason, message) { + this.service_.onPresentationConnectionClosed( + routeId, presentationConnectionCloseReasonToMojo_(reason), message); + }; + + /** + * Object containing callbacks set by the provider manager. + * + * @constructor + * @struct + */ + function MediaRouterHandlers() { + /** + * @type {function(!string, !string, !string, !string, !number} + */ + this.createRoute = null; + + /** + * @type {function(!string, !string, !string, !number)} + */ + this.joinRoute = null; + + /** + * @type {function(string)} + */ + this.terminateRoute = null; + + /** + * @type {function(string)} + */ + this.startObservingMediaSinks = null; + + /** + * @type {function(string)} + */ + this.stopObservingMediaSinks = null; + + /** + * @type {function(string, string): Promise} + */ + this.sendRouteMessage = null; + + /** + * @type {function(string, Uint8Array): Promise} + */ + this.sendRouteBinaryMessage = null; + + /** + * @type {function(string): + * Promise.<{messages: Array.<RouteMessage>, error: boolean}>} + */ + this.listenForRouteMessages = null; + + /** + * @type {function(string)} + */ + this.stopListeningForRouteMessages = null; + + /** + * @type {function(string)} + */ + this.detachRoute = null; + + /** + * @type {function()} + */ + this.startObservingMediaRoutes = null; + + /** + * @type {function()} + */ + this.stopObservingMediaRoutes = null; + + /** + * @type {function()} + */ + this.connectRouteByRouteId = null; + + /** + * @type {function()} + */ + this.enableMdnsDiscovery = null; + + /** + * @type {function()} + */ + this.updateMediaSinks = null; + }; + + /** + * Routes calls from Media Router to the provider manager extension. + * Registered with the MediaRouter stub. + * @param {!MediaRouter} MediaRouter proxy to call into the + * Media Router mojo interface. + * @constructor + */ + function MediaRouteProvider(mediaRouter) { + mediaRouterMojom.MediaRouteProvider.stubClass.call(this); + + /** + * Object containing JS callbacks into Provider Manager code. + * @type {!MediaRouterHandlers} + */ + this.handlers_ = new MediaRouterHandlers(); + + /** + * Proxy class to the browser's Media Router Mojo service. + * @type {!MediaRouter} + */ + this.mediaRouter_ = mediaRouter; + } + MediaRouteProvider.prototype = Object.create( + mediaRouterMojom.MediaRouteProvider.stubClass.prototype); + + /* + * Sets the callback handler used to invoke methods in the provider manager. + * + * @param {!MediaRouterHandlers} handlers + */ + MediaRouteProvider.prototype.setHandlers = function(handlers) { + this.handlers_ = handlers; + var requiredHandlers = [ + 'stopObservingMediaRoutes', + 'startObservingMediaRoutes', + 'sendRouteMessage', + 'sendRouteBinaryMessage', + 'listenForRouteMessages', + 'stopListeningForRouteMessages', + 'detachRoute', + 'terminateRoute', + 'joinRoute', + 'createRoute', + 'stopObservingMediaSinks', + 'startObservingMediaRoutes', + 'connectRouteByRouteId', + 'enableMdnsDiscovery', + 'updateMediaSinks', + ]; + requiredHandlers.forEach(function(nextHandler) { + if (handlers[nextHandler] === undefined) { + console.error(nextHandler + ' handler not registered.'); + } + }); + } + + /** + * Starts querying for sinks capable of displaying the media source + * designated by |sourceUrn|. Results are returned by calling + * OnSinksReceived. + * @param {!string} sourceUrn + */ + MediaRouteProvider.prototype.startObservingMediaSinks = + function(sourceUrn) { + this.handlers_.startObservingMediaSinks(sourceUrn); + }; + + /** + * Stops querying for sinks capable of displaying |sourceUrn|. + * @param {!string} sourceUrn + */ + MediaRouteProvider.prototype.stopObservingMediaSinks = + function(sourceUrn) { + this.handlers_.stopObservingMediaSinks(sourceUrn); + }; + + /** + * Requests that |sinkId| render the media referenced by |sourceUrn|. If the + * request is from the Presentation API, then origin and tabId will + * be populated. + * @param {!string} sourceUrn Media source to render. + * @param {!string} sinkId Media sink ID. + * @param {!string} presentationId Presentation ID from the site + * requesting presentation. TODO(mfoltz): Remove. + * @param {!string} origin Origin of site requesting presentation. + * @param {!number} tabId ID of tab requesting presentation. + * @param {!number} timeoutMillis If positive, the timeout duration for the + * request, measured in seconds. Otherwise, the default duration will be + * used. + * @param {!boolean} offTheRecord If true, the route is being requested by + * an off the record (incognito) profile. + * @return {!Promise.<!Object>} A Promise resolving to an object describing + * the newly created media route, or rejecting with an error message on + * failure. + */ + MediaRouteProvider.prototype.createRoute = + function(sourceUrn, sinkId, presentationId, origin, tabId, + timeoutMillis, offTheRecord) { + return this.handlers_.createRoute( + sourceUrn, sinkId, presentationId, origin, tabId, timeoutMillis, + offTheRecord) + .then(function(route) { + return toSuccessRouteResponse_(route); + }, + function(err) { + return toErrorRouteResponse_(err); + }); + }; + + /** + * Handles a request via the Presentation API to join an existing route given + * by |sourceUrn| and |presentationId|. |origin| and |tabId| are used for + * validating same-origin/tab scope. + * @param {!string} sourceUrn Media source to render. + * @param {!string} presentationId Presentation ID to join. + * @param {!string} origin Origin of site requesting join. + * @param {!number} tabId ID of tab requesting join. + * @param {!number} timeoutMillis If positive, the timeout duration for the + * request, measured in seconds. Otherwise, the default duration will be + * used. + * @param {!boolean} offTheRecord If true, the route is being requested by + * an off the record (incognito) profile. + * @return {!Promise.<!Object>} A Promise resolving to an object describing + * the newly created media route, or rejecting with an error message on + * failure. + */ + MediaRouteProvider.prototype.joinRoute = + function(sourceUrn, presentationId, origin, tabId, timeoutMillis, + offTheRecord) { + return this.handlers_.joinRoute( + sourceUrn, presentationId, origin, tabId, timeoutMillis, offTheRecord) + .then(function(route) { + return toSuccessRouteResponse_(route); + }, + function(err) { + return toErrorRouteResponse_(err); + }); + }; + + /** + * Handles a request via the Presentation API to join an existing route given + * by |sourceUrn| and |routeId|. |origin| and |tabId| are used for + * validating same-origin/tab scope. + * @param {!string} sourceUrn Media source to render. + * @param {!string} routeId Route ID to join. + * @param {!string} presentationId Presentation ID to join. + * @param {!string} origin Origin of site requesting join. + * @param {!number} tabId ID of tab requesting join. + * @param {!number} timeoutMillis If positive, the timeout duration for the + * request, measured in seconds. Otherwise, the default duration will be + * used. + * @param {!boolean} offTheRecord If true, the route is being requested by + * an off the record (incognito) profile. + * @return {!Promise.<!Object>} A Promise resolving to an object describing + * the newly created media route, or rejecting with an error message on + * failure. + */ + MediaRouteProvider.prototype.connectRouteByRouteId = + function(sourceUrn, routeId, presentationId, origin, tabId, + timeoutMillis, offTheRecord) { + return this.handlers_.connectRouteByRouteId( + sourceUrn, routeId, presentationId, origin, tabId, timeoutMillis, + offTheRecord) + .then(function(route) { + return toSuccessRouteResponse_(route); + }, + function(err) { + return toErrorRouteResponse_(err); + }); + }; + + /** + * Terminates the route specified by |routeId|. + * @param {!string} routeId + */ + MediaRouteProvider.prototype.terminateRoute = function(routeId) { + this.handlers_.terminateRoute(routeId); + }; + + /** + * Posts a message to the route designated by |routeId|. + * @param {!string} routeId + * @param {!string} message + * @return {!Promise.<boolean>} Resolved with true if the message was sent, + * or false on failure. + */ + MediaRouteProvider.prototype.sendRouteMessage = function( + routeId, message) { + return this.handlers_.sendRouteMessage(routeId, message) + .then(function() { + return {'sent': true}; + }, function() { + return {'sent': false}; + }); + }; + + /** + * Sends a binary message to the route designated by |routeId|. + * @param {!string} routeId + * @param {!Uint8Array} data + * @return {!Promise.<boolean>} Resolved with true if the data was sent, + * or false on failure. + */ + MediaRouteProvider.prototype.sendRouteBinaryMessage = function( + routeId, data) { + return this.handlers_.sendRouteBinaryMessage(routeId, data) + .then(function() { + return {'sent': true}; + }, function() { + return {'sent': false}; + }); + }; + + /** + * Listen for next batch of messages from one of the routeIds. + * @param {!string} routeId + * @return {!Promise.<{messages: Array.<RouteMessage>, error: boolean}>} + * Resolved with a list of messages, and a boolean indicating if an error + * occurred. + */ + MediaRouteProvider.prototype.listenForRouteMessages = function(routeId) { + return this.handlers_.listenForRouteMessages(routeId) + .then(function(messages) { + return {'messages': messages.map(messageToMojo_), 'error': false}; + }, function() { + return {'messages': [], 'error': true}; + }); + }; + + /** + * If there is an outstanding |listenForRouteMessages| promise for + * |routeId|, resolve that promise with an empty array. + * @param {!string} routeId + */ + MediaRouteProvider.prototype.stopListeningForRouteMessages = function( + routeId) { + return this.handlers_.stopListeningForRouteMessages(routeId); + }; + + /** + * Indicates that the presentation connection that was connected to |routeId| + * is no longer connected to it. + * @param {!string} routeId + */ + MediaRouteProvider.prototype.detachRoute = function( + routeId) { + this.handlers_.detachRoute(routeId); + }; + + /** + * Requests that the provider manager start sending information about active + * media routes to the Media Router. + * @param {!string} sourceUrn + */ + MediaRouteProvider.prototype.startObservingMediaRoutes = function(sourceUrn) { + this.handlers_.startObservingMediaRoutes(sourceUrn); + }; + + /** + * Requests that the provider manager stop sending information about active + * media routes to the Media Router. + * @param {!string} sourceUrn + */ + MediaRouteProvider.prototype.stopObservingMediaRoutes = function(sourceUrn) { + this.handlers_.stopObservingMediaRoutes(sourceUrn); + }; + + /** + * Enables mDNS device discovery. + */ + MediaRouteProvider.prototype.enableMdnsDiscovery = function() { + this.handlers_.enableMdnsDiscovery(); + }; + + /** + * Requests that the provider manager update media sinks. + * @param {!string} sourceUrn + */ + MediaRouteProvider.prototype.updateMediaSinks = function(sourceUrn) { + this.handlers_.updateMediaSinks(sourceUrn); + }; + + mediaRouter = new MediaRouter(connector.bindHandleToProxy( + serviceProvider.connectToService( + mediaRouterMojom.MediaRouter.name), + mediaRouterMojom.MediaRouter)); + + return mediaRouter; +}); + diff --git a/chromium/extensions/renderer/resources/messaging.js b/chromium/extensions/renderer/resources/messaging.js new file mode 100644 index 00000000000..8ca84ea4655 --- /dev/null +++ b/chromium/extensions/renderer/resources/messaging.js @@ -0,0 +1,446 @@ +// 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. + +// chrome.runtime.messaging API implementation. +// TODO(robwu): Fix this indentation. + + // TODO(kalman): factor requiring chrome out of here. + var chrome = requireNative('chrome').GetChrome(); + var Event = require('event_bindings').Event; + var lastError = require('lastError'); + var logActivity = requireNative('activityLogger'); + var logging = requireNative('logging'); + var messagingNatives = requireNative('messaging_natives'); + var processNatives = requireNative('process'); + var utils = require('utils'); + var messagingUtils = require('messaging_utils'); + + // The reserved channel name for the sendRequest/send(Native)Message APIs. + // Note: sendRequest is deprecated. + var kRequestChannel = "chrome.extension.sendRequest"; + var kMessageChannel = "chrome.runtime.sendMessage"; + var kNativeMessageChannel = "chrome.runtime.sendNativeMessage"; + + // Map of port IDs to port object. + var ports = {__proto__: null}; + + // Change even to odd and vice versa, to get the other side of a given + // channel. + function getOppositePortId(portId) { return portId ^ 1; } + + // Port object. Represents a connection to another script context through + // which messages can be passed. + function PortImpl(portId, opt_name) { + this.portId_ = portId; + this.name = opt_name; + + // Note: Keep these schemas in sync with the documentation in runtime.json + var portSchema = { + __proto__: null, + name: 'port', + $ref: 'runtime.Port', + }; + var messageSchema = { + __proto__: null, + name: 'message', + type: 'any', + optional: true, + }; + var options = { + __proto__: null, + unmanaged: true, + }; + this.onDisconnect = new Event(null, [portSchema], options); + this.onMessage = new Event(null, [messageSchema, portSchema], options); + this.onDestroy_ = null; + } + $Object.setPrototypeOf(PortImpl.prototype, null); + + // Sends a message asynchronously to the context on the other end of this + // port. + PortImpl.prototype.postMessage = function(msg) { + // JSON.stringify doesn't support a root object which is undefined. + if (msg === undefined) + msg = null; + msg = $JSON.stringify(msg); + if (msg === undefined) { + // JSON.stringify can fail with unserializable objects. Log an error and + // drop the message. + // + // TODO(kalman/mpcomplete): it would be better to do the same validation + // here that we do for runtime.sendMessage (and variants), i.e. throw an + // schema validation Error, but just maintain the old behaviour until + // there's a good reason not to (http://crbug.com/263077). + console.error('Illegal argument to Port.postMessage'); + return; + } + messagingNatives.PostMessage(this.portId_, msg); + }; + + // Disconnects the port from the other end. + PortImpl.prototype.disconnect = function() { + messagingNatives.CloseChannel(this.portId_, true); + this.destroy_(); + }; + + PortImpl.prototype.destroy_ = function() { + if (this.onDestroy_) { + this.onDestroy_(); + this.onDestroy_ = null; + } + privates(this.onDisconnect).impl.destroy_(); + privates(this.onMessage).impl.destroy_(); + // TODO(robwu): Remove port lifetime management because it is completely + // handled in the browser. The renderer's only roles are + // 1) rejecting ports so that the browser knows that the renderer is not + // interested in the port (this is merely an optimization) + // 2) acknowledging port creations, so that the browser knows that the port + // was successfully created (from the perspective of the extension), but + // then closed for some non-fatal reason. + // 3) notifying the browser of explicit port closure via .disconnect(). + // In other cases (navigations), the browser automatically cleans up the + // port. + messagingNatives.PortRelease(this.portId_); + delete ports[this.portId_]; + }; + + // Returns true if the specified port id is in this context. This is used by + // the C++ to avoid creating the javascript message for all the contexts that + // don't care about a particular message. + function hasPort(portId) { + return portId in ports; + }; + + // Hidden port creation function. We don't want to expose an API that lets + // people add arbitrary port IDs to the port list. + function createPort(portId, opt_name) { + if (ports[portId]) + throw new Error("Port '" + portId + "' already exists."); + var port = new Port(portId, opt_name); + ports[portId] = port; + messagingNatives.PortAddRef(portId); + return port; + }; + + // Helper function for dispatchOnRequest. + function handleSendRequestError(isSendMessage, + responseCallbackPreserved, + sourceExtensionId, + targetExtensionId, + sourceUrl) { + var errorMsg; + var eventName = isSendMessage ? 'runtime.onMessage' : 'extension.onRequest'; + if (isSendMessage && !responseCallbackPreserved) { + errorMsg = + 'The chrome.' + eventName + ' listener must return true if you ' + + 'want to send a response after the listener returns'; + } else { + errorMsg = + 'Cannot send a response more than once per chrome.' + eventName + + ' listener per document'; + } + errorMsg += ' (message was sent by extension' + sourceExtensionId; + if (sourceExtensionId && sourceExtensionId !== targetExtensionId) + errorMsg += ' for extension ' + targetExtensionId; + if (sourceUrl) + errorMsg += ' for URL ' + sourceUrl; + errorMsg += ').'; + lastError.set(eventName, errorMsg, null, chrome); + } + + // Helper function for dispatchOnConnect + function dispatchOnRequest(portId, channelName, sender, + sourceExtensionId, targetExtensionId, sourceUrl, + isExternal) { + var isSendMessage = channelName == kMessageChannel; + var requestEvent = null; + if (isSendMessage) { + if (chrome.runtime) { + requestEvent = isExternal ? chrome.runtime.onMessageExternal + : chrome.runtime.onMessage; + } + } else { + if (chrome.extension) { + requestEvent = isExternal ? chrome.extension.onRequestExternal + : chrome.extension.onRequest; + } + } + if (!requestEvent) + return false; + if (!requestEvent.hasListeners()) + return false; + var port = createPort(portId, channelName); + + function messageListener(request) { + var responseCallbackPreserved = false; + var responseCallback = function(response) { + if (port) { + port.postMessage(response); + privates(port).impl.destroy_(); + port = null; + } else { + // We nulled out port when sending the response, and now the page + // is trying to send another response for the same request. + handleSendRequestError(isSendMessage, responseCallbackPreserved, + sourceExtensionId, targetExtensionId); + } + }; + // In case the extension never invokes the responseCallback, and also + // doesn't keep a reference to it, we need to clean up the port. Do + // so by attaching to the garbage collection of the responseCallback + // using some native hackery. + // + // If the context is destroyed before this has a chance to execute, + // BindToGC knows to release |portId| (important for updating C++ state + // both in this renderer and on the other end). We don't need to clear + // any JavaScript state, as calling destroy_() would usually do - but + // the context has been destroyed, so there isn't any JS state to clear. + messagingNatives.BindToGC(responseCallback, function() { + if (port) { + privates(port).impl.destroy_(); + port = null; + } + }, portId); + var rv = requestEvent.dispatch(request, sender, responseCallback); + if (isSendMessage) { + responseCallbackPreserved = + rv && rv.results && $Array.indexOf(rv.results, true) > -1; + if (!responseCallbackPreserved && port) { + // If they didn't access the response callback, they're not + // going to send a response, so clean up the port immediately. + privates(port).impl.destroy_(); + port = null; + } + } + } + + privates(port).impl.onDestroy_ = function() { + port.onMessage.removeListener(messageListener); + }; + port.onMessage.addListener(messageListener); + + var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; + if (isExternal) + eventName += "External"; + logActivity.LogEvent(targetExtensionId, + eventName, + [sourceExtensionId, sourceUrl]); + return true; + } + + // Called by native code when a channel has been opened to this context. + function dispatchOnConnect(portId, + channelName, + sourceTab, + sourceFrameId, + guestProcessId, + guestRenderFrameRoutingId, + sourceExtensionId, + targetExtensionId, + sourceUrl, + tlsChannelId) { + // Only create a new Port if someone is actually listening for a connection. + // In addition to being an optimization, this also fixes a bug where if 2 + // channels were opened to and from the same process, closing one would + // close both. + var extensionId = processNatives.GetExtensionId(); + + // messaging_bindings.cc should ensure that this method only gets called for + // the right extension. + logging.CHECK(targetExtensionId == extensionId); + + if (ports[getOppositePortId(portId)]) + return false; // this channel was opened by us, so ignore it + + // Determine whether this is coming from another extension, so we can use + // the right event. + var isExternal = sourceExtensionId != extensionId; + + var sender = {}; + if (sourceExtensionId != '') + sender.id = sourceExtensionId; + if (sourceUrl) + sender.url = sourceUrl; + if (sourceTab) + sender.tab = sourceTab; + if (sourceFrameId >= 0) + sender.frameId = sourceFrameId; + if (typeof guestProcessId !== 'undefined' && + typeof guestRenderFrameRoutingId !== 'undefined') { + // Note that |guestProcessId| and |guestRenderFrameRoutingId| are not + // standard fields on MessageSender and should not be exposed to drive-by + // extensions; it is only exposed to component extensions. + logging.CHECK(processNatives.IsComponentExtension(), + "GuestProcessId can only be exposed to component extensions."); + sender.guestProcessId = guestProcessId; + sender.guestRenderFrameRoutingId = guestRenderFrameRoutingId; + } + if (typeof tlsChannelId != 'undefined') + sender.tlsChannelId = tlsChannelId; + + // Special case for sendRequest/onRequest and sendMessage/onMessage. + if (channelName == kRequestChannel || channelName == kMessageChannel) { + return dispatchOnRequest(portId, channelName, sender, + sourceExtensionId, targetExtensionId, sourceUrl, + isExternal); + } + + var connectEvent = null; + if (chrome.runtime) { + connectEvent = isExternal ? chrome.runtime.onConnectExternal + : chrome.runtime.onConnect; + } + if (!connectEvent) + return false; + if (!connectEvent.hasListeners()) + return false; + + var port = createPort(portId, channelName); + port.sender = sender; + if (processNatives.manifestVersion < 2) + port.tab = port.sender.tab; + + var eventName = (isExternal ? + "runtime.onConnectExternal" : "runtime.onConnect"); + connectEvent.dispatch(port); + logActivity.LogEvent(targetExtensionId, + eventName, + [sourceExtensionId]); + return true; + }; + + // Called by native code when a channel has been closed. + function dispatchOnDisconnect(portId, errorMessage) { + var port = ports[portId]; + if (port) { + // Update the renderer's port bookkeeping, without notifying the browser. + messagingNatives.CloseChannel(portId, false); + if (errorMessage) + lastError.set('Port', errorMessage, null, chrome); + try { + port.onDisconnect.dispatch(port); + } finally { + privates(port).impl.destroy_(); + lastError.clear(chrome); + } + } + }; + + // Called by native code when a message has been sent to the given port. + function dispatchOnMessage(msg, portId) { + var port = ports[portId]; + if (port) { + if (msg) + msg = $JSON.parse(msg); + port.onMessage.dispatch(msg, port); + } + }; + + // Shared implementation used by tabs.sendMessage and runtime.sendMessage. + function sendMessageImpl(port, request, responseCallback) { + if (port.name != kNativeMessageChannel) + port.postMessage(request); + + if (port.name == kMessageChannel && !responseCallback) { + // TODO(mpcomplete): Do this for the old sendRequest API too, after + // verifying it doesn't break anything. + // Go ahead and disconnect immediately if the sender is not expecting + // a response. + port.disconnect(); + return; + } + + function sendResponseAndClearCallback(response) { + // Save a reference so that we don't re-entrantly call responseCallback. + var sendResponse = responseCallback; + responseCallback = null; + if (arguments.length === 0) { + // According to the documentation of chrome.runtime.sendMessage, the + // callback is invoked without any arguments when an error occurs. + sendResponse(); + } else { + sendResponse(response); + } + } + + + // Note: make sure to manually remove the onMessage/onDisconnect listeners + // that we added before destroying the Port, a workaround to a bug in Port + // where any onMessage/onDisconnect listeners added but not removed will + // be leaked when the Port is destroyed. + // http://crbug.com/320723 tracks a sustainable fix. + + function disconnectListener() { + if (!responseCallback) + return; + + if (lastError.hasError(chrome)) { + sendResponseAndClearCallback(); + } else { + lastError.set( + port.name, 'The message port closed before a reponse was received.', + null, chrome); + try { + sendResponseAndClearCallback(); + } finally { + lastError.clear(chrome); + } + } + } + + function messageListener(response) { + try { + if (responseCallback) + sendResponseAndClearCallback(response); + } finally { + port.disconnect(); + } + } + + privates(port).impl.onDestroy_ = function() { + port.onDisconnect.removeListener(disconnectListener); + port.onMessage.removeListener(messageListener); + }; + port.onDisconnect.addListener(disconnectListener); + port.onMessage.addListener(messageListener); + }; + + function sendMessageUpdateArguments(functionName, hasOptionsArgument) { + // skip functionName and hasOptionsArgument + var args = $Array.slice(arguments, 2); + var alignedArgs = messagingUtils.alignSendMessageArguments(args, + hasOptionsArgument); + if (!alignedArgs) + throw new Error('Invalid arguments to ' + functionName + '.'); + return alignedArgs; + } + + function Port() { + privates(Port).constructPrivate(this, arguments); + } + utils.expose(Port, PortImpl, { + functions: [ + 'disconnect', + 'postMessage', + ], + properties: [ + 'name', + 'onDisconnect', + 'onMessage', + ], + }); + +exports.$set('kRequestChannel', kRequestChannel); +exports.$set('kMessageChannel', kMessageChannel); +exports.$set('kNativeMessageChannel', kNativeMessageChannel); +exports.$set('Port', Port); +exports.$set('createPort', createPort); +exports.$set('sendMessageImpl', sendMessageImpl); +exports.$set('sendMessageUpdateArguments', sendMessageUpdateArguments); + +// For C++ code to call. +exports.$set('hasPort', hasPort); +exports.$set('dispatchOnConnect', dispatchOnConnect); +exports.$set('dispatchOnDisconnect', dispatchOnDisconnect); +exports.$set('dispatchOnMessage', dispatchOnMessage); diff --git a/chromium/extensions/renderer/resources/messaging_utils.js b/chromium/extensions/renderer/resources/messaging_utils.js new file mode 100644 index 00000000000..381fbeb572b --- /dev/null +++ b/chromium/extensions/renderer/resources/messaging_utils.js @@ -0,0 +1,53 @@ +// 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. + +// Routines used to normalize arguments to messaging functions. + +function alignSendMessageArguments(args, hasOptionsArgument) { + // Align missing (optional) function arguments with the arguments that + // schema validation is expecting, e.g. + // extension.sendRequest(req) -> extension.sendRequest(null, req) + // extension.sendRequest(req, cb) -> extension.sendRequest(null, req, cb) + if (!args || !args.length) + return null; + var lastArg = args.length - 1; + + // responseCallback (last argument) is optional. + var responseCallback = null; + if (typeof args[lastArg] == 'function') + responseCallback = args[lastArg--]; + + var options = null; + if (hasOptionsArgument && lastArg >= 1) { + // options (third argument) is optional. It can also be ambiguous which + // argument it should match. If there are more than two arguments remaining, + // options is definitely present: + if (lastArg > 1) { + options = args[lastArg--]; + } else { + // Exactly two arguments remaining. If the first argument is a string, + // it should bind to targetId, and the second argument should bind to + // request, which is required. In other words, when two arguments remain, + // only bind options when the first argument cannot bind to targetId. + if (!(args[0] === null || typeof args[0] == 'string')) + options = args[lastArg--]; + } + } + + // request (second argument) is required. + var request = args[lastArg--]; + + // targetId (first argument, extensionId in the manifest) is optional. + var targetId = null; + if (lastArg >= 0) + targetId = args[lastArg--]; + + if (lastArg != -1) + return null; + if (hasOptionsArgument) + return [targetId, request, options, responseCallback]; + return [targetId, request, responseCallback]; +} + +exports.$set('alignSendMessageArguments', alignSendMessageArguments); diff --git a/chromium/extensions/renderer/resources/mime_handler_private_custom_bindings.js b/chromium/extensions/renderer/resources/mime_handler_private_custom_bindings.js new file mode 100644 index 00000000000..8e9eb20ebe5 --- /dev/null +++ b/chromium/extensions/renderer/resources/mime_handler_private_custom_bindings.js @@ -0,0 +1,75 @@ +// Copyright 2015 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. + +/** + * Custom bindings for the mime handler API. + */ + +var binding = require('binding').Binding.create('mimeHandlerPrivate'); + +var NO_STREAM_ERROR = + 'Streams are only available from a mime handler view guest.'; +var STREAM_ABORTED_ERROR = 'Stream has been aborted.'; + +var servicePromise = Promise.all([ + requireAsync('content/public/renderer/frame_service_registry'), + requireAsync('extensions/common/api/mime_handler.mojom'), + requireAsync('mojo/public/js/router'), +]).then(function(modules) { + var serviceProvider = modules[0]; + var mojom = modules[1]; + var routerModule = modules[2]; + return new mojom.MimeHandlerService.proxyClass(new routerModule.Router( + serviceProvider.connectToService(mojom.MimeHandlerService.name))); +}); + +// Stores a promise to the GetStreamInfo() result to avoid making additional +// calls in response to getStreamInfo() calls. +var streamInfoPromise; + +function throwNoStreamError() { + throw new Error(NO_STREAM_ERROR); +} + +function createStreamInfoPromise() { + return servicePromise.then(function(service) { + return service.getStreamInfo(); + }).then(function(result) { + if (!result.stream_info) + throw new Error(STREAM_ABORTED_ERROR); + return result.stream_info; + }, throwNoStreamError); +} + +function constructStreamInfoDict(streamInfo) { + var headers = {}; + for (var header of streamInfo.response_headers) { + headers[header[0]] = header[1]; + } + return { + mimeType: streamInfo.mime_type, + originalUrl: streamInfo.original_url, + streamUrl: streamInfo.stream_url, + tabId: streamInfo.tab_id, + embedded: !!streamInfo.embedded, + responseHeaders: headers, + }; +} + +binding.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + apiFunctions.setHandleRequestWithPromise('getStreamInfo', function() { + if (!streamInfoPromise) + streamInfoPromise = createStreamInfoPromise(); + return streamInfoPromise.then(constructStreamInfoDict); + }); + + apiFunctions.setHandleRequestWithPromise('abortStream', function() { + return servicePromise.then(function(service) { + return service.abortStream().then(function() {}); + }).catch(throwNoStreamError); + }); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/mojo_private_custom_bindings.js b/chromium/extensions/renderer/resources/mojo_private_custom_bindings.js new file mode 100644 index 00000000000..18d6016ba3b --- /dev/null +++ b/chromium/extensions/renderer/resources/mojo_private_custom_bindings.js @@ -0,0 +1,23 @@ +// Copyright 2015 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. + +/** + * Custom bindings for the mojoPrivate API. + */ + +let binding = require('binding').Binding.create('mojoPrivate'); + +binding.registerCustomHook(function(bindingsAPI) { + let apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setHandleRequest('define', function(name, deps, factory) { + define(name, deps || [], factory); + }); + + apiFunctions.setHandleRequest('requireAsync', function(moduleName) { + return requireAsync(moduleName); + }); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/permissions_custom_bindings.js b/chromium/extensions/renderer/resources/permissions_custom_bindings.js new file mode 100644 index 00000000000..492360a9ef1 --- /dev/null +++ b/chromium/extensions/renderer/resources/permissions_custom_bindings.js @@ -0,0 +1,92 @@ +// 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. + +// Custom binding for the Permissions API. + +var binding = require('binding').Binding.create('permissions'); + +var Event = require('event_bindings').Event; + +// These custom binding are only necessary because it is not currently +// possible to have a union of types as the type of the items in an array. +// Once that is fixed, this entire file should go away. +// See, +// https://code.google.com/p/chromium/issues/detail?id=162044 +// https://code.google.com/p/chromium/issues/detail?id=162042 +// TODO(bryeung): delete this file. +binding.registerCustomHook(function(api) { + var apiFunctions = api.apiFunctions; + var permissions = api.compiledApi; + + function maybeConvertToObject(str) { + var parts = $String.split(str, '|'); + if (parts.length != 2) + return str; + + var ret = {}; + ret[parts[0]] = JSON.parse(parts[1]); + return ret; + } + + function convertObjectPermissionsToStrings() { + if (arguments.length < 1) + return arguments; + + var args = arguments[0].permissions; + if (!args) + return arguments; + + for (var i = 0; i < args.length; i += 1) { + if (typeof(args[i]) == 'object') { + var a = args[i]; + var keys = $Object.keys(a); + if (keys.length != 1) { + throw new Error("Too many keys in object-style permission."); + } + arguments[0].permissions[i] = keys[0] + '|' + + JSON.stringify(a[keys[0]]); + } + } + + return arguments; + } + + // Convert complex permissions to strings so they validate against the schema + apiFunctions.setUpdateArgumentsPreValidate( + 'contains', convertObjectPermissionsToStrings); + apiFunctions.setUpdateArgumentsPreValidate( + 'remove', convertObjectPermissionsToStrings); + apiFunctions.setUpdateArgumentsPreValidate( + 'request', convertObjectPermissionsToStrings); + + // Convert complex permissions back to objects + apiFunctions.setCustomCallback('getAll', + function(name, request, callback, response) { + for (var i = 0; i < response.permissions.length; i += 1) { + response.permissions[i] = + maybeConvertToObject(response.permissions[i]); + } + + // Since the schema says Permissions.permissions contains strings and + // not objects, validation will fail after the for-loop above. This + // skips validation and calls the callback directly. + if (callback) + callback(response); + }); + + // Also convert complex permissions back to objects for events. The + // dispatchToListener call happens after argument validation, which works + // around the problem that Permissions.permissions is supposed to be a list + // of strings. + permissions.onAdded.dispatchToListener = function(callback, args) { + for (var i = 0; i < args[0].permissions.length; i += 1) { + args[0].permissions[i] = maybeConvertToObject(args[0].permissions[i]); + } + $Function.call(Event.prototype.dispatchToListener, this, callback, args); + }; + permissions.onRemoved.dispatchToListener = + permissions.onAdded.dispatchToListener; +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/platform_app.css b/chromium/extensions/renderer/resources/platform_app.css new file mode 100644 index 00000000000..fa811310bd9 --- /dev/null +++ b/chromium/extensions/renderer/resources/platform_app.css @@ -0,0 +1,35 @@ +/* + * 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. + * + * A style sheet for Chrome apps. + */ + +@namespace "http://www.w3.org/1999/xhtml"; + +body { + -webkit-user-select: none; + cursor: default; + font-family: $FONTFAMILY; + font-size: $FONTSIZE; +} + +webview, appview { + display: inline-block; + width: 300px; + height: 300px; +} + +html, body { + overflow: hidden; +} + +img, a { + -webkit-user-drag: none; +} + +[contenteditable], input { + -webkit-user-select: auto; +} + diff --git a/chromium/extensions/renderer/resources/platform_app.js b/chromium/extensions/renderer/resources/platform_app.js new file mode 100644 index 00000000000..9f386d790d2 --- /dev/null +++ b/chromium/extensions/renderer/resources/platform_app.js @@ -0,0 +1,232 @@ +// 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. + +var logging = requireNative('logging'); + +/** + * Returns a function that logs a 'not available' error to the console and + * returns undefined. + * + * @param {string} messagePrefix text to prepend to the exception message. + */ +function generateDisabledMethodStub(messagePrefix, opt_messageSuffix) { + var message = messagePrefix + ' is not available in packaged apps.'; + if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; + return function() { + console.error(message); + return; + }; +} + +/** + * Returns a function that throws a 'not available' error. + * + * @param {string} messagePrefix text to prepend to the exception message. + */ +function generateThrowingMethodStub(messagePrefix, opt_messageSuffix) { + var message = messagePrefix + ' is not available in packaged apps.'; + if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; + return function() { + throw new Error(message); + }; +} + +/** + * Replaces the given methods of the passed in object with stubs that log + * 'not available' errors to the console and return undefined. + * + * This should be used on methods attached via non-configurable properties, + * such as window.alert. disableGetters should be used when possible, because + * it is friendlier towards feature detection. + * + * In most cases, the useThrowingStubs should be false, so the stubs used to + * replace the methods log an error to the console, but allow the calling code + * to continue. We shouldn't break library code that uses feature detection + * responsibly, such as: + * if(window.confirm) { + * var result = window.confirm('Are you sure you want to delete ...?'); + * ... + * } + * + * useThrowingStubs should only be true for methods that are deprecated in the + * Web platform, and should not be used by a responsible library, even in + * conjunction with feature detection. A great example is document.write(), as + * the HTML5 specification recommends against using it, and says that its + * behavior is unreliable. No reasonable library code should ever use it. + * HTML5 spec: http://www.w3.org/TR/html5/dom.html#dom-document-write + * + * @param {Object} object The object with methods to disable. The prototype is + * preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the stub (this is the name that the object is commonly referred + * to by web developers, e.g. "document" instead of "HTMLDocument"). + * @param {Array<string>} methodNames names of methods to disable. + * @param {Boolean} useThrowingStubs if true, the replaced methods will throw + * an error instead of silently returning undefined + */ +function disableMethods(object, objectName, methodNames, useThrowingStubs) { + $Array.forEach(methodNames, function(methodName) { + logging.DCHECK($Object.getOwnPropertyDescriptor(object, methodName), + objectName + ': ' + methodName); + var messagePrefix = objectName + '.' + methodName + '()'; + $Object.defineProperty(object, methodName, { + configurable: false, + enumerable: false, + value: useThrowingStubs ? + generateThrowingMethodStub(messagePrefix) : + generateDisabledMethodStub(messagePrefix) + }); + }); +} + +/** + * Replaces the given properties of the passed in object with stubs that log + * 'not available' warnings to the console and return undefined when gotten. If + * a property's setter is later invoked, the getter and setter are restored to + * default behaviors. + * + * @param {Object} object The object with properties to disable. The prototype + * is preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the getter stub (this is the name that the object is commonly + * referred to by web developers, e.g. "document" instead of + * "HTMLDocument"). + * @param {Array<string>} propertyNames names of properties to disable. + * @param {?string=} opt_messageSuffix An optional suffix for the message. + * @param {boolean=} opt_ignoreMissingProperty True if we allow disabling + * getters for non-existent properties. + */ +function disableGetters(object, objectName, propertyNames, opt_messageSuffix, + opt_ignoreMissingProperty) { + $Array.forEach(propertyNames, function(propertyName) { + logging.DCHECK(opt_ignoreMissingProperty || + $Object.getOwnPropertyDescriptor(object, propertyName), + objectName + ': ' + propertyName); + var stub = generateDisabledMethodStub(objectName + '.' + propertyName, + opt_messageSuffix); + stub._is_platform_app_disabled_getter = true; + $Object.defineProperty(object, propertyName, { + configurable: true, + enumerable: false, + get: stub, + set: function(value) { + var descriptor = $Object.getOwnPropertyDescriptor(this, propertyName); + if (!descriptor || !descriptor.get || + descriptor.get._is_platform_app_disabled_getter) { + // The stub getter is still defined. Blow-away the property to + // restore default getter/setter behaviors and re-create it with the + // given value. + delete this[propertyName]; + this[propertyName] = value; + } else { + // Do nothing. If some custom getter (not ours) has been defined, + // there would be no way to read back the value stored by a default + // setter. Also, the only way to clear a custom getter is to first + // delete the property. Therefore, the value we have here should + // just go into a black hole. + } + } + }); + }); +} + +/** + * Replaces the given properties of the passed in object with stubs that log + * 'not available' warnings to the console when set. + * + * @param {Object} object The object with properties to disable. The prototype + * is preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the setter stub (this is the name that the object is commonly + * referred to by web developers, e.g. "document" instead of + * "HTMLDocument"). + * @param {Array<string>} propertyNames names of properties to disable. + */ +function disableSetters(object, objectName, propertyNames, opt_messageSuffix) { + $Array.forEach(propertyNames, function(propertyName) { + logging.DCHECK($Object.getOwnPropertyDescriptor(object, propertyName), + objectName + ': ' + propertyName); + var stub = generateDisabledMethodStub(objectName + '.' + propertyName, + opt_messageSuffix); + $Object.defineProperty(object, propertyName, { + configurable: false, + enumerable: false, + get: function() { + return; + }, + set: stub + }); + }); +} + +// Disable benign Document methods. +disableMethods(Document.prototype, 'document', ['open', 'close']); +disableMethods(HTMLDocument.prototype, 'document', ['clear']); + +// Replace evil Document methods with exception-throwing stubs. +disableMethods(Document.prototype, 'document', ['write', 'writeln'], true); + +// Disable history. +Object.defineProperty(window, "history", { value: {} }); +// Note: we just blew away the history object, so we need to ignore the fact +// that these properties aren't defined on the object. +disableGetters(window.history, 'history', + ['back', 'forward', 'go', 'length', 'pushState', 'replaceState', 'state'], + null, true); + +// Disable find. +disableMethods(window, 'window', ['find']); + +// Disable modal dialogs. Shell windows disable these anyway, but it's nice to +// warn. +disableMethods(window, 'window', ['alert', 'confirm', 'prompt']); + +// Disable window.*bar. +disableGetters(window, 'window', + ['locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', + 'toolbar']); + +// Disable window.localStorage. +// Sometimes DOM security policy prevents us from doing this (e.g. for data: +// URLs) so wrap in try-catch. +try { + disableGetters(window, 'window', + ['localStorage'], + 'Use chrome.storage.local instead.'); +} catch (e) {} + +// Document instance properties that we wish to disable need to be set when +// the document begins loading, since only then will the "document" reference +// point to the page's document (it will be reset between now and then). +// We can't listen for the "readystatechange" event on the document (because +// the object that it's dispatched on doesn't exist yet), but we can instead +// do it at the window level in the capturing phase. +window.addEventListener('readystatechange', function(event) { + if (document.readyState != 'loading') + return; + + // Deprecated document properties from + // https://developer.mozilla.org/en/DOM/document. + // To deprecate document.all, simply changing its getter and setter would + // activate its cache mechanism, and degrade the performance. Here we assign + // it first to 'undefined' to avoid this. + document.all = undefined; + disableGetters(document, 'document', + ['alinkColor', 'all', 'bgColor', 'fgColor', 'linkColor', 'vlinkColor'], + null, true); +}, true); + +// Disable onunload, onbeforeunload. +disableSetters(window, 'window', ['onbeforeunload', 'onunload']); +var eventTargetAddEventListener = EventTarget.prototype.addEventListener; +EventTarget.prototype.addEventListener = function(type) { + var args = $Array.slice(arguments); + // Note: Force conversion to a string in order to catch any funny attempts + // to pass in something that evals to 'unload' but wouldn't === 'unload'. + var type = (args[0] += ''); + if (type === 'unload' || type === 'beforeunload') + generateDisabledMethodStub(type)(); + else + return $Function.apply(eventTargetAddEventListener, this, args); +}; diff --git a/chromium/extensions/renderer/resources/printer_provider_custom_bindings.js b/chromium/extensions/renderer/resources/printer_provider_custom_bindings.js new file mode 100644 index 00000000000..4bf6f12f760 --- /dev/null +++ b/chromium/extensions/renderer/resources/printer_provider_custom_bindings.js @@ -0,0 +1,123 @@ +// Copyright 2015 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. + +var binding = require('binding').Binding.create('printerProvider'); +var printerProviderInternal = require('binding').Binding.create( + 'printerProviderInternal').generate(); +var eventBindings = require('event_bindings'); +var blobNatives = requireNative('blob_natives'); + +var printerProviderSchema = + requireNative('schema_registry').GetSchema('printerProvider') +var utils = require('utils'); +var validate = require('schemaUtils').validate; + +// Custom bindings for chrome.printerProvider API. +// The bindings are used to implement callbacks for the API events. Internally +// each event is passed requestId argument used to identify the callback +// associated with the event. This argument is massaged out from the event +// arguments before dispatching the event to consumers. A callback is appended +// to the event arguments. The callback wraps an appropriate +// chrome.printerProviderInternal API function that is used to report the event +// result from the extension. The function is passed requestId and values +// provided by the extension. It validates that the values provided by the +// extension match chrome.printerProvider event callback schemas. It also +// ensures that a callback is run at most once. In case there is an exception +// during event dispatching, the chrome.printerProviderInternal function +// is called with a default error value. +// + +// Handles a chrome.printerProvider event as described in the file comment. +// |eventName|: The event name. +// |prepareArgsForDispatch|: Function called before dispatching the event to +// the extension. It's called with original event |args| list and callback +// that should be called when the |args| are ready for dispatch. The +// callbacks should report whether the argument preparation was successful. +// The function should not change the first argument, which contains the +// request id. +// |resultreporter|: The function that should be called to report event result. +// One of chrome.printerProviderInternal API functions. +function handleEvent(eventName, prepareArgsForDispatch, resultReporter) { + eventBindings.registerArgumentMassager( + 'printerProvider.' + eventName, + function(args, dispatch) { + var responded = false; + + // Validates that the result passed by the extension to the event + // callback matches the callback schema. Throws an exception in case of + // an error. + var validateResult = function(result) { + var eventSchema = + utils.lookup(printerProviderSchema.events, 'name', eventName); + var callbackSchema = + utils.lookup(eventSchema.parameters, 'type', 'function'); + + validate([result], callbackSchema.parameters); + }; + + // Function provided to the extension as the event callback argument. + // It makes sure that the event result hasn't previously been returned + // and that the provided result matches the callback schema. In case of + // an error it throws an exception. + var reportResult = function(result) { + if (responded) { + throw new Error( + 'Event callback must not be called more than once.'); + } + + var finalResult = null; + try { + validateResult(result); // throws on failure + finalResult = result; + } finally { + responded = true; + resultReporter(args[0] /* requestId */, finalResult); + } + }; + + prepareArgsForDispatch(args, function(success) { + if (!success) { + // Do not throw an exception since the extension should not yet be + // aware of the event. + resultReporter(args[0] /* requestId */, null); + return; + } + dispatch(args.slice(1).concat(reportResult)); + }); + }); +} + +// Sets up printJob.document property for a print request. +function createPrintRequestBlobArguments(args, callback) { + printerProviderInternal.getPrintData(args[0] /* requestId */, + function(blobInfo) { + if (chrome.runtime.lastError) { + callback(false); + return; + } + + // |args[1]| is printJob. + args[1].document = blobNatives.TakeBrowserProcessBlob( + blobInfo.blobUuid, blobInfo.type, blobInfo.size); + callback(true); + }); +} + +handleEvent('onGetPrintersRequested', + function(args, callback) { callback(true); }, + printerProviderInternal.reportPrinters); + +handleEvent('onGetCapabilityRequested', + function(args, callback) { callback(true); }, + printerProviderInternal.reportPrinterCapability); + +handleEvent('onPrintRequested', + createPrintRequestBlobArguments, + printerProviderInternal.reportPrintResult); + +handleEvent('onGetUsbPrinterInfoRequested', + function(args, callback) { callback(true); }, + printerProviderInternal.reportUsbPrinterInfo); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/runtime_custom_bindings.js b/chromium/extensions/renderer/resources/runtime_custom_bindings.js new file mode 100644 index 00000000000..1f829d4deb1 --- /dev/null +++ b/chromium/extensions/renderer/resources/runtime_custom_bindings.js @@ -0,0 +1,206 @@ +// 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. + +// Custom binding for the runtime API. + +var binding = require('binding').Binding.create('runtime'); + +var messaging = require('messaging'); +var runtimeNatives = requireNative('runtime'); +var process = requireNative('process'); +var forEach = require('utils').forEach; + +var backgroundPage = window; +var backgroundRequire = require; +var contextType = process.GetContextType(); +if (contextType == 'BLESSED_EXTENSION' || + contextType == 'UNBLESSED_EXTENSION') { + var manifest = runtimeNatives.GetManifest(); + if (manifest.app && manifest.app.background) { + // Get the background page if one exists. Otherwise, default to the current + // window. + backgroundPage = runtimeNatives.GetExtensionViews(-1, 'BACKGROUND')[0]; + if (backgroundPage) { + var GetModuleSystem = requireNative('v8_context').GetModuleSystem; + backgroundRequire = GetModuleSystem(backgroundPage).require; + } else { + backgroundPage = window; + } + } +} + +// For packaged apps, all windows use the bindFileEntryCallback from the +// background page so their FileEntry objects have the background page's context +// as their own. This allows them to be used from other windows (including the +// background page) after the original window is closed. +if (window == backgroundPage) { + var lastError = require('lastError'); + var fileSystemNatives = requireNative('file_system_natives'); + var GetIsolatedFileSystem = fileSystemNatives.GetIsolatedFileSystem; + var bindDirectoryEntryCallback = function(functionName, apiFunctions) { + apiFunctions.setCustomCallback(functionName, + function(name, request, callback, response) { + if (callback) { + if (!response) { + callback(); + return; + } + var fileSystemId = response.fileSystemId; + var baseName = response.baseName; + var fs = GetIsolatedFileSystem(fileSystemId); + + try { + fs.root.getDirectory(baseName, {}, callback, function(fileError) { + lastError.run('runtime.' + functionName, + 'Error getting Entry, code: ' + fileError.code, + request.stack, + callback); + }); + } catch (e) { + lastError.run('runtime.' + functionName, + 'Error: ' + e.stack, + request.stack, + callback); + } + } + }); + }; +} else { + // Force the runtime API to be loaded in the background page. Using + // backgroundPageModuleSystem.require('runtime') is insufficient as + // requireNative is only allowed while lazily loading an API. + backgroundPage.chrome.runtime; + var bindDirectoryEntryCallback = backgroundRequire( + 'runtime').bindDirectoryEntryCallback; +} + +binding.registerCustomHook(function(binding, id, contextType) { + var apiFunctions = binding.apiFunctions; + var runtime = binding.compiledApi; + + // + // Unprivileged APIs. + // + + if (id != '') + runtime.id = id; + + apiFunctions.setHandleRequest('getManifest', function() { + return runtimeNatives.GetManifest(); + }); + + apiFunctions.setHandleRequest('getURL', function(path) { + path = String(path); + if (!path.length || path[0] != '/') + path = '/' + path; + return 'chrome-extension://' + id + path; + }); + + var sendMessageUpdateArguments = messaging.sendMessageUpdateArguments; + apiFunctions.setUpdateArgumentsPreValidate('sendMessage', + $Function.bind(sendMessageUpdateArguments, null, 'sendMessage', + true /* hasOptionsArgument */)); + apiFunctions.setUpdateArgumentsPreValidate('sendNativeMessage', + $Function.bind(sendMessageUpdateArguments, null, 'sendNativeMessage', + false /* hasOptionsArgument */)); + + apiFunctions.setHandleRequest('sendMessage', + function(targetId, message, options, responseCallback) { + var connectOptions = {name: messaging.kMessageChannel}; + forEach(options, function(k, v) { + connectOptions[k] = v; + }); + var port = runtime.connect(targetId || runtime.id, connectOptions); + messaging.sendMessageImpl(port, message, responseCallback); + }); + + apiFunctions.setHandleRequest('sendNativeMessage', + function(targetId, message, responseCallback) { + var port = runtime.connectNative(targetId); + messaging.sendMessageImpl(port, message, responseCallback); + }); + + apiFunctions.setUpdateArgumentsPreValidate('connect', function() { + // Align missing (optional) function arguments with the arguments that + // schema validation is expecting, e.g. + // runtime.connect() -> runtime.connect(null, null) + // runtime.connect({}) -> runtime.connect(null, {}) + var nextArg = 0; + + // targetId (first argument) is optional. + var targetId = null; + if (typeof(arguments[nextArg]) == 'string') + targetId = arguments[nextArg++]; + + // connectInfo (second argument) is optional. + var connectInfo = null; + if (typeof(arguments[nextArg]) == 'object') + connectInfo = arguments[nextArg++]; + + if (nextArg != arguments.length) + throw new Error('Invalid arguments to connect.'); + return [targetId, connectInfo]; + }); + + apiFunctions.setUpdateArgumentsPreValidate('connectNative', + function(appName) { + if (typeof(appName) !== 'string') { + throw new Error('Invalid arguments to connectNative.'); + } + return [appName]; + }); + + apiFunctions.setHandleRequest('connect', function(targetId, connectInfo) { + if (!targetId) { + // runtime.id is only defined inside extensions. If we're in a webpage, + // the best we can do at this point is to fail. + if (!runtime.id) { + throw new Error('chrome.runtime.connect() called from a webpage must ' + + 'specify an Extension ID (string) for its first ' + + 'argument'); + } + targetId = runtime.id; + } + + var name = ''; + if (connectInfo && connectInfo.name) + name = connectInfo.name; + + var includeTlsChannelId = + !!(connectInfo && connectInfo.includeTlsChannelId); + + var portId = runtimeNatives.OpenChannelToExtension(targetId, name, + includeTlsChannelId); + if (portId >= 0) + return messaging.createPort(portId, name); + }); + + // + // Privileged APIs. + // + if (contextType != 'BLESSED_EXTENSION') + return; + + apiFunctions.setHandleRequest('connectNative', + function(nativeAppName) { + var portId = runtimeNatives.OpenChannelToNativeApp(runtime.id, + nativeAppName); + if (portId >= 0) + return messaging.createPort(portId, ''); + throw new Error('Error connecting to native app: ' + nativeAppName); + }); + + apiFunctions.setCustomCallback('getBackgroundPage', + function(name, request, callback, response) { + if (callback) { + var bg = runtimeNatives.GetExtensionViews(-1, 'BACKGROUND')[0] || null; + callback(bg); + } + }); + + bindDirectoryEntryCallback('getPackageDirectoryEntry', apiFunctions); +}); + +exports.$set('bindDirectoryEntryCallback', bindDirectoryEntryCallback); +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/schema_utils.js b/chromium/extensions/renderer/resources/schema_utils.js new file mode 100644 index 00000000000..b14d2eb548d --- /dev/null +++ b/chromium/extensions/renderer/resources/schema_utils.js @@ -0,0 +1,159 @@ +// 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. + +// Routines used to validate and normalize arguments. +// TODO(benwells): unit test this file. + +var JSONSchemaValidator = require('json_schema').JSONSchemaValidator; + +var schemaValidator = new JSONSchemaValidator(); + +// Validate arguments. +function validate(args, parameterSchemas) { + if (args.length > parameterSchemas.length) + throw new Error("Too many arguments."); + for (var i = 0; i < parameterSchemas.length; i++) { + if (i in args && args[i] !== null && args[i] !== undefined) { + schemaValidator.resetErrors(); + schemaValidator.validate(args[i], parameterSchemas[i]); + if (schemaValidator.errors.length == 0) + continue; + var message = "Invalid value for argument " + (i + 1) + ". "; + for (var i = 0, err; + err = schemaValidator.errors[i]; i++) { + if (err.path) { + message += "Property '" + err.path + "': "; + } + message += err.message; + message = message.substring(0, message.length - 1); + message += ", "; + } + message = message.substring(0, message.length - 2); + message += "."; + throw new Error(message); + } else if (!parameterSchemas[i].optional) { + throw new Error("Parameter " + (i + 1) + " (" + + parameterSchemas[i].name + ") is required."); + } + } +} + +// Generate all possible signatures for a given API function. +function getSignatures(parameterSchemas) { + if (parameterSchemas.length === 0) + return [[]]; + var signatures = []; + var remaining = getSignatures($Array.slice(parameterSchemas, 1)); + for (var i = 0; i < remaining.length; i++) + $Array.push(signatures, $Array.concat([parameterSchemas[0]], remaining[i])) + if (parameterSchemas[0].optional) + return $Array.concat(signatures, remaining); + return signatures; +}; + +// Return true if arguments match a given signature's schema. +function argumentsMatchSignature(args, candidateSignature) { + if (args.length != candidateSignature.length) + return false; + for (var i = 0; i < candidateSignature.length; i++) { + var argType = JSONSchemaValidator.getType(args[i]); + if (!schemaValidator.isValidSchemaType(argType, + candidateSignature[i])) + return false; + } + return true; +}; + +// Finds the function signature for the given arguments. +function resolveSignature(args, definedSignature) { + var candidateSignatures = getSignatures(definedSignature); + for (var i = 0; i < candidateSignatures.length; i++) { + if (argumentsMatchSignature(args, candidateSignatures[i])) + return candidateSignatures[i]; + } + return null; +}; + +// Returns a string representing the defined signature of the API function. +// Example return value for chrome.windows.getCurrent: +// "windows.getCurrent(optional object populate, function callback)" +function getParameterSignatureString(name, definedSignature) { + var getSchemaTypeString = function(schema) { + var schemaTypes = schemaValidator.getAllTypesForSchema(schema); + var typeName = schemaTypes.join(" or ") + " " + schema.name; + if (schema.optional) + return "optional " + typeName; + return typeName; + }; + var typeNames = $Array.map(definedSignature, getSchemaTypeString); + return name + "(" + typeNames.join(", ") + ")"; +}; + +// Returns a string representing a call to an API function. +// Example return value for call: chrome.windows.get(1, callback) is: +// "windows.get(int, function)" +function getArgumentSignatureString(name, args) { + var typeNames = $Array.map(args, JSONSchemaValidator.getType); + return name + "(" + typeNames.join(", ") + ")"; +}; + +// Finds the correct signature for the given arguments, then validates the +// arguments against that signature. Returns a 'normalized' arguments list +// where nulls are inserted where optional parameters were omitted. +// |args| is expected to be an array. +function normalizeArgumentsAndValidate(args, funDef) { + if (funDef.allowAmbiguousOptionalArguments) { + validate(args, funDef.definition.parameters); + return args; + } + var definedSignature = funDef.definition.parameters; + var resolvedSignature = resolveSignature(args, definedSignature); + if (!resolvedSignature) + throw new Error("Invocation of form " + + getArgumentSignatureString(funDef.name, args) + + " doesn't match definition " + + getParameterSignatureString(funDef.name, definedSignature)); + validate(args, resolvedSignature); + var normalizedArgs = []; + var ai = 0; + for (var si = 0; si < definedSignature.length; si++) { + // Handle integer -0 as 0. + if (JSONSchemaValidator.getType(args[ai]) === "integer" && args[ai] === 0) + args[ai] = 0; + if (definedSignature[si] === resolvedSignature[ai]) + $Array.push(normalizedArgs, args[ai++]); + else + $Array.push(normalizedArgs, null); + } + return normalizedArgs; +}; + +// Validates that a given schema for an API function is not ambiguous. +function isFunctionSignatureAmbiguous(functionDef) { + if (functionDef.allowAmbiguousOptionalArguments) + return false; + var signaturesAmbiguous = function(signature1, signature2) { + if (signature1.length != signature2.length) + return false; + for (var i = 0; i < signature1.length; i++) { + if (!schemaValidator.checkSchemaOverlap( + signature1[i], signature2[i])) + return false; + } + return true; + }; + var candidateSignatures = getSignatures(functionDef.parameters); + for (var i = 0; i < candidateSignatures.length; i++) { + for (var j = i + 1; j < candidateSignatures.length; j++) { + if (signaturesAmbiguous(candidateSignatures[i], candidateSignatures[j])) + return true; + } + } + return false; +}; + +exports.$set('isFunctionSignatureAmbiguous', isFunctionSignatureAmbiguous); +exports.$set('normalizeArgumentsAndValidate', normalizeArgumentsAndValidate); +exports.$set('schemaValidator', schemaValidator); +exports.$set('validate', validate); diff --git a/chromium/extensions/renderer/resources/send_request.js b/chromium/extensions/renderer/resources/send_request.js new file mode 100644 index 00000000000..9fed10e7f42 --- /dev/null +++ b/chromium/extensions/renderer/resources/send_request.js @@ -0,0 +1,151 @@ +// 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. + +var exceptionHandler = require('uncaught_exception_handler'); +var lastError = require('lastError'); +var logging = requireNative('logging'); +var natives = requireNative('sendRequest'); +var validate = require('schemaUtils').validate; + +// All outstanding requests from sendRequest(). +var requests = {}; + +// Used to prevent double Activity Logging for API calls that use both custom +// bindings and ExtensionFunctions (via sendRequest). +var calledSendRequest = false; + +// Runs a user-supplied callback safely. +function safeCallbackApply(name, request, callback, args) { + try { + $Function.apply(callback, request, args); + } catch (e) { + exceptionHandler.handle('Error in response to ' + name, e, request.stack); + } +} + +// Callback handling. +function handleResponse(requestId, name, success, responseList, error) { + // The chrome objects we will set lastError on. Really we should only be + // setting this on the callback's chrome object, but set on ours too since + // it's conceivable that something relies on that. + var callerChrome = chrome; + + try { + var request = requests[requestId]; + logging.DCHECK(request != null); + + // lastError needs to be set on the caller's chrome object no matter what, + // though chances are it's the same as ours (it will be different when + // calling API methods on other contexts). + if (request.callback) { + var global = natives.GetGlobal(request.callback); + callerChrome = global ? global.chrome : callerChrome; + } + + lastError.clear(chrome); + if (callerChrome !== chrome) + lastError.clear(callerChrome); + + if (!success) { + if (!error) + error = "Unknown error."; + lastError.set(name, error, request.stack, chrome); + if (callerChrome !== chrome) + lastError.set(name, error, request.stack, callerChrome); + } + + if (request.customCallback) { + safeCallbackApply(name, + request, + request.customCallback, + $Array.concat([name, request, request.callback], + responseList)); + } else if (request.callback) { + // Validate callback in debug only -- and only when the + // caller has provided a callback. Implementations of api + // calls may not return data if they observe the caller + // has not provided a callback. + if (logging.DCHECK_IS_ON() && !error) { + if (!request.callbackSchema.parameters) + throw new Error(name + ": no callback schema defined"); + validate(responseList, request.callbackSchema.parameters); + } + safeCallbackApply(name, request, request.callback, responseList); + } + + if (error && !lastError.hasAccessed(chrome)) { + // The native call caused an error, but the developer might not have + // checked runtime.lastError. + lastError.reportIfUnchecked(name, callerChrome, request.stack); + } + } finally { + delete requests[requestId]; + lastError.clear(chrome); + if (callerChrome !== chrome) + lastError.clear(callerChrome); + } +} + +function prepareRequest(args, argSchemas) { + var request = {}; + var argCount = args.length; + + // Look for callback param. + if (argSchemas.length > 0 && + argSchemas[argSchemas.length - 1].type == "function") { + request.callback = args[args.length - 1]; + request.callbackSchema = argSchemas[argSchemas.length - 1]; + --argCount; + } + + request.args = []; + for (var k = 0; k < argCount; k++) { + request.args[k] = args[k]; + } + + return request; +} + +// Send an API request and optionally register a callback. +// |optArgs| is an object with optional parameters as follows: +// - customCallback: a callback that should be called instead of the standard +// callback. +// - forIOThread: true if this function should be handled on the browser IO +// thread. +// - preserveNullInObjects: true if it is safe for null to be in objects. +// - stack: An optional string that contains the stack trace, to be displayed +// to the user if an error occurs. +function sendRequest(functionName, args, argSchemas, optArgs) { + calledSendRequest = true; + if (!optArgs) + optArgs = {}; + var request = prepareRequest(args, argSchemas); + request.stack = optArgs.stack || exceptionHandler.getExtensionStackTrace(); + if (optArgs.customCallback) { + request.customCallback = optArgs.customCallback; + } + + var hasCallback = request.callback || optArgs.customCallback; + var requestId = + natives.StartRequest(functionName, request.args, hasCallback, + optArgs.forIOThread, optArgs.preserveNullInObjects); + request.id = requestId; + requests[requestId] = request; +} + +function getCalledSendRequest() { + return calledSendRequest; +} + +function clearCalledSendRequest() { + calledSendRequest = false; +} + +exports.$set('sendRequest', sendRequest); +exports.$set('getCalledSendRequest', getCalledSendRequest); +exports.$set('clearCalledSendRequest', clearCalledSendRequest); +exports.$set('safeCallbackApply', safeCallbackApply); + +// Called by C++. +exports.$set('handleResponse', handleResponse); diff --git a/chromium/extensions/renderer/resources/serial_custom_bindings.js b/chromium/extensions/renderer/resources/serial_custom_bindings.js new file mode 100644 index 00000000000..d77631cf645 --- /dev/null +++ b/chromium/extensions/renderer/resources/serial_custom_bindings.js @@ -0,0 +1,117 @@ +// 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. + +/** + * Custom bindings for the Serial API. + * + * The bindings are implemented by asynchronously delegating to the + * serial_service module. The functions that apply to a particular connection + * are delegated to the appropriate method on the Connection object specified by + * the ID parameter. + */ + +var binding = require('binding').Binding.create('serial'); +var context = requireNative('v8_context'); +var eventBindings = require('event_bindings'); +var utils = require('utils'); + +var serialServicePromise = function() { + // getBackgroundPage is not available in unit tests so fall back to the + // current page's serial_service module. + if (!chrome.runtime.getBackgroundPage) + return requireAsync('serial_service'); + + // Load the serial_service module from the background page if one exists. This + // is necessary for serial connections created in one window to be usable + // after that window is closed. This is necessary because the Mojo async + // waiter only functions while the v8 context remains. + return utils.promise(chrome.runtime.getBackgroundPage).then(function(bgPage) { + return context.GetModuleSystem(bgPage).requireAsync('serial_service'); + }).catch(function() { + return requireAsync('serial_service'); + }); +}(); + +function forwardToConnection(methodName) { + return function(connectionId) { + var args = $Array.slice(arguments, 1); + return serialServicePromise.then(function(serialService) { + return serialService.getConnection(connectionId); + }).then(function(connection) { + return $Function.apply(connection[methodName], connection, args); + }); + }; +} + +function addEventListeners(connection, id) { + connection.onData = function(data) { + eventBindings.dispatchEvent( + 'serial.onReceive', [{connectionId: id, data: data}]); + }; + connection.onError = function(error) { + eventBindings.dispatchEvent( + 'serial.onReceiveError', [{connectionId: id, error: error}]); + }; +} + +serialServicePromise.then(function(serialService) { + return serialService.getConnections().then(function(connections) { + for (var entry of connections) { + var connection = entry[1]; + addEventListeners(connection, entry[0]); + connection.resumeReceives(); + }; + }); +}); + +binding.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + apiFunctions.setHandleRequestWithPromise('getDevices', function() { + return serialServicePromise.then(function(serialService) { + return serialService.getDevices(); + }); + }); + + apiFunctions.setHandleRequestWithPromise('connect', function(path, options) { + return serialServicePromise.then(function(serialService) { + return serialService.createConnection(path, options); + }).then(function(result) { + addEventListeners(result.connection, result.info.connectionId); + return result.info; + }).catch (function(e) { + throw new Error('Failed to connect to the port.'); + }); + }); + + apiFunctions.setHandleRequestWithPromise( + 'disconnect', forwardToConnection('close')); + apiFunctions.setHandleRequestWithPromise( + 'getInfo', forwardToConnection('getInfo')); + apiFunctions.setHandleRequestWithPromise( + 'update', forwardToConnection('setOptions')); + apiFunctions.setHandleRequestWithPromise( + 'getControlSignals', forwardToConnection('getControlSignals')); + apiFunctions.setHandleRequestWithPromise( + 'setControlSignals', forwardToConnection('setControlSignals')); + apiFunctions.setHandleRequestWithPromise( + 'flush', forwardToConnection('flush')); + apiFunctions.setHandleRequestWithPromise( + 'setPaused', forwardToConnection('setPaused')); + apiFunctions.setHandleRequestWithPromise( + 'send', forwardToConnection('send')); + + apiFunctions.setHandleRequestWithPromise('getConnections', function() { + return serialServicePromise.then(function(serialService) { + return serialService.getConnections(); + }).then(function(connections) { + var promises = []; + for (var connection of connections.values()) { + promises.push(connection.getInfo()); + } + return Promise.all(promises); + }); + }); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/serial_service.js b/chromium/extensions/renderer/resources/serial_service.js new file mode 100644 index 00000000000..26b990b227e --- /dev/null +++ b/chromium/extensions/renderer/resources/serial_service.js @@ -0,0 +1,554 @@ +// 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. + +define('serial_service', [ + 'content/public/renderer/frame_service_registry', + 'data_receiver', + 'data_sender', + 'device/serial/serial.mojom', + 'device/serial/serial_serialization.mojom', + 'mojo/public/js/core', + 'mojo/public/js/router', + 'stash_client', +], function(serviceProvider, + dataReceiver, + dataSender, + serialMojom, + serialization, + core, + routerModule, + stashClient) { + /** + * A Javascript client for the serial service and connection Mojo services. + * + * This provides a thick client around the Mojo services, exposing a JS-style + * interface to serial connections and information about serial devices. This + * converts parameters and result between the Apps serial API types and the + * Mojo types. + */ + + var service = new serialMojom.SerialService.proxyClass( + new routerModule.Router( + serviceProvider.connectToService(serialMojom.SerialService.name))); + + function getDevices() { + return service.getDevices().then(function(response) { + return $Array.map(response.devices, function(device) { + var result = {path: device.path}; + if (device.has_vendor_id) + result.vendorId = device.vendor_id; + if (device.has_product_id) + result.productId = device.product_id; + if (device.display_name) + result.displayName = device.display_name; + return result; + }); + }); + } + + var DATA_BITS_TO_MOJO = { + undefined: serialMojom.DataBits.NONE, + 'seven': serialMojom.DataBits.SEVEN, + 'eight': serialMojom.DataBits.EIGHT, + }; + var STOP_BITS_TO_MOJO = { + undefined: serialMojom.StopBits.NONE, + 'one': serialMojom.StopBits.ONE, + 'two': serialMojom.StopBits.TWO, + }; + var PARITY_BIT_TO_MOJO = { + undefined: serialMojom.ParityBit.NONE, + 'no': serialMojom.ParityBit.NO, + 'odd': serialMojom.ParityBit.ODD, + 'even': serialMojom.ParityBit.EVEN, + }; + var SEND_ERROR_TO_MOJO = { + undefined: serialMojom.SendError.NONE, + 'disconnected': serialMojom.SendError.DISCONNECTED, + 'pending': serialMojom.SendError.PENDING, + 'timeout': serialMojom.SendError.TIMEOUT, + 'system_error': serialMojom.SendError.SYSTEM_ERROR, + }; + var RECEIVE_ERROR_TO_MOJO = { + undefined: serialMojom.ReceiveError.NONE, + 'disconnected': serialMojom.ReceiveError.DISCONNECTED, + 'device_lost': serialMojom.ReceiveError.DEVICE_LOST, + 'timeout': serialMojom.ReceiveError.TIMEOUT, + 'break': serialMojom.ReceiveError.BREAK, + 'frame_error': serialMojom.ReceiveError.FRAME_ERROR, + 'overrun': serialMojom.ReceiveError.OVERRUN, + 'buffer_overflow': serialMojom.ReceiveError.BUFFER_OVERFLOW, + 'parity_error': serialMojom.ReceiveError.PARITY_ERROR, + 'system_error': serialMojom.ReceiveError.SYSTEM_ERROR, + }; + + function invertMap(input) { + var output = {}; + for (var key in input) { + if (key == 'undefined') + output[input[key]] = undefined; + else + output[input[key]] = key; + } + return output; + } + var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO); + var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO); + var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO); + var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO); + var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO); + + function getServiceOptions(options) { + var out = {}; + if (options.dataBits) + out.data_bits = DATA_BITS_TO_MOJO[options.dataBits]; + if (options.stopBits) + out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits]; + if (options.parityBit) + out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit]; + if ('ctsFlowControl' in options) { + out.has_cts_flow_control = true; + out.cts_flow_control = options.ctsFlowControl; + } + if ('bitrate' in options) + out.bitrate = options.bitrate; + return out; + } + + function convertServiceInfo(result) { + if (!result.info) + throw new Error('Failed to get ConnectionInfo.'); + return { + ctsFlowControl: !!result.info.cts_flow_control, + bitrate: result.info.bitrate || undefined, + dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits], + stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits], + parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit], + }; + } + + // Update client-side options |clientOptions| from the user-provided + // |options|. + function updateClientOptions(clientOptions, options) { + if ('name' in options) + clientOptions.name = options.name; + if ('receiveTimeout' in options) + clientOptions.receiveTimeout = options.receiveTimeout; + if ('sendTimeout' in options) + clientOptions.sendTimeout = options.sendTimeout; + if ('bufferSize' in options) + clientOptions.bufferSize = options.bufferSize; + if ('persistent' in options) + clientOptions.persistent = options.persistent; + }; + + function Connection(connection, router, receivePipe, receiveClientPipe, + sendPipe, id, options) { + var state = new serialization.ConnectionState(); + state.connectionId = id; + updateClientOptions(state, options); + var receiver = new dataReceiver.DataReceiver( + receivePipe, receiveClientPipe, state.bufferSize, + serialMojom.ReceiveError.DISCONNECTED); + var sender = new dataSender.DataSender(sendPipe, state.bufferSize, + serialMojom.SendError.DISCONNECTED); + this.init_(state, + connection, + router, + receiver, + sender, + null, + serialMojom.ReceiveError.NONE); + connections_.set(id, this); + this.startReceive_(); + } + + // Initializes this Connection from the provided args. + Connection.prototype.init_ = function(state, + connection, + router, + receiver, + sender, + queuedReceiveData, + queuedReceiveError) { + this.state_ = state; + + // queuedReceiveData_ or queuedReceiveError_ will store the receive result + // or error, respectively, if a receive completes or fails while this + // connection is paused. At most one of the the two may be non-null: a + // receive completed while paused will only set one of them, no further + // receives will be performed while paused and a queued result is dispatched + // before any further receives are initiated when unpausing. + if (queuedReceiveError != serialMojom.ReceiveError.NONE) + this.queuedReceiveError_ = {error: queuedReceiveError}; + if (queuedReceiveData) { + this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length); + new Int8Array(this.queuedReceiveData_).set(queuedReceiveData); + } + this.router_ = router; + this.remoteConnection_ = connection; + this.receivePipe_ = receiver; + this.sendPipe_ = sender; + this.sendInProgress_ = false; + }; + + Connection.create = function(path, options) { + options = options || {}; + var serviceOptions = getServiceOptions(options); + var pipe = core.createMessagePipe(); + var sendPipe = core.createMessagePipe(); + var receivePipe = core.createMessagePipe(); + var receivePipeClient = core.createMessagePipe(); + service.connect(path, + serviceOptions, + pipe.handle0, + sendPipe.handle0, + receivePipe.handle0, + receivePipeClient.handle0); + var router = new routerModule.Router(pipe.handle1); + var connection = new serialMojom.Connection.proxyClass(router); + return connection.getInfo().then(convertServiceInfo).then(function(info) { + return Promise.all([info, allocateConnectionId()]); + }).catch(function(e) { + router.close(); + core.close(sendPipe.handle1); + core.close(receivePipe.handle1); + core.close(receivePipeClient.handle1); + throw e; + }).then(function(results) { + var info = results[0]; + var id = results[1]; + var serialConnectionClient = new Connection(connection, + router, + receivePipe.handle1, + receivePipeClient.handle1, + sendPipe.handle1, + id, + options); + var clientInfo = serialConnectionClient.getClientInfo_(); + for (var key in clientInfo) { + info[key] = clientInfo[key]; + } + return { + connection: serialConnectionClient, + info: info, + }; + }); + }; + + Connection.prototype.close = function() { + this.router_.close(); + this.receivePipe_.close(); + this.sendPipe_.close(); + clearTimeout(this.receiveTimeoutId_); + clearTimeout(this.sendTimeoutId_); + connections_.delete(this.state_.connectionId); + return true; + }; + + Connection.prototype.getClientInfo_ = function() { + return { + connectionId: this.state_.connectionId, + paused: this.state_.paused, + persistent: this.state_.persistent, + name: this.state_.name, + receiveTimeout: this.state_.receiveTimeout, + sendTimeout: this.state_.sendTimeout, + bufferSize: this.state_.bufferSize, + }; + }; + + Connection.prototype.getInfo = function() { + var info = this.getClientInfo_(); + return this.remoteConnection_.getInfo().then(convertServiceInfo).then( + function(result) { + for (var key in result) { + info[key] = result[key]; + } + return info; + }).catch(function() { + return info; + }); + }; + + Connection.prototype.setOptions = function(options) { + updateClientOptions(this.state_, options); + var serviceOptions = getServiceOptions(options); + if ($Object.keys(serviceOptions).length == 0) + return true; + return this.remoteConnection_.setOptions(serviceOptions).then( + function(result) { + return !!result.success; + }).catch(function() { + return false; + }); + }; + + Connection.prototype.getControlSignals = function() { + return this.remoteConnection_.getControlSignals().then(function(result) { + if (!result.signals) + throw new Error('Failed to get control signals.'); + var signals = result.signals; + return { + dcd: !!signals.dcd, + cts: !!signals.cts, + ri: !!signals.ri, + dsr: !!signals.dsr, + }; + }); + }; + + Connection.prototype.setControlSignals = function(signals) { + var controlSignals = {}; + if ('dtr' in signals) { + controlSignals.has_dtr = true; + controlSignals.dtr = signals.dtr; + } + if ('rts' in signals) { + controlSignals.has_rts = true; + controlSignals.rts = signals.rts; + } + return this.remoteConnection_.setControlSignals(controlSignals).then( + function(result) { + return !!result.success; + }); + }; + + Connection.prototype.flush = function() { + return this.remoteConnection_.flush().then(function(result) { + return !!result.success; + }); + }; + + Connection.prototype.setPaused = function(paused) { + this.state_.paused = paused; + if (paused) { + clearTimeout(this.receiveTimeoutId_); + this.receiveTimeoutId_ = null; + } else if (!this.receiveInProgress_) { + this.startReceive_(); + } + }; + + Connection.prototype.send = function(data) { + if (this.sendInProgress_) + return Promise.resolve({bytesSent: 0, error: 'pending'}); + + if (this.state_.sendTimeout) { + this.sendTimeoutId_ = setTimeout(function() { + this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT); + }.bind(this), this.state_.sendTimeout); + } + this.sendInProgress_ = true; + return this.sendPipe_.send(data).then(function(bytesSent) { + return {bytesSent: bytesSent}; + }).catch(function(e) { + return { + bytesSent: e.bytesSent, + error: SEND_ERROR_FROM_MOJO[e.error], + }; + }).then(function(result) { + if (this.sendTimeoutId_) + clearTimeout(this.sendTimeoutId_); + this.sendTimeoutId_ = null; + this.sendInProgress_ = false; + return result; + }.bind(this)); + }; + + Connection.prototype.startReceive_ = function() { + this.receiveInProgress_ = true; + var receivePromise = null; + // If we have a queued receive result, dispatch it immediately instead of + // starting a new receive. + if (this.queuedReceiveData_) { + receivePromise = Promise.resolve(this.queuedReceiveData_); + this.queuedReceiveData_ = null; + } else if (this.queuedReceiveError_) { + receivePromise = Promise.reject(this.queuedReceiveError_); + this.queuedReceiveError_ = null; + } else { + receivePromise = this.receivePipe_.receive(); + } + receivePromise.then(this.onDataReceived_.bind(this)).catch( + this.onReceiveError_.bind(this)); + this.startReceiveTimeoutTimer_(); + }; + + Connection.prototype.onDataReceived_ = function(data) { + this.startReceiveTimeoutTimer_(); + this.receiveInProgress_ = false; + if (this.state_.paused) { + this.queuedReceiveData_ = data; + return; + } + if (this.onData) { + this.onData(data); + } + if (!this.state_.paused) { + this.startReceive_(); + } + }; + + Connection.prototype.onReceiveError_ = function(e) { + clearTimeout(this.receiveTimeoutId_); + this.receiveInProgress_ = false; + if (this.state_.paused) { + this.queuedReceiveError_ = e; + return; + } + var error = e.error; + this.state_.paused = true; + if (this.onError) + this.onError(RECEIVE_ERROR_FROM_MOJO[error]); + }; + + Connection.prototype.startReceiveTimeoutTimer_ = function() { + clearTimeout(this.receiveTimeoutId_); + if (this.state_.receiveTimeout && !this.state_.paused) { + this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this), + this.state_.receiveTimeout); + } + }; + + Connection.prototype.onReceiveTimeout_ = function() { + if (this.onError) + this.onError('timeout'); + this.startReceiveTimeoutTimer_(); + }; + + Connection.prototype.serialize = function() { + connections_.delete(this.state_.connectionId); + this.onData = null; + this.onError = null; + var handle = this.router_.connector_.handle_; + this.router_.connector_.handle_ = null; + this.router_.close(); + clearTimeout(this.receiveTimeoutId_); + clearTimeout(this.sendTimeoutId_); + + // Serializing receivePipe_ will cancel an in-progress receive, which would + // pause the connection, so save it ahead of time. + var paused = this.state_.paused; + return Promise.all([ + this.receivePipe_.serialize(), + this.sendPipe_.serialize(), + ]).then(function(serializedComponents) { + var queuedReceiveError = serialMojom.ReceiveError.NONE; + if (this.queuedReceiveError_) + queuedReceiveError = this.queuedReceiveError_.error; + this.state_.paused = paused; + var serialized = new serialization.SerializedConnection(); + serialized.state = this.state_; + serialized.queuedReceiveError = queuedReceiveError; + serialized.queuedReceiveData = + this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) : + null; + serialized.connection = handle; + serialized.receiver = serializedComponents[0]; + serialized.sender = serializedComponents[1]; + return serialized; + }.bind(this)); + }; + + Connection.deserialize = function(serialized) { + var serialConnection = $Object.create(Connection.prototype); + var router = new routerModule.Router(serialized.connection); + var connection = new serialMojom.Connection.proxyClass(router); + var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver); + var sender = dataSender.DataSender.deserialize(serialized.sender); + + // Ensure that paused and persistent are booleans. + serialized.state.paused = !!serialized.state.paused; + serialized.state.persistent = !!serialized.state.persistent; + serialConnection.init_(serialized.state, + connection, + router, + receiver, + sender, + serialized.queuedReceiveData, + serialized.queuedReceiveError); + serialConnection.awaitingResume_ = true; + var connectionId = serialized.state.connectionId; + connections_.set(connectionId, serialConnection); + if (connectionId >= nextConnectionId_) + nextConnectionId_ = connectionId + 1; + return serialConnection; + }; + + // Resume receives on a deserialized connection. + Connection.prototype.resumeReceives = function() { + if (!this.awaitingResume_) + return; + this.awaitingResume_ = false; + if (!this.state_.paused) + this.startReceive_(); + }; + + // All accesses to connections_ and nextConnectionId_ other than those + // involved in deserialization should ensure that + // connectionDeserializationComplete_ has resolved first. + var connectionDeserializationComplete_ = stashClient.retrieve( + 'serial', serialization.SerializedConnection).then(function(decoded) { + if (!decoded) + return; + return Promise.all($Array.map(decoded, Connection.deserialize)); + }); + + // The map of connection ID to connection object. + var connections_ = new Map(); + + // The next connection ID to be allocated. + var nextConnectionId_ = 0; + + function getConnections() { + return connectionDeserializationComplete_.then(function() { + return new Map(connections_); + }); + } + + function getConnection(id) { + return getConnections().then(function(connections) { + if (!connections.has(id)) + throw new Error('Serial connection not found.'); + return connections.get(id); + }); + } + + function allocateConnectionId() { + return connectionDeserializationComplete_.then(function() { + return nextConnectionId_++; + }); + } + + stashClient.registerClient( + 'serial', serialization.SerializedConnection, function() { + return connectionDeserializationComplete_.then(function() { + var clientPromises = []; + for (var connection of connections_.values()) { + if (connection.state_.persistent) + clientPromises.push(connection.serialize()); + else + connection.close(); + } + return Promise.all($Array.map(clientPromises, function(promise) { + return promise.then(function(serialization) { + return { + serialization: serialization, + monitorHandles: !serialization.paused, + }; + }); + })); + }); + }); + + return { + getDevices: getDevices, + createConnection: Connection.create, + getConnection: getConnection, + getConnections: getConnections, + // For testing. + Connection: Connection, + }; +}); diff --git a/chromium/extensions/renderer/resources/service_worker_bindings.js b/chromium/extensions/renderer/resources/service_worker_bindings.js new file mode 100644 index 00000000000..532f51ddd2b --- /dev/null +++ b/chromium/extensions/renderer/resources/service_worker_bindings.js @@ -0,0 +1,67 @@ +// Copyright 2015 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. + +// This function is returned to DidInitializeServiceWorkerContextOnWorkerThread +// then executed, passing in dependencies as function arguments. +// +// |backgroundUrl| is the URL of the extension's background page. +// |wakeEventPage| is a function that wakes up the current extension's event +// page, then runs its callback on completion or failure. +// |logging| is an object equivalent to a subset of base/debug/logging.h, with +// CHECK/DCHECK/etc. +(function(backgroundUrl, wakeEventPage, logging) { + 'use strict'; + self.chrome = self.chrome || {}; + self.chrome.runtime = self.chrome.runtime || {}; + + // Returns a Promise that resolves to the background page's client, or null + // if there is no background client. + function findBackgroundClient() { + return self.clients.matchAll({ + includeUncontrolled: true, + type: 'window' + }).then(function(clients) { + return clients.find(function(client) { + return client.url == backgroundUrl; + }); + }); + } + + // Returns a Promise wrapper around wakeEventPage, that resolves on success, + // or rejects on failure. + function makeWakeEventPagePromise() { + return new Promise(function(resolve, reject) { + wakeEventPage(function(success) { + if (success) + resolve(); + else + reject('Failed to start background client "' + backgroundUrl + '"'); + }); + }); + } + + // The chrome.runtime.getBackgroundClient function is documented in + // runtime.json. It returns a Promise that resolves to the background page's + // client, or is rejected if there is no background client or if the + // background client failed to wake. + self.chrome.runtime.getBackgroundClient = function() { + return findBackgroundClient().then(function(client) { + if (client) { + // Background client is already awake, or it was persistent. + return client; + } + + // Event page needs to be woken. + return makeWakeEventPagePromise().then(function() { + return findBackgroundClient(); + }).then(function(client) { + if (!client) { + return Promise.reject( + 'Background client "' + backgroundUrl + '" not found'); + } + return client; + }); + }); + }; +}); diff --git a/chromium/extensions/renderer/resources/set_icon.js b/chromium/extensions/renderer/resources/set_icon.js new file mode 100644 index 00000000000..b0ed2b5b5fa --- /dev/null +++ b/chromium/extensions/renderer/resources/set_icon.js @@ -0,0 +1,112 @@ +// 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. + +var SetIconCommon = requireNative('setIcon').SetIconCommon; +var sendRequest = require('sendRequest').sendRequest; + +function loadImagePath(path, callback) { + var img = new Image(); + img.onerror = function() { + console.error('Could not load action icon \'' + path + '\'.'); + }; + img.onload = function() { + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + var canvas_context = canvas.getContext('2d'); + canvas_context.clearRect(0, 0, canvas.width, canvas.height); + canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); + var imageData = canvas_context.getImageData(0, 0, canvas.width, + canvas.height); + callback(imageData); + }; + img.src = path; +} + +function smellsLikeImageData(imageData) { + // See if this object at least looks like an ImageData element. + // Unfortunately, we cannot use instanceof because the ImageData + // constructor is not public. + // + // We do this manually instead of using JSONSchema to avoid having these + // properties show up in the doc. + return (typeof imageData == 'object') && ('width' in imageData) && + ('height' in imageData) && ('data' in imageData); +} + +function verifyImageData(imageData) { + if (!smellsLikeImageData(imageData)) { + throw new Error( + 'The imageData property must contain an ImageData object or' + + ' dictionary of ImageData objects.'); + } +} + +/** + * Normalizes |details| to a format suitable for sending to the browser, + * for example converting ImageData to a binary representation. + * + * @param {ImageDetails} details + * The ImageDetails passed into an extension action-style API. + * @param {Function} callback + * The callback function to pass processed imageData back to. Note that this + * callback may be called reentrantly. + */ +function setIcon(details, callback) { + // Note that iconIndex is actually deprecated, and only available to the + // pageAction API. + // TODO(kalman): Investigate whether this is for the pageActions API, and if + // so, delete it. + if ('iconIndex' in details) { + callback(details); + return; + } + + if ('imageData' in details) { + if (smellsLikeImageData(details.imageData)) { + var imageData = details.imageData; + details.imageData = {}; + details.imageData[imageData.width.toString()] = imageData; + } else if (typeof details.imageData == 'object' && + Object.getOwnPropertyNames(details.imageData).length !== 0) { + for (var sizeKey in details.imageData) { + verifyImageData(details.imageData[sizeKey]); + } + } else { + verifyImageData(false); + } + + callback(SetIconCommon(details)); + return; + } + + if ('path' in details) { + if (typeof details.path == 'object') { + details.imageData = {}; + var detailKeyCount = 0; + for (var iconSize in details.path) { + ++detailKeyCount; + loadImagePath(details.path[iconSize], function(size, imageData) { + details.imageData[size] = imageData; + if (--detailKeyCount == 0) + callback(SetIconCommon(details)); + }.bind(null, iconSize)); + } + if (detailKeyCount == 0) + throw new Error('The path property must not be empty.'); + } else if (typeof details.path == 'string') { + details.imageData = {}; + loadImagePath(details.path, function(imageData) { + details.imageData[imageData.width.toString()] = imageData; + delete details.path; + callback(SetIconCommon(details)); + }); + } + return; + } + throw new Error('Either the path or imageData property must be specified.'); +} + +exports.$set('setIcon', setIcon); diff --git a/chromium/extensions/renderer/resources/stash_client.js b/chromium/extensions/renderer/resources/stash_client.js new file mode 100644 index 00000000000..240a676463d --- /dev/null +++ b/chromium/extensions/renderer/resources/stash_client.js @@ -0,0 +1,168 @@ +// Copyright 2015 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. + +define('stash_client', [ + 'async_waiter', + 'content/public/renderer/frame_service_registry', + 'extensions/common/mojo/stash.mojom', + 'mojo/public/js/buffer', + 'mojo/public/js/codec', + 'mojo/public/js/core', + 'mojo/public/js/router', +], function(asyncWaiter, serviceProvider, stashMojom, bufferModule, + codec, core, routerModule) { + /** + * @module stash_client + */ + + var service = new stashMojom.StashService.proxyClass(new routerModule.Router( + serviceProvider.connectToService(stashMojom.StashService.name))); + + /** + * A callback invoked to obtain objects to stash from a particular client. + * @callback module:stash_client.StashCallback + * @return {!Promise<!Array<!Object>>|!Array<!Object>} An array of objects to + * stash or a promise that will resolve to an array of objects to stash. + * The exact type of each object should match the type passed alongside + * this callback. + */ + + /** + * A stash client registration. + * @constructor + * @private + * @alias module:stash_client~Registration + */ + function Registration(id, type, callback) { + /** + * The client id. + * @type {string} + * @private + */ + this.id_ = id; + + /** + * The type of the objects to be stashed. + * @type {!Object} + * @private + */ + this.type_ = type; + + /** + * The callback to invoke to obtain the objects to stash. + * @type {module:stash_client.StashCallback} + * @private + */ + this.callback_ = callback; + } + + /** + * Serializes and returns this client's stashable objects. + * @return + * {!Promise<!Array<module:extensions/common/stash.mojom.StashedObject>>} The + * serialized stashed objects. + */ + Registration.prototype.serialize = function() { + return Promise.resolve(this.callback_()).then($Function.bind( + function(stashedObjects) { + if (!stashedObjects) + return []; + return $Array.map(stashedObjects, function(stashed) { + var builder = new codec.MessageBuilder( + 0, codec.align(this.type_.encodedSize)); + builder.encodeStruct(this.type_, stashed.serialization); + var encoded = builder.finish(); + return new stashMojom.StashedObject({ + id: this.id_, + data: new Uint8Array(encoded.buffer.arrayBuffer), + stashed_handles: encoded.handles, + monitor_handles: stashed.monitorHandles, + }); + }, this); + }, this)).catch(function(e) { return []; }); + }; + + /** + * The registered stash clients. + * @type {!Array<!Registration>} + */ + var clients = []; + + /** + * Registers a client to provide objects to stash during shut-down. + * + * @param {string} id The id of the client. This can be passed to retrieve to + * retrieve the stashed objects. + * @param {!Object} type The type of the objects that callback will return. + * @param {module:stash_client.StashCallback} callback The callback that + * returns objects to stash. + * @alias module:stash_client.registerClient + */ + function registerClient(id, type, callback) { + clients.push(new Registration(id, type, callback)); + } + + var retrievedStash = service.retrieveStash().then(function(result) { + if (!result || !result.stash) + return {}; + var stashById = {}; + $Array.forEach(result.stash, function(stashed) { + if (!stashById[stashed.id]) + stashById[stashed.id] = []; + stashById[stashed.id].push(stashed); + }); + return stashById; + }, function() { + // If the stash is not available, act as if the stash was empty. + return {}; + }); + + /** + * Retrieves the objects that were stashed with the given |id|, deserializing + * them into structs with type |type|. + * + * @param {string} id The id of the client. This should be unique to this + * client and should be passed as the id to registerClient(). + * @param {!Object} type The mojo struct type that was serialized into the + * each stashed object. + * @return {!Promise<!Array<!Object>>} The stashed objects. The exact type of + * each object is that of the |type| parameter. + * @alias module:stash_client.retrieve + */ + function retrieve(id, type) { + return retrievedStash.then(function(stash) { + var stashedObjects = stash[id]; + if (!stashedObjects) + return Promise.resolve([]); + + return Promise.all($Array.map(stashedObjects, function(stashed) { + var encodedData = new ArrayBuffer(stashed.data.length); + new Uint8Array(encodedData).set(stashed.data); + var reader = new codec.MessageReader(new codec.Message( + new bufferModule.Buffer(encodedData), stashed.stashed_handles)); + var decoded = reader.decodeStruct(type); + return decoded; + })); + }); + } + + function saveStashForTesting() { + Promise.all($Array.map(clients, function(client) { + return client.serialize(); + })).then(function(stashedObjects) { + var flattenedObjectsToStash = []; + $Array.forEach(stashedObjects, function(stashedObjects) { + flattenedObjectsToStash = + $Array.concat(flattenedObjectsToStash, stashedObjects); + }); + service.addToStash(flattenedObjectsToStash); + }); + } + + return { + registerClient: registerClient, + retrieve: retrieve, + saveStashForTesting: saveStashForTesting, + }; +}); diff --git a/chromium/extensions/renderer/resources/storage_area.js b/chromium/extensions/renderer/resources/storage_area.js new file mode 100644 index 00000000000..4ff6bbdb6d0 --- /dev/null +++ b/chromium/extensions/renderer/resources/storage_area.js @@ -0,0 +1,40 @@ +// 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. + +var normalizeArgumentsAndValidate = + require('schemaUtils').normalizeArgumentsAndValidate +var sendRequest = require('sendRequest').sendRequest; + +function extendSchema(schema) { + var extendedSchema = $Array.slice(schema); + $Array.unshift(extendedSchema, {'type': 'string'}); + return extendedSchema; +} + +function StorageArea(namespace, schema) { + // Binds an API function for a namespace to its browser-side call, e.g. + // storage.sync.get('foo') -> (binds to) -> + // storage.get('sync', 'foo'). + // + // TODO(kalman): Put as a method on CustombindingObject and re-use (or + // even generate) for other APIs that need to do this. Same for other + // callers of registerCustomType(). + var self = this; + function bindApiFunction(functionName) { + self[functionName] = function() { + var funSchema = this.functionSchemas[functionName]; + var args = $Array.slice(arguments); + args = normalizeArgumentsAndValidate(args, funSchema); + return sendRequest( + 'storage.' + functionName, + $Array.concat([namespace], args), + extendSchema(funSchema.definition.parameters), + {preserveNullInObjects: true}); + }; + } + var apiFunctions = ['get', 'set', 'remove', 'clear', 'getBytesInUse']; + $Array.forEach(apiFunctions, bindApiFunction); +} + +exports.$set('StorageArea', StorageArea); diff --git a/chromium/extensions/renderer/resources/test_custom_bindings.js b/chromium/extensions/renderer/resources/test_custom_bindings.js new file mode 100644 index 00000000000..6538efac1e3 --- /dev/null +++ b/chromium/extensions/renderer/resources/test_custom_bindings.js @@ -0,0 +1,364 @@ +// 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. + +// test_custom_bindings.js +// mini-framework for ExtensionApiTest browser tests + +var binding = require('binding').Binding.create('test'); + +var environmentSpecificBindings = require('test_environment_specific_bindings'); +var GetExtensionAPIDefinitionsForTest = + requireNative('apiDefinitions').GetExtensionAPIDefinitionsForTest; +var GetAPIFeatures = requireNative('test_features').GetAPIFeatures; +var natives = requireNative('test_native_handler'); +var uncaughtExceptionHandler = require('uncaught_exception_handler'); +var userGestures = requireNative('user_gestures'); + +var RunWithNativesEnabled = requireNative('v8_context').RunWithNativesEnabled; +var GetModuleSystem = requireNative('v8_context').GetModuleSystem; + +binding.registerCustomHook(function(api) { + var chromeTest = api.compiledApi; + var apiFunctions = api.apiFunctions; + + chromeTest.tests = chromeTest.tests || []; + + var currentTest = null; + var lastTest = null; + var testsFailed = 0; + var testCount = 1; + var failureException = 'chrome.test.failure'; + + // Helper function to get around the fact that function names in javascript + // are read-only, and you can't assign one to anonymous functions. + function testName(test) { + return test ? (test.name || test.generatedName) : "(no test)"; + } + + function testDone() { + environmentSpecificBindings.testDone(chromeTest.runNextTest); + } + + function allTestsDone() { + if (testsFailed == 0) { + chromeTest.notifyPass(); + } else { + chromeTest.notifyFail('Failed ' + testsFailed + ' of ' + + testCount + ' tests'); + } + } + + var pendingCallbacks = 0; + + apiFunctions.setHandleRequest('callbackAdded', function() { + pendingCallbacks++; + + var called = null; + return function() { + if (called != null) { + var redundantPrefix = 'Error\n'; + chromeTest.fail( + 'Callback has already been run. ' + + 'First call:\n' + + $String.slice(called, redundantPrefix.length) + '\n' + + 'Second call:\n' + + $String.slice(new Error().stack, redundantPrefix.length)); + } + called = new Error().stack; + + pendingCallbacks--; + if (pendingCallbacks == 0) { + chromeTest.succeed(); + } + }; + }); + + apiFunctions.setHandleRequest('runNextTest', function() { + // There may have been callbacks which were interrupted by failure + // exceptions. + pendingCallbacks = 0; + + lastTest = currentTest; + currentTest = chromeTest.tests.shift(); + + if (!currentTest) { + allTestsDone(); + return; + } + + try { + chromeTest.log("( RUN ) " + testName(currentTest)); + uncaughtExceptionHandler.setHandler(function(message, e) { + if (e !== failureException) + chromeTest.fail('uncaught exception: ' + message); + }); + currentTest.call(); + } catch (e) { + uncaughtExceptionHandler.handle(e.message, e); + } + }); + + apiFunctions.setHandleRequest('fail', function(message) { + chromeTest.log("( FAILED ) " + testName(currentTest)); + + var stack = {}; + Error.captureStackTrace(stack, chromeTest.fail); + + if (!message) + message = "FAIL (no message)"; + + message += "\n" + stack.stack; + console.log("[FAIL] " + testName(currentTest) + ": " + message); + testsFailed++; + testDone(); + + // Interrupt the rest of the test. + throw failureException; + }); + + apiFunctions.setHandleRequest('succeed', function() { + console.log("[SUCCESS] " + testName(currentTest)); + chromeTest.log("( SUCCESS )"); + testDone(); + }); + + apiFunctions.setHandleRequest('runWithNativesEnabled', function(callback) { + RunWithNativesEnabled(callback); + }); + + apiFunctions.setHandleRequest('getModuleSystem', function(context) { + return GetModuleSystem(context); + }); + + apiFunctions.setHandleRequest('assertTrue', function(test, message) { + chromeTest.assertBool(test, true, message); + }); + + apiFunctions.setHandleRequest('assertFalse', function(test, message) { + chromeTest.assertBool(test, false, message); + }); + + apiFunctions.setHandleRequest('assertBool', + function(test, expected, message) { + if (test !== expected) { + if (typeof(test) == "string") { + if (message) + message = test + "\n" + message; + else + message = test; + } + chromeTest.fail(message); + } + }); + + apiFunctions.setHandleRequest('checkDeepEq', function(expected, actual) { + if ((expected === null) != (actual === null)) + return false; + + if (expected === actual) + return true; + + if (typeof(expected) !== typeof(actual)) + return false; + + for (var p in actual) { + if ($Object.hasOwnProperty(actual, p) && + !$Object.hasOwnProperty(expected, p)) { + return false; + } + } + for (var p in expected) { + if ($Object.hasOwnProperty(expected, p) && + !$Object.hasOwnProperty(actual, p)) { + return false; + } + } + + for (var p in expected) { + var eq = true; + switch (typeof(expected[p])) { + case 'object': + eq = chromeTest.checkDeepEq(expected[p], actual[p]); + break; + case 'function': + eq = (typeof(actual[p]) != 'undefined' && + expected[p].toString() == actual[p].toString()); + break; + default: + eq = (expected[p] == actual[p] && + typeof(expected[p]) == typeof(actual[p])); + break; + } + if (!eq) + return false; + } + return true; + }); + + apiFunctions.setHandleRequest('assertEq', + function(expected, actual, message) { + var error_msg = "API Test Error in " + testName(currentTest); + if (message) + error_msg += ": " + message; + if (typeof(expected) == 'object') { + if (!chromeTest.checkDeepEq(expected, actual)) { + error_msg += "\nActual: " + $JSON.stringify(actual) + + "\nExpected: " + $JSON.stringify(expected); + chromeTest.fail(error_msg); + } + return; + } + if (expected != actual) { + chromeTest.fail(error_msg + + "\nActual: " + actual + "\nExpected: " + expected); + } + if (typeof(expected) != typeof(actual)) { + chromeTest.fail(error_msg + + " (type mismatch)\nActual Type: " + typeof(actual) + + "\nExpected Type:" + typeof(expected)); + } + }); + + apiFunctions.setHandleRequest('assertNoLastError', function() { + if (chrome.runtime.lastError != undefined) { + chromeTest.fail("lastError.message == " + + chrome.runtime.lastError.message); + } + }); + + apiFunctions.setHandleRequest('assertLastError', function(expectedError) { + chromeTest.assertEq(typeof(expectedError), 'string'); + chromeTest.assertTrue(chrome.runtime.lastError != undefined, + "No lastError, but expected " + expectedError); + chromeTest.assertEq(expectedError, chrome.runtime.lastError.message); + }); + + apiFunctions.setHandleRequest('assertThrows', + function(fn, self, args, message) { + chromeTest.assertTrue(typeof fn == 'function'); + try { + fn.apply(self, args); + chromeTest.fail('Did not throw error: ' + fn); + } catch (e) { + if (e != failureException && message !== undefined) { + if (message instanceof RegExp) { + chromeTest.assertTrue(message.test(e.message), + e.message + ' should match ' + message) + } else { + chromeTest.assertEq(message, e.message); + } + } + } + }); + + function safeFunctionApply(func, args) { + try { + if (func) + return $Function.apply(func, undefined, args); + } catch (e) { + var msg = "uncaught exception " + e; + chromeTest.fail(msg); + } + }; + + // Wrapper for generating test functions, that takes care of calling + // assertNoLastError() and (optionally) succeed() for you. + apiFunctions.setHandleRequest('callback', function(func, expectedError) { + if (func) { + chromeTest.assertEq(typeof(func), 'function'); + } + var callbackCompleted = chromeTest.callbackAdded(); + + return function() { + if (expectedError == null) { + chromeTest.assertNoLastError(); + } else { + chromeTest.assertLastError(expectedError); + } + + var result; + if (func) { + result = safeFunctionApply(func, arguments); + } + + callbackCompleted(); + return result; + }; + }); + + apiFunctions.setHandleRequest('listenOnce', function(event, func) { + var callbackCompleted = chromeTest.callbackAdded(); + var listener = function() { + event.removeListener(listener); + safeFunctionApply(func, arguments); + callbackCompleted(); + }; + event.addListener(listener); + }); + + apiFunctions.setHandleRequest('listenForever', function(event, func) { + var callbackCompleted = chromeTest.callbackAdded(); + + var listener = function() { + safeFunctionApply(func, arguments); + }; + + var done = function() { + event.removeListener(listener); + callbackCompleted(); + }; + + event.addListener(listener); + return done; + }); + + apiFunctions.setHandleRequest('callbackPass', function(func) { + return chromeTest.callback(func); + }); + + apiFunctions.setHandleRequest('callbackFail', function(expectedError, func) { + return chromeTest.callback(func, expectedError); + }); + + apiFunctions.setHandleRequest('runTests', function(tests) { + chromeTest.tests = tests; + testCount = chromeTest.tests.length; + chromeTest.runNextTest(); + }); + + apiFunctions.setHandleRequest('getApiDefinitions', function() { + return GetExtensionAPIDefinitionsForTest(); + }); + + apiFunctions.setHandleRequest('getApiFeatures', function() { + return GetAPIFeatures(); + }); + + apiFunctions.setHandleRequest('isProcessingUserGesture', function() { + return userGestures.IsProcessingUserGesture(); + }); + + apiFunctions.setHandleRequest('runWithUserGesture', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + return userGestures.RunWithUserGesture(callback); + }); + + apiFunctions.setHandleRequest('runWithoutUserGesture', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + return userGestures.RunWithoutUserGesture(callback); + }); + + apiFunctions.setHandleRequest('setExceptionHandler', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + uncaughtExceptionHandler.setHandler(callback); + }); + + apiFunctions.setHandleRequest('getWakeEventPage', function() { + return natives.GetWakeEventPage(); + }); + + environmentSpecificBindings.registerHooks(api); +}); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/uncaught_exception_handler.js b/chromium/extensions/renderer/resources/uncaught_exception_handler.js new file mode 100644 index 00000000000..de4d71e71dc --- /dev/null +++ b/chromium/extensions/renderer/resources/uncaught_exception_handler.js @@ -0,0 +1,110 @@ +// 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. + +// Handles uncaught exceptions thrown by extensions. By default this is to +// log an error message, but tests may override this behaviour. +var handler = function(message, e) { + console.error(message); +}; + +/** + * Append the error description and stack trace to |message|. + * + * @param {string} message - The prefix of the error message. + * @param {Error|*} e - The thrown error object. This object is potentially + * unsafe, because it could be generated by an extension. + * @param {string=} priorStackTrace - The stack trace to be appended to the + * error message. This stack trace must not include stack frames of |e.stack|, + * because both stack traces are concatenated. Overlapping stack traces will + * confuse extension developers. + * @return {string} The formatted error message. + */ +function formatErrorMessage(message, e, priorStackTrace) { + if (e) + message += ': ' + safeErrorToString(e, false); + + var stack; + try { + // If the stack was set, use it. + // |e.stack| could be void in the following common example: + // throw "Error message"; + stack = $String.self(e && e.stack); + } catch (e) {} + + // If a stack is not provided, capture a stack trace. + if (!priorStackTrace && !stack) + stack = getStackTrace(); + + stack = filterExtensionStackTrace(stack); + if (stack) + message += '\n' + stack; + + // If an asynchronouse stack trace was set, append it. + if (priorStackTrace) + message += '\n' + priorStackTrace; + + return message; +} + +function filterExtensionStackTrace(stack) { + if (!stack) + return ''; + // Remove stack frames in the stack trace that weren't associated with the + // extension, to not confuse extension developers with internal details. + stack = $String.split(stack, '\n'); + stack = $Array.filter(stack, function(line) { + return $String.indexOf(line, 'chrome-extension://') >= 0; + }); + return $Array.join(stack, '\n'); +} + +function getStackTrace() { + var e = {}; + $Error.captureStackTrace(e, getStackTrace); + return e.stack; +} + +function getExtensionStackTrace() { + return filterExtensionStackTrace(getStackTrace()); +} + +/** + * Convert an object to a string. + * + * @param {Error|*} e - A thrown object (possibly user-supplied). + * @param {boolean=} omitType - Whether to try to serialize |e.message| instead + * of |e.toString()|. + * @return {string} The error message. + */ +function safeErrorToString(e, omitType) { + try { + return $String.self(omitType && e.message || e); + } catch (e) { + // This error is exceptional and could be triggered by + // throw {toString: function() { throw 'Haha' } }; + return '(cannot get error message)'; + } +} + +/** + * Formats the error message and invokes the error handler. + * + * @param {string} message - Error message prefix. + * @param {Error|*} e - Thrown object. + * @param {string=} priorStackTrace - Error message suffix. + * @see formatErrorMessage + */ +exports.$set('handle', function(message, e, priorStackTrace) { + message = formatErrorMessage(message, e, priorStackTrace); + handler(message, e); +}); + +// |newHandler| A function which matches |handler|. +exports.$set('setHandler', function(newHandler) { + handler = newHandler; +}); + +exports.$set('getStackTrace', getStackTrace); +exports.$set('getExtensionStackTrace', getExtensionStackTrace); +exports.$set('safeErrorToString', safeErrorToString); diff --git a/chromium/extensions/renderer/resources/utils.js b/chromium/extensions/renderer/resources/utils.js new file mode 100644 index 00000000000..26aa5e8ed3e --- /dev/null +++ b/chromium/extensions/renderer/resources/utils.js @@ -0,0 +1,240 @@ +// 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. + +var nativeDeepCopy = requireNative('utils').deepCopy; +var schemaRegistry = requireNative('schema_registry'); +var CHECK = requireNative('logging').CHECK; +var DCHECK = requireNative('logging').DCHECK; +var WARNING = requireNative('logging').WARNING; + +/** + * An object forEach. Calls |f| with each (key, value) pair of |obj|, using + * |self| as the target. + * @param {Object} obj The object to iterate over. + * @param {function} f The function to call in each iteration. + * @param {Object} self The object to use as |this| in each function call. + */ +function forEach(obj, f, self) { + for (var key in obj) { + if ($Object.hasOwnProperty(obj, key)) + $Function.call(f, self, key, obj[key]); + } +} + +/** + * Assuming |array_of_dictionaries| is structured like this: + * [{id: 1, ... }, {id: 2, ...}, ...], you can use + * lookup(array_of_dictionaries, 'id', 2) to get the dictionary with id == 2. + * @param {Array<Object<?>>} array_of_dictionaries + * @param {string} field + * @param {?} value + */ +function lookup(array_of_dictionaries, field, value) { + var filter = function (dict) {return dict[field] == value;}; + var matches = $Array.filter(array_of_dictionaries, filter); + if (matches.length == 0) { + return undefined; + } else if (matches.length == 1) { + return matches[0] + } else { + throw new Error("Failed lookup of field '" + field + "' with value '" + + value + "'"); + } +} + +function loadTypeSchema(typeName, defaultSchema) { + var parts = $String.split(typeName, '.'); + if (parts.length == 1) { + if (defaultSchema == null) { + WARNING('Trying to reference "' + typeName + '" ' + + 'with neither namespace nor default schema.'); + return null; + } + var types = defaultSchema.types; + } else { + var schemaName = $Array.join($Array.slice(parts, 0, parts.length - 1), '.'); + var types = schemaRegistry.GetSchema(schemaName).types; + } + for (var i = 0; i < types.length; ++i) { + if (types[i].id == typeName) + return types[i]; + } + return null; +} + +/** + * Sets a property |value| on |obj| with property name |key|. Like + * + * obj[key] = value; + * + * but without triggering setters. + */ +function defineProperty(obj, key, value) { + $Object.defineProperty(obj, key, { + __proto__: null, + configurable: true, + enumerable: true, + writable: true, + value: value, + }); +} + +/** + * Takes a private class implementation |privateClass| and exposes a subset of + * its methods |functions| and properties |properties| and |readonly| to a + * public wrapper class that should be passed in. Within bindings code, you can + * access the implementation from an instance of the wrapper class using + * privates(instance).impl, and from the implementation class you can access + * the wrapper using this.wrapper (or implInstance.wrapper if you have another + * instance of the implementation class). + * + * |publicClass| should be a constructor that calls constructPrivate() like so: + * + * privates(publicClass).constructPrivate(this, arguments); + * + * @param {function} publicClass The publicly exposed wrapper class. This must + * be a named function, and the name appears in stack traces. + * @param {Object} privateClass The class implementation. + * @param {{superclass: ?Function, + * functions: ?Array<string>, + * properties: ?Array<string>, + * readonly: ?Array<string>}} exposed The names of properties on the + * implementation class to be exposed. |superclass| represents the + * constructor of the class to be used as the superclass of the exposed + * class; |functions| represents the names of functions which should be + * delegated to the implementation; |properties| are gettable/settable + * properties and |readonly| are read-only properties. + */ +function expose(publicClass, privateClass, exposed) { + DCHECK(!(privateClass.prototype instanceof $Object.self)); + + $Object.setPrototypeOf(exposed, null); + + // This should be called by publicClass. + privates(publicClass).constructPrivate = function(self, args) { + if (!(self instanceof publicClass)) { + throw new Error('Please use "new ' + publicClass.name + '"'); + } + // The "instanceof publicClass" check can easily be spoofed, so we check + // whether the private impl is already set before continuing. + var privateSelf = privates(self); + if ('impl' in privateSelf) { + throw new Error('Object ' + publicClass.name + ' is already constructed'); + } + var privateObj = $Object.create(privateClass.prototype); + $Function.apply(privateClass, privateObj, args); + privateObj.wrapper = self; + privateSelf.impl = privateObj; + }; + + function getPrivateImpl(self) { + var impl = privates(self).impl; + if (!(impl instanceof privateClass)) { + // Either the object is not constructed, or the property descriptor is + // used on a target that is not an instance of publicClass. + throw new Error('impl is not an instance of ' + privateClass.name); + } + return impl; + } + + var publicClassPrototype = { + // The final prototype will be assigned at the end of this method. + __proto__: null, + constructor: publicClass, + }; + + if ('functions' in exposed) { + $Array.forEach(exposed.functions, function(func) { + publicClassPrototype[func] = function() { + var impl = getPrivateImpl(this); + return $Function.apply(impl[func], impl, arguments); + }; + }); + } + + if ('properties' in exposed) { + $Array.forEach(exposed.properties, function(prop) { + $Object.defineProperty(publicClassPrototype, prop, { + __proto__: null, + enumerable: true, + get: function() { + return getPrivateImpl(this)[prop]; + }, + set: function(value) { + var impl = getPrivateImpl(this); + delete impl[prop]; + impl[prop] = value; + } + }); + }); + } + + if ('readonly' in exposed) { + $Array.forEach(exposed.readonly, function(readonly) { + $Object.defineProperty(publicClassPrototype, readonly, { + __proto__: null, + enumerable: true, + get: function() { + return getPrivateImpl(this)[readonly]; + }, + }); + }); + } + + // The prototype properties have been installed. Now we can safely assign an + // unsafe prototype and export the class to the public. + var superclass = exposed.superclass || $Object.self; + $Object.setPrototypeOf(publicClassPrototype, superclass.prototype); + publicClass.prototype = publicClassPrototype; + + return publicClass; +} + +/** + * Returns a deep copy of |value|. The copy will have no references to nested + * values of |value|. + */ +function deepCopy(value) { + return nativeDeepCopy(value); +} + +/** + * Wrap an asynchronous API call to a function |func| in a promise. The + * remaining arguments will be passed to |func|. Returns a promise that will be + * resolved to the result passed to the callback or rejected if an error occurs + * (if chrome.runtime.lastError is set). If there are multiple results, the + * promise will be resolved with an array containing those results. + * + * For example, + * promise(chrome.storage.get, 'a').then(function(result) { + * // Use result. + * }).catch(function(error) { + * // Report error.message. + * }); + */ +function promise(func) { + var args = $Array.slice(arguments, 1); + DCHECK(typeof func == 'function'); + return new Promise(function(resolve, reject) { + args.push(function() { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + return; + } + if (arguments.length <= 1) + resolve(arguments[0]); + else + resolve($Array.slice(arguments)); + }); + $Function.apply(func, null, args); + }); +} + +exports.$set('forEach', forEach); +exports.$set('loadTypeSchema', loadTypeSchema); +exports.$set('lookup', lookup); +exports.$set('defineProperty', defineProperty); +exports.$set('expose', expose); +exports.$set('deepCopy', deepCopy); +exports.$set('promise', promise); diff --git a/chromium/extensions/renderer/resources/web_request_custom_bindings.js b/chromium/extensions/renderer/resources/web_request_custom_bindings.js new file mode 100644 index 00000000000..2dd7ce4f048 --- /dev/null +++ b/chromium/extensions/renderer/resources/web_request_custom_bindings.js @@ -0,0 +1,23 @@ +// Copyright (c) 2012 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. + +// Custom binding for the webRequest API. + +var binding = require('binding').Binding.create('webRequest'); +var sendRequest = require('sendRequest').sendRequest; +var WebRequestEvent = require('webRequestInternal').WebRequestEvent; + +binding.registerCustomHook(function(api) { + var apiFunctions = api.apiFunctions; + + apiFunctions.setHandleRequest('handlerBehaviorChanged', function() { + var args = $Array.slice(arguments); + sendRequest(this.name, args, this.definition.parameters, + {forIOThread: true}); + }); +}); + +binding.registerCustomEvent(WebRequestEvent); + +exports.$set('binding', binding.generate()); diff --git a/chromium/extensions/renderer/resources/web_request_internal_custom_bindings.js b/chromium/extensions/renderer/resources/web_request_internal_custom_bindings.js new file mode 100644 index 00000000000..32ad33b9b47 --- /dev/null +++ b/chromium/extensions/renderer/resources/web_request_internal_custom_bindings.js @@ -0,0 +1,196 @@ +// Copyright (c) 2012 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. + +// Custom binding for the webRequestInternal API. + +var binding = require('binding').Binding.create('webRequestInternal'); +var eventBindings = require('event_bindings'); +var sendRequest = require('sendRequest').sendRequest; +var validate = require('schemaUtils').validate; +var utils = require('utils'); +var idGeneratorNatives = requireNative('id_generator'); + +var webRequestInternal; + +function GetUniqueSubEventName(eventName) { + return eventName + '/' + idGeneratorNatives.GetNextId(); +} + +// WebRequestEventImpl object. This is used for special webRequest events +// with extra parameters. Each invocation of addListener creates a new named +// sub-event. That sub-event is associated with the extra parameters in the +// browser process, so that only it is dispatched when the main event occurs +// matching the extra parameters. +// +// Example: +// chrome.webRequest.onBeforeRequest.addListener( +// callback, {urls: 'http://*.google.com/*'}); +// ^ callback will only be called for onBeforeRequests matching the filter. +function WebRequestEventImpl(eventName, opt_argSchemas, opt_extraArgSchemas, + opt_eventOptions, opt_webViewInstanceId) { + if (typeof eventName != 'string') + throw new Error('chrome.WebRequestEvent requires an event name.'); + + this.eventName = eventName; + this.argSchemas = opt_argSchemas; + this.extraArgSchemas = opt_extraArgSchemas; + this.webViewInstanceId = opt_webViewInstanceId || 0; + this.subEvents = []; + this.eventOptions = eventBindings.parseEventOptions(opt_eventOptions); + if (this.eventOptions.supportsRules) { + this.eventForRules = + new eventBindings.Event(eventName, opt_argSchemas, opt_eventOptions, + opt_webViewInstanceId); + } +} +$Object.setPrototypeOf(WebRequestEventImpl.prototype, null); + +// Test if the given callback is registered for this event. +WebRequestEventImpl.prototype.hasListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error('This event does not support listeners.'); + return this.findListener_(cb) > -1; +}; + +// Test if any callbacks are registered fur thus event. +WebRequestEventImpl.prototype.hasListeners = function() { + if (!this.eventOptions.supportsListeners) + throw new Error('This event does not support listeners.'); + return this.subEvents.length > 0; +}; + +// Registers a callback to be called when this event is dispatched. If +// opt_filter is specified, then the callback is only called for events that +// match the given filters. If opt_extraInfo is specified, the given optional +// info is sent to the callback. +WebRequestEventImpl.prototype.addListener = + function(cb, opt_filter, opt_extraInfo) { + if (!this.eventOptions.supportsListeners) + throw new Error('This event does not support listeners.'); + // NOTE(benjhayden) New APIs should not use this subEventName trick! It does + // not play well with event pages. See downloads.onDeterminingFilename and + // ExtensionDownloadsEventRouter for an alternative approach. + var subEventName = GetUniqueSubEventName(this.eventName); + // Note: this could fail to validate, in which case we would not add the + // subEvent listener. + validate($Array.slice(arguments, 1), this.extraArgSchemas); + webRequestInternal.addEventListener( + cb, opt_filter, opt_extraInfo, this.eventName, subEventName, + this.webViewInstanceId); + + var subEvent = new eventBindings.Event(subEventName, this.argSchemas); + var subEventCallback = cb; + if (opt_extraInfo && opt_extraInfo.indexOf('blocking') >= 0) { + var eventName = this.eventName; + subEventCallback = function() { + var requestId = arguments[0].requestId; + try { + var result = $Function.apply(cb, null, arguments); + webRequestInternal.eventHandled( + eventName, subEventName, requestId, result); + } catch (e) { + webRequestInternal.eventHandled( + eventName, subEventName, requestId); + throw e; + } + }; + } else if (opt_extraInfo && opt_extraInfo.indexOf('asyncBlocking') >= 0) { + var eventName = this.eventName; + subEventCallback = function() { + var details = arguments[0]; + var requestId = details.requestId; + var handledCallback = function(response) { + webRequestInternal.eventHandled( + eventName, subEventName, requestId, response); + }; + $Function.apply(cb, null, [details, handledCallback]); + }; + } + $Array.push(this.subEvents, + {subEvent: subEvent, callback: cb, subEventCallback: subEventCallback}); + subEvent.addListener(subEventCallback); +}; + +// Unregisters a callback. +WebRequestEventImpl.prototype.removeListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error('This event does not support listeners.'); + var idx; + while ((idx = this.findListener_(cb)) >= 0) { + var e = this.subEvents[idx]; + e.subEvent.removeListener(e.subEventCallback); + if (e.subEvent.hasListeners()) { + console.error( + 'Internal error: webRequest subEvent has orphaned listeners.'); + } + $Array.splice(this.subEvents, idx, 1); + } +}; + +WebRequestEventImpl.prototype.findListener_ = function(cb) { + for (var i in this.subEvents) { + var e = this.subEvents[i]; + if (e.callback === cb) { + if (e.subEvent.hasListener(e.subEventCallback)) + return i; + console.error('Internal error: webRequest subEvent has no callback.'); + } + } + + return -1; +}; + +WebRequestEventImpl.prototype.addRules = function(rules, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error('This event does not support rules.'); + this.eventForRules.addRules(rules, opt_cb); +}; + +WebRequestEventImpl.prototype.removeRules = + function(ruleIdentifiers, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error('This event does not support rules.'); + this.eventForRules.removeRules(ruleIdentifiers, opt_cb); +}; + +WebRequestEventImpl.prototype.getRules = function(ruleIdentifiers, cb) { + if (!this.eventOptions.supportsRules) + throw new Error('This event does not support rules.'); + this.eventForRules.getRules(ruleIdentifiers, cb); +}; + +binding.registerCustomHook(function(api) { + var apiFunctions = api.apiFunctions; + + apiFunctions.setHandleRequest('addEventListener', function() { + var args = $Array.slice(arguments); + sendRequest(this.name, args, this.definition.parameters, + {forIOThread: true}); + }); + + apiFunctions.setHandleRequest('eventHandled', function() { + var args = $Array.slice(arguments); + sendRequest(this.name, args, this.definition.parameters, + {forIOThread: true}); + }); +}); + +function WebRequestEvent() { + privates(WebRequestEvent).constructPrivate(this, arguments); +} +utils.expose(WebRequestEvent, WebRequestEventImpl, { + functions: [ + 'hasListener', + 'hasListeners', + 'addListener', + 'removeListener', + 'addRules', + 'removeRules', + 'getRules', + ], +}); + +webRequestInternal = binding.generate(); +exports.$set('binding', webRequestInternal); +exports.$set('WebRequestEvent', WebRequestEvent); diff --git a/chromium/extensions/renderer/resources/window_controls.js b/chromium/extensions/renderer/resources/window_controls.js new file mode 100644 index 00000000000..75c88e63927 --- /dev/null +++ b/chromium/extensions/renderer/resources/window_controls.js @@ -0,0 +1,78 @@ +// Copyright 2013 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. + +// +// <window-controls> shadow element implementation. +// + +var chrome = requireNative('chrome').GetChrome(); +var forEach = require('utils').forEach; +var addTagWatcher = require('tagWatcher').addTagWatcher; +var appWindow = require('app.window'); +var getHtmlTemplate = + requireNative('app_window_natives').GetWindowControlsHtmlTemplate; + +/** + * @constructor + */ +function WindowControls(node) { + this.node_ = node; + this.shadowRoot_ = this.createShadowRoot_(node); + this.setupWindowControls_(); +} + +/** + * @private + */ +WindowControls.prototype.template_element = null; + +/** + * @private + */ +WindowControls.prototype.createShadowRoot_ = function(node) { + // Initialize |template| from HTML template resource and cache result. + var template = WindowControls.prototype.template_element; + if (!template) { + var element = document.createElement('div'); + element.innerHTML = getHtmlTemplate(); + WindowControls.prototype.template_element = element.firstChild; + template = WindowControls.prototype.template_element; + } + // Create shadow root element with template clone as first child. + var shadowRoot = node.createShadowRoot(); + shadowRoot.appendChild(template.content.cloneNode(true)); + return shadowRoot; +} + +/** + * @private + */ +WindowControls.prototype.setupWindowControls_ = function() { + var self = this; + this.shadowRoot_.querySelector("#close-control").addEventListener('click', + function(e) { + chrome.app.window.current().close(); + }); + + this.shadowRoot_.querySelector("#maximize-control").addEventListener('click', + function(e) { + self.maxRestore_(); + }); +} + +/** + * @private + * Restore or maximize depending on current state + */ +WindowControls.prototype.maxRestore_ = function() { + if (chrome.app.window.current().isMaximized()) { + chrome.app.window.current().restore(); + } else { + chrome.app.window.current().maximize(); + } +} + +addTagWatcher('WINDOW-CONTROLS', function(addedNode) { + new WindowControls(addedNode); +}); diff --git a/chromium/extensions/renderer/resources/window_controls_template.html b/chromium/extensions/renderer/resources/window_controls_template.html new file mode 100644 index 00000000000..6f468bc4b3a --- /dev/null +++ b/chromium/extensions/renderer/resources/window_controls_template.html @@ -0,0 +1,52 @@ +<template id="window-controls-template"> + <style> + .controls { + width:32px; + height:32px; + position:absolute; + z-index:200; + } + #close-control { + top:8px; + right:10px; + } + #maximize-control { + top:8px; + right:52px; + } + #close { + top:0; + right:0; + -webkit-mask-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALNJREFUeNrsllEOwyAIhmVXwIu487dXKEdy2PBgWuxwm/VhkPwvSuALAhFyzmGmPcJkcwAHcAAHMAMAQGItLGSFhlB8kpmgrGKL2NbiziIWKqHK2SY+qzluBwBKcg2iTr7fjQBoQZySd1W2E0CD2LSqjAQ4Qqh9YY37zRj+5iPx4RPUZac7n6DVhHRHE74bQxo9hvUiio1FRCMXURKIeNFSKD5Pa1zwX7EDOIAD/D3AS4ABAKWdkCCeGGsrAAAAAElFTkSuQmCC'); + } + .windowbutton { + width:32px; + height:32px; + position:absolute; + background-color:rgba(0, 0, 0, 0.49); + } + .windowbuttonbackground { + width:32px; + height:32px; + position:absolute; + } + .windowbuttonbackground:hover { + background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEBJREFUeNrszrENAEAIw8A8EvU32X9RGsQUaewFfM/2l9TKNBWcX10KBwAAAAAAAAAAAAAAAAAAABxggv9ZAQYAhakDi3I15kgAAAAASUVORK5CYII='); + + } + .windowbuttonbackground:active { + background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFtJREFUeNrs1zESgCAMRNEAkSZD5VW8/1k8hFaCCqfY5u9M6v/apIh2mH1hkqXba92uUvxU5Mfoe57xx0Rb7WziAQAAAAAAAAAAAAAAAAAAOcDXkzqvifrvL8AAWBcLpapo5CcAAAAASUVORK5CYII='); + } + #maximize { + -webkit-mask-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFFJREFUeNrs1jsOACAIREHWeP8royWdnwaDj9pi2OhGubtlTrPkAQAAAIC+e1DSUWXOhlWtBGIYq+W5hAAA1GzC23f+fALiVwwAAIDvAUOAAQAv/Aw+jTHzugAAAABJRU5ErkJggg=='); + } + </style> + <div id="close-control" class="controls"> + <div id="close" class="windowbutton"> </div> + <div id="close-background" class="windowbuttonbackground"> </div> + </div> + <div id="maximize-control" class="controls"> + <div id="maximize" class="windowbutton"></div> + <div id="maximize-background" class="windowbuttonbackground"></div> + </div> +</template> diff --git a/chromium/extensions/renderer/runtime_custom_bindings.cc b/chromium/extensions/renderer/runtime_custom_bindings.cc new file mode 100644 index 00000000000..8c6f2705e7e --- /dev/null +++ b/chromium/extensions/renderer/runtime_custom_bindings.cc @@ -0,0 +1,184 @@ +// 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/renderer/runtime_custom_bindings.h" + +#include <stdint.h> + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/features/feature.h" +#include "extensions/common/features/feature_provider.h" +#include "extensions/common/manifest.h" +#include "extensions/renderer/api_activity_logger.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" + +using content::V8ValueConverter; + +namespace extensions { + +RuntimeCustomBindings::RuntimeCustomBindings(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetManifest", + base::Bind(&RuntimeCustomBindings::GetManifest, base::Unretained(this))); + RouteFunction("OpenChannelToExtension", + base::Bind(&RuntimeCustomBindings::OpenChannelToExtension, + base::Unretained(this))); + RouteFunction("OpenChannelToNativeApp", + base::Bind(&RuntimeCustomBindings::OpenChannelToNativeApp, + base::Unretained(this))); + RouteFunction("GetExtensionViews", + base::Bind(&RuntimeCustomBindings::GetExtensionViews, + base::Unretained(this))); +} + +RuntimeCustomBindings::~RuntimeCustomBindings() { +} + +void RuntimeCustomBindings::OpenChannelToExtension( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Get the current RenderFrame so that we can send a routed IPC message from + // the correct source. + content::RenderFrame* renderframe = context()->GetRenderFrame(); + if (!renderframe) + return; + + // The Javascript code should validate/fill the arguments. + CHECK_EQ(args.Length(), 3); + CHECK(args[0]->IsString() && args[1]->IsString() && args[2]->IsBoolean()); + + ExtensionMsg_ExternalConnectionInfo info; + + // For messaging APIs, hosted apps should be considered a web page so hide + // its extension ID. + const Extension* extension = context()->extension(); + if (extension && !extension->is_hosted_app()) + info.source_id = extension->id(); + + info.target_id = *v8::String::Utf8Value(args[0]); + info.source_url = context()->url(); + std::string channel_name = *v8::String::Utf8Value(args[1]); + bool include_tls_channel_id = + args.Length() > 2 ? args[2]->BooleanValue() : false; + int port_id = -1; + renderframe->Send(new ExtensionHostMsg_OpenChannelToExtension( + renderframe->GetRoutingID(), info, channel_name, include_tls_channel_id, + &port_id)); + args.GetReturnValue().Set(static_cast<int32_t>(port_id)); +} + +void RuntimeCustomBindings::OpenChannelToNativeApp( + const v8::FunctionCallbackInfo<v8::Value>& args) { + // Verify that the extension has permission to use native messaging. + Feature::Availability availability = + FeatureProvider::GetPermissionFeatures() + ->GetFeature("nativeMessaging") + ->IsAvailableToContext(context()->extension(), + context()->context_type(), context()->url()); + if (!availability.is_available()) + return; + + content::RenderFrame* render_frame = context()->GetRenderFrame(); + if (!render_frame) + return; + + // The Javascript code should validate/fill the arguments. + CHECK(args.Length() >= 2 && args[0]->IsString() && args[1]->IsString()); + + std::string extension_id = *v8::String::Utf8Value(args[0]); + std::string native_app_name = *v8::String::Utf8Value(args[1]); + + int port_id = -1; + render_frame->Send(new ExtensionHostMsg_OpenChannelToNativeApp( + render_frame->GetRoutingID(), extension_id, native_app_name, &port_id)); + args.GetReturnValue().Set(static_cast<int32_t>(port_id)); +} + +void RuntimeCustomBindings::GetManifest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK(context()->extension()); + + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + args.GetReturnValue().Set(converter->ToV8Value( + context()->extension()->manifest()->value(), context()->v8_context())); +} + +void RuntimeCustomBindings::GetExtensionViews( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() != 2) + return; + + if (!args[0]->IsInt32() || !args[1]->IsString()) + return; + + // |browser_window_id| == extension_misc::kUnknownWindowId means getting + // all views for the current extension. + int browser_window_id = args[0]->Int32Value(); + + std::string view_type_string = + base::ToUpperASCII(*v8::String::Utf8Value(args[1])); + // |view_type| == VIEW_TYPE_INVALID means getting any type of + // views. + ViewType view_type = VIEW_TYPE_INVALID; + if (view_type_string == kViewTypeBackgroundPage) { + view_type = VIEW_TYPE_EXTENSION_BACKGROUND_PAGE; + } else if (view_type_string == kViewTypeTabContents) { + view_type = VIEW_TYPE_TAB_CONTENTS; + } else if (view_type_string == kViewTypePopup) { + view_type = VIEW_TYPE_EXTENSION_POPUP; + } else if (view_type_string == kViewTypeExtensionDialog) { + view_type = VIEW_TYPE_EXTENSION_DIALOG; + } else if (view_type_string == kViewTypeAppWindow) { + view_type = VIEW_TYPE_APP_WINDOW; + } else if (view_type_string == kViewTypeLauncherPage) { + view_type = VIEW_TYPE_LAUNCHER_PAGE; + } else if (view_type_string == kViewTypePanel) { + view_type = VIEW_TYPE_PANEL; + } else if (view_type_string != kViewTypeAll) { + return; + } + + std::string extension_id = context()->GetExtensionID(); + if (extension_id.empty()) + return; + + std::vector<content::RenderFrame*> frames = + ExtensionFrameHelper::GetExtensionFrames(extension_id, browser_window_id, + view_type); + v8::Local<v8::Context> v8_context = args.GetIsolate()->GetCurrentContext(); + v8::Local<v8::Array> v8_views = v8::Array::New(args.GetIsolate()); + int v8_index = 0; + for (content::RenderFrame* frame : frames) { + // We filter out iframes here. GetExtensionViews should only return the + // main views, not any subframes. (Returning subframes can cause broken + // behavior by treating an app window's iframe as its main frame, and maybe + // other nastiness). + if (frame->GetWebFrame()->top() != frame->GetWebFrame()) + continue; + + v8::Local<v8::Context> context = + frame->GetWebFrame()->mainWorldScriptContext(); + if (!context.IsEmpty()) { + v8::Local<v8::Value> window = context->Global(); + DCHECK(!window.IsEmpty()); + v8::Maybe<bool> maybe = + v8_views->CreateDataProperty(v8_context, v8_index++, window); + DCHECK(maybe.IsJust() && maybe.FromJust()); + } + } + + args.GetReturnValue().Set(v8_views); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/runtime_custom_bindings.h b/chromium/extensions/renderer/runtime_custom_bindings.h new file mode 100644 index 00000000000..17e96bbaf79 --- /dev/null +++ b/chromium/extensions/renderer/runtime_custom_bindings.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_RUNTIME_CUSTOM_BINDINGS_H_ +#define EXTENSIONS_RENDERER_RUNTIME_CUSTOM_BINDINGS_H_ + +#include "base/compiler_specific.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { + +// The native component of custom bindings for the chrome.runtime API. +class RuntimeCustomBindings : public ObjectBackedNativeHandler { + public: + explicit RuntimeCustomBindings(ScriptContext* context); + + ~RuntimeCustomBindings() override; + + // Creates a new messaging channel to the given extension. + void OpenChannelToExtension(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Creates a new messaging channels for the specified native application. + void OpenChannelToNativeApp(const v8::FunctionCallbackInfo<v8::Value>& args); + + private: + void GetManifest(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetExtensionViews(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_RUNTIME_CUSTOM_BINDINGS_H_ diff --git a/chromium/extensions/renderer/safe_builtins.cc b/chromium/extensions/renderer/safe_builtins.cc new file mode 100644 index 00000000000..914f4eaf940 --- /dev/null +++ b/chromium/extensions/renderer/safe_builtins.cc @@ -0,0 +1,259 @@ +// 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/renderer/safe_builtins.h" + +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/strings/stringprintf.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/v8_helpers.h" + +namespace extensions { + +using namespace v8_helpers; + +namespace { + +const char kClassName[] = "extensions::SafeBuiltins"; + +// Documentation for makeCallback in the JavaScript, out here to reduce the +// (very small) amount of effort that the v8 parser needs to do: +// +// Returns a new object with every function on |obj| configured to call()\n" +// itself with the given arguments.\n" +// E.g. given\n" +// var result = makeCallable(Function.prototype)\n" +// |result| will be a object including 'bind' such that\n" +// result.bind(foo, 1, 2, 3);\n" +// is equivalent to Function.prototype.bind.call(foo, 1, 2, 3), and so on.\n" +// This is a convenient way to save functions that user scripts may clobber.\n" +const char kScript[] = + "(function() {\n" + "'use strict';\n" + "native function Apply();\n" + "native function Save();\n" + "\n" + "// Used in the callback implementation, could potentially be clobbered.\n" + "function makeCallable(obj, target, isStatic, propertyNames) {\n" + " propertyNames.forEach(function(propertyName) {\n" + " var property = obj[propertyName];\n" + " target[propertyName] = function() {\n" + " var recv = obj;\n" + " var firstArgIndex = 0;\n" + " if (!isStatic) {\n" + " if (arguments.length == 0)\n" + " throw 'There must be at least one argument, the receiver';\n" + " recv = arguments[0];\n" + " firstArgIndex = 1;\n" + " }\n" + " return Apply(\n" + " property, recv, arguments, firstArgIndex, arguments.length);\n" + " };\n" + " });\n" + "}\n" + "\n" + "function saveBuiltin(builtin, protoPropertyNames, staticPropertyNames) {\n" + " var safe = function() {\n" + " throw 'Safe objects cannot be called nor constructed. ' +\n" + " 'Use $Foo.self() or new $Foo.self() instead.';\n" + " };\n" + " safe.self = builtin;\n" + " makeCallable(builtin.prototype, safe, false, protoPropertyNames);\n" + " if (staticPropertyNames)\n" + " makeCallable(builtin, safe, true, staticPropertyNames);\n" + " Save(builtin.name, safe);\n" + "}\n" + "\n" + "// Save only what is needed by the extension modules.\n" + "saveBuiltin(Object,\n" + " ['hasOwnProperty'],\n" + " ['create', 'defineProperty', 'freeze',\n" + " 'getOwnPropertyDescriptor', 'getPrototypeOf', 'keys',\n" + " 'assign', 'setPrototypeOf']);\n" + "saveBuiltin(Function,\n" + " ['apply', 'bind', 'call']);\n" + "saveBuiltin(Array,\n" + " ['concat', 'forEach', 'indexOf', 'join', 'push', 'slice',\n" + " 'splice', 'map', 'filter', 'unshift', 'pop', 'reverse'],\n" + " ['isArray']);\n" + "saveBuiltin(String,\n" + " ['indexOf', 'slice', 'split', 'substr', 'toUpperCase',\n" + " 'replace']);\n" + "// Use exec rather than test to defend against clobbering in the\n" + "// presence of ES2015 semantics, which read RegExp.prototype.exec.\n" + "saveBuiltin(RegExp,\n" + " ['exec']);\n" + "saveBuiltin(Error,\n" + " [],\n" + " ['captureStackTrace']);\n" + "\n" + "// JSON is trickier because extensions can override toJSON in\n" + "// incompatible ways, and we need to prevent that.\n" + "var builtinTypes = [\n" + " Object, Function, Array, String, Boolean, Number, Date, RegExp\n" + "];\n" + "var builtinToJSONs = builtinTypes.map(function(t) {\n" + " return t.toJSON;\n" + "});\n" + "var builtinArray = Array;\n" + "var builtinJSONStringify = JSON.stringify;\n" + "Save('JSON', {\n" + " parse: JSON.parse,\n" + " stringify: function(obj) {\n" + " var savedToJSONs = new builtinArray(builtinTypes.length);\n" + " try {\n" + " for (var i = 0; i < builtinTypes.length; ++i) {\n" + " try {\n" + " if (builtinTypes[i].prototype.toJSON !==\n" + " builtinToJSONs[i]) {\n" + " savedToJSONs[i] = builtinTypes[i].prototype.toJSON;\n" + " builtinTypes[i].prototype.toJSON = builtinToJSONs[i];\n" + " }\n" + " } catch (e) {}\n" + " }\n" + " } catch (e) {}\n" + " try {\n" + " return builtinJSONStringify(obj);\n" + " } finally {\n" + " for (var i = 0; i < builtinTypes.length; ++i) {\n" + " try {\n" + " if (i in savedToJSONs)\n" + " builtinTypes[i].prototype.toJSON = savedToJSONs[i];\n" + " } catch (e) {}\n" + " }\n" + " }\n" + " }\n" + "});\n" + "\n" + "}());\n"; + +v8::Local<v8::Private> MakeKey(const char* name, v8::Isolate* isolate) { + return v8::Private::ForApi( + isolate, ToV8StringUnsafe( + isolate, base::StringPrintf("%s::%s", kClassName, name))); +} + +void SaveImpl(const char* name, + v8::Local<v8::Value> value, + v8::Local<v8::Context> context) { + CHECK(!value.IsEmpty() && value->IsObject()) << name; + context->Global() + ->SetPrivate(context, MakeKey(name, context->GetIsolate()), value) + .FromJust(); +} + +v8::Local<v8::Object> Load(const char* name, v8::Local<v8::Context> context) { + v8::Local<v8::Value> value = + context->Global() + ->GetPrivate(context, MakeKey(name, context->GetIsolate())) + .ToLocalChecked(); + CHECK(value->IsObject()) << name; + return v8::Local<v8::Object>::Cast(value); +} + +class ExtensionImpl : public v8::Extension { + public: + ExtensionImpl() : v8::Extension(kClassName, kScript) {} + + private: + v8::Local<v8::FunctionTemplate> GetNativeFunctionTemplate( + v8::Isolate* isolate, + v8::Local<v8::String> name) override { + v8::Local<v8::Context> context = isolate->GetCurrentContext(); + if (IsTrue(name->Equals(context, ToV8StringUnsafe(isolate, "Apply")))) + return v8::FunctionTemplate::New(isolate, Apply); + if (IsTrue(name->Equals(context, ToV8StringUnsafe(isolate, "Save")))) + return v8::FunctionTemplate::New(isolate, Save); + NOTREACHED() << *v8::String::Utf8Value(name); + return v8::Local<v8::FunctionTemplate>(); + } + + static void Apply(const v8::FunctionCallbackInfo<v8::Value>& info) { + CHECK(info.Length() == 5 && info[0]->IsFunction() && // function + // info[1] could be an object or a string + info[2]->IsObject() && // args + info[3]->IsInt32() && // first_arg_index + info[4]->IsInt32()); // args_length + v8::Local<v8::Function> function = info[0].As<v8::Function>(); + v8::Local<v8::Object> recv; + if (info[1]->IsObject()) { + recv = v8::Local<v8::Object>::Cast(info[1]); + } else if (info[1]->IsString()) { + recv = v8::StringObject::New(v8::Local<v8::String>::Cast(info[1])) + .As<v8::Object>(); + } else { + info.GetIsolate()->ThrowException( + v8::Exception::TypeError(ToV8StringUnsafe( + info.GetIsolate(), + "The first argument is the receiver and must be an object"))); + return; + } + v8::Local<v8::Object> args = v8::Local<v8::Object>::Cast(info[2]); + int first_arg_index = info[3].As<v8::Int32>()->Value(); + int args_length = info[4].As<v8::Int32>()->Value(); + + v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext(); + int argc = args_length - first_arg_index; + scoped_ptr<v8::Local<v8::Value> []> argv(new v8::Local<v8::Value>[argc]); + for (int i = 0; i < argc; ++i) { + CHECK(IsTrue(args->Has(context, i + first_arg_index))); + // Getting a property value could throw an exception. + if (!GetProperty(context, args, i + first_arg_index, &argv[i])) + return; + } + + v8::MicrotasksScope microtasks( + info.GetIsolate(), v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::Local<v8::Value> return_value; + if (function->Call(context, recv, argc, argv.get()).ToLocal(&return_value)) + info.GetReturnValue().Set(return_value); + } + + static void Save(const v8::FunctionCallbackInfo<v8::Value>& info) { + CHECK(info.Length() == 2 && info[0]->IsString() && info[1]->IsObject()); + SaveImpl(*v8::String::Utf8Value(info[0]), + info[1], + info.GetIsolate()->GetCurrentContext()); + } +}; + +} // namespace + +// static +v8::Extension* SafeBuiltins::CreateV8Extension() { return new ExtensionImpl(); } + +SafeBuiltins::SafeBuiltins(ScriptContext* context) : context_(context) {} + +SafeBuiltins::~SafeBuiltins() {} + +v8::Local<v8::Object> SafeBuiltins::GetArray() const { + return Load("Array", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetFunction() const { + return Load("Function", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetJSON() const { + return Load("JSON", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetObjekt() const { + return Load("Object", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetRegExp() const { + return Load("RegExp", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetString() const { + return Load("String", context_->v8_context()); +} + +v8::Local<v8::Object> SafeBuiltins::GetError() const { + return Load("Error", context_->v8_context()); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/safe_builtins.h b/chromium/extensions/renderer/safe_builtins.h new file mode 100644 index 00000000000..83aaca9e537 --- /dev/null +++ b/chromium/extensions/renderer/safe_builtins.h @@ -0,0 +1,47 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SAFE_BUILTINS_H_ +#define EXTENSIONS_RENDERER_SAFE_BUILTINS_H_ + +#include "v8/include/v8.h" + +namespace extensions { +class ScriptContext; + +// A collection of safe builtin objects, in that they won't be tained by +// extensions overriding methods on them. +class SafeBuiltins { + public: + // Creates the v8::Extension which manages SafeBuiltins instances. + static v8::Extension* CreateV8Extension(); + + explicit SafeBuiltins(ScriptContext* context); + + virtual ~SafeBuiltins(); + + // Each method returns an object with methods taken from their respective + // builtin object's prototype, adapted to automatically call() themselves. + // + // Examples: + // Array.prototype.forEach.call(...) becomes Array.forEach(...) + // Object.prototype.toString.call(...) becomes Object.toString(...) + // Object.keys.call(...) becomes Object.keys(...) + v8::Local<v8::Object> GetArray() const; + v8::Local<v8::Object> GetFunction() const; + v8::Local<v8::Object> GetJSON() const; + // NOTE(kalman): VS2010 won't compile "GetObject", it mysteriously renames it + // to "GetObjectW" - hence GetObjekt. Sorry. + v8::Local<v8::Object> GetObjekt() const; + v8::Local<v8::Object> GetRegExp() const; + v8::Local<v8::Object> GetString() const; + v8::Local<v8::Object> GetError() const; + + private: + ScriptContext* context_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SAFE_BUILTINS_H_ diff --git a/chromium/extensions/renderer/safe_builtins_unittest.cc b/chromium/extensions/renderer/safe_builtins_unittest.cc new file mode 100644 index 00000000000..38910a2c5df --- /dev/null +++ b/chromium/extensions/renderer/safe_builtins_unittest.cc @@ -0,0 +1,66 @@ +// 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/renderer/module_system_test.h" + +namespace extensions { +namespace { + +class SafeBuiltinsUnittest : public ModuleSystemTest {}; + +TEST_F(SafeBuiltinsUnittest, TestNotOriginalObject) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "var assert = requireNative('assert');\n" + "Array.foo = 10;\n" + "assert.AssertTrue(!$Array.hasOwnProperty('foo'));\n"); + env()->module_system()->Require("test"); +} + +TEST_F(SafeBuiltinsUnittest, TestSelf) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "var assert = requireNative('assert');\n" + "Array.foo = 10;\n" + "assert.AssertTrue($Array.self.foo == 10);\n" + "var arr = $Array.self(1);\n" + "assert.AssertTrue(arr.length == 1);\n" + "assert.AssertTrue(arr[0] === undefined);\n"); + env()->module_system()->Require("test"); +} + +TEST_F(SafeBuiltinsUnittest, TestStaticFunction) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule("test", + "var assert = requireNative('assert');\n" + "Object.keys = function() {throw new Error()};\n" + "var obj = {a: 10};\n" + "var keys = $Object.keys(obj);\n" + "assert.AssertTrue(keys.length == 1);\n" + "assert.AssertTrue(keys[0] == 'a');\n"); + env()->module_system()->Require("test"); +} + +TEST_F(SafeBuiltinsUnittest, TestInstanceMethod) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->RegisterModule( + "test", + "var assert = requireNative('assert');\n" + "Array.prototype.push = function() {throw new Error();}\n" + "var arr = []\n" + "$Array.push(arr, 1);\n" + "assert.AssertTrue(arr.length == 1);\n" + "assert.AssertTrue(arr[0] == 1);\n"); + env()->module_system()->Require("test"); +} + +// NOTE: JSON is already tested in ExtensionApiTest.Messaging, via +// chrome/test/data/extensions/api_test/messaging/connect/page.js. + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/scoped_web_frame.cc b/chromium/extensions/renderer/scoped_web_frame.cc new file mode 100644 index 00000000000..88c4098b5c3 --- /dev/null +++ b/chromium/extensions/renderer/scoped_web_frame.cc @@ -0,0 +1,24 @@ +// Copyright 2015 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/scoped_web_frame.h" + +#include "third_party/WebKit/public/web/WebHeap.h" + +namespace extensions { + +ScopedWebFrame::ScopedWebFrame() : view_(nullptr), frame_(nullptr) { + view_ = blink::WebView::create(nullptr); + frame_ = blink::WebLocalFrame::create( + blink::WebTreeScopeType::Document, nullptr); + view_->setMainFrame(frame_); +} + +ScopedWebFrame::~ScopedWebFrame() { + view_->close(); + frame_->close(); + blink::WebHeap::collectAllGarbageForTesting(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/scoped_web_frame.h b/chromium/extensions/renderer/scoped_web_frame.h new file mode 100644 index 00000000000..948a289483b --- /dev/null +++ b/chromium/extensions/renderer/scoped_web_frame.h @@ -0,0 +1,34 @@ +// Copyright 2015 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. + +#ifndef SCOPED_WEB_FRAME_H_ +#define SCOPED_WEB_FRAME_H_ + +#include "base/macros.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace extensions { + +// ScopedWebFrame is a class to create a dummy webview and frame for testing. +// The dymmy webview and frame will be destructed when the scope exits. +class ScopedWebFrame { +public: + ScopedWebFrame(); + ~ScopedWebFrame(); + + blink::WebLocalFrame* frame() { return frame_; } + +private: + // The webview and the frame are kept alive by the ScopedWebFrame + // because they are not destructed unless ~ScopedWebFrame explicitly + // closes the webview and the frame. + blink::WebView* view_; + blink::WebLocalFrame* frame_; + DISALLOW_COPY_AND_ASSIGN(ScopedWebFrame); +}; + +} // namespace extensions + +#endif // SCOPED_WEB_FRAME_H_ diff --git a/chromium/extensions/renderer/script_context.cc b/chromium/extensions/renderer/script_context.cc new file mode 100644 index 00000000000..a1d8996a5da --- /dev/null +++ b/chromium/extensions/renderer/script_context.cc @@ -0,0 +1,489 @@ +// 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/renderer/script_context.h" + +#include "base/command_line.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_api.h" +#include "extensions/common/extension_urls.h" +#include "extensions/common/features/base_feature_provider.h" +#include "extensions/common/manifest_handlers/sandboxed_page_info.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "extensions/renderer/v8_helpers.h" +#include "gin/per_context_data.h" +#include "third_party/WebKit/public/platform/WebSecurityOrigin.h" +#include "third_party/WebKit/public/web/WebDataSource.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" +#include "v8/include/v8.h" + +using content::V8ValueConverter; + +namespace extensions { + +namespace { + +std::string GetContextTypeDescriptionString(Feature::Context context_type) { + switch (context_type) { + case Feature::UNSPECIFIED_CONTEXT: + return "UNSPECIFIED"; + case Feature::BLESSED_EXTENSION_CONTEXT: + return "BLESSED_EXTENSION"; + case Feature::UNBLESSED_EXTENSION_CONTEXT: + return "UNBLESSED_EXTENSION"; + case Feature::CONTENT_SCRIPT_CONTEXT: + return "CONTENT_SCRIPT"; + case Feature::WEB_PAGE_CONTEXT: + return "WEB_PAGE"; + case Feature::BLESSED_WEB_PAGE_CONTEXT: + return "BLESSED_WEB_PAGE"; + case Feature::WEBUI_CONTEXT: + return "WEBUI"; + case Feature::SERVICE_WORKER_CONTEXT: + return "SERVICE_WORKER"; + } + NOTREACHED(); + return std::string(); +} + +static std::string ToStringOrDefault( + const v8::Local<v8::String>& v8_string, + const std::string& dflt) { + if (v8_string.IsEmpty()) + return dflt; + std::string ascii_value = *v8::String::Utf8Value(v8_string); + return ascii_value.empty() ? dflt : ascii_value; +} + +} // namespace + +// A gin::Runner that delegates to its ScriptContext. +class ScriptContext::Runner : public gin::Runner { + public: + explicit Runner(ScriptContext* context); + + // gin::Runner overrides. + void Run(const std::string& source, + const std::string& resource_name) override; + v8::Local<v8::Value> Call(v8::Local<v8::Function> function, + v8::Local<v8::Value> receiver, + int argc, + v8::Local<v8::Value> argv[]) override; + gin::ContextHolder* GetContextHolder() override; + + private: + ScriptContext* context_; +}; + +ScriptContext::ScriptContext(const v8::Local<v8::Context>& v8_context, + blink::WebLocalFrame* web_frame, + const Extension* extension, + Feature::Context context_type, + const Extension* effective_extension, + Feature::Context effective_context_type) + : is_valid_(true), + v8_context_(v8_context->GetIsolate(), v8_context), + web_frame_(web_frame), + extension_(extension), + context_type_(context_type), + effective_extension_(effective_extension), + effective_context_type_(effective_context_type), + safe_builtins_(this), + isolate_(v8_context->GetIsolate()), + url_(web_frame_ ? GetDataSourceURLForFrame(web_frame_) : GURL()), + runner_(new Runner(this)) { + VLOG(1) << "Created context:\n" << GetDebugString(); + gin::PerContextData* gin_data = gin::PerContextData::From(v8_context); + CHECK(gin_data); + gin_data->set_runner(runner_.get()); +} + +ScriptContext::~ScriptContext() { + VLOG(1) << "Destroyed context for extension\n" + << " extension id: " << GetExtensionID() << "\n" + << " effective extension id: " + << (effective_extension_.get() ? effective_extension_->id() : ""); + CHECK(!is_valid_) << "ScriptContexts must be invalidated before destruction"; +} + +// static +bool ScriptContext::IsSandboxedPage(const GURL& url) { + // TODO(kalman): This is checking the wrong thing. See comment in + // HasAccessOrThrowError. + if (url.SchemeIs(kExtensionScheme)) { + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(url.host()); + if (extension) { + return SandboxedPageInfo::IsSandboxedPage(extension, url.path()); + } + } + return false; +} + +void ScriptContext::Invalidate() { + DCHECK(thread_checker_.CalledOnValidThread()); + CHECK(is_valid_); + is_valid_ = false; + + // TODO(kalman): Make ModuleSystem use AddInvalidationObserver. + // Ownership graph is a bit weird here. + if (module_system_) + module_system_->Invalidate(); + + // Swap |invalidate_observers_| to a local variable to clear it, and to make + // sure it's not mutated as we iterate. + std::vector<base::Closure> observers; + observers.swap(invalidate_observers_); + for (const base::Closure& observer : observers) { + observer.Run(); + } + DCHECK(invalidate_observers_.empty()) + << "Invalidation observers cannot be added during invalidation"; + + runner_.reset(); + v8_context_.Reset(); +} + +void ScriptContext::AddInvalidationObserver(const base::Closure& observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + invalidate_observers_.push_back(observer); +} + +const std::string& ScriptContext::GetExtensionID() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return extension_.get() ? extension_->id() : base::EmptyString(); +} + +content::RenderFrame* ScriptContext::GetRenderFrame() const { + DCHECK(thread_checker_.CalledOnValidThread()); + if (web_frame_) + return content::RenderFrame::FromWebFrame(web_frame_); + return NULL; +} + +v8::Local<v8::Value> ScriptContext::CallFunction( + const v8::Local<v8::Function>& function, + int argc, + v8::Local<v8::Value> argv[]) const { + DCHECK(thread_checker_.CalledOnValidThread()); + v8::EscapableHandleScope handle_scope(isolate()); + v8::Context::Scope scope(v8_context()); + + v8::MicrotasksScope microtasks( + isolate(), v8::MicrotasksScope::kDoNotRunMicrotasks); + if (!is_valid_) { + return handle_scope.Escape( + v8::Local<v8::Primitive>(v8::Undefined(isolate()))); + } + + v8::Local<v8::Object> global = v8_context()->Global(); + if (!web_frame_) + return handle_scope.Escape(function->Call(global, argc, argv)); + return handle_scope.Escape( + v8::Local<v8::Value>(web_frame_->callFunctionEvenIfScriptDisabled( + function, global, argc, argv))); +} + +v8::Local<v8::Value> ScriptContext::CallFunction( + const v8::Local<v8::Function>& function) const { + DCHECK(thread_checker_.CalledOnValidThread()); + return CallFunction(function, 0, nullptr); +} + +Feature::Availability ScriptContext::GetAvailability( + const std::string& api_name) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (base::StartsWith(api_name, "test", base::CompareCase::SENSITIVE)) { + bool allowed = base::CommandLine::ForCurrentProcess()-> + HasSwitch(::switches::kTestType); + Feature::AvailabilityResult result = + allowed ? Feature::IS_AVAILABLE : Feature::MISSING_COMMAND_LINE_SWITCH; + return Feature::Availability(result, + allowed ? "" : "Only allowed in tests"); + } + // Hack: Hosted apps should have the availability of messaging APIs based on + // the URL of the page (which might have access depending on some extension + // with externally_connectable), not whether the app has access to messaging + // (which it won't). + const Extension* extension = extension_.get(); + if (extension && extension->is_hosted_app() && + (api_name == "runtime.connect" || api_name == "runtime.sendMessage")) { + extension = NULL; + } + return ExtensionAPI::GetSharedInstance()->IsAvailable(api_name, extension, + context_type_, url()); +} + +void ScriptContext::DispatchEvent(const char* event_name, + v8::Local<v8::Array> args) const { + DCHECK(thread_checker_.CalledOnValidThread()); + v8::HandleScope handle_scope(isolate()); + v8::Context::Scope context_scope(v8_context()); + + v8::Local<v8::Value> argv[] = {v8::String::NewFromUtf8(isolate(), event_name), + args}; + module_system_->CallModuleMethod( + kEventBindings, "dispatchEvent", arraysize(argv), argv); +} + +std::string ScriptContext::GetContextTypeDescription() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return GetContextTypeDescriptionString(context_type_); +} + +std::string ScriptContext::GetEffectiveContextTypeDescription() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return GetContextTypeDescriptionString(effective_context_type_); +} + +bool ScriptContext::IsAnyFeatureAvailableToContext(const Feature& api) { + DCHECK(thread_checker_.CalledOnValidThread()); + return ExtensionAPI::GetSharedInstance()->IsAnyFeatureAvailableToContext( + api, extension(), context_type(), GetDataSourceURLForFrame(web_frame())); +} + +// static +GURL ScriptContext::GetDataSourceURLForFrame(const blink::WebFrame* frame) { + // Normally we would use frame->document().url() to determine the document's + // URL, but to decide whether to inject a content script, we use the URL from + // the data source. This "quirk" helps prevents content scripts from + // inadvertently adding DOM elements to the compose iframe in Gmail because + // the compose iframe's dataSource URL is about:blank, but the document URL + // changes to match the parent document after Gmail document.writes into + // it to create the editor. + // http://code.google.com/p/chromium/issues/detail?id=86742 + blink::WebDataSource* data_source = frame->provisionalDataSource() + ? frame->provisionalDataSource() + : frame->dataSource(); + return data_source ? GURL(data_source->request().url()) : GURL(); +} + +// static +GURL ScriptContext::GetEffectiveDocumentURL(const blink::WebFrame* frame, + const GURL& document_url, + bool match_about_blank) { + // Common scenario. If |match_about_blank| is false (as is the case in most + // extensions), or if the frame is not an about:-page, just return + // |document_url| (supposedly the URL of the frame). + if (!match_about_blank || !document_url.SchemeIs(url::kAboutScheme)) + return document_url; + + // Non-sandboxed about:blank and about:srcdoc pages inherit their security + // origin from their parent frame/window. So, traverse the frame/window + // hierarchy to find the closest non-about:-page and return its URL. + const blink::WebFrame* parent = frame; + do { + if (parent->parent()) + parent = parent->parent(); + else if (parent->opener() != parent) + parent = parent->opener(); + else + parent = nullptr; + } while (parent && !parent->document().isNull() && + GURL(parent->document().url()).SchemeIs(url::kAboutScheme)); + + if (parent && !parent->document().isNull()) { + // Only return the parent URL if the frame can access it. + const blink::WebDocument& parent_document = parent->document(); + if (frame->document().getSecurityOrigin().canAccess( + parent_document.getSecurityOrigin())) { + return parent_document.url(); + } + } + return document_url; +} + +ScriptContext* ScriptContext::GetContext() { + DCHECK(thread_checker_.CalledOnValidThread()); + return this; +} + +void ScriptContext::OnResponseReceived(const std::string& name, + int request_id, + bool success, + const base::ListValue& response, + const std::string& error) { + DCHECK(thread_checker_.CalledOnValidThread()); + v8::HandleScope handle_scope(isolate()); + + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + v8::Local<v8::Value> argv[] = { + v8::Integer::New(isolate(), request_id), + v8::String::NewFromUtf8(isolate(), name.c_str()), + v8::Boolean::New(isolate(), success), + converter->ToV8Value(&response, + v8::Local<v8::Context>::New(isolate(), v8_context_)), + v8::String::NewFromUtf8(isolate(), error.c_str())}; + + v8::Local<v8::Value> retval = module_system()->CallModuleMethod( + "sendRequest", "handleResponse", arraysize(argv), argv); + + // In debug, the js will validate the callback parameters and return a + // string if a validation error has occured. + DCHECK(retval.IsEmpty() || retval->IsUndefined()) + << *v8::String::Utf8Value(retval); +} + +bool ScriptContext::HasAPIPermission(APIPermission::ID permission) const { + DCHECK(thread_checker_.CalledOnValidThread()); + if (effective_extension_.get()) { + return effective_extension_->permissions_data()->HasAPIPermission( + permission); + } + if (context_type() == Feature::WEB_PAGE_CONTEXT) { + // Only web page contexts may be granted content capabilities. Other + // contexts are either privileged WebUI or extensions with their own set of + // permissions. + if (content_capabilities_.find(permission) != content_capabilities_.end()) + return true; + } + return false; +} + +bool ScriptContext::HasAccessOrThrowError(const std::string& name) { + DCHECK(thread_checker_.CalledOnValidThread()); + // Theoretically[1] we could end up with bindings being injected into + // sandboxed frames, for example content scripts. Don't let them execute API + // functions. + // + // In any case, this check is silly. The frame's document's security origin + // already tells us if it's sandboxed. The only problem is that until + // crbug.com/466373 is fixed, we don't know the security origin up-front and + // may not know it here, either. + // + // [1] citation needed. This ScriptContext should already be in a state that + // doesn't allow this, from ScriptContextSet::ClassifyJavaScriptContext. + if (extension() && + SandboxedPageInfo::IsSandboxedPage(extension(), url_.path())) { + static const char kMessage[] = + "%s cannot be used within a sandboxed frame."; + std::string error_msg = base::StringPrintf(kMessage, name.c_str()); + isolate()->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate(), error_msg.c_str()))); + return false; + } + + Feature::Availability availability = GetAvailability(name); + if (!availability.is_available()) { + isolate()->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate(), availability.message().c_str()))); + return false; + } + + return true; +} + +std::string ScriptContext::GetDebugString() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return base::StringPrintf( + " extension id: %s\n" + " frame: %p\n" + " URL: %s\n" + " context_type: %s\n" + " effective extension id: %s\n" + " effective context type: %s", + extension_.get() ? extension_->id().c_str() : "(none)", web_frame_, + url_.spec().c_str(), GetContextTypeDescription().c_str(), + effective_extension_.get() ? effective_extension_->id().c_str() + : "(none)", + GetEffectiveContextTypeDescription().c_str()); +} + +std::string ScriptContext::GetStackTraceAsString() const { + DCHECK(thread_checker_.CalledOnValidThread()); + v8::Local<v8::StackTrace> stack_trace = + v8::StackTrace::CurrentStackTrace(isolate(), 10); + if (stack_trace.IsEmpty() || stack_trace->GetFrameCount() <= 0) { + return " <no stack trace>"; + } + std::string result; + for (int i = 0; i < stack_trace->GetFrameCount(); ++i) { + v8::Local<v8::StackFrame> frame = stack_trace->GetFrame(i); + CHECK(!frame.IsEmpty()); + result += base::StringPrintf( + "\n at %s (%s:%d:%d)", + ToStringOrDefault(frame->GetFunctionName(), "<anonymous>").c_str(), + ToStringOrDefault(frame->GetScriptName(), "<anonymous>").c_str(), + frame->GetLineNumber(), frame->GetColumn()); + } + return result; +} + +v8::Local<v8::Value> ScriptContext::RunScript( + v8::Local<v8::String> name, + v8::Local<v8::String> code, + const RunScriptExceptionHandler& exception_handler) { + DCHECK(thread_checker_.CalledOnValidThread()); + v8::EscapableHandleScope handle_scope(isolate()); + v8::Context::Scope context_scope(v8_context()); + + // Prepend extensions:: to |name| so that internal code can be differentiated + // from external code in stack traces. This has no effect on behaviour. + std::string internal_name = + base::StringPrintf("extensions::%s", *v8::String::Utf8Value(name)); + + if (internal_name.size() >= v8::String::kMaxLength) { + NOTREACHED() << "internal_name is too long."; + return v8::Undefined(isolate()); + } + + v8::MicrotasksScope microtasks( + isolate(), v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::TryCatch try_catch(isolate()); + try_catch.SetCaptureMessage(true); + v8::ScriptOrigin origin( + v8_helpers::ToV8StringUnsafe(isolate(), internal_name.c_str())); + v8::Local<v8::Script> script; + if (!v8::Script::Compile(v8_context(), code, &origin).ToLocal(&script)) { + exception_handler.Run(try_catch); + return v8::Undefined(isolate()); + } + + v8::Local<v8::Value> result; + if (!script->Run(v8_context()).ToLocal(&result)) { + exception_handler.Run(try_catch); + return v8::Undefined(isolate()); + } + + return handle_scope.Escape(result); +} + +ScriptContext::Runner::Runner(ScriptContext* context) : context_(context) { +} + +void ScriptContext::Runner::Run(const std::string& source, + const std::string& resource_name) { + context_->module_system()->RunString(source, resource_name); +} + +v8::Local<v8::Value> ScriptContext::Runner::Call( + v8::Local<v8::Function> function, + v8::Local<v8::Value> receiver, + int argc, + v8::Local<v8::Value> argv[]) { + return context_->CallFunction(function, argc, argv); +} + +gin::ContextHolder* ScriptContext::Runner::GetContextHolder() { + v8::HandleScope handle_scope(context_->isolate()); + return gin::PerContextData::From(context_->v8_context())->context_holder(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_context.h b/chromium/extensions/renderer/script_context.h new file mode 100644 index 00000000000..48c873ce1fc --- /dev/null +++ b/chromium/extensions/renderer/script_context.h @@ -0,0 +1,259 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_CONTEXT_H_ +#define EXTENSIONS_RENDERER_SCRIPT_CONTEXT_H_ + +#include <string> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/threading/thread_checker.h" +#include "extensions/common/features/feature.h" +#include "extensions/common/permissions/api_permission_set.h" +#include "extensions/renderer/module_system.h" +#include "extensions/renderer/request_sender.h" +#include "extensions/renderer/safe_builtins.h" +#include "gin/runner.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +namespace blink { +class WebFrame; +class WebLocalFrame; +} + +namespace content { +class RenderFrame; +} + +namespace extensions { +class Extension; + +// Extensions wrapper for a v8::Context. +// +// v8::Contexts can be constructed on any thread, and must only be accessed or +// destroyed that thread. +// +// Note that ScriptContexts bound to worker threads will not have the full +// functionality as those bound to the main RenderThread. +class ScriptContext : public RequestSender::Source { + public: + using RunScriptExceptionHandler = base::Callback<void(const v8::TryCatch&)>; + + ScriptContext(const v8::Local<v8::Context>& context, + blink::WebLocalFrame* frame, + const Extension* extension, + Feature::Context context_type, + const Extension* effective_extension, + Feature::Context effective_context_type); + ~ScriptContext() override; + + // Returns whether |url| from any Extension in |extension_set| is sandboxed, + // as declared in each Extension's manifest. + // TODO(kalman): Delete this when crbug.com/466373 is fixed. + // See comment in HasAccessOrThrowError. + static bool IsSandboxedPage(const GURL& url); + + // Clears the WebFrame for this contexts and invalidates the associated + // ModuleSystem. + void Invalidate(); + + // Registers |observer| to be run when this context is invalidated. Closures + // are run immediately when Invalidate() is called, not in a message loop. + void AddInvalidationObserver(const base::Closure& observer); + + // Returns true if this context is still valid, false if it isn't. + // A context becomes invalid via Invalidate(). + bool is_valid() const { return is_valid_; } + + v8::Local<v8::Context> v8_context() const { + return v8::Local<v8::Context>::New(isolate_, v8_context_); + } + + const Extension* extension() const { return extension_.get(); } + + const Extension* effective_extension() const { + return effective_extension_.get(); + } + + blink::WebLocalFrame* web_frame() const { return web_frame_; } + + Feature::Context context_type() const { return context_type_; } + + Feature::Context effective_context_type() const { + return effective_context_type_; + } + + void set_module_system(scoped_ptr<ModuleSystem> module_system) { + module_system_ = std::move(module_system); + } + + ModuleSystem* module_system() { return module_system_.get(); } + + SafeBuiltins* safe_builtins() { return &safe_builtins_; } + + const SafeBuiltins* safe_builtins() const { return &safe_builtins_; } + + // Returns the ID of the extension associated with this context, or empty + // string if there is no such extension. + const std::string& GetExtensionID() const; + + // Returns the RenderFrame associated with this context. Can return NULL if + // the context is in the process of being destroyed. + content::RenderFrame* GetRenderFrame() const; + + // Runs |function| with appropriate scopes. Doesn't catch exceptions, callers + // must do that if they want. + // + // USE THIS METHOD RATHER THAN v8::Function::Call WHEREVER POSSIBLE. + v8::Local<v8::Value> CallFunction(const v8::Local<v8::Function>& function, + int argc, + v8::Local<v8::Value> argv[]) const; + v8::Local<v8::Value> CallFunction( + const v8::Local<v8::Function>& function) const; + + void DispatchEvent(const char* event_name, v8::Local<v8::Array> args) const; + + // Returns the availability of the API |api_name|. + Feature::Availability GetAvailability(const std::string& api_name); + + // Returns a string description of the type of context this is. + std::string GetContextTypeDescription() const; + + // Returns a string description of the effective type of context this is. + std::string GetEffectiveContextTypeDescription() const; + + v8::Isolate* isolate() const { return isolate_; } + + // Get the URL of this context's web frame. + // + // TODO(kalman): Remove this and replace with a GetOrigin() call which reads + // of WebDocument::getSecurityOrigin(): + // - The URL can change (e.g. pushState) but the origin cannot. Luckily it + // appears as though callers don't make security decisions based on the + // result of url() so it's not a problem... yet. + // - Origin is the correct check to be making. + // - It might let us remove the about:blank resolving? + const GURL& url() const { return url_; } + + // Sets the URL of this ScriptContext. Usually this will automatically be set + // on construction, unless this isn't constructed with enough information to + // determine the URL (e.g. frame was null). + // TODO(kalman): Make this a constructor parameter (as an origin). + void set_url(const GURL& url) { url_ = url; } + + // Returns whether the API |api| or any part of the API could be + // available in this context without taking into account the context's + // extension. + bool IsAnyFeatureAvailableToContext(const extensions::Feature& api); + + // Utility to get the URL we will match against for a frame. If the frame has + // committed, this is the commited URL. Otherwise it is the provisional URL. + // The returned URL may be invalid. + static GURL GetDataSourceURLForFrame(const blink::WebFrame* frame); + + // Returns the first non-about:-URL in the document hierarchy above and + // including |frame|. The document hierarchy is only traversed if + // |document_url| is an about:-URL and if |match_about_blank| is true. + static GURL GetEffectiveDocumentURL(const blink::WebFrame* frame, + const GURL& document_url, + bool match_about_blank); + + // RequestSender::Source implementation. + ScriptContext* GetContext() override; + void OnResponseReceived(const std::string& name, + int request_id, + bool success, + const base::ListValue& response, + const std::string& error) override; + + // Grants a set of content capabilities to this context. + void set_content_capabilities(const APIPermissionSet& capabilities) { + content_capabilities_ = capabilities; + } + + // Indicates if this context has an effective API permission either by being + // a context for an extension which has that permission, or by being a web + // context which has been granted the corresponding capability by an + // extension. + bool HasAPIPermission(APIPermission::ID permission) const; + + // Throws an Error in this context's JavaScript context, if this context does + // not have access to |name|. Returns true if this context has access (i.e. + // no exception thrown), false if it does not (i.e. an exception was thrown). + bool HasAccessOrThrowError(const std::string& name); + + // Returns a string representation of this ScriptContext, for debugging. + std::string GetDebugString() const; + + // Gets the current stack trace as a multi-line string to be logged. + std::string GetStackTraceAsString() const; + + // Runs |code|, labelling the script that gets created as |name| (the name is + // used in the devtools and stack traces). |exception_handler| will be called + // re-entrantly if an exception is thrown during the script's execution. + v8::Local<v8::Value> RunScript( + v8::Local<v8::String> name, + v8::Local<v8::String> code, + const RunScriptExceptionHandler& exception_handler); + + private: + class Runner; + + // Whether this context is valid. + bool is_valid_; + + // The v8 context the bindings are accessible to. + v8::Global<v8::Context> v8_context_; + + // The WebLocalFrame associated with this context. This can be NULL because + // this object can outlive is destroyed asynchronously. + blink::WebLocalFrame* web_frame_; + + // The extension associated with this context, or NULL if there is none. This + // might be a hosted app in the case that this context is hosting a web URL. + scoped_refptr<const Extension> extension_; + + // The type of context. + Feature::Context context_type_; + + // The effective extension associated with this context, or NULL if there is + // none. This is different from the above extension if this context is in an + // about:blank iframe for example. + scoped_refptr<const Extension> effective_extension_; + + // The type of context. + Feature::Context effective_context_type_; + + // Owns and structures the JS that is injected to set up extension bindings. + scoped_ptr<ModuleSystem> module_system_; + + // Contains safe copies of builtin objects like Function.prototype. + SafeBuiltins safe_builtins_; + + // The set of capabilities granted to this context by extensions. + APIPermissionSet content_capabilities_; + + // A list of base::Closure instances as an observer interface for + // invalidation. + std::vector<base::Closure> invalidate_observers_; + + v8::Isolate* isolate_; + + GURL url_; + + scoped_ptr<Runner> runner_; + + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(ScriptContext); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_CONTEXT_H_ diff --git a/chromium/extensions/renderer/script_context_browsertest.cc b/chromium/extensions/renderer/script_context_browsertest.cc new file mode 100644 index 00000000000..2559a692910 --- /dev/null +++ b/chromium/extensions/renderer/script_context_browsertest.cc @@ -0,0 +1,91 @@ +// 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 "chrome/test/base/chrome_render_view_test.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/test/frame_load_waiter.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "url/gurl.h" + +using blink::WebFrame; + +namespace extensions { +namespace { + +class ScriptContextTest : public ChromeRenderViewTest { + protected: + GURL GetEffectiveDocumentURL(const WebFrame* frame) { + return ScriptContext::GetEffectiveDocumentURL( + frame, frame->document().url(), true); + } +}; + +TEST_F(ScriptContextTest, GetEffectiveDocumentURL) { + GURL top_url("http://example.com/"); + GURL different_url("http://example.net/"); + GURL blank_url("about:blank"); + GURL srcdoc_url("about:srcdoc"); + + const char frame_html[] = + "<iframe name='frame1' srcdoc=\"" + " <iframe name='frame1_1'></iframe>" + " <iframe name='frame1_2' sandbox=''></iframe>" + "\"></iframe>" + "<iframe name='frame2' sandbox='' srcdoc=\"" + " <iframe name='frame2_1'></iframe>" + "\"></iframe>" + "<iframe name='frame3'></iframe>"; + + const char frame3_html[] = "<iframe name='frame3_1'></iframe>"; + + WebFrame* frame = GetMainFrame(); + ASSERT_TRUE(frame); + + frame->loadHTMLString(frame_html, top_url); + content::FrameLoadWaiter(content::RenderFrame::FromWebFrame(frame)).Wait(); + + WebFrame* frame1 = frame->findChildByName("frame1"); + ASSERT_TRUE(frame1); + WebFrame* frame1_1 = frame1->findChildByName("frame1_1"); + ASSERT_TRUE(frame1_1); + WebFrame* frame1_2 = frame1->findChildByName("frame1_2"); + ASSERT_TRUE(frame1_2); + WebFrame* frame2 = frame->findChildByName("frame2"); + ASSERT_TRUE(frame2); + WebFrame* frame2_1 = frame2->findChildByName("frame2_1"); + ASSERT_TRUE(frame2_1); + WebFrame* frame3 = frame->findChildByName("frame3"); + ASSERT_TRUE(frame3); + + // Load a blank document in a frame from a different origin. + frame3->loadHTMLString(frame3_html, different_url); + content::FrameLoadWaiter(content::RenderFrame::FromWebFrame(frame3)).Wait(); + + WebFrame* frame3_1 = frame->findChildByName("frame3"); + ASSERT_TRUE(frame3_1); + + // Top-level frame + EXPECT_EQ(GetEffectiveDocumentURL(frame), top_url); + // top -> srcdoc = inherit + EXPECT_EQ(GetEffectiveDocumentURL(frame1), top_url); + // top -> srcdoc -> about:blank = inherit + EXPECT_EQ(GetEffectiveDocumentURL(frame1_1), top_url); + // top -> srcdoc -> about:blank sandboxed = same URL + EXPECT_EQ(GetEffectiveDocumentURL(frame1_2), blank_url); + + // top -> srcdoc [sandboxed] = same URL + EXPECT_EQ(GetEffectiveDocumentURL(frame2), srcdoc_url); + // top -> srcdoc [sandboxed] -> about:blank = same URL + EXPECT_EQ(GetEffectiveDocumentURL(frame2_1), blank_url); + + // top -> different origin = different origin + EXPECT_EQ(GetEffectiveDocumentURL(frame3), different_url); + // top -> different origin -> about:blank = inherit + EXPECT_EQ(GetEffectiveDocumentURL(frame3_1), different_url); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/script_context_set.cc b/chromium/extensions/renderer/script_context_set.cc new file mode 100644 index 00000000000..33df67e58d1 --- /dev/null +++ b/chromium/extensions/renderer/script_context_set.cc @@ -0,0 +1,223 @@ +// 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/renderer/script_context_set.h" + +#include "base/message_loop/message_loop.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension.h" +#include "extensions/renderer/extension_groups.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_injection.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "v8/include/v8.h" + +namespace extensions { + +namespace { +// There is only ever one instance of the ScriptContextSet. +ScriptContextSet* g_context_set = nullptr; +} + +ScriptContextSet::ScriptContextSet(ExtensionIdSet* active_extension_ids) + : active_extension_ids_(active_extension_ids) { + DCHECK(!g_context_set); + g_context_set = this; +} + +ScriptContextSet::~ScriptContextSet() { + g_context_set = nullptr; +} + +ScriptContext* ScriptContextSet::Register( + blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& v8_context, + int extension_group, + int world_id) { + const Extension* extension = + GetExtensionFromFrameAndWorld(frame, world_id, false); + const Extension* effective_extension = + GetExtensionFromFrameAndWorld(frame, world_id, true); + + GURL frame_url = ScriptContext::GetDataSourceURLForFrame(frame); + Feature::Context context_type = + ClassifyJavaScriptContext(extension, extension_group, frame_url, + frame->document().getSecurityOrigin()); + Feature::Context effective_context_type = ClassifyJavaScriptContext( + effective_extension, extension_group, + ScriptContext::GetEffectiveDocumentURL(frame, frame_url, true), + frame->document().getSecurityOrigin()); + + ScriptContext* context = + new ScriptContext(v8_context, frame, extension, context_type, + effective_extension, effective_context_type); + contexts_.insert(context); // takes ownership + return context; +} + +void ScriptContextSet::Remove(ScriptContext* context) { + if (contexts_.erase(context)) { + context->Invalidate(); + base::MessageLoop::current()->DeleteSoon(FROM_HERE, context); + } +} + +ScriptContext* ScriptContextSet::GetCurrent() const { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate->InContext() ? GetByV8Context(isolate->GetCurrentContext()) + : nullptr; +} + +ScriptContext* ScriptContextSet::GetByV8Context( + const v8::Local<v8::Context>& v8_context) const { + for (ScriptContext* script_context : contexts_) { + if (script_context->v8_context() == v8_context) + return script_context; + } + return nullptr; +} + +ScriptContext* ScriptContextSet::GetContextByObject( + const v8::Local<v8::Object>& object) { + return GetContextByV8Context(object->CreationContext()); +} + +ScriptContext* ScriptContextSet::GetContextByV8Context( + const v8::Local<v8::Context>& v8_context) { + // g_context_set can be null in unittests. + return g_context_set ? g_context_set->GetByV8Context(v8_context) : nullptr; +} + +void ScriptContextSet::ForEach( + const std::string& extension_id, + content::RenderFrame* render_frame, + const base::Callback<void(ScriptContext*)>& callback) const { + // We copy the context list, because calling into javascript may modify it + // out from under us. + std::set<ScriptContext*> contexts_copy = contexts_; + + for (ScriptContext* context : contexts_copy) { + // For the same reason as above, contexts may become invalid while we run. + if (!context->is_valid()) + continue; + + if (!extension_id.empty()) { + const Extension* extension = context->extension(); + if (!extension || (extension_id != extension->id())) + continue; + } + + content::RenderFrame* context_render_frame = context->GetRenderFrame(); + if (!context_render_frame) + continue; + + if (render_frame && render_frame != context_render_frame) + continue; + + callback.Run(context); + } +} + +std::set<ScriptContext*> ScriptContextSet::OnExtensionUnloaded( + const std::string& extension_id) { + std::set<ScriptContext*> removed; + ForEach(extension_id, base::Bind(&ScriptContextSet::RecordAndRemove, + base::Unretained(this), &removed)); + return removed; +} + +const Extension* ScriptContextSet::GetExtensionFromFrameAndWorld( + const blink::WebLocalFrame* frame, + int world_id, + bool use_effective_url) { + std::string extension_id; + if (world_id != 0) { + // Isolated worlds (content script). + extension_id = ScriptInjection::GetHostIdForIsolatedWorld(world_id); + } else { + // Extension pages (chrome-extension:// URLs). + GURL frame_url = ScriptContext::GetDataSourceURLForFrame(frame); + frame_url = ScriptContext::GetEffectiveDocumentURL(frame, frame_url, + use_effective_url); + extension_id = + RendererExtensionRegistry::Get()->GetExtensionOrAppIDByURL(frame_url); + } + + // There are conditions where despite a context being associated with an + // extension, no extension actually gets found. Ignore "invalid" because CSP + // blocks extension page loading by switching the extension ID to "invalid". + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(extension_id); + if (!extension && !extension_id.empty() && extension_id != "invalid") { + // TODO(kalman): Do something here? + } + return extension; +} + +Feature::Context ScriptContextSet::ClassifyJavaScriptContext( + const Extension* extension, + int extension_group, + const GURL& url, + const blink::WebSecurityOrigin& origin) { + // WARNING: This logic must match ProcessMap::GetContextType, as much as + // possible. + + DCHECK_GE(extension_group, 0); + if (extension_group == EXTENSION_GROUP_CONTENT_SCRIPTS) { + return extension ? // TODO(kalman): when does this happen? + Feature::CONTENT_SCRIPT_CONTEXT + : Feature::UNSPECIFIED_CONTEXT; + } + + // We have an explicit check for sandboxed pages before checking whether the + // extension is active in this process because: + // 1. Sandboxed pages run in the same process as regular extension pages, so + // the extension is considered active. + // 2. ScriptContext creation (which triggers bindings injection) happens + // before the SecurityContext is updated with the sandbox flags (after + // reading the CSP header), so the caller can't check if the context's + // security origin is unique yet. + if (ScriptContext::IsSandboxedPage(url)) + return Feature::WEB_PAGE_CONTEXT; + + if (extension && active_extension_ids_->count(extension->id()) > 0) { + // |extension| is active in this process, but it could be either a true + // extension process or within the extent of a hosted app. In the latter + // case this would usually be considered a (blessed) web page context, + // unless the extension in question is a component extension, in which case + // we cheat and call it blessed. + return (extension->is_hosted_app() && + extension->location() != Manifest::COMPONENT) + ? Feature::BLESSED_WEB_PAGE_CONTEXT + : Feature::BLESSED_EXTENSION_CONTEXT; + } + + // TODO(kalman): This isUnique() check is wrong, it should be performed as + // part of ScriptContext::IsSandboxedPage(). + if (!origin.isUnique() && + RendererExtensionRegistry::Get()->ExtensionBindingsAllowed(url)) { + if (!extension) // TODO(kalman): when does this happen? + return Feature::UNSPECIFIED_CONTEXT; + return extension->is_hosted_app() ? Feature::BLESSED_WEB_PAGE_CONTEXT + : Feature::UNBLESSED_EXTENSION_CONTEXT; + } + + if (!url.is_valid()) + return Feature::UNSPECIFIED_CONTEXT; + + if (url.SchemeIs(content::kChromeUIScheme)) + return Feature::WEBUI_CONTEXT; + + return Feature::WEB_PAGE_CONTEXT; +} + +void ScriptContextSet::RecordAndRemove(std::set<ScriptContext*>* removed, + ScriptContext* context) { + removed->insert(context); + Remove(context); // Note: context deletion is deferred to the message loop. +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_context_set.h b/chromium/extensions/renderer/script_context_set.h new file mode 100644 index 00000000000..355139747a4 --- /dev/null +++ b/chromium/extensions/renderer/script_context_set.h @@ -0,0 +1,145 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_CONTEXT_SET_H_ +#define EXTENSIONS_RENDERER_SCRIPT_CONTEXT_SET_H_ + +#include <stddef.h> + +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "extensions/common/extension.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +class GURL; + +namespace base { +class ListValue; +} + +namespace blink { +class WebLocalFrame; +class WebSecurityOrigin; +} + +namespace content { +class RenderFrame; +} + +namespace extensions { +class ScriptContext; + +// A container of ScriptContexts, responsible for both creating and managing +// them. +// +// Since calling JavaScript within a context can cause any number of contexts +// to be created or destroyed, this has additional smarts to help with the set +// changing underneath callers. +class ScriptContextSet { + public: + ScriptContextSet( + // Set of the IDs of extensions that are active in this process. + // Must outlive this. TODO(kalman): Combine this and |extensions|. + ExtensionIdSet* active_extension_ids); + + ~ScriptContextSet(); + + // Returns the number of contexts being tracked by this set. + // This may also include invalid contexts. TODO(kalman): Useful? + size_t size() const { return contexts_.size(); } + + // Creates and starts managing a new ScriptContext. Ownership is held. + // Returns a weak reference to the new ScriptContext. + ScriptContext* Register(blink::WebLocalFrame* frame, + const v8::Local<v8::Context>& v8_context, + int extension_group, + int world_id); + + // If the specified context is contained in this set, remove it, then delete + // it asynchronously. After this call returns the context object will still + // be valid, but its frame() pointer will be cleared. + void Remove(ScriptContext* context); + + // Gets the ScriptContext corresponding to v8::Context::GetCurrent(), or + // NULL if no such context exists. + ScriptContext* GetCurrent() const; + + // Gets the ScriptContext corresponding to the specified + // v8::Context or NULL if no such context exists. + ScriptContext* GetByV8Context(const v8::Local<v8::Context>& context) const; + // Static equivalent of the above. + static ScriptContext* GetContextByV8Context( + const v8::Local<v8::Context>& context); + + // Returns the ScriptContext corresponding to the V8 context that created the + // given |object|. + static ScriptContext* GetContextByObject(const v8::Local<v8::Object>& object); + + // Synchronously runs |callback| with each ScriptContext that belongs to + // |extension_id| in |render_frame|. + // + // An empty |extension_id| will match all extensions, and a null + // |render_frame| will match all render views, but try to use the inline + // variants of these methods instead. + void ForEach(const std::string& extension_id, + content::RenderFrame* render_frame, + const base::Callback<void(ScriptContext*)>& callback) const; + // ForEach which matches all extensions. + void ForEach(content::RenderFrame* render_frame, + const base::Callback<void(ScriptContext*)>& callback) const { + ForEach(std::string(), render_frame, callback); + } + // ForEach which matches all render views. + void ForEach(const std::string& extension_id, + const base::Callback<void(ScriptContext*)>& callback) const { + ForEach(extension_id, nullptr, callback); + } + + // Cleans up contexts belonging to an unloaded extension. + // + // Returns the set of ScriptContexts that were removed as a result. These + // are safe to interact with until the end of the current event loop, since + // they're deleted asynchronously. + std::set<ScriptContext*> OnExtensionUnloaded(const std::string& extension_id); + + private: + // Finds the extension for the JavaScript context associated with the + // specified |frame| and isolated world. If |world_id| is zero, finds the + // extension ID associated with the main world's JavaScript context. If the + // JavaScript context isn't from an extension, returns empty string. + const Extension* GetExtensionFromFrameAndWorld( + const blink::WebLocalFrame* frame, + int world_id, + bool use_effective_url); + + // Returns the Feature::Context type of context for a JavaScript context. + Feature::Context ClassifyJavaScriptContext( + const Extension* extension, + int extension_group, + const GURL& url, + const blink::WebSecurityOrigin& origin); + + // Helper for OnExtensionUnloaded(). + void RecordAndRemove(std::set<ScriptContext*>* removed, + ScriptContext* context); + + // Weak reference to all installed Extensions that are also active in this + // process. + ExtensionIdSet* active_extension_ids_; + + // The set of all ScriptContexts we own. + std::set<ScriptContext*> contexts_; + + DISALLOW_COPY_AND_ASSIGN(ScriptContextSet); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_CONTEXT_SET_H_ diff --git a/chromium/extensions/renderer/script_context_set_unittest.cc b/chromium/extensions/renderer/script_context_set_unittest.cc new file mode 100644 index 00000000000..756c94290f5 --- /dev/null +++ b/chromium/extensions/renderer/script_context_set_unittest.cc @@ -0,0 +1,62 @@ +// 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 <vector> + +#include "base/message_loop/message_loop.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_set.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/scoped_web_frame.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "gin/public/context_holder.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "v8/include/v8.h" + +namespace extensions { + +TEST(ScriptContextSetTest, Lifecycle) { + base::MessageLoop loop; + ScopedWebFrame web_frame; + + // Do this after construction of the webview, since it may construct the + // Isolate. + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Context> v8_context = v8::Context::New(isolate); + v8::Context::Scope context_scope(v8_context); + // ScriptContext relies on gin, it just doesn't look like it from here. + gin::ContextHolder context_holder(isolate); + context_holder.SetContext(v8_context); + + ExtensionIdSet active_extensions; + ScriptContextSet context_set(&active_extensions); + ScriptContext* context = context_set.Register( + web_frame.frame(), v8_context, 0, 0); // no extension group or world ID + + // Context is valid and resembles correctness. + EXPECT_TRUE(context->is_valid()); + EXPECT_EQ(web_frame.frame(), context->web_frame()); + EXPECT_EQ(v8_context, context->v8_context()); + + // Context has been correctly added. + EXPECT_EQ(1u, context_set.size()); + EXPECT_EQ(context, context_set.GetByV8Context(v8_context)); + + // Test context is correctly removed. + context_set.Remove(context); + EXPECT_EQ(0u, context_set.size()); + EXPECT_EQ(nullptr, context_set.GetByV8Context(v8_context)); + + // After removal, the context should be marked for destruction. + EXPECT_FALSE(context->is_valid()); + + // Run loop to do the actual deletion. + loop.RunUntilIdle(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_context_unittest.cc b/chromium/extensions/renderer/script_context_unittest.cc new file mode 100644 index 00000000000..b0a846c6b66 --- /dev/null +++ b/chromium/extensions/renderer/script_context_unittest.cc @@ -0,0 +1,24 @@ +// Copyright 2015 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/module_system_test.h" +#include "extensions/renderer/script_context.h" +#include "gin/per_context_data.h" +#include "gin/runner.h" + +namespace extensions { + +using ScriptContextTest = ModuleSystemTest; + +TEST_F(ScriptContextTest, GinRunnerLifetime) { + ExpectNoAssertionsMade(); + base::WeakPtr<gin::Runner> weak_runner = + gin::PerContextData::From(env()->context()->v8_context()) + ->runner() + ->GetWeakPtr(); + env()->ShutdownModuleSystem(); + EXPECT_FALSE(weak_runner); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_injection.cc b/chromium/extensions/renderer/script_injection.cc new file mode 100644 index 00000000000..70464b646c4 --- /dev/null +++ b/chromium/extensions/renderer/script_injection.cc @@ -0,0 +1,319 @@ +// 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/renderer/script_injection.h" + +#include <map> +#include <utility> + +#include "base/lazy_instance.h" +#include "base/macros.h" +#include "base/metrics/histogram.h" +#include "base/timer/elapsed_timer.h" +#include "base/values.h" +#include "content/public/child/v8_value_converter.h" +#include "content/public/renderer/render_frame.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/host_id.h" +#include "extensions/renderer/dom_activity_logger.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/extension_groups.h" +#include "extensions/renderer/extensions_renderer_client.h" +#include "extensions/renderer/script_injection_callback.h" +#include "extensions/renderer/scripts_run_info.h" +#include "third_party/WebKit/public/platform/WebSecurityOrigin.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScriptSource.h" +#include "url/gurl.h" + +namespace extensions { + +namespace { + +using IsolatedWorldMap = std::map<std::string, int>; +base::LazyInstance<IsolatedWorldMap> g_isolated_worlds = + LAZY_INSTANCE_INITIALIZER; + +const int64_t kInvalidRequestId = -1; + +// The id of the next pending injection. +int64_t g_next_pending_id = 0; + +// Gets the isolated world ID to use for the given |injection_host| +// in the given |frame|. If no isolated world has been created for that +// |injection_host| one will be created and initialized. +int GetIsolatedWorldIdForInstance(const InjectionHost* injection_host, + blink::WebLocalFrame* frame) { + static int g_next_isolated_world_id = + ExtensionsRendererClient::Get()->GetLowestIsolatedWorldId(); + + IsolatedWorldMap& isolated_worlds = g_isolated_worlds.Get(); + + int id = 0; + const std::string& key = injection_host->id().id(); + IsolatedWorldMap::iterator iter = isolated_worlds.find(key); + if (iter != isolated_worlds.end()) { + id = iter->second; + } else { + id = g_next_isolated_world_id++; + // This map will tend to pile up over time, but realistically, you're never + // going to have enough injection hosts for it to matter. + isolated_worlds[key] = id; + } + + // We need to set the isolated world origin and CSP even if it's not a new + // world since these are stored per frame, and we might not have used this + // isolated world in this frame before. + frame->setIsolatedWorldSecurityOrigin( + id, blink::WebSecurityOrigin::create(injection_host->url())); + frame->setIsolatedWorldContentSecurityPolicy( + id, blink::WebString::fromUTF8( + injection_host->GetContentSecurityPolicy())); + frame->setIsolatedWorldHumanReadableName( + id, blink::WebString::fromUTF8(injection_host->name())); + + return id; +} + +} // namespace + +// Watches for the deletion of a RenderFrame, after which is_valid will return +// false. +class ScriptInjection::FrameWatcher : public content::RenderFrameObserver { + public: + FrameWatcher(content::RenderFrame* render_frame, + ScriptInjection* injection) + : content::RenderFrameObserver(render_frame), + injection_(injection) {} + ~FrameWatcher() override {} + + private: + void FrameDetached() override { injection_->invalidate_render_frame(); } + void OnDestruct() override { injection_->invalidate_render_frame(); } + + ScriptInjection* injection_; + + DISALLOW_COPY_AND_ASSIGN(FrameWatcher); +}; + +// static +std::string ScriptInjection::GetHostIdForIsolatedWorld(int isolated_world_id) { + const IsolatedWorldMap& isolated_worlds = g_isolated_worlds.Get(); + + for (const auto& iter : isolated_worlds) { + if (iter.second == isolated_world_id) + return iter.first; + } + return std::string(); +} + +// static +void ScriptInjection::RemoveIsolatedWorld(const std::string& host_id) { + g_isolated_worlds.Get().erase(host_id); +} + +ScriptInjection::ScriptInjection(scoped_ptr<ScriptInjector> injector, + content::RenderFrame* render_frame, + scoped_ptr<const InjectionHost> injection_host, + UserScript::RunLocation run_location) + : injector_(std::move(injector)), + render_frame_(render_frame), + injection_host_(std::move(injection_host)), + run_location_(run_location), + request_id_(kInvalidRequestId), + complete_(false), + did_inject_js_(false), + frame_watcher_(new FrameWatcher(render_frame, this)), + weak_ptr_factory_(this) { + CHECK(injection_host_.get()); +} + +ScriptInjection::~ScriptInjection() { + if (!complete_) + NotifyWillNotInject(ScriptInjector::WONT_INJECT); +} + +ScriptInjection::InjectionResult ScriptInjection::TryToInject( + UserScript::RunLocation current_location, + ScriptsRunInfo* scripts_run_info, + const CompletionCallback& async_completion_callback) { + if (current_location < run_location_) + return INJECTION_WAITING; // Wait for the right location. + + if (request_id_ != kInvalidRequestId) { + // We're waiting for permission right now, try again later. + return INJECTION_WAITING; + } + + if (!injection_host_) { + NotifyWillNotInject(ScriptInjector::EXTENSION_REMOVED); + return INJECTION_FINISHED; // We're done. + } + + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + switch (injector_->CanExecuteOnFrame( + injection_host_.get(), web_frame, + ExtensionFrameHelper::Get(render_frame_)->tab_id())) { + case PermissionsData::ACCESS_DENIED: + NotifyWillNotInject(ScriptInjector::NOT_ALLOWED); + return INJECTION_FINISHED; // We're done. + case PermissionsData::ACCESS_WITHHELD: + RequestPermissionFromBrowser(); + return INJECTION_WAITING; // Wait around for permission. + case PermissionsData::ACCESS_ALLOWED: + InjectionResult result = Inject(scripts_run_info); + // If the injection is blocked, we need to set the manager so we can + // notify it upon completion. + if (result == INJECTION_BLOCKED) + async_completion_callback_ = async_completion_callback; + return result; + } + + NOTREACHED(); + return INJECTION_FINISHED; +} + +ScriptInjection::InjectionResult ScriptInjection::OnPermissionGranted( + ScriptsRunInfo* scripts_run_info) { + if (!injection_host_) { + NotifyWillNotInject(ScriptInjector::EXTENSION_REMOVED); + return INJECTION_FINISHED; + } + + return Inject(scripts_run_info); +} + +void ScriptInjection::OnHostRemoved() { + injection_host_.reset(nullptr); +} + +void ScriptInjection::RequestPermissionFromBrowser() { + // If we are just notifying the browser of the injection, then send an + // invalid request (which is treated like a notification). + request_id_ = g_next_pending_id++; + render_frame_->Send(new ExtensionHostMsg_RequestScriptInjectionPermission( + render_frame_->GetRoutingID(), host_id().id(), injector_->script_type(), + run_location_, request_id_)); +} + +void ScriptInjection::NotifyWillNotInject( + ScriptInjector::InjectFailureReason reason) { + complete_ = true; + injector_->OnWillNotInject(reason, render_frame_); +} + +ScriptInjection::InjectionResult ScriptInjection::Inject( + ScriptsRunInfo* scripts_run_info) { + DCHECK(injection_host_); + DCHECK(scripts_run_info); + DCHECK(!complete_); + + bool should_inject_js = injector_->ShouldInjectJs(run_location_); + bool should_inject_css = injector_->ShouldInjectCss(run_location_); + DCHECK(should_inject_js || should_inject_css); + + if (should_inject_js) + InjectJs(); + if (should_inject_css) + InjectCss(); + + complete_ = did_inject_js_ || !should_inject_js; + + injector_->GetRunInfo(scripts_run_info, run_location_); + + if (complete_) { + injector_->OnInjectionComplete(std::move(execution_result_), run_location_, + render_frame_); + } else { + ++scripts_run_info->num_blocking_js; + } + + return complete_ ? INJECTION_FINISHED : INJECTION_BLOCKED; +} + +void ScriptInjection::InjectJs() { + DCHECK(!did_inject_js_); + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + std::vector<blink::WebScriptSource> sources = + injector_->GetJsSources(run_location_); + bool in_main_world = injector_->ShouldExecuteInMainWorld(); + int world_id = in_main_world + ? DOMActivityLogger::kMainWorldId + : GetIsolatedWorldIdForInstance(injection_host_.get(), + web_frame); + bool is_user_gesture = injector_->IsUserGesture(); + + scoped_ptr<blink::WebScriptExecutionCallback> callback( + new ScriptInjectionCallback( + base::Bind(&ScriptInjection::OnJsInjectionCompleted, + weak_ptr_factory_.GetWeakPtr()))); + + base::ElapsedTimer exec_timer; + if (injection_host_->id().type() == HostID::EXTENSIONS) + DOMActivityLogger::AttachToWorld(world_id, injection_host_->id().id()); + if (in_main_world) { + // We only inject in the main world for javascript: urls. + DCHECK_EQ(1u, sources.size()); + + web_frame->requestExecuteScriptAndReturnValue(sources.front(), + is_user_gesture, + callback.release()); + } else { + web_frame->requestExecuteScriptInIsolatedWorld( + world_id, + &sources.front(), + sources.size(), + EXTENSION_GROUP_CONTENT_SCRIPTS, + is_user_gesture, + callback.release()); + } + + if (injection_host_->id().type() == HostID::EXTENSIONS) + UMA_HISTOGRAM_TIMES("Extensions.InjectScriptTime", exec_timer.Elapsed()); +} + +void ScriptInjection::OnJsInjectionCompleted( + const blink::WebVector<v8::Local<v8::Value> >& results) { + DCHECK(!did_inject_js_); + + bool expects_results = injector_->ExpectsResults(); + if (expects_results) { + if (!results.isEmpty() && !results[0].IsEmpty()) { + // Right now, we only support returning single results (per frame). + scoped_ptr<content::V8ValueConverter> v8_converter( + content::V8ValueConverter::create()); + // It's safe to always use the main world context when converting + // here. V8ValueConverterImpl shouldn't actually care about the + // context scope, and it switches to v8::Object's creation context + // when encountered. + v8::Local<v8::Context> context = + render_frame_->GetWebFrame()->mainWorldScriptContext(); + execution_result_.reset(v8_converter->FromV8Value(results[0], context)); + } + if (!execution_result_.get()) + execution_result_ = base::Value::CreateNullValue(); + } + did_inject_js_ = true; + + // If |async_completion_callback_| is set, it means the script finished + // asynchronously, and we should run it. + if (!async_completion_callback_.is_null()) { + injector_->OnInjectionComplete(std::move(execution_result_), run_location_, + render_frame_); + // Warning: this object can be destroyed after this line! + async_completion_callback_.Run(this); + } +} + +void ScriptInjection::InjectCss() { + std::vector<std::string> css_sources = + injector_->GetCssSources(run_location_); + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + for (const std::string& css : css_sources) + web_frame->document().insertStyleSheet(blink::WebString::fromUTF8(css)); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_injection.h b/chromium/extensions/renderer/script_injection.h new file mode 100644 index 00000000000..6b3679573c3 --- /dev/null +++ b/chromium/extensions/renderer/script_injection.h @@ -0,0 +1,150 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_INJECTION_H_ +#define EXTENSIONS_RENDERER_SCRIPT_INJECTION_H_ + +#include <stdint.h> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "extensions/common/user_script.h" +#include "extensions/renderer/injection_host.h" +#include "extensions/renderer/script_injector.h" + +struct HostID; + +namespace blink { +template<typename T> class WebVector; +} + +namespace content { +class RenderFrame; +} + +namespace v8 { +class Value; +template <class T> class Local; +} + +namespace extensions { +struct ScriptsRunInfo; + +// A script wrapper which is aware of whether or not it is allowed to execute, +// and contains the implementation to do so. +class ScriptInjection { + public: + enum InjectionResult { + INJECTION_FINISHED, + INJECTION_BLOCKED, + INJECTION_WAITING + }; + + using CompletionCallback = base::Callback<void(ScriptInjection*)>; + + // Return the id of the injection host associated with the given world. + static std::string GetHostIdForIsolatedWorld(int world_id); + + // Remove the isolated world associated with the given injection host. + static void RemoveIsolatedWorld(const std::string& host_id); + + ScriptInjection(scoped_ptr<ScriptInjector> injector, + content::RenderFrame* render_frame, + scoped_ptr<const InjectionHost> injection_host, + UserScript::RunLocation run_location); + ~ScriptInjection(); + + // Try to inject the script at the |current_location|. This returns + // INJECTION_FINISHED if injection has injected or will never inject, returns + // INJECTION_BLOCKED if injection is running asynchronously and has not + // finished yet, returns INJECTION_WAITING if injections is delayed (either + // for permission purposes or because |current_location| is not the designated + // |run_location_|). + // If INJECTION_BLOCKED is returned, |async_completion_callback| will be + // called upon completion. + InjectionResult TryToInject( + UserScript::RunLocation current_location, + ScriptsRunInfo* scripts_run_info, + const CompletionCallback& async_completion_callback); + + // Called when permission for the given injection has been granted. + // Returns INJECTION_FINISHED if injection has injected or will never inject, + // returns INJECTION_BLOCKED if injection is ran asynchronously. + InjectionResult OnPermissionGranted(ScriptsRunInfo* scripts_run_info); + + // Resets the pointer of the injection host when the host is gone. + void OnHostRemoved(); + + void invalidate_render_frame() { render_frame_ = nullptr; } + + // Accessors. + content::RenderFrame* render_frame() const { return render_frame_; } + const HostID& host_id() const { return injection_host_->id(); } + int64_t request_id() const { return request_id_; } + + private: + class FrameWatcher; + + // Sends a message to the browser to request permission to inject. + void RequestPermissionFromBrowser(); + + // Injects the script. Returns INJECTION_FINISHED if injection has finished, + // otherwise INJECTION_BLOCKED. + InjectionResult Inject(ScriptsRunInfo* scripts_run_info); + + // Inject any JS scripts into the frame for the injection. + void InjectJs(); + + // Called when JS injection for the given frame has been completed. + void OnJsInjectionCompleted( + const blink::WebVector<v8::Local<v8::Value> >& results); + + // Inject any CSS source into the frame for the injection. + void InjectCss(); + + // Notify that we will not inject, and mark it as acknowledged. + void NotifyWillNotInject(ScriptInjector::InjectFailureReason reason); + + // The injector for this injection. + scoped_ptr<ScriptInjector> injector_; + + // The RenderFrame into which this should inject the script. + content::RenderFrame* render_frame_; + + // The associated injection host. + scoped_ptr<const InjectionHost> injection_host_; + + // The location in the document load at which we inject the script. + UserScript::RunLocation run_location_; + + // This injection's request id. This will be -1 unless the injection is + // currently waiting on permission. + int64_t request_id_; + + // Whether or not the injection is complete, either via injecting the script + // or because it will never complete. + bool complete_; + + // Whether or not the injection successfully injected JS. + bool did_inject_js_; + + // Results storage. + scoped_ptr<base::Value> execution_result_; + + // The callback to run upon completing asynchronously. + CompletionCallback async_completion_callback_; + + // A helper class to hold the render frame and watch for its deletion. + scoped_ptr<FrameWatcher> frame_watcher_; + + base::WeakPtrFactory<ScriptInjection> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjection); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_INJECTION_H_ diff --git a/chromium/extensions/renderer/script_injection_callback.cc b/chromium/extensions/renderer/script_injection_callback.cc new file mode 100644 index 00000000000..8b911d1c28c --- /dev/null +++ b/chromium/extensions/renderer/script_injection_callback.cc @@ -0,0 +1,23 @@ +// Copyright 2015 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/script_injection_callback.h" + +namespace extensions { + +ScriptInjectionCallback::ScriptInjectionCallback( + const CompleteCallback& injection_completed_callback) + : injection_completed_callback_(injection_completed_callback) { +} + +ScriptInjectionCallback::~ScriptInjectionCallback() { +} + +void ScriptInjectionCallback::completed( + const blink::WebVector<v8::Local<v8::Value> >& result) { + injection_completed_callback_.Run(result); + delete this; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_injection_callback.h b/chromium/extensions/renderer/script_injection_callback.h new file mode 100644 index 00000000000..14819277cbd --- /dev/null +++ b/chromium/extensions/renderer/script_injection_callback.h @@ -0,0 +1,42 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ +#define EXTENSIONS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "third_party/WebKit/public/web/WebScriptExecutionCallback.h" +#include "v8/include/v8.h" + +namespace blink { +template<typename T> class WebVector; +} + +namespace extensions { + +// A wrapper around a callback to notify a script injection when injection +// completes. +// This class manages its own lifetime. +class ScriptInjectionCallback : public blink::WebScriptExecutionCallback { + public: + using CompleteCallback = + base::Callback<void( + const blink::WebVector<v8::Local<v8::Value>>& result)>; + + ScriptInjectionCallback(const CompleteCallback& injection_completed_callback); + ~ScriptInjectionCallback() override; + + void completed( + const blink::WebVector<v8::Local<v8::Value> >& result) override; + + private: + CompleteCallback injection_completed_callback_; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjectionCallback); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ diff --git a/chromium/extensions/renderer/script_injection_manager.cc b/chromium/extensions/renderer/script_injection_manager.cc new file mode 100644 index 00000000000..0e234d0906b --- /dev/null +++ b/chromium/extensions/renderer/script_injection_manager.cc @@ -0,0 +1,514 @@ +// 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/renderer/script_injection_manager.h" + +#include <utility> + +#include "base/auto_reset.h" +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/values.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_messages.h" +#include "extensions/common/extension_set.h" +#include "extensions/renderer/extension_frame_helper.h" +#include "extensions/renderer/extension_injection_host.h" +#include "extensions/renderer/programmatic_script_injector.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/scripts_run_info.h" +#include "extensions/renderer/web_ui_injection_host.h" +#include "ipc/ipc_message_macros.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebView.h" +#include "url/gurl.h" + +namespace extensions { + +namespace { + +// The length of time to wait after the DOM is complete to try and run user +// scripts. +const int kScriptIdleTimeoutInMs = 200; + +// Returns the RunLocation that follows |run_location|. +UserScript::RunLocation NextRunLocation(UserScript::RunLocation run_location) { + switch (run_location) { + case UserScript::DOCUMENT_START: + return UserScript::DOCUMENT_END; + case UserScript::DOCUMENT_END: + return UserScript::DOCUMENT_IDLE; + case UserScript::DOCUMENT_IDLE: + return UserScript::RUN_LOCATION_LAST; + case UserScript::UNDEFINED: + case UserScript::RUN_DEFERRED: + case UserScript::BROWSER_DRIVEN: + case UserScript::RUN_LOCATION_LAST: + break; + } + NOTREACHED(); + return UserScript::RUN_LOCATION_LAST; +} + +} // namespace + +class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver { + public: + RFOHelper(content::RenderFrame* render_frame, + ScriptInjectionManager* manager); + ~RFOHelper() override; + + private: + // RenderFrameObserver implementation. + bool OnMessageReceived(const IPC::Message& message) override; + void DidCreateNewDocument() override; + void DidCreateDocumentElement() override; + void DidFailProvisionalLoad(const blink::WebURLError& error) override; + void DidFinishDocumentLoad() override; + void DidFinishLoad() override; + void FrameDetached() override; + void OnDestruct() override; + + virtual void OnExecuteCode(const ExtensionMsg_ExecuteCode_Params& params); + virtual void OnExecuteDeclarativeScript(int tab_id, + const ExtensionId& extension_id, + int script_id, + const GURL& url); + virtual void OnPermitScriptInjection(int64_t request_id); + + // Tells the ScriptInjectionManager to run tasks associated with + // document_idle. + void RunIdle(); + + void StartInjectScripts(UserScript::RunLocation run_location); + + // Indicate that the frame is no longer valid because it is starting + // a new load or closing. + void InvalidateAndResetFrame(); + + // The owning ScriptInjectionManager. + ScriptInjectionManager* manager_; + + bool should_run_idle_; + + base::WeakPtrFactory<RFOHelper> weak_factory_; +}; + +ScriptInjectionManager::RFOHelper::RFOHelper(content::RenderFrame* render_frame, + ScriptInjectionManager* manager) + : content::RenderFrameObserver(render_frame), + manager_(manager), + should_run_idle_(true), + weak_factory_(this) { +} + +ScriptInjectionManager::RFOHelper::~RFOHelper() { +} + +bool ScriptInjectionManager::RFOHelper::OnMessageReceived( + const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(ScriptInjectionManager::RFOHelper, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteCode, OnExecuteCode) + IPC_MESSAGE_HANDLER(ExtensionMsg_PermitScriptInjection, + OnPermitScriptInjection) + IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteDeclarativeScript, + OnExecuteDeclarativeScript) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void ScriptInjectionManager::RFOHelper::DidCreateNewDocument() { + // A new document is going to be shown, so invalidate the old document state. + // Check that the frame's state is known before invalidating the frame, + // because it is possible that a script injection was scheduled before the + // page was loaded, e.g. by navigating to a javascript: URL before the page + // has loaded. + if (manager_->frame_statuses_.count(render_frame()) != 0) + InvalidateAndResetFrame(); +} + +void ScriptInjectionManager::RFOHelper::DidCreateDocumentElement() { + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentStart( + base::Bind(&ScriptInjectionManager::RFOHelper::StartInjectScripts, + weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_START)); +} + +void ScriptInjectionManager::RFOHelper::DidFailProvisionalLoad( + const blink::WebURLError& error) { + FrameStatusMap::iterator it = manager_->frame_statuses_.find(render_frame()); + if (it != manager_->frame_statuses_.end() && + it->second == UserScript::DOCUMENT_START) { + // Since the provisional load failed, the frame stays at its previous loaded + // state and origin (or the parent's origin for new/about:blank frames). + // Reset the frame to DOCUMENT_IDLE in order to reflect that the frame is + // done loading, and avoid any deadlock in the system. + // + // We skip injection of DOCUMENT_END and DOCUMENT_IDLE scripts, because the + // injections closely follow the DOMContentLoaded (and onload) events, which + // are not triggered after a failed provisional load. + // This assumption is verified in the checkDOMContentLoadedEvent subtest of + // ExecuteScriptApiTest.FrameWithHttp204 (browser_tests). + InvalidateAndResetFrame(); + should_run_idle_ = false; + manager_->frame_statuses_[render_frame()] = UserScript::DOCUMENT_IDLE; + } +} + +void ScriptInjectionManager::RFOHelper::DidFinishDocumentLoad() { + DCHECK(content::RenderThread::Get()); + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentEnd( + base::Bind(&ScriptInjectionManager::RFOHelper::StartInjectScripts, + weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_END)); + + // We try to run idle in two places: here and DidFinishLoad. + // DidFinishDocumentLoad() corresponds to completing the document's load, + // whereas DidFinishLoad corresponds to completing the document and all + // subresources' load. We don't want to hold up script injection for a + // particularly slow subresource, so we set a delayed task from here - but if + // we finish everything before that point (i.e., DidFinishLoad() is + // triggered), then there's no reason to keep waiting. + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&ScriptInjectionManager::RFOHelper::RunIdle, + weak_factory_.GetWeakPtr()), + base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs)); +} + +void ScriptInjectionManager::RFOHelper::DidFinishLoad() { + DCHECK(content::RenderThread::Get()); + // Ensure that we don't block any UI progress by running scripts. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&ScriptInjectionManager::RFOHelper::RunIdle, + weak_factory_.GetWeakPtr())); +} + +void ScriptInjectionManager::RFOHelper::FrameDetached() { + // The frame is closing - invalidate. + InvalidateAndResetFrame(); +} + +void ScriptInjectionManager::RFOHelper::OnDestruct() { + manager_->RemoveObserver(this); +} + +void ScriptInjectionManager::RFOHelper::OnExecuteCode( + const ExtensionMsg_ExecuteCode_Params& params) { + manager_->HandleExecuteCode(params, render_frame()); +} + +void ScriptInjectionManager::RFOHelper::OnExecuteDeclarativeScript( + int tab_id, + const ExtensionId& extension_id, + int script_id, + const GURL& url) { + // TODO(markdittmer): URL-checking isn't the best security measure. + // Begin script injection workflow only if the current URL is identical to + // the one that matched declarative conditions in the browser. + if (render_frame()->GetWebFrame()->document().url() == url) { + manager_->HandleExecuteDeclarativeScript(render_frame(), + tab_id, + extension_id, + script_id, + url); + } +} + +void ScriptInjectionManager::RFOHelper::OnPermitScriptInjection( + int64_t request_id) { + manager_->HandlePermitScriptInjection(request_id); +} + +void ScriptInjectionManager::RFOHelper::RunIdle() { + // Only notify the manager if the frame hasn't either been removed or already + // had idle run since the task to RunIdle() was posted. + if (should_run_idle_) { + should_run_idle_ = false; + manager_->StartInjectScripts(render_frame(), UserScript::DOCUMENT_IDLE); + } +} + +void ScriptInjectionManager::RFOHelper::StartInjectScripts( + UserScript::RunLocation run_location) { + manager_->StartInjectScripts(render_frame(), run_location); +} + +void ScriptInjectionManager::RFOHelper::InvalidateAndResetFrame() { + // Invalidate any pending idle injections, and reset the frame inject on idle. + weak_factory_.InvalidateWeakPtrs(); + // We reset to inject on idle, because the frame can be reused (in the case of + // navigation). + should_run_idle_ = true; + manager_->InvalidateForFrame(render_frame()); +} + +ScriptInjectionManager::ScriptInjectionManager( + UserScriptSetManager* user_script_set_manager) + : user_script_set_manager_(user_script_set_manager), + user_script_set_manager_observer_(this) { + user_script_set_manager_observer_.Add(user_script_set_manager_); +} + +ScriptInjectionManager::~ScriptInjectionManager() { + for (const auto& injection : pending_injections_) + injection->invalidate_render_frame(); + for (const auto& injection : running_injections_) + injection->invalidate_render_frame(); +} + +void ScriptInjectionManager::OnRenderFrameCreated( + content::RenderFrame* render_frame) { + rfo_helpers_.push_back(make_scoped_ptr(new RFOHelper(render_frame, this))); +} + +void ScriptInjectionManager::OnExtensionUnloaded( + const std::string& extension_id) { + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if ((*iter)->host_id().id() == extension_id) { + (*iter)->OnHostRemoved(); + iter = pending_injections_.erase(iter); + } else { + ++iter; + } + } +} + +void ScriptInjectionManager::OnInjectionFinished( + ScriptInjection* injection) { + auto iter = + std::find_if(running_injections_.begin(), running_injections_.end(), + [injection](const scoped_ptr<ScriptInjection>& mode) { + return injection == mode.get(); + }); + if (iter != running_injections_.end()) + running_injections_.erase(iter); +} + +void ScriptInjectionManager::OnUserScriptsUpdated( + const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) { + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if (changed_hosts.count((*iter)->host_id()) > 0) + iter = pending_injections_.erase(iter); + else + ++iter; + } +} + +void ScriptInjectionManager::RemoveObserver(RFOHelper* helper) { + for (auto iter = rfo_helpers_.begin(); iter != rfo_helpers_.end(); ++iter) { + if (iter->get() == helper) { + rfo_helpers_.erase(iter); + break; + } + } +} + +void ScriptInjectionManager::InvalidateForFrame(content::RenderFrame* frame) { + // If the frame invalidated is the frame being injected into, we need to + // note it. + active_injection_frames_.erase(frame); + + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if ((*iter)->render_frame() == frame) + iter = pending_injections_.erase(iter); + else + ++iter; + } + + frame_statuses_.erase(frame); +} + +void ScriptInjectionManager::StartInjectScripts( + content::RenderFrame* frame, + UserScript::RunLocation run_location) { + FrameStatusMap::iterator iter = frame_statuses_.find(frame); + // We also don't execute if we detect that the run location is somehow out of + // order. This can happen if: + // - The first run location reported for the frame isn't DOCUMENT_START, or + // - The run location reported doesn't immediately follow the previous + // reported run location. + // We don't want to run because extensions may have requirements that scripts + // running in an earlier run location have run by the time a later script + // runs. Better to just not run. + // Note that we check run_location > NextRunLocation() in the second clause + // (as opposed to !=) because earlier signals (like DidCreateDocumentElement) + // can happen multiple times, so we can receive earlier/equal run locations. + if ((iter == frame_statuses_.end() && + run_location != UserScript::DOCUMENT_START) || + (iter != frame_statuses_.end() && + run_location > NextRunLocation(iter->second))) { + // We also invalidate the frame, because the run order of pending injections + // may also be bad. + InvalidateForFrame(frame); + return; + } else if (iter != frame_statuses_.end() && iter->second >= run_location) { + // Certain run location signals (like DidCreateDocumentElement) can happen + // multiple times. Ignore the subsequent signals. + return; + } + + // Otherwise, all is right in the world, and we can get on with the + // injections! + frame_statuses_[frame] = run_location; + InjectScripts(frame, run_location); +} + +void ScriptInjectionManager::InjectScripts( + content::RenderFrame* frame, + UserScript::RunLocation run_location) { + // Find any injections that want to run on the given frame. + ScriptInjectionVector frame_injections; + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if ((*iter)->render_frame() == frame) { + frame_injections.push_back(std::move(*iter)); + iter = pending_injections_.erase(iter); + } else { + ++iter; + } + } + + // Add any injections for user scripts. + int tab_id = ExtensionFrameHelper::Get(frame)->tab_id(); + user_script_set_manager_->GetAllInjections(&frame_injections, frame, tab_id, + run_location); + + // Note that we are running in |frame|. + active_injection_frames_.insert(frame); + + ScriptsRunInfo scripts_run_info(frame, run_location); + for (auto iter = frame_injections.begin(); iter != frame_injections.end();) { + // It's possible for the frame to be invalidated in the course of injection + // (if a script removes its own frame, for example). If this happens, abort. + if (!active_injection_frames_.count(frame)) + break; + scoped_ptr<ScriptInjection> injection(std::move(*iter)); + iter = frame_injections.erase(iter); + TryToInject(std::move(injection), run_location, &scripts_run_info); + } + + // We are done running in the frame. + active_injection_frames_.erase(frame); + + scripts_run_info.LogRun(); +} + +void ScriptInjectionManager::TryToInject( + scoped_ptr<ScriptInjection> injection, + UserScript::RunLocation run_location, + ScriptsRunInfo* scripts_run_info) { + // Try to inject the script. If the injection is waiting (i.e., for + // permission), add it to the list of pending injections. If the injection + // has blocked, add it to the list of running injections. + // The Unretained below is safe because this object owns all the + // ScriptInjections, so is guaranteed to outlive them. + switch (injection->TryToInject( + run_location, + scripts_run_info, + base::Bind(&ScriptInjectionManager::OnInjectionFinished, + base::Unretained(this)))) { + case ScriptInjection::INJECTION_WAITING: + pending_injections_.push_back(std::move(injection)); + break; + case ScriptInjection::INJECTION_BLOCKED: + running_injections_.push_back(std::move(injection)); + break; + case ScriptInjection::INJECTION_FINISHED: + break; + } +} + +void ScriptInjectionManager::HandleExecuteCode( + const ExtensionMsg_ExecuteCode_Params& params, + content::RenderFrame* render_frame) { + scoped_ptr<const InjectionHost> injection_host; + if (params.host_id.type() == HostID::EXTENSIONS) { + injection_host = ExtensionInjectionHost::Create(params.host_id.id()); + if (!injection_host) + return; + } else if (params.host_id.type() == HostID::WEBUI) { + injection_host.reset( + new WebUIInjectionHost(params.host_id)); + } + + scoped_ptr<ScriptInjection> injection(new ScriptInjection( + scoped_ptr<ScriptInjector>( + new ProgrammaticScriptInjector(params, render_frame)), + render_frame, std::move(injection_host), + static_cast<UserScript::RunLocation>(params.run_at))); + + FrameStatusMap::const_iterator iter = frame_statuses_.find(render_frame); + UserScript::RunLocation run_location = + iter == frame_statuses_.end() ? UserScript::UNDEFINED : iter->second; + + ScriptsRunInfo scripts_run_info(render_frame, run_location); + TryToInject(std::move(injection), run_location, &scripts_run_info); +} + +void ScriptInjectionManager::HandleExecuteDeclarativeScript( + content::RenderFrame* render_frame, + int tab_id, + const ExtensionId& extension_id, + int script_id, + const GURL& url) { + scoped_ptr<ScriptInjection> injection = + user_script_set_manager_->GetInjectionForDeclarativeScript( + script_id, + render_frame, + tab_id, + url, + extension_id); + if (injection.get()) { + ScriptsRunInfo scripts_run_info(render_frame, UserScript::BROWSER_DRIVEN); + // TODO(markdittmer): Use return value of TryToInject for error handling. + TryToInject(std::move(injection), UserScript::BROWSER_DRIVEN, + &scripts_run_info); + + scripts_run_info.LogRun(); + } +} + +void ScriptInjectionManager::HandlePermitScriptInjection(int64_t request_id) { + auto iter = pending_injections_.begin(); + for (; iter != pending_injections_.end(); ++iter) { + if ((*iter)->request_id() == request_id) { + DCHECK((*iter)->host_id().type() == HostID::EXTENSIONS); + break; + } + } + if (iter == pending_injections_.end()) + return; + + // At this point, because the request is present in pending_injections_, we + // know that this is the same page that issued the request (otherwise, + // RFOHelper::InvalidateAndResetFrame would have caused it to be cleared out). + + scoped_ptr<ScriptInjection> injection(std::move(*iter)); + pending_injections_.erase(iter); + + ScriptsRunInfo scripts_run_info(injection->render_frame(), + UserScript::RUN_DEFERRED); + ScriptInjection::InjectionResult res = injection->OnPermissionGranted( + &scripts_run_info); + if (res == ScriptInjection::INJECTION_BLOCKED) + running_injections_.push_back(std::move(injection)); + scripts_run_info.LogRun(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/script_injection_manager.h b/chromium/extensions/renderer/script_injection_manager.h new file mode 100644 index 00000000000..c0f5da82cce --- /dev/null +++ b/chromium/extensions/renderer/script_injection_manager.h @@ -0,0 +1,126 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_INJECTION_MANAGER_H_ +#define EXTENSIONS_RENDERER_SCRIPT_INJECTION_MANAGER_H_ + +#include <stdint.h> + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/scoped_observer.h" +#include "extensions/common/user_script.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/user_script_set_manager.h" + +struct ExtensionMsg_ExecuteCode_Params; + +namespace content { +class RenderFrame; +} + +namespace extensions { +class Extension; + +// The ScriptInjectionManager manages extensions injecting scripts into frames +// via both content/user scripts and tabs.executeScript(). It is responsible for +// maintaining any pending injections awaiting permission or the appropriate +// load point, and injecting them when ready. +class ScriptInjectionManager : public UserScriptSetManager::Observer { + public: + explicit ScriptInjectionManager( + UserScriptSetManager* user_script_set_manager); + virtual ~ScriptInjectionManager(); + + // Notifies that a new render view has been created. + void OnRenderFrameCreated(content::RenderFrame* render_frame); + + // Removes pending injections of the unloaded extension. + void OnExtensionUnloaded(const std::string& extension_id); + + private: + // A RenderFrameObserver implementation which watches the various render + // frames in order to notify the ScriptInjectionManager of different + // document load states and IPCs. + class RFOHelper; + + using FrameStatusMap = + std::map<content::RenderFrame*, UserScript::RunLocation>; + + using ScriptInjectionVector = std::vector<scoped_ptr<ScriptInjection>>; + + // Notifies that an injection has been finished. + void OnInjectionFinished(ScriptInjection* injection); + + // UserScriptSetManager::Observer implementation. + void OnUserScriptsUpdated(const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) override; + + // Notifies that an RFOHelper should be removed. + void RemoveObserver(RFOHelper* helper); + + // Invalidate any pending tasks associated with |frame|. + void InvalidateForFrame(content::RenderFrame* frame); + + // Starts the process to inject appropriate scripts into |frame|. + void StartInjectScripts(content::RenderFrame* frame, + UserScript::RunLocation run_location); + + // Actually injects the scripts into |frame|. + void InjectScripts(content::RenderFrame* frame, + UserScript::RunLocation run_location); + + // Try to inject and store injection if it has not finished. + void TryToInject(scoped_ptr<ScriptInjection> injection, + UserScript::RunLocation run_location, + ScriptsRunInfo* scripts_run_info); + + // Handle the ExecuteCode extension message. + void HandleExecuteCode(const ExtensionMsg_ExecuteCode_Params& params, + content::RenderFrame* render_frame); + + // Handle the ExecuteDeclarativeScript extension message. + void HandleExecuteDeclarativeScript(content::RenderFrame* web_frame, + int tab_id, + const ExtensionId& extension_id, + int script_id, + const GURL& url); + + // Handle the GrantInjectionPermission extension message. + void HandlePermitScriptInjection(int64_t request_id); + + // The map of active web frames to their corresponding statuses. The + // RunLocation of the frame corresponds to the last location that has ran. + FrameStatusMap frame_statuses_; + + // The frames currently being injected into, so long as that frame is valid. + std::set<content::RenderFrame*> active_injection_frames_; + + // The collection of RFOHelpers. + std::vector<scoped_ptr<RFOHelper>> rfo_helpers_; + + // The set of UserScripts associated with extensions. Owned by the Dispatcher. + UserScriptSetManager* user_script_set_manager_; + + // Pending injections which are waiting for either the proper run location or + // user consent. + ScriptInjectionVector pending_injections_; + + // Running injections which are waiting for async callbacks from blink. + ScriptInjectionVector running_injections_; + + ScopedObserver<UserScriptSetManager, UserScriptSetManager::Observer> + user_script_set_manager_observer_; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjectionManager); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_INJECTION_MANAGER_H_ diff --git a/chromium/extensions/renderer/script_injector.h b/chromium/extensions/renderer/script_injector.h new file mode 100644 index 00000000000..a51e1d5a9a0 --- /dev/null +++ b/chromium/extensions/renderer/script_injector.h @@ -0,0 +1,98 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPT_INJECTOR_H_ +#define EXTENSIONS_RENDERER_SCRIPT_INJECTOR_H_ + +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/common/user_script.h" +#include "third_party/WebKit/public/web/WebScriptSource.h" + +class GURL; +class InjectionHost; + +namespace blink { +class WebLocalFrame; +} + +namespace extensions { +struct ScriptsRunInfo; + +// The pseudo-delegate class for a ScriptInjection that provides all necessary +// information about how to inject the script, including what code to inject, +// when (run location), and where (world), but without any injection logic. +class ScriptInjector { + public: + // The possible reasons for not injecting the script. + enum InjectFailureReason { + EXTENSION_REMOVED, // The extension was removed before injection. + NOT_ALLOWED, // The script is not allowed to inject. + WONT_INJECT // The injection won't inject because the user rejected + // (or just did not accept) the injection. + }; + + virtual ~ScriptInjector() {} + + // Returns the script type of this particular injection. + virtual UserScript::InjectionType script_type() const = 0; + + // Returns true if the script should execute in the main world. + virtual bool ShouldExecuteInMainWorld() const = 0; + + // Returns true if the script is running inside a user gesture. + virtual bool IsUserGesture() const = 0; + + // Returns true if the script expects results. + virtual bool ExpectsResults() const = 0; + + // Returns true if the script should inject JS source at the given + // |run_location|. + virtual bool ShouldInjectJs(UserScript::RunLocation run_location) const = 0; + + // Returns true if the script should inject CSS at the given |run_location|. + virtual bool ShouldInjectCss(UserScript::RunLocation run_location) const = 0; + + // Returns true if the script should execute on the given |frame|. + virtual PermissionsData::AccessType CanExecuteOnFrame( + const InjectionHost* injection_host, + blink::WebLocalFrame* web_frame, + int tab_id) const = 0; + + // Returns the javascript sources to inject at the given |run_location|. + // Only called if ShouldInjectJs() is true. + virtual std::vector<blink::WebScriptSource> GetJsSources( + UserScript::RunLocation run_location) const = 0; + + // Returns the css to inject at the given |run_location|. + // Only called if ShouldInjectCss() is true. + virtual std::vector<std::string> GetCssSources( + UserScript::RunLocation run_location) const = 0; + + // Fill scriptrs run info based on information about injection. + virtual void GetRunInfo( + ScriptsRunInfo* scripts_run_info, + UserScript::RunLocation run_location) const = 0; + + // Notifies the script that injection has completed, with a possibly-populated + // list of results (depending on whether or not ExpectsResults() was true). + // |render_frame| contains the render frame, or null if the frame was + // invalidated. + virtual void OnInjectionComplete( + scoped_ptr<base::Value> execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) = 0; + + // Notifies the script that injection will never occur. + // |render_frame| contains the render frame, or null if the frame was + // invalidated. + virtual void OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) = 0; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPT_INJECTOR_H_ diff --git a/chromium/extensions/renderer/scripts_run_info.cc b/chromium/extensions/renderer/scripts_run_info.cc new file mode 100644 index 00000000000..b31bbe07ba1 --- /dev/null +++ b/chromium/extensions/renderer/scripts_run_info.cc @@ -0,0 +1,77 @@ +// 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/renderer/scripts_run_info.h" + +#include "base/metrics/histogram.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" + +namespace extensions { + +ScriptsRunInfo::ScriptsRunInfo(content::RenderFrame* render_frame, + UserScript::RunLocation location) + : num_css(0u), + num_js(0u), + num_blocking_js(0u), + routing_id_(render_frame->GetRoutingID()), + run_location_(location), + frame_url_(ScriptContext::GetDataSourceURLForFrame( + render_frame->GetWebFrame())) { +} + +ScriptsRunInfo::~ScriptsRunInfo() { +} + +void ScriptsRunInfo::LogRun() { + // Notify the browser if any extensions are now executing scripts. + if (!executing_scripts.empty()) { + content::RenderThread::Get()->Send( + new ExtensionHostMsg_ContentScriptsExecuting( + routing_id_, executing_scripts, frame_url_)); + } + + switch (run_location_) { + case UserScript::DOCUMENT_START: + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectStart_CssCount", num_css); + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectStart_ScriptCount", num_js); + if (num_blocking_js) { + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectStart_BlockingScriptCount", + num_blocking_js); + } else if (num_css || num_js) { + UMA_HISTOGRAM_TIMES("Extensions.InjectStart_Time", timer.Elapsed()); + } + break; + case UserScript::DOCUMENT_END: + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectEnd_ScriptCount", num_js); + if (num_blocking_js) { + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectEnd_BlockingScriptCount", + num_blocking_js); + } else if (num_js) { + UMA_HISTOGRAM_TIMES("Extensions.InjectEnd_Time", timer.Elapsed()); + } + break; + case UserScript::DOCUMENT_IDLE: + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectIdle_ScriptCount", num_js); + if (num_blocking_js) { + UMA_HISTOGRAM_COUNTS_100("Extensions.InjectIdle_BlockingScriptCount", + num_blocking_js); + } else if (num_js) { + UMA_HISTOGRAM_TIMES("Extensions.InjectIdle_Time", timer.Elapsed()); + } + break; + case UserScript::RUN_DEFERRED: + case UserScript::BROWSER_DRIVEN: + // TODO(rdevlin.cronin): Add histograms. + break; + case UserScript::UNDEFINED: + case UserScript::RUN_LOCATION_LAST: + NOTREACHED(); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/scripts_run_info.h b/chromium/extensions/renderer/scripts_run_info.h new file mode 100644 index 00000000000..ebd51dfa43b --- /dev/null +++ b/chromium/extensions/renderer/scripts_run_info.h @@ -0,0 +1,64 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SCRIPTS_RUN_INFO_H_ +#define EXTENSIONS_RENDERER_SCRIPTS_RUN_INFO_H_ + +#include <stddef.h> + +#include <map> +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/timer/elapsed_timer.h" +#include "extensions/common/user_script.h" + +namespace content { +class RenderFrame; +} + +namespace extensions { + +// A struct containing information about a script run. +struct ScriptsRunInfo { + // Map of extensions IDs to the executing script paths. + typedef std::map<std::string, std::set<std::string> > ExecutingScriptsMap; + + ScriptsRunInfo(content::RenderFrame* render_frame, + UserScript::RunLocation location); + ~ScriptsRunInfo(); + + // The number of CSS scripts injected. + size_t num_css; + // The number of JS scripts injected. + size_t num_js; + // The number of blocked JS scripts injected. + size_t num_blocking_js; + // A map of extension ids to executing script paths. + ExecutingScriptsMap executing_scripts; + // The elapsed time since the ScriptsRunInfo was constructed. + base::ElapsedTimer timer; + + // Log information about a given script run. + void LogRun(); + + private: + // The routinig id to use to notify the browser of any injections. Since the + // frame may be deleted in injection, we don't hold on to a reference to it + // directly. + int routing_id_; + + // The run location at which injection is happening. + UserScript::RunLocation run_location_; + + // The url of the frame, preserved for the same reason as the routing id. + GURL frame_url_; + + DISALLOW_COPY_AND_ASSIGN(ScriptsRunInfo); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SCRIPTS_RUN_INFO_H_ diff --git a/chromium/extensions/renderer/send_request_natives.cc b/chromium/extensions/renderer/send_request_natives.cc new file mode 100644 index 00000000000..ef3e93be9d9 --- /dev/null +++ b/chromium/extensions/renderer/send_request_natives.cc @@ -0,0 +1,79 @@ +// 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/renderer/send_request_natives.h" + +#include <stdint.h> + +#include "base/json/json_reader.h" +#include "content/public/child/v8_value_converter.h" +#include "extensions/renderer/request_sender.h" +#include "extensions/renderer/script_context.h" + +using content::V8ValueConverter; + +namespace extensions { + +SendRequestNatives::SendRequestNatives(RequestSender* request_sender, + ScriptContext* context) + : ObjectBackedNativeHandler(context), request_sender_(request_sender) { + RouteFunction( + "StartRequest", + base::Bind(&SendRequestNatives::StartRequest, base::Unretained(this))); + RouteFunction( + "GetGlobal", + base::Bind(&SendRequestNatives::GetGlobal, base::Unretained(this))); +} + +// Starts an API request to the browser, with an optional callback. The +// callback will be dispatched to EventBindings::HandleResponse. +void SendRequestNatives::StartRequest( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(5, args.Length()); + std::string name = *v8::String::Utf8Value(args[0]); + bool has_callback = args[2]->BooleanValue(); + bool for_io_thread = args[3]->BooleanValue(); + bool preserve_null_in_objects = args[4]->BooleanValue(); + + int request_id = request_sender_->GetNextRequestId(); + args.GetReturnValue().Set(static_cast<int32_t>(request_id)); + + scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); + + // See http://crbug.com/149880. The context menus APIs relies on this, but + // we shouldn't really be doing it (e.g. for the sake of the storage API). + converter->SetFunctionAllowed(true); + + if (!preserve_null_in_objects) + converter->SetStripNullFromObjects(true); + + scoped_ptr<base::Value> value_args( + converter->FromV8Value(args[1], context()->v8_context())); + if (!value_args.get() || !value_args->IsType(base::Value::TYPE_LIST)) { + NOTREACHED() << "Unable to convert args passed to StartRequest"; + return; + } + + request_sender_->StartRequest( + context(), + name, + request_id, + has_callback, + for_io_thread, + static_cast<base::ListValue*>(value_args.get())); +} + +void SendRequestNatives::GetGlobal( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsObject()); + v8::Local<v8::Context> v8_context = + v8::Local<v8::Object>::Cast(args[0])->CreationContext(); + if (ContextCanAccessObject(context()->v8_context(), v8_context->Global(), + false)) { + args.GetReturnValue().Set(v8_context->Global()); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/send_request_natives.h b/chromium/extensions/renderer/send_request_natives.h new file mode 100644 index 00000000000..69212e1dbb1 --- /dev/null +++ b/chromium/extensions/renderer/send_request_natives.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SEND_REQUEST_NATIVES_H_ +#define EXTENSIONS_RENDERER_SEND_REQUEST_NATIVES_H_ + +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { +class RequestSender; +class ScriptContext; + +// Native functions exposed to extensions via JS for calling API functions in +// the browser. +class SendRequestNatives : public ObjectBackedNativeHandler { + public: + SendRequestNatives(RequestSender* request_sender, ScriptContext* context); + + private: + // Starts an API request to the browser, with an optional callback. The + // callback will be dispatched to EventBindings::HandleResponse. + void StartRequest(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Gets a reference to an object's global object. + void GetGlobal(const v8::FunctionCallbackInfo<v8::Value>& args); + + RequestSender* request_sender_; + + DISALLOW_COPY_AND_ASSIGN(SendRequestNatives); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SEND_REQUEST_NATIVES_H_ diff --git a/chromium/extensions/renderer/set_icon_natives.cc b/chromium/extensions/renderer/set_icon_natives.cc new file mode 100644 index 00000000000..0df21459cb4 --- /dev/null +++ b/chromium/extensions/renderer/set_icon_natives.cc @@ -0,0 +1,155 @@ +// 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/renderer/set_icon_natives.h" + +#include <stddef.h> +#include <stdint.h> + +#include <limits> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "content/public/common/common_param_traits.h" +#include "extensions/renderer/request_sender.h" +#include "extensions/renderer/script_context.h" +#include "ipc/ipc_message_utils.h" +#include "third_party/WebKit/public/web/WebArrayBufferConverter.h" +#include "third_party/skia/include/core/SkBitmap.h" + +namespace { + +const char kInvalidDimensions[] = "ImageData has invalid dimensions."; +const char kInvalidData[] = "ImageData data length does not match dimensions."; +const char kNoMemory[] = "Chrome was unable to initialize icon."; + +} // namespace + +namespace extensions { + +SetIconNatives::SetIconNatives(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "SetIconCommon", + base::Bind(&SetIconNatives::SetIconCommon, base::Unretained(this))); +} + +bool SetIconNatives::ConvertImageDataToBitmapValue( + const v8::Local<v8::Object> image_data, + v8::Local<v8::Value>* image_data_bitmap) { + v8::Isolate* isolate = context()->v8_context()->GetIsolate(); + v8::Local<v8::Object> data = + image_data->Get(v8::String::NewFromUtf8(isolate, "data")) + ->ToObject(isolate); + int width = + image_data->Get(v8::String::NewFromUtf8(isolate, "width"))->Int32Value(); + int height = + image_data->Get(v8::String::NewFromUtf8(isolate, "height"))->Int32Value(); + + if (width <= 0 || height <= 0) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kInvalidDimensions))); + return false; + } + + // We need to be able to safely check |data_length| == 4 * width * height + // without overflowing below. + int max_width = (std::numeric_limits<int>::max() / 4) / height; + if (width > max_width) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, kInvalidDimensions))); + return false; + } + + int data_length = + data->Get(v8::String::NewFromUtf8(isolate, "length"))->Int32Value(); + if (data_length != 4 * width * height) { + isolate->ThrowException( + v8::Exception::Error(v8::String::NewFromUtf8(isolate, kInvalidData))); + return false; + } + + SkBitmap bitmap; + if (!bitmap.tryAllocN32Pixels(width, height)) { + isolate->ThrowException( + v8::Exception::Error(v8::String::NewFromUtf8(isolate, kNoMemory))); + return false; + } + bitmap.eraseARGB(0, 0, 0, 0); + + uint32_t* pixels = bitmap.getAddr32(0, 0); + for (int t = 0; t < width * height; t++) { + // |data| is RGBA, pixels is ARGB. + pixels[t] = SkPreMultiplyColor( + ((data->Get(v8::Integer::New(isolate, 4 * t + 3))->Int32Value() & 0xFF) + << 24) | + ((data->Get(v8::Integer::New(isolate, 4 * t + 0))->Int32Value() & 0xFF) + << 16) | + ((data->Get(v8::Integer::New(isolate, 4 * t + 1))->Int32Value() & 0xFF) + << 8) | + ((data->Get(v8::Integer::New(isolate, 4 * t + 2))->Int32Value() & 0xFF) + << 0)); + } + + // Construct the Value object. + IPC::Message bitmap_pickle; + IPC::WriteParam(&bitmap_pickle, bitmap); + blink::WebArrayBuffer buffer = + blink::WebArrayBuffer::create(bitmap_pickle.size(), 1); + memcpy(buffer.data(), bitmap_pickle.data(), bitmap_pickle.size()); + *image_data_bitmap = blink::WebArrayBufferConverter::toV8Value( + &buffer, context()->v8_context()->Global(), isolate); + + return true; +} + +bool SetIconNatives::ConvertImageDataSetToBitmapValueSet( + v8::Local<v8::Object>& details, + v8::Local<v8::Object>* bitmap_set_value) { + v8::Isolate* isolate = context()->v8_context()->GetIsolate(); + v8::Local<v8::Object> image_data_set = + details->Get(v8::String::NewFromUtf8(isolate, "imageData")) + ->ToObject(isolate); + + DCHECK(bitmap_set_value); + + v8::Local<v8::Array> property_names(image_data_set->GetOwnPropertyNames()); + for (size_t i = 0; i < property_names->Length(); ++i) { + v8::Local<v8::Value> key(property_names->Get(i)); + v8::String::Utf8Value utf8_key(key); + int size; + if (!base::StringToInt(std::string(*utf8_key), &size)) + continue; + v8::Local<v8::Object> image_data = + image_data_set->Get(key)->ToObject(isolate); + v8::Local<v8::Value> image_data_bitmap; + if (!ConvertImageDataToBitmapValue(image_data, &image_data_bitmap)) + return false; + (*bitmap_set_value)->Set(key, image_data_bitmap); + } + return true; +} + +void SetIconNatives::SetIconCommon( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsObject()); + v8::Local<v8::Object> details = args[0]->ToObject(args.GetIsolate()); + v8::Local<v8::Object> bitmap_set_value(v8::Object::New(args.GetIsolate())); + if (!ConvertImageDataSetToBitmapValueSet(details, &bitmap_set_value)) + return; + + v8::Local<v8::Object> dict(v8::Object::New(args.GetIsolate())); + dict->Set(v8::String::NewFromUtf8(args.GetIsolate(), "imageData"), + bitmap_set_value); + if (details->Has(v8::String::NewFromUtf8(args.GetIsolate(), "tabId"))) { + dict->Set( + v8::String::NewFromUtf8(args.GetIsolate(), "tabId"), + details->Get(v8::String::NewFromUtf8(args.GetIsolate(), "tabId"))); + } + args.GetReturnValue().Set(dict); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/set_icon_natives.h b/chromium/extensions/renderer/set_icon_natives.h new file mode 100644 index 00000000000..f42589188de --- /dev/null +++ b/chromium/extensions/renderer/set_icon_natives.h @@ -0,0 +1,33 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_SET_ICON_NATIVES_H_ +#define EXTENSIONS_RENDERER_SET_ICON_NATIVES_H_ + +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { +class ScriptContext; + +// Functions exposed to extension JS to implement the setIcon extension API. +class SetIconNatives : public ObjectBackedNativeHandler { + public: + explicit SetIconNatives(ScriptContext* context); + + private: + bool ConvertImageDataToBitmapValue(const v8::Local<v8::Object> image_data, + v8::Local<v8::Value>* image_data_bitmap); + bool ConvertImageDataSetToBitmapValueSet( + v8::Local<v8::Object>& details, + v8::Local<v8::Object>* bitmap_set_value); + void SetIconCommon(const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(SetIconNatives); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_SET_ICON_NATIVES_H_ diff --git a/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.cc b/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.cc new file mode 100644 index 00000000000..3f54b5ddfb5 --- /dev/null +++ b/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.cc @@ -0,0 +1,26 @@ +// 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/renderer/static_v8_external_one_byte_string_resource.h" + +namespace extensions { + +StaticV8ExternalOneByteStringResource::StaticV8ExternalOneByteStringResource( + const base::StringPiece& buffer) + : buffer_(buffer) { +} + +StaticV8ExternalOneByteStringResource:: + ~StaticV8ExternalOneByteStringResource() { +} + +const char* StaticV8ExternalOneByteStringResource::data() const { + return buffer_.data(); +} + +size_t StaticV8ExternalOneByteStringResource::length() const { + return buffer_.length(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.h b/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.h new file mode 100644 index 00000000000..3f569585a7b --- /dev/null +++ b/chromium/extensions/renderer/static_v8_external_one_byte_string_resource.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_STATIC_V8_EXTERNAL_ONE_BYTE_STRING_RESOURCE_H_ +#define EXTENSIONS_RENDERER_STATIC_V8_EXTERNAL_ONE_BYTE_STRING_RESOURCE_H_ + +#include <stddef.h> + +#include "base/compiler_specific.h" +#include "base/strings/string_piece.h" +#include "v8/include/v8.h" + +namespace extensions { + +// A very simple implementation of v8::ExternalAsciiStringResource that just +// wraps a buffer. The buffer must outlive the v8 runtime instance this resource +// is used in. +class StaticV8ExternalOneByteStringResource + : public v8::String::ExternalOneByteStringResource { + public: + explicit StaticV8ExternalOneByteStringResource( + const base::StringPiece& buffer); + ~StaticV8ExternalOneByteStringResource() override; + + const char* data() const override; + size_t length() const override; + + private: + base::StringPiece buffer_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_STATIC_V8_EXTERNAL_ONE_BYTE_STRING_RESOURCE_H_ diff --git a/chromium/extensions/renderer/test_extensions_renderer_client.cc b/chromium/extensions/renderer/test_extensions_renderer_client.cc new file mode 100644 index 00000000000..5299878ac8a --- /dev/null +++ b/chromium/extensions/renderer/test_extensions_renderer_client.cc @@ -0,0 +1,22 @@ +// 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/renderer/test_extensions_renderer_client.h" + +namespace extensions { + +TestExtensionsRendererClient::TestExtensionsRendererClient() {} + +TestExtensionsRendererClient::~TestExtensionsRendererClient() {} + +bool TestExtensionsRendererClient::IsIncognitoProcess() const { + return false; +} + +int TestExtensionsRendererClient::GetLowestIsolatedWorldId() const { + // Note that 0 is reserved for the global world. + return 1; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/test_extensions_renderer_client.h b/chromium/extensions/renderer/test_extensions_renderer_client.h new file mode 100644 index 00000000000..f79c6a95e35 --- /dev/null +++ b/chromium/extensions/renderer/test_extensions_renderer_client.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_TEST_EXTENSIONS_RENDERER_CLIENT_H_ +#define EXTENSIONS_RENDERER_TEST_EXTENSIONS_RENDERER_CLIENT_H_ + +#include "base/macros.h" +#include "extensions/renderer/extensions_renderer_client.h" + +namespace extensions { + +class TestExtensionsRendererClient : public ExtensionsRendererClient { + public: + TestExtensionsRendererClient(); + ~TestExtensionsRendererClient() override; + + // ExtensionsRendererClient implementation. + bool IsIncognitoProcess() const override; + int GetLowestIsolatedWorldId() const override; + + private: + DISALLOW_COPY_AND_ASSIGN(TestExtensionsRendererClient); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_TEST_EXTENSIONS_RENDERER_CLIENT_H_ diff --git a/chromium/extensions/renderer/test_features_native_handler.cc b/chromium/extensions/renderer/test_features_native_handler.cc new file mode 100644 index 00000000000..0f228555809 --- /dev/null +++ b/chromium/extensions/renderer/test_features_native_handler.cc @@ -0,0 +1,32 @@ +// 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/renderer/test_features_native_handler.h" + +#include "base/bind.h" +#include "content/public/child/v8_value_converter.h" +#include "extensions/common/extensions_client.h" +#include "extensions/common/features/json_feature_provider_source.h" +#include "extensions/renderer/script_context.h" + +namespace extensions { + +TestFeaturesNativeHandler::TestFeaturesNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("GetAPIFeatures", + base::Bind(&TestFeaturesNativeHandler::GetAPIFeatures, + base::Unretained(this))); +} + +void TestFeaturesNativeHandler::GetAPIFeatures( + const v8::FunctionCallbackInfo<v8::Value>& args) { + scoped_ptr<JSONFeatureProviderSource> source( + ExtensionsClient::Get()->CreateFeatureProviderSource("api")); + scoped_ptr<content::V8ValueConverter> converter( + content::V8ValueConverter::create()); + args.GetReturnValue().Set( + converter->ToV8Value(&source->dictionary(), context()->v8_context())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/test_features_native_handler.h b/chromium/extensions/renderer/test_features_native_handler.h new file mode 100644 index 00000000000..2422178a1e0 --- /dev/null +++ b/chromium/extensions/renderer/test_features_native_handler.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_TEST_FEATURES_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_TEST_FEATURES_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class TestFeaturesNativeHandler : public ObjectBackedNativeHandler { + public: + explicit TestFeaturesNativeHandler(ScriptContext* context); + + private: + void GetAPIFeatures(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_TEST_FEATURES_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/test_native_handler.cc b/chromium/extensions/renderer/test_native_handler.cc new file mode 100644 index 00000000000..31e7dcb9d75 --- /dev/null +++ b/chromium/extensions/renderer/test_native_handler.cc @@ -0,0 +1,24 @@ +// Copyright 2015 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/test_native_handler.h" + +#include "extensions/renderer/wake_event_page.h" + +namespace extensions { + +TestNativeHandler::TestNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "GetWakeEventPage", + base::Bind(&TestNativeHandler::GetWakeEventPage, base::Unretained(this))); +} + +void TestNativeHandler::GetWakeEventPage( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(0, args.Length()); + args.GetReturnValue().Set(WakeEventPage::Get()->GetForContext(context())); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/test_native_handler.h b/chromium/extensions/renderer/test_native_handler.h new file mode 100644 index 00000000000..88e5d239845 --- /dev/null +++ b/chromium/extensions/renderer/test_native_handler.h @@ -0,0 +1,29 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_TEST_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_TEST_NATIVE_HANDLER_H_ + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "v8/include/v8.h" + +namespace extensions { +class ScriptContext; + +// NativeHandler for the chrome.test API. +class TestNativeHandler : public ObjectBackedNativeHandler { + public: + explicit TestNativeHandler(ScriptContext* context); + + private: + void GetWakeEventPage(const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(TestNativeHandler); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_TEST_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/user_gestures_native_handler.cc b/chromium/extensions/renderer/user_gestures_native_handler.cc new file mode 100644 index 00000000000..c01a4ae7240 --- /dev/null +++ b/chromium/extensions/renderer/user_gestures_native_handler.cc @@ -0,0 +1,55 @@ +// 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/renderer/user_gestures_native_handler.h" + +#include "base/bind.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebScopedUserGesture.h" +#include "third_party/WebKit/public/web/WebUserGestureIndicator.h" + +namespace extensions { + +UserGesturesNativeHandler::UserGesturesNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction("IsProcessingUserGesture", + "test", + base::Bind(&UserGesturesNativeHandler::IsProcessingUserGesture, + base::Unretained(this))); + RouteFunction("RunWithUserGesture", + "test", + base::Bind(&UserGesturesNativeHandler::RunWithUserGesture, + base::Unretained(this))); + RouteFunction("RunWithoutUserGesture", + "test", + base::Bind(&UserGesturesNativeHandler::RunWithoutUserGesture, + base::Unretained(this))); +} + +void UserGesturesNativeHandler::IsProcessingUserGesture( + const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set(v8::Boolean::New( + args.GetIsolate(), + blink::WebUserGestureIndicator::isProcessingUserGesture())); +} + +void UserGesturesNativeHandler::RunWithUserGesture( + const v8::FunctionCallbackInfo<v8::Value>& args) { + blink::WebScopedUserGesture user_gesture; + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + v8::Local<v8::Value> no_args; + context()->CallFunction(v8::Local<v8::Function>::Cast(args[0]), 0, &no_args); +} + +void UserGesturesNativeHandler::RunWithoutUserGesture( + const v8::FunctionCallbackInfo<v8::Value>& args) { + blink::WebUserGestureIndicator::consumeUserGesture(); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + v8::Local<v8::Value> no_args; + context()->CallFunction(v8::Local<v8::Function>::Cast(args[0]), 0, &no_args); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/user_gestures_native_handler.h b/chromium/extensions/renderer/user_gestures_native_handler.h new file mode 100644 index 00000000000..fbe15fbebc3 --- /dev/null +++ b/chromium/extensions/renderer/user_gestures_native_handler.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_USER_GESTURES_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_USER_GESTURES_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class UserGesturesNativeHandler : public ObjectBackedNativeHandler { + public: + explicit UserGesturesNativeHandler(ScriptContext* context); + + private: + void IsProcessingUserGesture(const v8::FunctionCallbackInfo<v8::Value>& args); + void RunWithUserGesture(const v8::FunctionCallbackInfo<v8::Value>& args); + void RunWithoutUserGesture(const v8::FunctionCallbackInfo<v8::Value>& args); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_USER_GESTURES_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/user_script_injector.cc b/chromium/extensions/renderer/user_script_injector.cc new file mode 100644 index 00000000000..9885b4ea8d3 --- /dev/null +++ b/chromium/extensions/renderer/user_script_injector.cc @@ -0,0 +1,268 @@ +// 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/renderer/user_script_injector.h" + +#include <tuple> +#include <vector> + +#include "base/lazy_instance.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_view.h" +#include "extensions/common/extension.h" +#include "extensions/common/guest_view/extensions_guest_view_messages.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/renderer/injection_host.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/scripts_run_info.h" +#include "grit/extensions_renderer_resources.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScriptSource.h" +#include "ui/base/resource/resource_bundle.h" +#include "url/gurl.h" + +namespace extensions { + +namespace { + +struct RoutingInfoKey { + int routing_id; + int script_id; + + RoutingInfoKey(int routing_id, int script_id) + : routing_id(routing_id), script_id(script_id) {} + + bool operator<(const RoutingInfoKey& other) const { + return std::tie(routing_id, script_id) < + std::tie(other.routing_id, other.script_id); + } +}; + +using RoutingInfoMap = std::map<RoutingInfoKey, bool>; + +// These two strings are injected before and after the Greasemonkey API and +// user script to wrap it in an anonymous scope. +const char kUserScriptHead[] = "(function (unsafeWindow) {\n"; +const char kUserScriptTail[] = "\n})(window);"; + +// A map records whether a given |script_id| from a webview-added user script +// is allowed to inject on the render of given |routing_id|. +// Once a script is added, the decision of whether or not allowed to inject +// won't be changed. +// After removed by the webview, the user scipt will also be removed +// from the render. Therefore, there won't be any query from the same +// |script_id| and |routing_id| pair. +base::LazyInstance<RoutingInfoMap> g_routing_info_map = + LAZY_INSTANCE_INITIALIZER; + +// Greasemonkey API source that is injected with the scripts. +struct GreasemonkeyApiJsString { + GreasemonkeyApiJsString(); + blink::WebScriptSource GetSource() const; + + private: + std::string source_; +}; + +// The below constructor, monstrous as it is, just makes a WebScriptSource from +// the GreasemonkeyApiJs resource. +GreasemonkeyApiJsString::GreasemonkeyApiJsString() + : source_(ResourceBundle::GetSharedInstance() + .GetRawDataResource(IDR_GREASEMONKEY_API_JS) + .as_string()) { +} + +blink::WebScriptSource GreasemonkeyApiJsString::GetSource() const { + return blink::WebScriptSource(blink::WebString::fromUTF8(source_)); +} + +base::LazyInstance<GreasemonkeyApiJsString> g_greasemonkey_api = + LAZY_INSTANCE_INITIALIZER; + +} // namespace + +UserScriptInjector::UserScriptInjector(const UserScript* script, + UserScriptSet* script_list, + bool is_declarative) + : script_(script), + script_id_(script_->id()), + host_id_(script_->host_id()), + is_declarative_(is_declarative), + user_script_set_observer_(this) { + user_script_set_observer_.Add(script_list); +} + +UserScriptInjector::~UserScriptInjector() { +} + +void UserScriptInjector::OnUserScriptsUpdated( + const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) { + // If the host causing this injection changed, then this injection + // will be removed, and there's no guarantee the backing script still exists. + if (changed_hosts.count(host_id_) > 0) + return; + + for (std::vector<UserScript*>::const_iterator iter = scripts.begin(); + iter != scripts.end(); + ++iter) { + // We need to compare to |script_id_| (and not to script_->id()) because the + // old |script_| may be deleted by now. + if ((*iter)->id() == script_id_) { + script_ = *iter; + break; + } + } +} + +UserScript::InjectionType UserScriptInjector::script_type() const { + return UserScript::CONTENT_SCRIPT; +} + +bool UserScriptInjector::ShouldExecuteInMainWorld() const { + return false; +} + +bool UserScriptInjector::IsUserGesture() const { + return false; +} + +bool UserScriptInjector::ExpectsResults() const { + return false; +} + +bool UserScriptInjector::ShouldInjectJs( + UserScript::RunLocation run_location) const { + return script_->run_location() == run_location && + !script_->js_scripts().empty(); +} + +bool UserScriptInjector::ShouldInjectCss( + UserScript::RunLocation run_location) const { + return run_location == UserScript::DOCUMENT_START && + !script_->css_scripts().empty(); +} + +PermissionsData::AccessType UserScriptInjector::CanExecuteOnFrame( + const InjectionHost* injection_host, + blink::WebLocalFrame* web_frame, + int tab_id) const { + if (script_->consumer_instance_type() == + UserScript::ConsumerInstanceType::WEBVIEW) { + int routing_id = content::RenderView::FromWebView(web_frame->top()->view()) + ->GetRoutingID(); + + RoutingInfoKey key(routing_id, script_->id()); + + RoutingInfoMap& map = g_routing_info_map.Get(); + auto iter = map.find(key); + + bool allowed = false; + if (iter != map.end()) { + allowed = iter->second; + } else { + // Send a SYNC IPC message to the browser to check if this is allowed. + // This is not ideal, but is mitigated by the fact that this is only done + // for webviews, and then only once per host. + // TODO(hanxi): Find a more efficient way to do this. + content::RenderThread::Get()->Send( + new ExtensionsGuestViewHostMsg_CanExecuteContentScriptSync( + routing_id, script_->id(), &allowed)); + map.insert(std::pair<RoutingInfoKey, bool>(key, allowed)); + } + + return allowed ? PermissionsData::ACCESS_ALLOWED + : PermissionsData::ACCESS_DENIED; + } + + GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL( + web_frame, web_frame->document().url(), script_->match_about_blank()); + + return injection_host->CanExecuteOnFrame( + effective_document_url, + content::RenderFrame::FromWebFrame(web_frame), + tab_id, + is_declarative_); +} + +std::vector<blink::WebScriptSource> UserScriptInjector::GetJsSources( + UserScript::RunLocation run_location) const { + DCHECK_EQ(script_->run_location(), run_location); + + std::vector<blink::WebScriptSource> sources; + const UserScript::FileList& js_scripts = script_->js_scripts(); + bool is_standalone_or_emulate_greasemonkey = + script_->is_standalone() || script_->emulate_greasemonkey(); + + for (UserScript::FileList::const_iterator iter = js_scripts.begin(); + iter != js_scripts.end(); + ++iter) { + std::string content = iter->GetContent().as_string(); + + // We add this dumb function wrapper for standalone user script to + // emulate what Greasemonkey does. + // TODO(aa): I think that maybe "is_standalone" scripts don't exist + // anymore. Investigate. + if (is_standalone_or_emulate_greasemonkey) { + content.insert(0, kUserScriptHead); + content += kUserScriptTail; + } + sources.push_back(blink::WebScriptSource( + blink::WebString::fromUTF8(content), iter->url())); + } + + // Emulate Greasemonkey API for scripts that were converted to extensions + // and "standalone" user scripts. + if (is_standalone_or_emulate_greasemonkey) + sources.insert(sources.begin(), g_greasemonkey_api.Get().GetSource()); + + return sources; +} + +std::vector<std::string> UserScriptInjector::GetCssSources( + UserScript::RunLocation run_location) const { + DCHECK_EQ(UserScript::DOCUMENT_START, run_location); + + std::vector<std::string> sources; + const UserScript::FileList& css_scripts = script_->css_scripts(); + for (UserScript::FileList::const_iterator iter = css_scripts.begin(); + iter != css_scripts.end(); + ++iter) { + sources.push_back(iter->GetContent().as_string()); + } + return sources; +} + +void UserScriptInjector::GetRunInfo( + ScriptsRunInfo* scripts_run_info, + UserScript::RunLocation run_location) const { + if (ShouldInjectJs(run_location)) { + const UserScript::FileList& js_scripts = script_->js_scripts(); + scripts_run_info->num_js += js_scripts.size(); + for (UserScript::FileList::const_iterator iter = js_scripts.begin(); + iter != js_scripts.end(); + ++iter) { + scripts_run_info->executing_scripts[host_id_.id()].insert( + iter->url().path()); + } + } + + if (ShouldInjectCss(run_location)) + scripts_run_info->num_css += script_->css_scripts().size(); +} + +void UserScriptInjector::OnInjectionComplete( + scoped_ptr<base::Value> execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) { +} + +void UserScriptInjector::OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) { +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/user_script_injector.h b/chromium/extensions/renderer/user_script_injector.h new file mode 100644 index 00000000000..61909894e31 --- /dev/null +++ b/chromium/extensions/renderer/user_script_injector.h @@ -0,0 +1,86 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_USER_SCRIPT_INJECTOR_H_ +#define EXTENSIONS_RENDERER_USER_SCRIPT_INJECTOR_H_ + +#include <string> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/scoped_observer.h" +#include "extensions/common/user_script.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/user_script_set.h" + +class InjectionHost; + +namespace blink { +class WebLocalFrame; +} + +namespace extensions { + +// A ScriptInjector for UserScripts. +class UserScriptInjector : public ScriptInjector, + public UserScriptSet::Observer { + public: + UserScriptInjector(const UserScript* user_script, + UserScriptSet* user_script_set, + bool is_declarative); + ~UserScriptInjector() override; + + private: + // UserScriptSet::Observer implementation. + void OnUserScriptsUpdated(const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) override; + + // ScriptInjector implementation. + UserScript::InjectionType script_type() const override; + bool ShouldExecuteInMainWorld() const override; + bool IsUserGesture() const override; + bool ExpectsResults() const override; + bool ShouldInjectJs(UserScript::RunLocation run_location) const override; + bool ShouldInjectCss(UserScript::RunLocation run_location) const override; + PermissionsData::AccessType CanExecuteOnFrame( + const InjectionHost* injection_host, + blink::WebLocalFrame* web_frame, + int tab_id) const override; + std::vector<blink::WebScriptSource> GetJsSources( + UserScript::RunLocation run_location) const override; + std::vector<std::string> GetCssSources( + UserScript::RunLocation run_location) const override; + void GetRunInfo(ScriptsRunInfo* scripts_run_info, + UserScript::RunLocation run_location) const override; + void OnInjectionComplete(scoped_ptr<base::Value> execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) override; + void OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) override; + + // The associated user script. Owned by the UserScriptInjector that created + // this object. + const UserScript* script_; + + // The id of the associated user script. We cache this because when we update + // the |script_| associated with this injection, the old referance may be + // deleted. + int script_id_; + + // The associated host id, preserved for the same reason as |script_id|. + HostID host_id_; + + // Indicates whether or not this script is declarative. This influences which + // script permissions are checked before injection. + bool is_declarative_; + + ScopedObserver<UserScriptSet, UserScriptSet::Observer> + user_script_set_observer_; + + DISALLOW_COPY_AND_ASSIGN(UserScriptInjector); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_USER_SCRIPT_INJECTOR_H_ diff --git a/chromium/extensions/renderer/user_script_set.cc b/chromium/extensions/renderer/user_script_set.cc new file mode 100644 index 00000000000..e289f3fc56a --- /dev/null +++ b/chromium/extensions/renderer/user_script_set.cc @@ -0,0 +1,235 @@ +// 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/renderer/user_script_set.h" + +#include <stddef.h> + +#include <utility> + +#include "base/memory/ref_counted.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension.h" +#include "extensions/common/extensions_client.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/renderer/extension_injection_host.h" +#include "extensions/renderer/extensions_renderer_client.h" +#include "extensions/renderer/injection_host.h" +#include "extensions/renderer/renderer_extension_registry.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/user_script_injector.h" +#include "extensions/renderer/web_ui_injection_host.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "url/gurl.h" + +namespace extensions { + +namespace { + +GURL GetDocumentUrlForFrame(blink::WebLocalFrame* frame) { + GURL data_source_url = ScriptContext::GetDataSourceURLForFrame(frame); + if (!data_source_url.is_empty() && frame->isViewSourceModeEnabled()) { + data_source_url = GURL(content::kViewSourceScheme + std::string(":") + + data_source_url.spec()); + } + + return data_source_url; +} + +} // namespace + +UserScriptSet::UserScriptSet() {} + +UserScriptSet::~UserScriptSet() { +} + +void UserScriptSet::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserScriptSet::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void UserScriptSet::GetActiveExtensionIds( + std::set<std::string>* ids) const { + for (const UserScript* script : scripts_) { + if (script->host_id().type() != HostID::EXTENSIONS) + continue; + DCHECK(!script->extension_id().empty()); + ids->insert(script->extension_id()); + } +} + +void UserScriptSet::GetInjections( + std::vector<scoped_ptr<ScriptInjection>>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location) { + GURL document_url = GetDocumentUrlForFrame(render_frame->GetWebFrame()); + for (const UserScript* script : scripts_) { + scoped_ptr<ScriptInjection> injection = GetInjectionForScript( + script, + render_frame, + tab_id, + run_location, + document_url, + false /* is_declarative */); + if (injection.get()) + injections->push_back(std::move(injection)); + } +} + +bool UserScriptSet::UpdateUserScripts(base::SharedMemoryHandle shared_memory, + const std::set<HostID>& changed_hosts, + bool whitelisted_only) { + bool only_inject_incognito = + ExtensionsRendererClient::Get()->IsIncognitoProcess(); + + // Create the shared memory object (read only). + shared_memory_.reset(new base::SharedMemory(shared_memory, true)); + if (!shared_memory_.get()) + return false; + + // First get the size of the memory block. + if (!shared_memory_->Map(sizeof(base::Pickle::Header))) + return false; + base::Pickle::Header* pickle_header = + reinterpret_cast<base::Pickle::Header*>(shared_memory_->memory()); + + // Now map in the rest of the block. + int pickle_size = sizeof(base::Pickle::Header) + pickle_header->payload_size; + shared_memory_->Unmap(); + if (!shared_memory_->Map(pickle_size)) + return false; + + // Unpickle scripts. + uint32_t num_scripts = 0; + base::Pickle pickle(reinterpret_cast<char*>(shared_memory_->memory()), + pickle_size); + base::PickleIterator iter(pickle); + CHECK(iter.ReadUInt32(&num_scripts)); + + scripts_.clear(); + scripts_.reserve(num_scripts); + for (uint32_t i = 0; i < num_scripts; ++i) { + scoped_ptr<UserScript> script(new UserScript()); + script->Unpickle(pickle, &iter); + + // Note that this is a pointer into shared memory. We don't own it. It gets + // cleared up when the last renderer or browser process drops their + // reference to the shared memory. + for (size_t j = 0; j < script->js_scripts().size(); ++j) { + const char* body = NULL; + int body_length = 0; + CHECK(iter.ReadData(&body, &body_length)); + script->js_scripts()[j].set_external_content( + base::StringPiece(body, body_length)); + } + for (size_t j = 0; j < script->css_scripts().size(); ++j) { + const char* body = NULL; + int body_length = 0; + CHECK(iter.ReadData(&body, &body_length)); + script->css_scripts()[j].set_external_content( + base::StringPiece(body, body_length)); + } + + if (only_inject_incognito && !script->is_incognito_enabled()) + continue; // This script shouldn't run in an incognito tab. + + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(script->extension_id()); + if (whitelisted_only && + (!extension || + !PermissionsData::CanExecuteScriptEverywhere(extension))) { + continue; + } + + scripts_.push_back(std::move(script)); + } + + FOR_EACH_OBSERVER(Observer, + observers_, + OnUserScriptsUpdated(changed_hosts, scripts_.get())); + return true; +} + +scoped_ptr<ScriptInjection> UserScriptSet::GetDeclarativeScriptInjection( + int script_id, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url) { + for (const UserScript* script : scripts_) { + if (script->id() == script_id) { + return GetInjectionForScript(script, + render_frame, + tab_id, + run_location, + document_url, + true /* is_declarative */); + } + } + return scoped_ptr<ScriptInjection>(); +} + +scoped_ptr<ScriptInjection> UserScriptSet::GetInjectionForScript( + const UserScript* script, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url, + bool is_declarative) { + scoped_ptr<ScriptInjection> injection; + scoped_ptr<const InjectionHost> injection_host; + blink::WebLocalFrame* web_frame = render_frame->GetWebFrame(); + + const HostID& host_id = script->host_id(); + if (host_id.type() == HostID::EXTENSIONS) { + injection_host = ExtensionInjectionHost::Create(host_id.id()); + if (!injection_host) + return injection; + } else { + DCHECK_EQ(host_id.type(), HostID::WEBUI); + injection_host.reset(new WebUIInjectionHost(host_id)); + } + + if (web_frame->parent() && !script->match_all_frames()) + return injection; // Only match subframes if the script declared it. + + GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL( + web_frame, document_url, script->match_about_blank()); + + if (!script->MatchesURL(effective_document_url)) + return injection; + + scoped_ptr<ScriptInjector> injector(new UserScriptInjector(script, + this, + is_declarative)); + + if (injector->CanExecuteOnFrame( + injection_host.get(), + web_frame, + tab_id) == + PermissionsData::ACCESS_DENIED) { + return injection; + } + + bool inject_css = !script->css_scripts().empty() && + run_location == UserScript::DOCUMENT_START; + bool inject_js = + !script->js_scripts().empty() && script->run_location() == run_location; + if (inject_css || inject_js) { + injection.reset(new ScriptInjection(std::move(injector), render_frame, + std::move(injection_host), + run_location)); + } + return injection; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/user_script_set.h b/chromium/extensions/renderer/user_script_set.h new file mode 100644 index 00000000000..128b33f0be9 --- /dev/null +++ b/chromium/extensions/renderer/user_script_set.h @@ -0,0 +1,100 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_USER_SCRIPT_SET_H_ +#define EXTENSIONS_RENDERER_USER_SCRIPT_SET_H_ + +#include <set> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/memory/shared_memory.h" +#include "base/observer_list.h" +#include "content/public/renderer/render_process_observer.h" +#include "extensions/common/user_script.h" + +class GURL; + +namespace content { +class RenderFrame; +} + +namespace extensions { +class ScriptInjection; + +// The UserScriptSet is a collection of UserScripts which knows how to update +// itself from SharedMemory and create ScriptInjections for UserScripts to +// inject on a page. +class UserScriptSet { + public: + class Observer { + public: + virtual void OnUserScriptsUpdated( + const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) = 0; + }; + + UserScriptSet(); + ~UserScriptSet(); + + // Adds or removes observers. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Appends the ids of the extensions that have user scripts to |ids|. + void GetActiveExtensionIds(std::set<std::string>* ids) const; + + // Append any ScriptInjections that should run on the given |render_frame| and + // |tab_id|, at the given |run_location|, to |injections|. + // |extensions| is passed in to verify the corresponding extension is still + // valid. + void GetInjections(std::vector<scoped_ptr<ScriptInjection>>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location); + + scoped_ptr<ScriptInjection> GetDeclarativeScriptInjection( + int script_id, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url); + + // Updates scripts given the shared memory region containing user scripts. + // Returns true if the scripts were successfully updated. + bool UpdateUserScripts(base::SharedMemoryHandle shared_memory, + const std::set<HostID>& changed_hosts, + bool whitelisted_only); + + const std::vector<UserScript*>& scripts() const { return scripts_.get(); } + + private: + // Returns a new ScriptInjection for the given |script| to execute in the + // |render_frame|, or NULL if the script should not execute. + scoped_ptr<ScriptInjection> GetInjectionForScript( + const UserScript* script, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url, + bool is_declarative); + + // Shared memory containing raw script data. + scoped_ptr<base::SharedMemory> shared_memory_; + + // The UserScripts this injector manages. + ScopedVector<UserScript> scripts_; + + // The associated observers. + base::ObserverList<Observer> observers_; + + DISALLOW_COPY_AND_ASSIGN(UserScriptSet); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_USER_SCRIPT_SET_H_ diff --git a/chromium/extensions/renderer/user_script_set_manager.cc b/chromium/extensions/renderer/user_script_set_manager.cc new file mode 100644 index 00000000000..0404e0552b9 --- /dev/null +++ b/chromium/extensions/renderer/user_script_set_manager.cc @@ -0,0 +1,159 @@ +// 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/renderer/user_script_set_manager.h" + +#include "components/crx_file/id_util.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/script_injection.h" +#include "extensions/renderer/user_script_set.h" +#include "ipc/ipc_message.h" +#include "ipc/ipc_message_macros.h" + +namespace extensions { + +UserScriptSetManager::UserScriptSetManager() { + content::RenderThread::Get()->AddObserver(this); +} + +UserScriptSetManager::~UserScriptSetManager() { +} + +void UserScriptSetManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserScriptSetManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +scoped_ptr<ScriptInjection> +UserScriptSetManager::GetInjectionForDeclarativeScript( + int script_id, + content::RenderFrame* render_frame, + int tab_id, + const GURL& url, + const std::string& extension_id) { + UserScriptSet* user_script_set = + GetProgrammaticScriptsByHostID(HostID(HostID::EXTENSIONS, extension_id)); + if (!user_script_set) + return scoped_ptr<ScriptInjection>(); + + return user_script_set->GetDeclarativeScriptInjection( + script_id, + render_frame, + tab_id, + UserScript::BROWSER_DRIVEN, + url); +} + +bool UserScriptSetManager::OnControlMessageReceived( + const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(UserScriptSetManager, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateUserScripts, OnUpdateUserScripts) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void UserScriptSetManager::GetAllInjections( + std::vector<scoped_ptr<ScriptInjection>>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location) { + static_scripts_.GetInjections(injections, render_frame, tab_id, run_location); + for (UserScriptSetMap::iterator it = programmatic_scripts_.begin(); + it != programmatic_scripts_.end(); + ++it) { + it->second->GetInjections(injections, render_frame, tab_id, run_location); + } +} + +void UserScriptSetManager::GetAllActiveExtensionIds( + std::set<std::string>* ids) const { + DCHECK(ids); + static_scripts_.GetActiveExtensionIds(ids); + for (UserScriptSetMap::const_iterator it = programmatic_scripts_.begin(); + it != programmatic_scripts_.end(); + ++it) { + it->second->GetActiveExtensionIds(ids); + } +} + +UserScriptSet* UserScriptSetManager::GetProgrammaticScriptsByHostID( + const HostID& host_id) { + UserScriptSetMap::const_iterator it = programmatic_scripts_.find(host_id); + return it != programmatic_scripts_.end() ? it->second.get() : NULL; +} + +void UserScriptSetManager::OnUpdateUserScripts( + base::SharedMemoryHandle shared_memory, + const HostID& host_id, + const std::set<HostID>& changed_hosts, + bool whitelisted_only) { + if (!base::SharedMemory::IsHandleValid(shared_memory)) { + NOTREACHED() << "Bad scripts handle"; + return; + } + + for (const HostID& host_id : changed_hosts) { + if (host_id.type() == HostID::EXTENSIONS && + !crx_file::id_util::IdIsValid(host_id.id())) { + NOTREACHED() << "Invalid extension id: " << host_id.id(); + return; + } + } + + UserScriptSet* scripts = NULL; + if (!host_id.id().empty()) { + // The expectation when there is a host that "owns" this shared + // memory region is that the |changed_hosts| is either the empty list + // or just the owner. + CHECK(changed_hosts.size() <= 1); + if (programmatic_scripts_.find(host_id) == programmatic_scripts_.end()) { + scripts = new UserScriptSet(); + programmatic_scripts_[host_id] = make_linked_ptr(scripts); + } else { + scripts = programmatic_scripts_[host_id].get(); + } + } else { + scripts = &static_scripts_; + } + DCHECK(scripts); + + // If no hosts are included in the set, that indicates that all + // hosts were updated. Add them all to the set so that observers and + // individual UserScriptSets don't need to know this detail. + const std::set<HostID>* effective_hosts = &changed_hosts; + std::set<HostID> all_hosts; + if (changed_hosts.empty()) { + // The meaning of "all hosts(extensions)" varies, depending on whether some + // host "owns" this shared memory region. + // No owner => all known hosts. + // Owner => just the owner host. + if (host_id.id().empty()) { + std::set<std::string> extension_ids = + RendererExtensionRegistry::Get()->GetIDs(); + for (const std::string& extension_id : extension_ids) + all_hosts.insert(HostID(HostID::EXTENSIONS, extension_id)); + } else { + all_hosts.insert(host_id); + } + effective_hosts = &all_hosts; + } + + if (scripts->UpdateUserScripts(shared_memory, + *effective_hosts, + whitelisted_only)) { + FOR_EACH_OBSERVER( + Observer, + observers_, + OnUserScriptsUpdated(*effective_hosts, scripts->scripts())); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/user_script_set_manager.h b/chromium/extensions/renderer/user_script_set_manager.h new file mode 100644 index 00000000000..5cba81e7bc2 --- /dev/null +++ b/chromium/extensions/renderer/user_script_set_manager.h @@ -0,0 +1,113 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_USER_SCRIPT_SET_MANAGER_H_ +#define EXTENSIONS_RENDERER_USER_SCRIPT_SET_MANAGER_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/linked_ptr.h" +#include "base/memory/shared_memory.h" +#include "base/observer_list.h" +#include "content/public/renderer/render_process_observer.h" +#include "extensions/common/extension.h" +#include "extensions/common/user_script.h" +#include "extensions/renderer/user_script_set.h" + +namespace content { +class RenderFrame; +} + +namespace IPC { +class Message; +} + +namespace extensions { + +class ScriptInjection; + +// Manager for separate UserScriptSets, one for each shared memory region. +// Regions are organized as follows: +// static_scripts -- contains all extensions' scripts that are statically +// declared in the extension manifest. +// programmatic_scripts -- one region per host (extension or WebUI) containing +// only programmatically-declared scripts, instantiated +// when an extension first creates a declarative rule +// that would, if triggered, request a script injection. +class UserScriptSetManager : public content::RenderProcessObserver { + public: + // Like a UserScriptSet::Observer, but automatically subscribes to all sets + // associated with the manager. + class Observer { + public: + virtual void OnUserScriptsUpdated( + const std::set<HostID>& changed_hosts, + const std::vector<UserScript*>& scripts) = 0; + }; + + UserScriptSetManager(); + + ~UserScriptSetManager() override; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Looks up the script injection associated with |script_id| and + // |extension_id| in the context of the given |web_frame|, |tab_id|, + // and |url|. + scoped_ptr<ScriptInjection> GetInjectionForDeclarativeScript( + int script_id, + content::RenderFrame* render_frame, + int tab_id, + const GURL& url, + const std::string& extension_id); + + // Append all injections from |static_scripts| and each of + // |programmatic_scripts_| to |injections|. + void GetAllInjections(std::vector<scoped_ptr<ScriptInjection>>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location); + + // Get active extension IDs from |static_scripts| and each of + // |programmatic_scripts_|. + void GetAllActiveExtensionIds(std::set<std::string>* ids) const; + + const UserScriptSet* static_scripts() const { return &static_scripts_; } + + private: + // Map for per-extension sets that may be defined programmatically. + typedef std::map<HostID, linked_ptr<UserScriptSet> > UserScriptSetMap; + + // content::RenderProcessObserver implementation. + bool OnControlMessageReceived(const IPC::Message& message) override; + + UserScriptSet* GetProgrammaticScriptsByHostID(const HostID& host_id); + + // Handle the UpdateUserScripts extension message. + void OnUpdateUserScripts(base::SharedMemoryHandle shared_memory, + const HostID& host_id, + const std::set<HostID>& changed_hosts, + bool whitelisted_only); + + // Scripts statically defined in extension manifests. + UserScriptSet static_scripts_; + + // Scripts programmatically-defined through API calls (initialized and stored + // per-extension). + UserScriptSetMap programmatic_scripts_; + + // The associated observers. + base::ObserverList<Observer> observers_; + + DISALLOW_COPY_AND_ASSIGN(UserScriptSetManager); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_USER_SCRIPT_SET_MANAGER_H_ diff --git a/chromium/extensions/renderer/utils_native_handler.cc b/chromium/extensions/renderer/utils_native_handler.cc new file mode 100644 index 00000000000..741a111b779 --- /dev/null +++ b/chromium/extensions/renderer/utils_native_handler.cc @@ -0,0 +1,29 @@ +// 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/renderer/utils_native_handler.h" + +#include "base/macros.h" +#include "extensions/renderer/script_context.h" +#include "third_party/WebKit/public/web/WebSerializedScriptValue.h" + +namespace extensions { + +UtilsNativeHandler::UtilsNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context) { + RouteFunction( + "deepCopy", + base::Bind(&UtilsNativeHandler::DeepCopy, base::Unretained(this))); +} + +UtilsNativeHandler::~UtilsNativeHandler() {} + +void UtilsNativeHandler::DeepCopy( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + args.GetReturnValue().Set( + blink::WebSerializedScriptValue::serialize(args[0]).deserialize()); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/utils_native_handler.h b/chromium/extensions/renderer/utils_native_handler.h new file mode 100644 index 00000000000..6a2ae4da350 --- /dev/null +++ b/chromium/extensions/renderer/utils_native_handler.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_UTILS_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_UTILS_NATIVE_HANDLER_H_ + +#include "base/macros.h" +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { +class ScriptContext; + +class UtilsNativeHandler : public ObjectBackedNativeHandler { + public: + explicit UtilsNativeHandler(ScriptContext* context); + ~UtilsNativeHandler() override; + + private: + // |args| consists of one argument: an arbitrary value. Returns a deep copy of + // that value. The copy will have no references to nested values of the + // argument. + void DeepCopy(const v8::FunctionCallbackInfo<v8::Value>& args); + + DISALLOW_COPY_AND_ASSIGN(UtilsNativeHandler); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_UTILS_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/utils_unittest.cc b/chromium/extensions/renderer/utils_unittest.cc new file mode 100644 index 00000000000..485c214ea66 --- /dev/null +++ b/chromium/extensions/renderer/utils_unittest.cc @@ -0,0 +1,77 @@ +// 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 "base/strings/stringprintf.h" +#include "extensions/renderer/module_system_test.h" +#include "gin/dictionary.h" +#include "grit/extensions_renderer_resources.h" + +namespace extensions { +namespace { + +class UtilsUnittest : public ModuleSystemTest { + private: + void SetUp() override { + ModuleSystemTest::SetUp(); + + env()->RegisterModule("utils", IDR_UTILS_JS); + env()->RegisterTestFile("utils_unittest", "utils_unittest.js"); + env()->OverrideNativeHandler("schema_registry", + "exports.$set('GetSchema', function() {});"); + env()->OverrideNativeHandler("logging", + "exports.$set('CHECK', function() {});\n" + "exports.$set('DCHECK', function() {});\n" + "exports.$set('WARNING', function() {});"); + env()->OverrideNativeHandler("v8_context", ""); + gin::Dictionary chrome(env()->isolate(), env()->CreateGlobal("chrome")); + gin::Dictionary chrome_runtime( + gin::Dictionary::CreateEmpty(env()->isolate())); + chrome.Set("runtime", chrome_runtime); + } +}; + +TEST_F(UtilsUnittest, TestNothing) { + ExpectNoAssertionsMade(); +} + +TEST_F(UtilsUnittest, SuperClass) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->CallModuleMethod("utils_unittest", "testSuperClass"); +} + +TEST_F(UtilsUnittest, PromiseNoResult) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->CallModuleMethod("utils_unittest", + "testPromiseNoResult"); + RunResolvedPromises(); +} + +TEST_F(UtilsUnittest, PromiseOneResult) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->CallModuleMethod("utils_unittest", + "testPromiseOneResult"); + RunResolvedPromises(); +} + +TEST_F(UtilsUnittest, PromiseTwoResults) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->CallModuleMethod("utils_unittest", + "testPromiseTwoResults"); + RunResolvedPromises(); +} + +TEST_F(UtilsUnittest, PromiseError) { + ModuleSystem::NativesEnabledScope natives_enabled_scope( + env()->module_system()); + env()->module_system()->CallModuleMethod("utils_unittest", + "testPromiseError"); + RunResolvedPromises(); +} + +} // namespace +} // namespace extensions diff --git a/chromium/extensions/renderer/v8_context_native_handler.cc b/chromium/extensions/renderer/v8_context_native_handler.cc new file mode 100644 index 00000000000..1ac53c5be00 --- /dev/null +++ b/chromium/extensions/renderer/v8_context_native_handler.cc @@ -0,0 +1,64 @@ +// 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/renderer/v8_context_native_handler.h" + +#include "base/bind.h" +#include "extensions/common/features/feature.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/script_context_set.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" + +namespace extensions { + +V8ContextNativeHandler::V8ContextNativeHandler(ScriptContext* context) + : ObjectBackedNativeHandler(context), context_(context) { + RouteFunction("GetAvailability", + base::Bind(&V8ContextNativeHandler::GetAvailability, + base::Unretained(this))); + RouteFunction("GetModuleSystem", + base::Bind(&V8ContextNativeHandler::GetModuleSystem, + base::Unretained(this))); + RouteFunction( + "RunWithNativesEnabled", + base::Bind(&V8ContextNativeHandler::RunWithNativesEnabled, + base::Unretained(this))); +} + +void V8ContextNativeHandler::GetAvailability( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(args.Length(), 1); + v8::Isolate* isolate = args.GetIsolate(); + std::string api_name = *v8::String::Utf8Value(args[0]); + Feature::Availability availability = context_->GetAvailability(api_name); + + v8::Local<v8::Object> ret = v8::Object::New(isolate); + ret->Set(v8::String::NewFromUtf8(isolate, "is_available"), + v8::Boolean::New(isolate, availability.is_available())); + ret->Set(v8::String::NewFromUtf8(isolate, "message"), + v8::String::NewFromUtf8(isolate, availability.message().c_str())); + ret->Set(v8::String::NewFromUtf8(isolate, "result"), + v8::Integer::New(isolate, availability.result())); + args.GetReturnValue().Set(ret); +} + +void V8ContextNativeHandler::GetModuleSystem( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + ScriptContext* context = ScriptContextSet::GetContextByObject( + v8::Local<v8::Object>::Cast(args[0])); + if (blink::WebFrame::scriptCanAccess(context->web_frame())) + args.GetReturnValue().Set(context->module_system()->NewInstance()); +} + +void V8ContextNativeHandler::RunWithNativesEnabled( + const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + ModuleSystem::NativesEnabledScope natives_enabled(context()->module_system()); + context()->CallFunction(v8::Local<v8::Function>::Cast(args[0])); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/v8_context_native_handler.h b/chromium/extensions/renderer/v8_context_native_handler.h new file mode 100644 index 00000000000..956ef6c68ce --- /dev/null +++ b/chromium/extensions/renderer/v8_context_native_handler.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_V8_CONTEXT_NATIVE_HANDLER_H_ +#define EXTENSIONS_RENDERER_V8_CONTEXT_NATIVE_HANDLER_H_ + +#include "extensions/renderer/object_backed_native_handler.h" + +namespace extensions { + +class Dispatcher; + +class V8ContextNativeHandler : public ObjectBackedNativeHandler { + public: + explicit V8ContextNativeHandler(ScriptContext* context); + + private: + void GetAvailability(const v8::FunctionCallbackInfo<v8::Value>& args); + void GetModuleSystem(const v8::FunctionCallbackInfo<v8::Value>& args); + + void RunWithNativesEnabled(const v8::FunctionCallbackInfo<v8::Value>& args); + + ScriptContext* context_; +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_V8_CONTEXT_NATIVE_HANDLER_H_ diff --git a/chromium/extensions/renderer/v8_helpers.h b/chromium/extensions/renderer/v8_helpers.h new file mode 100644 index 00000000000..0a3b2ebaad9 --- /dev/null +++ b/chromium/extensions/renderer/v8_helpers.h @@ -0,0 +1,166 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_V8_HELPERS_H_ +#define EXTENSIONS_RENDERER_V8_HELPERS_H_ + +#include <stdint.h> +#include <string.h> + +#include "base/strings/string_number_conversions.h" +#include "v8/include/v8.h" + +namespace extensions { +namespace v8_helpers { + +// Helper functions for V8 APIs. + +// Converts |str| to a V8 string. Returns true on success. +inline bool ToV8String(v8::Isolate* isolate, + const char* str, + v8::Local<v8::String>* out) { + return v8::String::NewFromUtf8(isolate, str, v8::NewStringType::kNormal) + .ToLocal(out); +} + +inline bool ToV8String(v8::Isolate* isolate, + const std::string& str, + v8::Local<v8::String>* out) { + return ToV8String(isolate, str.c_str(), out); +} + +// Converts |str| to a V8 string. +// This crashes when strlen(str) > v8::String::kMaxLength. +inline v8::Local<v8::String> ToV8StringUnsafe( + v8::Isolate* isolate, + const char* str, + v8::NewStringType string_type = v8::NewStringType::kNormal) { + DCHECK(strlen(str) <= v8::String::kMaxLength); + return v8::String::NewFromUtf8(isolate, str, string_type) + .ToLocalChecked(); +} + +inline v8::Local<v8::String> ToV8StringUnsafe( + v8::Isolate* isolate, + const std::string& str, + v8::NewStringType string_type = v8::NewStringType::kNormal) { + return ToV8StringUnsafe(isolate, str.c_str(), string_type); +} + +// Returns true if |maybe| is both a value, and that value is true. +inline bool IsTrue(v8::Maybe<bool> maybe) { + return maybe.IsJust() && maybe.FromJust(); +} + +// Returns true if |value| is empty or undefined. +inline bool IsEmptyOrUndefied(v8::Local<v8::Value> value) { + return value.IsEmpty() || value->IsUndefined(); +} + +// SetProperty() family wraps V8::Object::DefineOwnProperty(). +// Returns true on success. +// NOTE: Think about whether you want this or SetPrivateProperty() below. +// TODO(devlin): Sort through more of the callers of this and see if we can +// convert more to be private. +inline bool SetProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + v8::Local<v8::String> key, + v8::Local<v8::Value> value) { + return IsTrue(object->DefineOwnProperty(context, key, value)); +} + +// Wraps v8::Object::SetPrivate(). When possible, prefer this to SetProperty(). +inline bool SetPrivateProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + v8::Local<v8::String> key, + v8::Local<v8::Value> value) { + return IsTrue(object->SetPrivate( + context, v8::Private::ForApi(context->GetIsolate(), key), value)); +} + +inline bool SetPrivateProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + const char* key, + v8::Local<v8::Value> value) { + v8::Local<v8::String> v8_key; + return ToV8String(context->GetIsolate(), key, &v8_key) && + IsTrue(object->SetPrivate( + context, v8::Private::ForApi(context->GetIsolate(), v8_key), + value)); +} + +// GetProperty() family calls V8::Object::Get() and extracts a value from +// returned MaybeLocal. Returns true on success. +// NOTE: Think about whether you want this or GetPrivateProperty() below. +template <typename Key> +inline bool GetProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + Key key, + v8::Local<v8::Value>* out) { + return object->Get(context, key).ToLocal(out); +} + +inline bool GetProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + const char* key, + v8::Local<v8::Value>* out) { + v8::Local<v8::String> v8_key; + if (!ToV8String(context->GetIsolate(), key, &v8_key)) + return false; + return GetProperty(context, object, v8_key, out); +} + +// Wraps v8::Object::GetPrivate(). When possible, prefer this to GetProperty(). +inline bool GetPrivateProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + v8::Local<v8::String> key, + v8::Local<v8::Value>* out) { + return object + ->GetPrivate(context, v8::Private::ForApi(context->GetIsolate(), key)) + .ToLocal(out); +} + +inline bool GetPrivateProperty(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + const char* key, + v8::Local<v8::Value>* out) { + v8::Local<v8::String> v8_key; + return ToV8String(context->GetIsolate(), key, &v8_key) && + GetPrivateProperty(context, object, v8_key, out); +} + +// GetPropertyUnsafe() family wraps v8::Object::Get(). They crash when an +// exception is thrown. +inline v8::Local<v8::Value> GetPropertyUnsafe(v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + v8::Local<v8::Value> key) { + return object->Get(context, key).ToLocalChecked(); +} + +inline v8::Local<v8::Value> GetPropertyUnsafe( + v8::Local<v8::Context> context, + v8::Local<v8::Object> object, + const char* key, + v8::NewStringType string_type = v8::NewStringType::kNormal) { + return object->Get(context, + ToV8StringUnsafe(context->GetIsolate(), key, string_type)) + .ToLocalChecked(); +} + +// Wraps v8::Function::Call(). Returns true on success. +inline bool CallFunction(v8::Local<v8::Context> context, + v8::Local<v8::Function> function, + v8::Local<v8::Value> recv, + int argc, + v8::Local<v8::Value> argv[], + v8::Local<v8::Value>* out) { + v8::MicrotasksScope microtasks_scope( + context->GetIsolate(), v8::MicrotasksScope::kDoNotRunMicrotasks); + return function->Call(context, recv, argc, argv).ToLocal(out); +} + +} // namespace v8_helpers +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_V8_HELPERS_H_ diff --git a/chromium/extensions/renderer/v8_schema_registry.cc b/chromium/extensions/renderer/v8_schema_registry.cc new file mode 100644 index 00000000000..3bd7c479122 --- /dev/null +++ b/chromium/extensions/renderer/v8_schema_registry.cc @@ -0,0 +1,142 @@ +// 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/renderer/v8_schema_registry.h" + +#include <stddef.h> + +#include <utility> + +#include "base/logging.h" +#include "base/values.h" +#include "content/public/child/v8_value_converter.h" +#include "extensions/common/extension_api.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "extensions/renderer/script_context.h" + +using content::V8ValueConverter; + +namespace extensions { + +namespace { + +// Recursively freezes every v8 object on |object|. +void DeepFreeze(const v8::Local<v8::Object>& object, + const v8::Local<v8::Context>& context) { + // Don't let the object trace upwards via the prototype. + v8::Maybe<bool> maybe = + object->SetPrototype(context, v8::Null(context->GetIsolate())); + CHECK(maybe.IsJust() && maybe.FromJust()); + v8::Local<v8::Array> property_names = object->GetOwnPropertyNames(); + for (uint32_t i = 0; i < property_names->Length(); ++i) { + v8::Local<v8::Value> child = object->Get(property_names->Get(i)); + if (child->IsObject()) + DeepFreeze(v8::Local<v8::Object>::Cast(child), context); + } + object->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen); +} + +class SchemaRegistryNativeHandler : public ObjectBackedNativeHandler { + public: + SchemaRegistryNativeHandler(V8SchemaRegistry* registry, + scoped_ptr<ScriptContext> context) + : ObjectBackedNativeHandler(context.get()), + context_(std::move(context)), + registry_(registry) { + RouteFunction("GetSchema", + base::Bind(&SchemaRegistryNativeHandler::GetSchema, + base::Unretained(this))); + } + + ~SchemaRegistryNativeHandler() override { context_->Invalidate(); } + + private: + void GetSchema(const v8::FunctionCallbackInfo<v8::Value>& args) { + args.GetReturnValue().Set( + registry_->GetSchema(*v8::String::Utf8Value(args[0]))); + } + + scoped_ptr<ScriptContext> context_; + V8SchemaRegistry* registry_; +}; + +} // namespace + +V8SchemaRegistry::V8SchemaRegistry() { +} + +V8SchemaRegistry::~V8SchemaRegistry() { +} + +scoped_ptr<NativeHandler> V8SchemaRegistry::AsNativeHandler() { + scoped_ptr<ScriptContext> context( + new ScriptContext(GetOrCreateContext(v8::Isolate::GetCurrent()), + NULL, // no frame + NULL, // no extension + Feature::UNSPECIFIED_CONTEXT, + NULL, // no effective extension + Feature::UNSPECIFIED_CONTEXT)); + return scoped_ptr<NativeHandler>( + new SchemaRegistryNativeHandler(this, std::move(context))); +} + +v8::Local<v8::Array> V8SchemaRegistry::GetSchemas( + const std::vector<std::string>& apis) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::EscapableHandleScope handle_scope(isolate); + v8::Context::Scope context_scope(GetOrCreateContext(isolate)); + + v8::Local<v8::Array> v8_apis(v8::Array::New(isolate, apis.size())); + size_t api_index = 0; + for (std::vector<std::string>::const_iterator i = apis.begin(); + i != apis.end(); + ++i) { + v8_apis->Set(api_index++, GetSchema(*i)); + } + return handle_scope.Escape(v8_apis); +} + +v8::Local<v8::Object> V8SchemaRegistry::GetSchema(const std::string& api) { + if (schema_cache_ != NULL) { + v8::Local<v8::Object> cached_schema = schema_cache_->Get(api); + if (!cached_schema.IsEmpty()) { + return cached_schema; + } + } + + // Slow path: Need to build schema first. + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::EscapableHandleScope handle_scope(isolate); + v8::Local<v8::Context> context = GetOrCreateContext(isolate); + v8::Context::Scope context_scope(context); + + const base::DictionaryValue* schema = + ExtensionAPI::GetSharedInstance()->GetSchema(api); + CHECK(schema) << api; + scoped_ptr<V8ValueConverter> v8_value_converter(V8ValueConverter::create()); + v8::Local<v8::Value> value = v8_value_converter->ToV8Value(schema, context); + CHECK(!value.IsEmpty()); + + v8::Local<v8::Object> v8_schema(v8::Local<v8::Object>::Cast(value)); + DeepFreeze(v8_schema, context); + schema_cache_->Set(api, v8_schema); + + return handle_scope.Escape(v8_schema); +} + +v8::Local<v8::Context> V8SchemaRegistry::GetOrCreateContext( + v8::Isolate* isolate) { + // It's ok to create local handles in this function, since this is only called + // when we have a HandleScope. + if (!context_holder_) { + context_holder_.reset(new gin::ContextHolder(isolate)); + context_holder_->SetContext(v8::Context::New(isolate)); + schema_cache_.reset(new SchemaCache(isolate)); + return context_holder_->context(); + } + return context_holder_->context(); +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/v8_schema_registry.h b/chromium/extensions/renderer/v8_schema_registry.h new file mode 100644 index 00000000000..08f54b62658 --- /dev/null +++ b/chromium/extensions/renderer/v8_schema_registry.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef EXTENSIONS_RENDERER_V8_SCHEMA_REGISTRY_H_ +#define EXTENSIONS_RENDERER_V8_SCHEMA_REGISTRY_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "gin/public/context_holder.h" +#include "v8/include/v8-util.h" +#include "v8/include/v8.h" + +namespace extensions { +class NativeHandler; + +// A registry for the v8::Value representations of extension API schemas. +// In a way, the v8 counterpart to ExtensionAPI. +class V8SchemaRegistry { + public: + V8SchemaRegistry(); + ~V8SchemaRegistry(); + + // Creates a NativeHandler wrapper |this|. Supports GetSchema. + scoped_ptr<NativeHandler> AsNativeHandler(); + + // Returns a v8::Array with all the schemas for the APIs in |apis|. + v8::Local<v8::Array> GetSchemas(const std::vector<std::string>& apis); + + // Returns a v8::Object for the schema for |api|, possibly from the cache. + v8::Local<v8::Object> GetSchema(const std::string& api); + + private: + // Gets the separate context that backs the registry, creating a new one if + // if necessary. Will also initialize schema_cache_. + v8::Local<v8::Context> GetOrCreateContext(v8::Isolate* isolate); + + // Cache of schemas. Created lazily by GetOrCreateContext. + typedef v8::StdGlobalValueMap<std::string, v8::Object> SchemaCache; + scoped_ptr<SchemaCache> schema_cache_; + + // Single per-instance gin::ContextHolder to create v8::Values. + // Created lazily via GetOrCreateContext. + scoped_ptr<gin::ContextHolder> context_holder_; + + DISALLOW_COPY_AND_ASSIGN(V8SchemaRegistry); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_V8_SCHEMA_REGISTRY_H_ diff --git a/chromium/extensions/renderer/wake_event_page.cc b/chromium/extensions/renderer/wake_event_page.cc new file mode 100644 index 00000000000..e3e21899bef --- /dev/null +++ b/chromium/extensions/renderer/wake_event_page.cc @@ -0,0 +1,200 @@ +// Copyright 2015 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/wake_event_page.h" + +#include <utility> + +#include "base/atomic_sequence_num.h" +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "content/public/child/worker_thread.h" +#include "content/public/renderer/render_thread.h" +#include "extensions/common/extension_messages.h" +#include "extensions/renderer/object_backed_native_handler.h" +#include "extensions/renderer/script_context.h" +#include "extensions/renderer/v8_helpers.h" +#include "ipc/ipc_message.h" +#include "ipc/ipc_message_macros.h" + +namespace extensions { + +using namespace v8_helpers; + +namespace { + +base::LazyInstance<WakeEventPage> g_instance = LAZY_INSTANCE_INITIALIZER; + +} // namespace + +class WakeEventPage::WakeEventPageNativeHandler + : public ObjectBackedNativeHandler { + public: + // Handles own lifetime. + WakeEventPageNativeHandler(ScriptContext* context, + const std::string& name, + const MakeRequestCallback& make_request) + : ObjectBackedNativeHandler(context), + make_request_(make_request), + weak_ptr_factory_(this) { + // Use Unretained not a WeakPtr because RouteFunction is tied to the + // lifetime of this, so there is no way for DoWakeEventPage to be called + // after destruction. + RouteFunction(name, base::Bind(&WakeEventPageNativeHandler::DoWakeEventPage, + base::Unretained(this))); + // Delete self on invalidation. base::Unretained because by definition this + // can't be deleted before it's deleted. + context->AddInvalidationObserver(base::Bind( + &WakeEventPageNativeHandler::DeleteSelf, base::Unretained(this))); + }; + + ~WakeEventPageNativeHandler() override {} + + private: + void DeleteSelf() { + Invalidate(); + delete this; + } + + // Called by JavaScript with a single argument, the function to call when the + // event page has been woken. + void DoWakeEventPage(const v8::FunctionCallbackInfo<v8::Value>& args) { + CHECK_EQ(1, args.Length()); + CHECK(args[0]->IsFunction()); + v8::Global<v8::Function> callback(args.GetIsolate(), + args[0].As<v8::Function>()); + + const std::string& extension_id = context()->GetExtensionID(); + CHECK(!extension_id.empty()); + + make_request_.Run( + extension_id, + base::Bind(&WakeEventPageNativeHandler::OnEventPageIsAwake, + weak_ptr_factory_.GetWeakPtr(), base::Passed(&callback))); + } + + void OnEventPageIsAwake(v8::Global<v8::Function> callback, bool success) { + v8::Isolate* isolate = context()->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Value> args[] = { + v8::Boolean::New(isolate, success), + }; + context()->CallFunction(v8::Local<v8::Function>::New(isolate, callback), + arraysize(args), args); + } + + MakeRequestCallback make_request_; + base::WeakPtrFactory<WakeEventPageNativeHandler> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(WakeEventPageNativeHandler); +}; + +// static +WakeEventPage* WakeEventPage::Get() { + return g_instance.Pointer(); +} + +void WakeEventPage::Init(content::RenderThread* render_thread) { + DCHECK(render_thread); + DCHECK_EQ(content::RenderThread::Get(), render_thread); + DCHECK(!message_filter_); + + message_filter_ = render_thread->GetSyncMessageFilter(); + render_thread->AddObserver(this); +} + +v8::Local<v8::Function> WakeEventPage::GetForContext(ScriptContext* context) { + DCHECK(message_filter_); + + v8::Isolate* isolate = context->isolate(); + v8::EscapableHandleScope handle_scope(isolate); + v8::Handle<v8::Context> v8_context = context->v8_context(); + v8::Context::Scope context_scope(v8_context); + + // Cache the imported function as a hidden property on the global object of + // |v8_context|. Creating it isn't free. + v8::Local<v8::Private> kWakeEventPageKey = + v8::Private::ForApi(isolate, ToV8StringUnsafe(isolate, "WakeEventPage")); + v8::Local<v8::Value> wake_event_page; + if (!v8_context->Global() + ->GetPrivate(v8_context, kWakeEventPageKey) + .ToLocal(&wake_event_page) || + wake_event_page->IsUndefined()) { + // Implement this using a NativeHandler, which requires a function name + // (arbitrary in this case). Handles own lifetime. + const char* kFunctionName = "WakeEventPage"; + WakeEventPageNativeHandler* native_handler = new WakeEventPageNativeHandler( + context, kFunctionName, base::Bind(&WakeEventPage::MakeRequest, + // Safe, owned by a LazyInstance. + base::Unretained(this))); + + // Extract and cache the wake-event-page function from the native handler. + wake_event_page = GetPropertyUnsafe( + v8_context, native_handler->NewInstance(), kFunctionName); + v8_context->Global() + ->SetPrivate(v8_context, kWakeEventPageKey, wake_event_page) + .FromJust(); + } + + CHECK(wake_event_page->IsFunction()); + return handle_scope.Escape(wake_event_page.As<v8::Function>()); +} + +WakeEventPage::RequestData::RequestData(int thread_id, + const OnResponseCallback& on_response) + : thread_id(thread_id), on_response(on_response) {} + +WakeEventPage::RequestData::~RequestData() {} + +WakeEventPage::WakeEventPage() {} + +WakeEventPage::~WakeEventPage() {} + +void WakeEventPage::MakeRequest(const std::string& extension_id, + const OnResponseCallback& on_response) { + static base::AtomicSequenceNumber sequence_number; + int request_id = sequence_number.GetNext(); + { + scoped_ptr<RequestData> request_data( + new RequestData(content::WorkerThread::GetCurrentId(), on_response)); + base::AutoLock lock(requests_lock_); + requests_.set(request_id, std::move(request_data)); + } + message_filter_->Send( + new ExtensionHostMsg_WakeEventPage(request_id, extension_id)); +} + +bool WakeEventPage::OnControlMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(WakeEventPage, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_WakeEventPageResponse, + OnWakeEventPageResponse) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void WakeEventPage::OnWakeEventPageResponse(int request_id, bool success) { + scoped_ptr<RequestData> request_data; + { + base::AutoLock lock(requests_lock_); + request_data = requests_.take(request_id); + } + CHECK(request_data) << "No request with ID " << request_id; + if (request_data->thread_id == 0) { + // Thread ID of 0 means it wasn't called on a worker thread, so safe to + // call immediately. + request_data->on_response.Run(success); + } else { + content::WorkerThread::PostTask( + request_data->thread_id, + base::Bind(request_data->on_response, success)); + } +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/wake_event_page.h b/chromium/extensions/renderer/wake_event_page.h new file mode 100644 index 00000000000..48bfd4bedb4 --- /dev/null +++ b/chromium/extensions/renderer/wake_event_page.h @@ -0,0 +1,116 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_WAKE_EVENT_PAGE_H_ +#define EXTENSIONS_RENDERER_WAKE_EVENT_PAGE_H_ + +#include <string> + +#include "base/callback.h" +#include "base/containers/scoped_ptr_hash_map.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/synchronization/lock.h" +#include "content/public/renderer/render_process_observer.h" +#include "ipc/ipc_sync_message_filter.h" +#include "v8/include/v8.h" + +namespace content { +class RenderThread; +} + +namespace extensions { +class ScriptContext; + +// This class implements the wake-event-page JavaScript function, which wakes +// an event page and runs a callback when done. +// +// Note, the function will do a round trip to the browser even if event page is +// open. Any optimisation to prevent this must be at the JavaScript level. +class WakeEventPage : public content::RenderProcessObserver { + public: + WakeEventPage(); + ~WakeEventPage() override; + + // Returns the single instance of the WakeEventPage object. + // + // Thread safe. + static WakeEventPage* Get(); + + // Initializes the WakeEventPage. + // + // This must be called before any bindings are installed, and must be called + // on the render thread. + void Init(content::RenderThread* render_thread); + + // Returns the wake-event-page function bound to a given context. The + // function will be cached as a hidden value in the context's global object. + // + // To mix C++ and JavaScript, example usage might be: + // + // WakeEventPage::Get().GetForContext(context)(function() { + // ... + // }); + // + // Thread safe. + v8::Local<v8::Function> GetForContext(ScriptContext* context); + + private: + class WakeEventPageNativeHandler; + + // The response from an ExtensionHostMsg_WakeEvent call, passed true if the + // call was successful, false on failure. + using OnResponseCallback = base::Callback<void(bool)>; + + // Makes an ExtensionHostMsg_WakeEvent request for an extension ID. The + // second argument is a callback to run when the request has completed. + using MakeRequestCallback = + base::Callback<void(const std::string&, const OnResponseCallback&)>; + + // For |requests_|. + struct RequestData { + RequestData(int thread_id, const OnResponseCallback& on_response); + ~RequestData(); + + // The thread ID the request was made on. |on_response| must be called on + // that thread. + int thread_id; + + // Callback to run when the response to the request arrives. + OnResponseCallback on_response; + }; + + // Runs |on_response|, passing it |success|. + static void RunOnResponseWithResult(const OnResponseCallback& on_response, + bool success); + + // Sends the ExtensionHostMsg_WakeEvent IPC for |extension_id|, and + // updates |requests_| bookkeeping. + void MakeRequest(const std::string& extension_id, + const OnResponseCallback& on_response); + + // content::RenderProcessObserver: + bool OnControlMessageReceived(const IPC::Message& message) override; + + // OnControlMessageReceived handlers: + void OnWakeEventPageResponse(int request_id, bool success); + + // IPC sender. Belongs to the render thread, but thread safe. + scoped_refptr<IPC::SyncMessageFilter> message_filter_; + + // All in-flight requests, keyed by request ID. Used on multiple threads, so + // must be guarded by |requests_lock_|. + base::ScopedPtrHashMap<int, scoped_ptr<RequestData>> requests_; + + // Lock for |requests_|. + base::Lock requests_lock_; + + DISALLOW_COPY_AND_ASSIGN(WakeEventPage); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_WAKE_EVENT_PAGE_H_ diff --git a/chromium/extensions/renderer/web_ui_injection_host.cc b/chromium/extensions/renderer/web_ui_injection_host.cc new file mode 100644 index 00000000000..1fd436452a8 --- /dev/null +++ b/chromium/extensions/renderer/web_ui_injection_host.cc @@ -0,0 +1,34 @@ +// Copyright 2015 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/web_ui_injection_host.h" + +WebUIInjectionHost::WebUIInjectionHost(const HostID& host_id) + : InjectionHost(host_id), + url_(host_id.id()) { +} + +WebUIInjectionHost::~WebUIInjectionHost() { +} + +std::string WebUIInjectionHost::GetContentSecurityPolicy() const { + return std::string(); +} + +const GURL& WebUIInjectionHost::url() const { + return url_; +} + +const std::string& WebUIInjectionHost::name() const { + return id().id(); +} + +extensions::PermissionsData::AccessType WebUIInjectionHost::CanExecuteOnFrame( + const GURL& document_url, + content::RenderFrame* render_frame, + int tab_id, + bool is_declarative) const { + // Content scripts are allowed to inject on webviews created by WebUI. + return extensions::PermissionsData::AccessType::ACCESS_ALLOWED; +} diff --git a/chromium/extensions/renderer/web_ui_injection_host.h b/chromium/extensions/renderer/web_ui_injection_host.h new file mode 100644 index 00000000000..a4df5f79fb1 --- /dev/null +++ b/chromium/extensions/renderer/web_ui_injection_host.h @@ -0,0 +1,33 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_WEBUI_INJECTION_HOST_H_ +#define EXTENSIONS_RENDERER_WEBUI_INJECTION_HOST_H_ + +#include "base/macros.h" +#include "extensions/renderer/injection_host.h" + +class WebUIInjectionHost : public InjectionHost { + public: + WebUIInjectionHost(const HostID& host_id); + ~WebUIInjectionHost() override; + + private: + // InjectionHost: + std::string GetContentSecurityPolicy() const override; + const GURL& url() const override; + const std::string& name() const override; + extensions::PermissionsData::AccessType CanExecuteOnFrame( + const GURL& document_url, + content::RenderFrame* render_frame, + int tab_id, + bool is_declarative) const override; + + private: + GURL url_; + + DISALLOW_COPY_AND_ASSIGN(WebUIInjectionHost); +}; + +#endif // EXTENSIONS_RENDERER_WEBUI_INJECTION_HOST_H_ diff --git a/chromium/extensions/renderer/worker_script_context_set.cc b/chromium/extensions/renderer/worker_script_context_set.cc new file mode 100644 index 00000000000..a05fdbc77c6 --- /dev/null +++ b/chromium/extensions/renderer/worker_script_context_set.cc @@ -0,0 +1,82 @@ +// Copyright 2015 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/worker_script_context_set.h" + +#include <algorithm> +#include <utility> + +#include "extensions/renderer/script_context.h" + +namespace extensions { + +using ContextVector = ScopedVector<ScriptContext>; + +namespace { + +// Returns an iterator to the ScriptContext associated with |v8_context| from +// |contexts|, or |contexts|->end() if not found. +ContextVector::iterator FindContext(ContextVector* contexts, + v8::Local<v8::Context> v8_context) { + auto context_matches = [&v8_context](ScriptContext* context) { + v8::HandleScope handle_scope(context->isolate()); + v8::Context::Scope context_scope(context->v8_context()); + return context->v8_context() == v8_context; + }; + return std::find_if(contexts->begin(), contexts->end(), context_matches); +} + +} // namespace + +WorkerScriptContextSet::WorkerScriptContextSet() {} + +WorkerScriptContextSet::~WorkerScriptContextSet() {} + +void WorkerScriptContextSet::Insert(scoped_ptr<ScriptContext> context) { + DCHECK_GT(content::WorkerThread::GetCurrentId(), 0) + << "Must be called on a worker thread"; + ContextVector* contexts = contexts_tls_.Get(); + if (!contexts) { + // First context added for this thread. Create a new set, then wait for + // this thread's shutdown. + contexts = new ContextVector(); + contexts_tls_.Set(contexts); + content::WorkerThread::AddObserver(this); + } + CHECK(FindContext(contexts, context->v8_context()) == contexts->end()) + << "Worker for " << context->url() << " is already in this set"; + contexts->push_back(std::move(context)); +} + +void WorkerScriptContextSet::Remove(v8::Local<v8::Context> v8_context, + const GURL& url) { + DCHECK_GT(content::WorkerThread::GetCurrentId(), 0) + << "Must be called on a worker thread"; + ContextVector* contexts = contexts_tls_.Get(); + if (!contexts) { + // Thread has already been torn down, and |v8_context| removed. I'm not + // sure this can actually happen (depends on in what order blink fires + // events), but SW lifetime has bitten us before, so be cautious. + return; + } + auto context_it = FindContext(contexts, v8_context); + CHECK(context_it != contexts->end()) << "Worker for " << url + << " is not in this set"; + ScriptContext* context = *context_it; + DCHECK_EQ(url, context->url()); + context->Invalidate(); + contexts->erase(context_it); +} + +void WorkerScriptContextSet::WillStopCurrentWorkerThread() { + content::WorkerThread::RemoveObserver(this); + ContextVector* contexts = contexts_tls_.Get(); + DCHECK(contexts); + for (ScriptContext* context : *contexts) + context->Invalidate(); + contexts_tls_.Set(nullptr); + delete contexts; +} + +} // namespace extensions diff --git a/chromium/extensions/renderer/worker_script_context_set.h b/chromium/extensions/renderer/worker_script_context_set.h new file mode 100644 index 00000000000..dd33a77d6a7 --- /dev/null +++ b/chromium/extensions/renderer/worker_script_context_set.h @@ -0,0 +1,46 @@ +// Copyright 2015 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. + +#ifndef EXTENSIONS_RENDERER_WORKER_SCRIPT_CONTEXT_SET_H_ +#define EXTENSIONS_RENDERER_WORKER_SCRIPT_CONTEXT_SET_H_ + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/threading/thread_local.h" +#include "content/public/child/worker_thread.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +namespace extensions { + +class ScriptContext; + +// A set of ScriptContexts owned by worker threads. Thread safe. +class WorkerScriptContextSet : public content::WorkerThread::Observer { + public: + WorkerScriptContextSet(); + + ~WorkerScriptContextSet() override; + + // Inserts |context| into the set. Contexts are stored in TLS. + void Insert(scoped_ptr<ScriptContext> context); + + // Removes the ScriptContext associated with |v8_context| which was added for + // |url| (used for sanity checking). + void Remove(v8::Local<v8::Context> v8_context, const GURL& url); + + private: + // WorkerThread::Observer: + void WillStopCurrentWorkerThread() override; + + // Implement thread safety by storing each ScriptContext in TLS. + base::ThreadLocalPointer<ScopedVector<ScriptContext>> contexts_tls_; + + DISALLOW_COPY_AND_ASSIGN(WorkerScriptContextSet); +}; + +} // namespace extensions + +#endif // EXTENSIONS_RENDERER_WORKER_SCRIPT_CONTEXT_SET_H_ |