diff options
author | initial.commit@chromium.org <initial.commit@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-11-02 02:14:31 +0000 |
---|---|---|
committer | initial.commit@chromium.org <initial.commit@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-11-02 02:14:31 +0000 |
commit | 5a7bdf208c28c210b39cff63d1cf91302b02821b (patch) | |
tree | 5ff484561562f78b29d2670168a9e3b40b48dcab /ceee/ie | |
parent | 26a54a5e0f4603c8fda2fe51789d331125993c9a (diff) | |
download | chromium_src-5a7bdf208c28c210b39cff63d1cf91302b02821b.zip chromium_src-5a7bdf208c28c210b39cff63d1cf91302b02821b.tar.gz chromium_src-5a7bdf208c28c210b39cff63d1cf91302b02821b.tar.bz2 |
Checking in the initial version of CEEE (Chrome Extensions Execution
Environment), an optional feature of Chrome Frame that acts as an
adapter layer for a subset of the Chrome Extension APIs. This enables
extensions that stick to the supported subset of APIs to work in the
context of Chrome Frame with minimal or sometimes no changes.
See http://www.chromium.org/developers/design-documents/ceee for an
overview of the design of CEEE.
TEST=unit tests (run ceee/smoke_tests.bat as an administrator on Windows)
BUG=none
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@64712 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ceee/ie')
192 files changed, 41211 insertions, 0 deletions
diff --git a/ceee/ie/broker/api_dispatcher.cc b/ceee/ie/broker/api_dispatcher.cc new file mode 100644 index 0000000..218a198 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher.cc @@ -0,0 +1,306 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Dispatcher and registry for Chrome Extension APIs. + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/broker/infobar_api_module.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/broker/webnavigation_api_module.h" +#include "ceee/ie/broker/webrequest_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "chrome/browser/automation/extension_automation_constants.h" + +namespace keys = extension_automation_constants; + +bool ApiDispatcher::IsRunningInSingleThread() { + return thread_id_ == ::GetCurrentThreadId(); +} + + +void ApiDispatcher::HandleApiRequest(BSTR message_text, BSTR* response) { + DCHECK(IsRunningInSingleThread()); + DCHECK(message_text); + + scoped_ptr<Value> base_value(base::JSONReader::Read(CW2A(message_text).m_psz, + false)); + DCHECK(base_value.get() && base_value->GetType() == Value::TYPE_DICTIONARY); + if (!base_value.get() || base_value->GetType() != Value::TYPE_DICTIONARY) { + return; + } + DictionaryValue* value = static_cast<DictionaryValue*>(base_value.get()); + + std::string function_name; + if (!value->GetString(keys::kAutomationNameKey, &function_name)) { + DCHECK(false); + return; + } + + std::string args_string; + if (!value->GetString(keys::kAutomationArgsKey, &args_string)) { + DCHECK(false); + return; + } + + base::JSONReader reader; + scoped_ptr<Value> args(reader.JsonToValue(args_string, false, false)); + DCHECK(args.get() && args->GetType() == Value::TYPE_LIST); + if (!args.get() || args->GetType() != Value::TYPE_LIST) { + return; + } + ListValue* args_list = static_cast<ListValue*>(args.get()); + + int request_id = -1; + if (!value->GetInteger(keys::kAutomationRequestIdKey, &request_id)) { + DCHECK(false); + return; + } + DLOG(INFO) << "Request: " << request_id << ", for: " << function_name; + FactoryMap::const_iterator iter = factories_.find(function_name); + DCHECK(iter != factories_.end()); + if (iter == factories_.end()) { + return; + } + + scoped_ptr<Invocation> invocation(iter->second()); + DCHECK(invocation.get()); + invocation->Execute(*args_list, request_id); +} + +void ApiDispatcher::FireEvent(BSTR event_name, BSTR event_args) { + DCHECK(IsRunningInSingleThread()); + DLOG(INFO) << "ApiDispatcher::FireEvent. " << event_name << " - " << + event_args; + std::string event_args_str(CW2A(event_args).m_psz); + std::string event_name_str(CW2A(event_name).m_psz); + // Start by going through the permanent event handlers map. + PermanentEventHandlersMap::const_iterator iter = + permanent_event_handlers_.find(event_name_str); + bool fire_event = true; + std::string converted_event_args; + if (iter != permanent_event_handlers_.end()) { + fire_event = iter->second(event_args_str, &converted_event_args, this); + } else { + // Some events don't need a handler and + // have all the info already available. + converted_event_args = event_args_str; + } + + if (fire_event) { + // The notification message is an array containing event name as a string + // at index 0 and then a JSON encoded string of the other arguments. + ListValue message; + message.Append(Value::CreateStringValue(event_name_str)); + + // Event messages expect a string for the arguments. + message.Append(Value::CreateStringValue(converted_event_args)); + + std::string message_str; + base::JSONWriter::Write(&message, false, &message_str); + ChromePostman::GetInstance()->PostMessage(CComBSTR(message_str.c_str()), + CComBSTR(keys::kAutomationBrowserEventRequestTarget)); + } + + // We do this after firing the event, to be in the same order as Chrome. + // And to allow reusing the args conversion from the permanent handler. + EphemeralEventHandlersMap::iterator map_iter = + ephemeral_event_handlers_.find(event_name_str); + if (map_iter != ephemeral_event_handlers_.end() && + !map_iter->second.empty()) { + // We must work with a copy since other handlers might get registered + // as we call the current set of handlers (some of them chain themselves). + EphemeralEventHandlersList handlers_list; + map_iter->second.swap(handlers_list); + // Swap emptied our current list, so we will need to add back the items + // that are not affected by this call (the ones that we wouldn't want + // to remove from the list just yet return S_FALSE). This scheme is much + // simpler than trying to keep up with the live list as it may change. + EphemeralEventHandlersList::iterator list_iter = handlers_list.begin(); + for (;list_iter != handlers_list.end(); ++list_iter) { + if (list_iter->handler(converted_event_args, + list_iter->user_data, this) == S_FALSE) { + map_iter->second.push_back(*list_iter); + } + } + } +} + +void ApiDispatcher::GetExecutor(HWND window, REFIID iid, void** executor) { + DWORD thread_id = ::GetWindowThreadProcessId(window, NULL); + HRESULT hr = Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>::get()-> + GetExecutor(thread_id, window, iid, executor); + DLOG_IF(INFO, FAILED(hr)) << "Failed to get executor for window: " << + window << ". In thread: " << thread_id << ". " << com::LogHr(hr); +} + + +HWND ApiDispatcher::GetTabHandleFromId(int tab_id) const { + return Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + GetTabHandleFromId(tab_id); +} + +HWND ApiDispatcher::GetWindowHandleFromId(int window_id) const { + return reinterpret_cast<HWND>(window_id); +} + +int ApiDispatcher::GetTabIdFromHandle(HWND tab_handle) const { + return Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + GetTabIdFromHandle(tab_handle); +} + +int ApiDispatcher::GetWindowIdFromHandle(HWND window_handle) const { + return reinterpret_cast<int>(window_handle); +} + + +void ApiDispatcher::SetTabIdForHandle(long tab_id, HWND handle) { + Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + SetTabIdForHandle(tab_id, handle); +} + +void ApiDispatcher::DeleteTabHandle(HWND handle) { + Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + DeleteTabHandle(handle); +} + +void ApiDispatcher::RegisterInvocation(const char* function_name, + InvocationFactory factory) { + DCHECK(function_name && factory); + // Re-registration is not expected. + DCHECK(function_name && factories_.find(function_name) == factories_.end()); + if (function_name && factory) + factories_[function_name] = factory; +} + +void ApiDispatcher::RegisterPermanentEventHandler( + const char* event_name, PermanentEventHandler event_handler) { + DCHECK(event_name && event_handler); + // Re-registration is not expected. + DCHECK(event_name && permanent_event_handlers_.find(event_name) == + permanent_event_handlers_.end()); + if (event_name && event_handler) + permanent_event_handlers_[event_name] = event_handler; +} + +void ApiDispatcher::RegisterEphemeralEventHandler( + const char* event_name, + EphemeralEventHandler event_handler, + InvocationResult* user_data) { + DCHECK(IsRunningInSingleThread()); + // user_data could be NULL, we don't care. + DCHECK(event_handler != NULL); + if (event_handler != NULL) { + ephemeral_event_handlers_[event_name].push_back( + EphemeralEventHandlerTuple(event_handler, user_data)); + } +} + +ApiDispatcher::InvocationResult::~InvocationResult() { + // We must have posted the response before we died. + DCHECK(request_id_ == kNoRequestId); +} + +// A quick helper function to reuse the call to ChromePostman. +void ApiDispatcher::InvocationResult::PostResponseToChromeFrame( + const char* response_key, const std::string& response_str) { + DictionaryValue response_dict; + response_dict.SetInteger(keys::kAutomationRequestIdKey, request_id_); + response_dict.SetString(response_key, response_str); + + std::string response; + base::JSONWriter::Write(&response_dict, false, &response); + ChromePostman::GetInstance()->PostMessage( + CComBSTR(response.c_str()), CComBSTR(keys::kAutomationResponseTarget)); +} + +// Once an invocation completes successfully, it must call this method on its +// result object to send it back to Chrome. +void ApiDispatcher::InvocationResult::PostResult() { + // We should never post results twice, or for non requested results. + DCHECK(request_id_ != kNoRequestId); + if (request_id_ != kNoRequestId) { + std::string result_str; + // Some invocations don't return values. We can set an empty string here. + if (value_.get() != NULL) + base::JSONWriter::Write(value_.get(), false, &result_str); + PostResponseToChromeFrame(keys::kAutomationResponseKey, result_str); + // We should only post once! + request_id_ = kNoRequestId; + } +} + +// Invocations can use this method to post an error to Chrome when it +// couldn't complete the invocation successfully. +void ApiDispatcher::InvocationResult::PostError(const std::string& error) { + LOG(ERROR) << error; + // Event handlers reuse InvocationResult code without a requestId, + // so don't DCHECK as in PostResult here. + // TODO(mad@chromium.org): Might be better to use a derived class instead. + if (request_id_ != kNoRequestId) { + PostResponseToChromeFrame(keys::kAutomationErrorKey, error); + // We should only post once! + request_id_ = kNoRequestId; + } +} + +void ApiDispatcher::InvocationResult::SetValue(const char* name, Value* value) { + DCHECK(name != NULL && value != NULL); + scoped_ptr<Value> value_holder(value); + if (name == NULL || value == NULL) + return; + + if (temp_values_.get() == NULL) { + temp_values_.reset(new DictionaryValue); + } + + // scoped_ptr::release() lets go of its ownership. + temp_values_->Set(name, value_holder.release()); +} + +const Value* ApiDispatcher::InvocationResult::GetValue(const char* name) { + if (temp_values_.get() == NULL) + return NULL; + + Value* value = NULL; + bool success = temp_values_->Get(name, &value); + // Make sure that success means NotNull and vice versa. + DCHECK((value != NULL || !success) && (success || value == NULL)); + return value; +} + +ApiDispatcher* ApiDispatcher::InvocationResult::GetDispatcher() { + return ProductionApiDispatcher::get(); +} + +ApiDispatcher* ApiDispatcher::Invocation::GetDispatcher() { + return ProductionApiDispatcher::get(); +} + +// Function registration preprocessor magic. See api_registration.h for details. +#ifdef REGISTER_ALL_API_FUNCTIONS +#error Must not include api_registration.h previously in this compilation unit. +#endif // REGISTER_ALL_API_FUNCTIONS +#define PRODUCTION_API_DISPATCHER +#define REGISTER_TAB_API_FUNCTIONS() tab_api::RegisterInvocations(this) +#define REGISTER_WINDOW_API_FUNCTIONS() window_api::RegisterInvocations(this) +#define REGISTER_COOKIE_API_FUNCTIONS() cookie_api::RegisterInvocations(this) +#define REGISTER_INFOBAR_API_FUNCTIONS() infobar_api::RegisterInvocations(this) +#define REGISTER_WEBNAVIGATION_API_FUNCTIONS() \ + webnavigation_api::RegisterInvocations(this) +#define REGISTER_WEBREQUEST_API_FUNCTIONS() \ + webrequest_api::RegisterInvocations(this) +#include "ceee/ie/common/api_registration.h" + +ProductionApiDispatcher::ProductionApiDispatcher() { + REGISTER_ALL_API_FUNCTIONS(); +} diff --git a/ceee/ie/broker/api_dispatcher.h b/ceee/ie/broker/api_dispatcher.h new file mode 100644 index 0000000..db13110 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher.h @@ -0,0 +1,377 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Dispatcher and registry for Chrome Extension APIs. + +#ifndef CEEE_IE_BROKER_API_DISPATCHER_H_ +#define CEEE_IE_BROKER_API_DISPATCHER_H_ + +#include <list> +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "base/values.h" + +#include "broker_lib.h" // NOLINT + +class ExecutorsManager; + +// Keeps a registry of all API Invocation implementations and implements +// the logic needed to deserialize API Invocation requests, dispatch them, +// and serialize and return the response. +class ApiDispatcher { + public: + ApiDispatcher() : thread_id_(0) {} + virtual ~ApiDispatcher() {} + + // Dispatches a Chrome Extensions API request and sends back the response. + // + // @param message_text The raw JSON-encoded text of the API request over + // the automation interface. + // @param response Where to return the JSON-encoded response. + virtual void HandleApiRequest(BSTR message_text, BSTR* response); + + // Fire the given event message to Chrome Frame and potentially use it + // to complete pending extension API execution. + // + // @param event_name The name of the event to fire. + // @param event_args The JSON encoded event arguments. + virtual void FireEvent(BSTR event_name, BSTR event_args); + + // This class holds on the result and can be derived from to generate results + // that are either specific to tabs or windows for example. + class InvocationResult { + public: + static const int kNoRequestId = -1; + explicit InvocationResult(int request_id) + : request_id_(request_id) { + } + virtual ~InvocationResult(); + + // Post the response string from our current result to Chrome Frame. + virtual void PostResult(); + + // Post the given error string to Chrome Frame. + virtual void PostError(const std::string& error); + + // Identifies if we are pending a post (for which we need a request id). + virtual bool Pending() const { return request_id_ != kNoRequestId; } + + // Returns the result of the API invocation as a value object which + // is still owned by this object - the caller does not take ownership. + // May return NULL if execution didn't produce any results yet. + const Value* value() const { + return value_.get(); + } + void set_value(Value* value) { + value_.reset(value); + } + + // Sets a value in a dictionary. The Value pointer is kept in the + // dictionary which takes ownership so the caller should NOT deallocate it. + // Even in case of errors, the value will be deallocated. + // This value can then be retrieved by GetValue below, this can be useful + // to keep information in the InvocationResult during async handling. + virtual void SetValue(const char* name, Value* value); + + // Returns a value set by SetValue above. Note that the value is only + // valid during the lifetime of the InvocationResult object or until another + // value is set for the same name. Also, the returned value should NOT + // be deallocated by the caller, it will get deallocated on destruction + // of the object, or when another value is set for the same name. + // Returns NULL if no value of this name have been previously set. + virtual const Value* GetValue(const char* name); + + protected: + // A unit test seam. + virtual ApiDispatcher* GetDispatcher(); + + // A helper function to post the given response to Chrome Frame. + virtual void PostResponseToChromeFrame(const char* response_key, + const std::string& response_str); + // Where to store temporary values. + scoped_ptr<DictionaryValue> temp_values_; + + // Where to store the result value. + scoped_ptr<Value> value_; + + // Invocation request identifier. + int request_id_; + + DISALLOW_COPY_AND_ASSIGN(InvocationResult); + }; + + // Base class for API Invocation Execution registered with this object. + // A new execution object is created for each API invocation call even though + // the state data is stored in the classes deriving from InvocationResult + // (declared above). This is to allow easier testing of the Invocation + // implementation by having virtual methods like GetDispatcher to be used + // as a test seam, or other specific methods (like + // ApiResultCreator<>::CreateApiResult). Otherwise, Invocation::Execute + // could be a static callback as the event handlers described below. + class Invocation { + public: + Invocation() {} + virtual ~Invocation() {} + + // Called when a request to invoke the execution of an API is received. + // + // @param args The list value object for the arguments passed. + // @param request_id The identifier of the request being executed. + virtual void Execute(const ListValue& args, int request_id) = 0; + + protected: + // Unit test seam. + virtual ApiDispatcher* GetDispatcher(); + + DISALLOW_COPY_AND_ASSIGN(Invocation); + }; + + // The Invocation factory pointers to be stored in the factory map. + typedef Invocation* (*InvocationFactory)(); + + // The permanent event handlers are stored in a specific map and don't get + // removed after they are called. They are also registered without user data. + // The handler can stop the broadcast of the event by returning false. + typedef bool (*PermanentEventHandler)(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); + + // The ephemeral event handlers are stored in a specific map and they get + // removed after a successfully completed call or an error (a successful call + // is one which returns S_OK, S_FALSE is returned to identify that this + // set of arguments is not the one we were waiting for, so we need to wait + // some more, and returning a FAILED HRESULT causes the handler to be removed + // from the queue). + // EphemeralEventHandlers can specify user data to be passed back to them + // when the event occurs. The dynamic data allocation and release is the + // responsibility of the caller/handler pair, the dispatcher only keeps the + // pointer and passes it back to the handler as is without any post-cleanup. + // Also note that the Ephemeral event handlers are called after the Permanent + // handlers which may have had the chance to augment the content of the + // arguments which are passed to the ephemeral handlers. So these are useful + // for asynchronous invocation completion. + // We also pass in the ApiDispatcher for ease of test mocking. + typedef HRESULT (*EphemeralEventHandler)(const std::string& input_args, + InvocationResult* user_data, + ApiDispatcher* dispatcher); + + // Registers a factory for an API Invocation of a given name. Note that + // registering the same name more than once is not permitted. + // + // @param function_name The name of the function handled by the Invocation. + // @param factory The factory function to call to create a new instance of + // the Invocation type to handle this function. + virtual void RegisterInvocation(const char* function_name, + InvocationFactory factory); + + // Registers a permanent handler for an event of a given name. Note that + // registering the same name more than once is not permitted. + // + // @param event_name The name of the event to handle. + // @param event_handler The event handler to call to handle the event. + virtual void RegisterPermanentEventHandler( + const char* event_name, PermanentEventHandler event_handler); + + // Registers an ephemeral handler for an event of a given name. Note that + // registering the same name more than once is permitted. When an event is + // fired, all ephemeral handlers are called after the permanent one if + // one was registered. + // + // @param event_name The name of the event to handle. + // @param event_handler The event handler to call to handle the event. + // @param user_data The data that has to be passed back to the handler. + virtual void RegisterEphemeralEventHandler( + const char* event_name, + EphemeralEventHandler event_handler, + InvocationResult* user_data); + + // Sets the thread id of the ApiInvocation thread so that we can make sure + // that we are only ran from that thread. Only used for debug purposes. + virtual void SetApiInvocationThreadId(DWORD thread_id) { + thread_id_ = thread_id; + } + + // Fetch the appropriate executor to execute code for the given window. + // + // @param window The window for which we want an executor. + // @param iid The identifier of the interface we want to work with. + // @param executor Where to return the requested executor interface pointer. + virtual void GetExecutor(HWND window, REFIID iid, void** executor); + + // Return a tab handle associated with the id. + // + // @param tab_id The tab identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if tab_id isn't + // found). + virtual HWND GetTabHandleFromId(int tab_id) const; + + // Return a window handle associated with the id. + // + // @param window_id The window identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if window_id isn't + // found). + virtual HWND GetWindowHandleFromId(int window_id) const; + + // Return a tab id associated with the HWND. + // + // @param tab_handle The tab HWND. + // @return The corresponding tab id (or 0 if tab_handle isn't found). + virtual int GetTabIdFromHandle(HWND tab_handle) const; + + // Return a window id associated with the HWND. + // + // @param window_handle The window HWND. + // @return The corresponding window id (or 0 if window_handle isn't found). + virtual int GetWindowIdFromHandle(HWND window_handle) const; + + // Register the relation between a tab_id and a HWND. + virtual void SetTabIdForHandle(long tab_id, HWND tab_handle); + + // Unregister the HWND and its corresponding tab_id. + virtual void DeleteTabHandle(HWND handle); + + protected: + typedef std::map<std::string, InvocationFactory> FactoryMap; + FactoryMap factories_; + + typedef std::map<std::string, PermanentEventHandler> + PermanentEventHandlersMap; + PermanentEventHandlersMap permanent_event_handlers_; + + // For ephemeral event handlers we also need to keep the user data. + struct EphemeralEventHandlerTuple { + EphemeralEventHandlerTuple( + EphemeralEventHandler handler, + InvocationResult* user_data) + : handler(handler), user_data(user_data) {} + EphemeralEventHandler handler; + InvocationResult* user_data; + }; + + // We can have a list of ephemeral event handlers for a given event name. + typedef std::vector<EphemeralEventHandlerTuple> + EphemeralEventHandlersList; + typedef std::map<std::string, EphemeralEventHandlersList> + EphemeralEventHandlersMap; + EphemeralEventHandlersMap ephemeral_event_handlers_; + + // The thread we are running into. + DWORD thread_id_; + + // Make sure this is always called from the same thread, + bool IsRunningInSingleThread(); + + DISALLOW_COPY_AND_ASSIGN(ApiDispatcher); +}; + +// Convenience InvocationFactory implementation. +template <class InvocationType> +ApiDispatcher::Invocation* NewApiInvocation() { + return new InvocationType(); +} + +// A singleton that initializes and keeps the ApiDispatcher used by production +// code. +class ProductionApiDispatcher : public ApiDispatcher, + public Singleton<ProductionApiDispatcher> { + private: + // This ensures no construction is possible outside of the class itself. + friend struct DefaultSingletonTraits<ProductionApiDispatcher>; + DISALLOW_IMPLICIT_CONSTRUCTORS(ProductionApiDispatcher); +}; + +// A convenience class that can be derived from by API function classes instead +// of ApiDispatcher::Invocation. Mainly benefits ease of testing, so that we +// can specify a mocked result object. +template<class ResultType = ApiDispatcher::InvocationResult> +class ApiResultCreator : public ApiDispatcher::Invocation { + protected: + // Allocates a new instance of ResultType. The caller of this function takes + // ownership of this instance and is responsible for freeing it. + virtual ResultType* CreateApiResult(int request_id) { + return new ResultType(request_id); + } +}; + +template<class InvocationBase> +class IterativeApiResult : public InvocationBase { + public: + explicit IterativeApiResult(int request_id) + : InvocationBase(request_id), result_accumulator_(new ListValue()) { + } + + // Unlike the base class, this hack subverts the process by accumulating + // responses (positive and errors) rather than posting them right away. + virtual void PostResult() { + DCHECK(value() != NULL); + DCHECK(result_accumulator_ != NULL); + + if (result_accumulator_ != NULL) + result_accumulator_->Append(value_.release()); + } + + virtual void PostError(const std::string& error) { + error_accumulator_.push_back(error); + } + + // Finally post whatever was reported as result. + virtual void FlushAllPosts() { + if (AllFailed()) { + CallRealPostError(error_accumulator_.back()); + } else { + // Post success as per base class implementation. Declare all is fine + // even if !AllSucceeded. + // TODO(motek@google.com): Perhaps we should post a different + // message post for 'not quite ok, but almost'? + set_value(result_accumulator_.release()); + CallRealPostResult(); + } + error_accumulator_.clear(); + } + + bool AllSucceeded() const { + DCHECK(result_accumulator_ != NULL); + return result_accumulator_ != NULL && !result_accumulator_->empty() && + error_accumulator_.empty(); + } + + bool AllFailed() const { + DCHECK(result_accumulator_ != NULL); + return !error_accumulator_.empty() && + (result_accumulator_ == NULL || result_accumulator_->empty()); + } + + bool IsEmpty() const { + DCHECK(result_accumulator_ != NULL); + return (result_accumulator_ == NULL || result_accumulator_->empty()) && + error_accumulator_.empty(); + } + + const std::string LastError() const { + if (!error_accumulator_.empty()) + return error_accumulator_.back(); + return std::string(); + } + + private: + // Redirected invocations of base class's 'post function' create test seam. + virtual void CallRealPostResult() { + InvocationBase::PostResult(); + } + virtual void CallRealPostError(const std::string& error) { + InvocationBase::PostError(error); + } + + scoped_ptr<ListValue> result_accumulator_; + std::list<std::string> error_accumulator_; +}; + + +#endif // CEEE_IE_BROKER_API_DISPATCHER_H_ diff --git a/ceee/ie/broker/api_dispatcher_docs.h b/ceee/ie/broker/api_dispatcher_docs.h new file mode 100644 index 0000000..ae6b711 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher_docs.h @@ -0,0 +1,420 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ + +/** @page ApiDispatcherDoc Detailed documentation of the ApiDispatcher. + +@section ApiDispatcherIntro Introduction + +The API Dispatcher implemented by the ApiDispatcher class +is used by the CeeeBroker object to dispatch execution of Api calls +coming from the "Chrome Extensions Execution Environment" or CEEE. + +It keeps a registry of all API invocation implementations and takes care of +the logic needed to de-serialize API function requests, dispatch them, +and then serialize & return the response. + +One of the main complexities of the API invocation implementations is that some +of them need to interact with windows, and these interactions must happen in +the same thread that created the window. So the dispatcher need to delegate +calls to Executor objects that they get from the ExecutorsManager. + +So the implementation of these APIs are actually split between the classes +inheriting from ApiDispatcher::Invocation and the CeeeExecutor COM object. + +Another complexity is the fact that some of the APIs can not be completed +synchronously (e.g., the tab creation is an asynchronous process), so the +dispatcher must remember the pending execution and get them completed when a +given event is fired. + +And to make it even more fun, there might be two concurrent API invocation +that depend on the same event being fired (e.g., GetSelectedTab is called +while a new tab is being created as selected, but the asynchronous selection +isn't completed yet... pfffewww). So the list of pending asynchronous execution +must allow more than one to be executed when a given event is fired. + +@section InvocationFactory Invocation Factory + +The ApiDispatcher uses the @link ApiDispatcher::Invocation Invocation @endlink +nested class as a base for all API invocation implementation so that it can +easily dispatch to them. It keeps pointers to registered static functions +(called Invocation factories) that can be called to instantiation a new +Invocation object in the @link ApiDispatcher::factories_ factories_ @endlink +@link ApiDispatcher::FactoryMap map@endlink. + +When @link ApiDispatcher::HandleApiRequest HandleApiRequest @endlink is called, +it looks up the Invocation Factory to find the Invocation object to execute with +the data that it de-serialized from the JSON string it was given. + +@subsection InvocationExecution Invocation Execution + +The Invocation base class has an @link ApiDispatcher::Invocation::Execute +Execute @endlink pure virtual method that is to be called by HandleApiRequest. +This method receives the argument values, as well as a pointer to a string +where to return the response. + +When the execution can be completed synchronously, the result (or the error +message in case of failure) is written in the @link +ApiDispatcher::Invocation::result_ result_ @endlink member variable +(or @link ApiDispatcher::Invocation::error_ error_ @endlink for errors). If the +execution must be completed asynchronously, a new AsynchronousHandler object +is instantiated and stored in the @link +ApiDispatcher::Invocation::async_handler_ async_handler_ @endlink data member +so that the dispatcher can find it. + +@subsection ExecutorsDelegation Delegating to Executors + +When an Invocation implementation needs to interact with windows, it can do so +by delegating the execution to an Executor object via the ICeeeTabExecutor +or ICeeeWindowExecutor interface pointer that it can get from a call to +ApiDispatcher::Invocation::GetExecutor which gets it from the Executors Manager. + +@section EventHandling Event Handling + +Event handling actually serves two purposes in the API Dispatcher, 1) handle +events (of course) and 2) complete asynchronous executions. Events are received +by the @link ApiDispatcher::FireEvent FireEvent @endlink member function. + +@subsection HandlingEvents Handling Events + +Events are being fired by the browser and must be propagated to extensions via +the Chrome Frame. The @link EventDispatchingDoc Event Dispatching @endlink +documentation describes how the EventFunnel derived classes can be used to help +fire events. Once they got through the funnel, the events get to the API +Dispatcher, which is responsible to post them to Chrome Frame. + +We do the event propagation to Chrome Frame from the API dispatcher so that +we can also deal with the completion of the asynchronous API invocation at the +same time... the same place... + +We also reuse some of the code written for API invocation since some of the +events are posted with the same information that is returned to the API +invocation callbacks (e.g., tabs.create and tabs.onCreated). + +So the ApiDispatcher::Invocation derived classes can be used to handle events +by filling in the blanks in the arguments posted with the event (because some of +these arguments can be filled by the source of the event, we only need to fill +in the blanks that were left because, for example, the information must be +fetched from other threads than the one from where the event come from). Those +classes that can be used to handle events must expose a static method with the +syntax defined as ApiDispatcher::EventHandler and register it by calling +ApiDispatcher::RegisterEventHandler so that it get stored in the +ApiDispatcher::EventHandlersMap. + +When ApiDispatcher::FireEvent is called, the event handlers map is looked up to +see if a handler was registered for the given event. If so, the event handler +is called to fill in the blanks in the event arguments. Then the newly filled +argumentes can be posted with the event. + +@subsection AsynchronousResponse Asynchronous API Invocation response + +When an API invocation can't be completed synchronously, a new instance of an +ApiDispatcher::AsynchronousHandler object is stored in the +ApiDispatcher::AsyncHandlersMap. These objects are associated to a given +event (which can be identified by calling the +ApiDispatcher::AsyncHandlersMap::event_name member function). So when an event +is fired, the map is looked up for the list of handlers registered for the +given event name (because there can be more than one handler registerd for a +given name, for the cases of concurrent API invocations, and also for cases +where more than one invocation need to wait for the same event, e.g., the +tabs.create and tabs.getSelected APIs). + +Since there may be more than one handler for a given event, and we may have +pending handlers for more than one event with the same name, we need to be able +to properly match the events with their pending handlers. To identify if an +event is the one a specific handler is waiting for, we have to ask the handlers +if they recognize it as theirs. Even if they do, we may still need to let +other handlers process the same event. This is why a handler can specify that +it is a pass-through handler (i.e., it doesn't swallow the event, it can let +other handlers use it, even multiple instances of itself can all use the same +one, as long as they recognize it as theirs). If a handler recognize an event +as its own, and isn't a pass-through handler, then, we must stop looking for +more handlers. + +This means that all pass-through handlers must be placed at the beginning of +the list of handlers for a given event, and all the other ones (the +non-pass-through handlers) must be put at the end. + +@section InvocationImplementation Invocation Implementation + +The implementation of API invocations are done by classes derived from the +Invocation nested class of the ApiDispatcher class. Here is the current list of +those derived classes and some details about their implementation. + +@subsection WindowsApi Windows API + +Here is the list of invocations implementing the Windows API. +You can also read a more detailed information about the Chrome Extension +Windows api here: http://code.google.com/chrome/extensions/windows.html + +@subsubsection GetWindow + +This invocation receives a single argument which is the identifier of the window +to return information for. The window identifier is simply the window's HWND +value. We only provide such information for IEFrame windows (so we validate +that before continuing) and since we must get this information from the thread +that created the window, we delegate this call to the @link +ICeeeWindowExecutor::GetWindow @endlink method of the +ICeeeWindowExecutor interface. + +The returned information contains the following: +<table> + <tr><td>type</td><td>Info</td></tr> + <tr><td>int</td><td>Window identifier</td></tr> + <tr><td>bool</td><td>Focused</td></tr> + <tr><td>int</td><td>Left position</td></tr> + <tr><td>int</td><td>Top position</td></tr> + <tr><td>int</td><td>Width</td></tr> + <tr><td>int</td><td>Height</td></tr> +</table> + +@subsubsection GetCurrentWindow + +This invocation does the same thing as @ref GetWindow except that it doesn't +receive a windows identifier argument (it doesn't receive any argument). Instead +it must find the current window from where the invocation started from. + +@note We currently don't have a way to find the window from which the invocation +came from, so until we do, we return the top most IE Frame window as +@ref GetLastFocusedWindow does. + +@subsubsection GetLastFocusedWindow + +This invocation return information about the last focused IE window, which it +gets by calling @link window_api::WindowApiResult::TopIeWindow +TopIeWindow@endlink, which finds it by calling +window_utils::FindDescendentWindow with NULL as the parent (which means to ask +for top level windows). + +@subsubsection CreateWindowFunc + +This invocation creates a new window with the parameters specified in the +received argument and navigate it to an optionally provided URL or about:blank +otherwise. It creates the new window by simply calling the +ICeeeTabExecutor::Navigate method with a _blank target and the +navOpenInNewWindow flag. + +It then needs to find that window (by diff'ing the list of IEFrame windows +before and after the navigation), and then it can apply the provided parameters. + +The provided parameters are as follows (and are all optional): +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>int</td><td>Left position</td></tr> +<tr><td>int</td><td>Top position</td></tr> +<tr><td>int</td><td>Width</td></tr> +<tr><td>int</td><td>Height</td></tr> +</table> + +The application of the window parameters must be done in the same thread that +created the window, so we must delegate this task to the @link +ICeeeWindowExecutor::UpdateWindow UpdateWindow @endlink method of +the ICeeeWindowExecutor interface. This UpdateWindow method also returns +information about the updated window so that we can return it. The returned +information is the same as the one returned by the @ref GetWindow method +described above. + +@subsubsection UpdateWindow + +This invocation updates an existing window. So it must be given the window +identifier, as well as the new parameters to update the window with. + +It simply reuses the same code as the @ref CreateWindowFunc invocation, except +that it doesn't create a new window, it modifies the one specified by the +identifier, after validating that it is indeed an IEFrame window. + +@subsubsection RemoveWindow + +This invocation simply closes the window specified as a windows identifier +argument. It simply delegates the call to the @link +ICeeeWindowExecutor::RemoveWindow RemoveWindow @endlink method of the +ICeeeWindowExecutor interface. + +@subsubsection GetAllWindows + +This invocation returns an array of window information, one for each of the +current IEFrame window. It also fills an extra array of tabs information if the +populate flag is set in the single (and optional, defaults to false) argument. + +The information returned for each window is the same as in the @ref GetWindow +invocation. The information for each tab (if requested) is provided as an extra +entry in the window information in the form of an array of tab information. The +tab information is the same as the one returned by the @ref GetTab invocation +described below. + +It delegates the call to the @link ICeeeWindowExecutor::GetAllWindows +GetAllWindows @endlink method of the ICeeeWindowExecutor interface. This +methods can't return all the information about every tab since the windows for +these tabs may have been created in other threads/processes. So it only returns +the identifiers of these tabs. Then we must delegate to different executors for +each of the tabs potentially created in different threads/process by calling the +@link ICeeeTabExecutor::GetTab GetTab @endlink method of the +ICeeeTabExecutor interface as is done for the @ref GetTab API described +below. We do the same in @ref GetAllTabsInWindow described below. + +@subsection TabsApi Tabs API + +Here is the list of invocations implementing the Tabs API. +You can also read a more detailed information about the Chrome Extension +Tabs api here: http://code.google.com/chrome/extensions/tabs.html + +@subsubsection GetTab + +This invocation receives a single argument which is the identifier of the tab +to return information for. The tab identifier is the HWND value of the window +of class "TabWindowClass" that holds on the real tab window. + +We only provide such information for these windows (so we validate +that before continuing). Also, we must get some of the information from the +thread that created the tab window, and some other information are only +available via the tab window manager which lives in the thread that created the +frame window. So we must delegate this call in two phases, the first +one to the @link ICeeeTabExecutor::GetTab GetTab @endlink method of the +ICeeeTabExecutor interface which returns the following information: +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>string</td><td>Title</td></tr> +<tr><td>string</td><td>Loading/Complete status</td></tr> +<tr><td>string</td><td>favIconUrl (not supported yet in CEEE)</td></tr> +</table> + +The information above is retrivied via the HTML Document and Web Browser +associated to the given tab, and these must be fetched and interacted with from +the same thread that created the tab window. + +So we are left with the following: +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>int</td><td>Tab identifier</td></tr> +<tr><td>int</td><td>Window identifier</td></tr> +<tr><td>bool</td><td>Selected state</td></tr> +<tr><td>int</td><td>Tab index</td></tr> +</table> + +Most of the other information about the tab can be retrieved from any thread +by simply using Win32 calls. The tab identifier is the HWND of the window of +class "TabWindowClass", the window identifier is simply the HWND of +the top level parent of the tab window, and the selected state can be determined +by the visibility of the window which we can get from anywhere. + +The last piece of information, the tab index, must be retrieved from the tab +window manager. We must get another executor so that we can +run it in the appropriate thread. We do this via the @link +ICeeeTabExecutor::GetTabIndex GetTabIndex @endlink method of the +ICeeeTabExecutor interface. + +@subsubsection GetSelectedTab + +This invocation does the same thing as @ref GetTab except that it doesn't +receive a tab identifier argument, it gets the information of the currently +selected tab instead. But it can receive an optional window identifier argument, +specifying which window's selected tab we want. If no window identifier is +specified, we use the current window. The current window is the same as the one +returned from @ref GetCurrentWindow. + +@subsubsection GetAllTabsInWindow + +This invocation returns the information for all the tabs of a given window. If +no window identifier is provided as an optional argument, then the current +window is used instead. The returned information is an array of tab information. +The tab information is the same as the one returned by @ref GetTab. + +We delegate the first part of this call to the +@link ICeeeTabExecutor::GetAllTabs GetAllTabs @endlink method of the +ICeeeTabExecutor interface which gets the list of tabs from the tab window +manager in the frame window thread. Then we need to sequentially call different +instances of executors to call their @ref GetTab method, since they were not +created in the same thread where GetAllTabs needs to be executed. + +@subsubsection CreateTab + +This invocation creates a new tab with the parameters specified in the received +argument and navigate it to an optionally provided URL or about:blank otherwise. + +The new tab is to be created in a specified window or the current window if the +optional window identifier is not provided as an argument. The new tab +is created by first finding an existing tab in the any window and then +calling ICeeeTabExecutor::Navigate with the appropriate URL with the _blank +target and the navOpenInNewTab or navOpenInBackgroundTab whether the selected +bool optional parameter is set to true or not (defaults to true). + +The new tab may also need to be moved to a specific index if the optional index +parameter argument was provided. This can be done using the tab window manager +so must be done in the appropriate thread by delegating this task to the @link +ICeeeTabExecutor::MoveTab MoveTab @endlink method of the +ICeeeTabExecutor interface. + +And finally, the same tab information as from @ref GetTab must be returned. +Since some of this information isn't available right after the call to Navigate, +we must handle the rest of the CreateTab API invocation Asynchronously. We +register the async handler to be called when the tabs.onCreated event is fired. +Then, we know that all the information is available to return the tab info. + +The provided parameters are as follows (and are all optional): +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>int</td><td>Parent window identifier</td></tr> +<tr><td>int</td><td>Index</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>bool</td><td>Selected</td></tr> +</table> + +@note <b>TODO(mad@chromium.org)</b>: We currently can't get the tab id of the +newly created tab since it is created asynchronously. So we will need to return +this info asynchronously once we have the support for asynchronous events. + +@subsubsection UpdateTab + +This invocation updates an existing tab. So it must be given the tab identifier, +as well as the new parameters to update the tab with. + +If the optional URL parameter is provided as an argument, then we must call +ICeeeTabExecutor::Navigate with this new URL (if it is different from the +current one). + +If the optional selected parameter is provided, then we must use the tab window +manager to select the tab. But we must do it in the IE Frame thread so we must +delegate the tab selection to the @link ICeeeTabExecutor::SelectTab +SelectTab @endlink method of another instance of +the executor object implementing the ICeeeTabExecutor interface, since it +must be execute in the thread that created the frame window, this is the one we +must use when accessing the ITabWindowManager interface. + +No information is returned from UpdateTab. + +@subsubsection MoveTab + +This invocation moves the tab from one index to another. So it must receive the +identifier of the tab, as well as the destination index. An optional window +identifier can also be provided if we want to specify in which window we want +to move the tab. When not specified, the window where the tab currently lies is +used (and it is the only case that is currently supported in CEEE). + +This code is delegated to the @link ICeeeTabExecutor::MoveTab MoveTab +@endlink method of the ICeeeTabExecutor interface. + +No information is returned from MoveTab. + +@subsubsection RemoveTab + +This invocation closes a tab identified by the single mandatory parameter +argument. The removal of the tab is delegated to the ITabWindowManager interface +so it must be executed in the IEFrame thread by calling the @link +ICeeeTabExecutor::RemoveTab RemoveTab @endlink method on +the ICeeeTabExecutor interface. + +@note <b>TODO(mad@chromium.org)</b>: An alternative to having the RemoveTab and +MoveTab and SelectTab methods on the ICeeeTabExecutor interface, would be to +simply have a GetTabWindowManager method which returns a proxy to the +ITabWindowManager interface that we can safely use from the Broker process. + +No information is returned from RemoveTab. + +**/ + +#endif // CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ diff --git a/ceee/ie/broker/api_dispatcher_unittest.cc b/ceee/ie/broker/api_dispatcher_unittest.cc new file mode 100644 index 0000000..1987733 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher_unittest.cc @@ -0,0 +1,275 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ApiDispatcher. + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + + +namespace { + +namespace keys = extension_automation_constants; + +using testing::_; +using testing::StrEq; +using testing::StrictMock; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class MockInvocation + : public ApiDispatcher::Invocation { + public: + MOCK_METHOD2(Execute, void(const ListValue& args, int request_id)); + + // A factory for the registry. + static ApiDispatcher::Invocation* TestingInstance() { + ApiDispatcher::Invocation* return_value = testing_instance_.release(); + testing_instance_.reset(new MockInvocation); + return return_value; + } + + static scoped_ptr<MockInvocation> testing_instance_; +}; + +scoped_ptr<MockInvocation> MockInvocation::testing_instance_; + + +TEST(ApiDispatcher, InvalidRegistrationNoCrash) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation(NULL, NewApiInvocation<MockInvocation>); + dispatcher.RegisterInvocation("hi", NULL); +} + +TEST(ApiDispatcher, DuplicateRegistrationNoCrash) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("hi", NewApiInvocation<MockInvocation>); + dispatcher.RegisterInvocation("hi", NewApiInvocation<MockInvocation>); +} + +// Encodes a request in JSON. Optionally skip encoding one of the required +// keys. +std::string MakeRequest(const char* function_name, const Value* args, + int request_id, const std::string& skip_key = "") { + std::string json; + DictionaryValue value; + if (skip_key != keys::kAutomationNameKey) + value.SetString(keys::kAutomationNameKey, function_name); + if (skip_key != keys::kAutomationArgsKey) { + std::string json; + if (args) { + base::JSONWriter::Write(args, false, &json); + } + value.SetString(keys::kAutomationArgsKey, json); + } + if (skip_key != keys::kAutomationRequestIdKey) + value.SetInteger(keys::kAutomationRequestIdKey, request_id); + base::JSONWriter::Write(&value, false, &json); + return json; +} + +static const int kRequestId = 42; + +class ApiDispatcherTests : public testing::Test { + public: + virtual void SetUp() { + // We must create the testing instance before the factory returns it + // so that we can set expectations on it beforehand. + MockInvocation::testing_instance_.reset(new MockInvocation); + } + virtual void TearDown() { + // We don't want to wait for the static instance to get destroyed, + // it will be too late and will look like we leaked. + MockInvocation::testing_instance_.reset(NULL); + } +}; + + +TEST_F(ApiDispatcherTests, BasicDispatching) { + testing::LogDisabler no_dchecks; + + scoped_ptr<ListValue> args1(new ListValue()); + args1->Append(Value::CreateStringValue("hello world")); + scoped_ptr<ListValue> args2(new ListValue()); + args2->Append(Value::CreateIntegerValue(711)); + scoped_ptr<ListValue> args3(new ListValue()); + args3->Append(Value::CreateBooleanValue(true)); + DictionaryValue* dict_value = new DictionaryValue(); + dict_value->SetString("foo", "moo"); + dict_value->SetInteger("blat", 42); + scoped_ptr<ListValue> args4(new ListValue()); + args4->Append(dict_value); + + ListValue* values[] = { args1.get(), args2.get(), args3.get(), args4.get() }; + + for (int i = 0; i < arraysize(values); ++i) { + ListValue* test_value = values[i]; + EXPECT_CALL(*MockInvocation::testing_instance_, + Execute(ValuesEqual(test_value), kRequestId)).Times(1); + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("DoStuff", &MockInvocation::TestingInstance); + std::string json = MakeRequest("DoStuff", test_value, kRequestId); + dispatcher.HandleApiRequest(CComBSTR(json.c_str()), NULL); + } +} + +TEST_F(ApiDispatcherTests, ErrorHandlingDispatching) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("DoStuff", &MockInvocation::TestingInstance); + + std::string test_data[] = { + MakeRequest("DoStuff", NULL, 42, keys::kAutomationNameKey), + MakeRequest("DoStuff", NULL, 43, keys::kAutomationArgsKey), + MakeRequest("DoStuff", NULL, 44, keys::kAutomationRequestIdKey), + MakeRequest("NotRegistered", NULL, 45), + }; + + for (int i = 0; i < arraysize(test_data); ++i) { + EXPECT_CALL(*MockInvocation::testing_instance_, Execute(_, _)).Times(0); + CComBSTR json(test_data[i].c_str()); + dispatcher.HandleApiRequest(json, NULL); + } +} + +class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); +}; + +TEST(ApiDispatcher, PostResult) { + testing::LogDisabler no_dchecks; + + // Simply instantiating the postman makes it the single instance + // available to all clients. + CComObjectStackEx< StrictMock< MockChromePostman > > postman; + + // We use different (numbered) result instances, because they are not + // meant to post more than once (and we validate that too). + ApiDispatcher::InvocationResult result1(kRequestId); + + // First check that we can correctly create an error response. + static const char* kError = "error"; + DictionaryValue expected_value; + expected_value.SetInteger(keys::kAutomationRequestIdKey, kRequestId); + expected_value.SetString(keys::kAutomationErrorKey, kError); + + std::string expected_response; + base::JSONWriter::Write(&expected_value, false, &expected_response); + + CComBSTR request_name(keys::kAutomationResponseTarget); + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + result1.PostError(kError); + + // PostResult and PostError must not call the postman anymore, + // and StrictMock will validate it. + result1.PostResult(); + result1.PostError(kError); + + // Now check that we can create an empty response when there is no value. + expected_value.Remove(keys::kAutomationErrorKey, NULL); + expected_value.SetString(keys::kAutomationResponseKey, ""); + base::JSONWriter::Write(&expected_value, false, &expected_response); + + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + ApiDispatcher::InvocationResult result2(kRequestId); + result2.PostResult(); + // These must not get to PostMan, and StrictMock will validate it. + result2.PostError(""); + result2.PostResult(); + + // And now check that we can create an full response. + ApiDispatcher::InvocationResult result3(kRequestId); + result3.set_value(Value::CreateIntegerValue(42)); + std::string result_str; + base::JSONWriter::Write(result3.value(), false, &result_str); + + expected_value.SetString(keys::kAutomationResponseKey, result_str); + base::JSONWriter::Write(&expected_value, false, &expected_response); + + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + result3.PostResult(); + // These must not get to PostMan, and StrictMock will validate it. + result3.PostResult(); + result3.PostError(""); +} + +bool EventHandler1(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + EXPECT_STREQ(input_args.c_str(), "EventHandler1Args"); + EXPECT_TRUE(converted_args != NULL); + *converted_args = "EventHandler1ConvertedArgs"; + return true; +} + +bool EventHandler2(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + EXPECT_STREQ(input_args.c_str(), "EventHandler2Args"); + EXPECT_TRUE(converted_args != NULL); + *converted_args = "EventHandler2ConvertedArgs"; + return true; +} + +bool EventHandler3(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + return false; +} + +TEST(ApiDispatcher, PermanentEventHandler) { + testing::LogDisabler no_dchecks; + + // Simply instantiating the postman makes it the single instance + // available to all clients. + CComObjectStackEx< StrictMock< MockChromePostman > > postman; + ApiDispatcher dispatcher; + dispatcher.RegisterPermanentEventHandler("Event1", EventHandler1); + dispatcher.RegisterPermanentEventHandler("Event2", EventHandler2); + dispatcher.RegisterPermanentEventHandler("Event3", EventHandler3); + + ListValue message1; + message1.Append(Value::CreateStringValue("Event1")); + message1.Append(Value::CreateStringValue("EventHandler1ConvertedArgs")); + std::string message1_str; + base::JSONWriter::Write(&message1, false, &message1_str); + CComBSTR request_name(keys::kAutomationBrowserEventRequestTarget); + EXPECT_CALL(postman, PostMessage(StrEq(CComBSTR(message1_str.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + dispatcher.FireEvent(CComBSTR("Event1"), CComBSTR("EventHandler1Args")); + + ListValue message2; + message2.Append(Value::CreateStringValue("Event2")); + message2.Append(Value::CreateStringValue("EventHandler2ConvertedArgs")); + std::string message2_str; + base::JSONWriter::Write(&message2, false, &message2_str); + EXPECT_CALL(postman, PostMessage(StrEq(CComBSTR(message2_str.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + dispatcher.FireEvent(CComBSTR("Event2"), CComBSTR("EventHandler2Args")); + + // There shouldn't be a post when the event handler returns false. + dispatcher.FireEvent(CComBSTR("Event3"), CComBSTR("")); +} + +// TODO(mad@chromium.org): Add tests for the EphemeralEventHandlers. + +} // namespace diff --git a/ceee/ie/broker/api_module_constants.cc b/ceee/ie/broker/api_module_constants.cc new file mode 100644 index 0000000..a24a830d --- /dev/null +++ b/ceee/ie/broker/api_module_constants.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Constants used by the API modules. + +#include "ceee/ie/broker/api_module_constants.h" + +namespace api_module_constants { + +const char kInvalidArgumentsError[] = "Invalid arguments."; +const char kInternalErrorError[] = "Internal error."; + +} // namespace api_module_constants diff --git a/ceee/ie/broker/api_module_constants.h b/ceee/ie/broker/api_module_constants.h new file mode 100644 index 0000000..0555531 --- /dev/null +++ b/ceee/ie/broker/api_module_constants.h @@ -0,0 +1,18 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Constants used by the API modules. + +#ifndef CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ +#define CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ + +namespace api_module_constants { + +// Errors. +extern const char kInvalidArgumentsError[]; +extern const char kInternalErrorError[]; + +} // namespace api_module_constants + +#endif // CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ diff --git a/ceee/ie/broker/api_module_util.cc b/ceee/ie/broker/api_module_util.cc new file mode 100644 index 0000000..96c8568 --- /dev/null +++ b/ceee/ie/broker/api_module_util.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utilities used by the API modules. + +#include "ceee/ie/broker/api_module_util.h" + +namespace api_module_util { + +bool GetListFromJsonString(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + DCHECK(list != NULL); + scoped_ptr<Value> input_args_val(base::JSONReader::Read(input_list_args, + true)); + DCHECK(input_args_val != NULL && input_args_val->IsType(Value::TYPE_LIST)); + if (input_args_val == NULL || !input_args_val->IsType(Value::TYPE_LIST)) + return false; + list->reset(static_cast<ListValue*>(input_args_val.release())); + return true; +} + +Value* GetListAndFirstValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + if (!GetListFromJsonString(input_list_args, list)) + return NULL; + Value* value = NULL; + if (!(*list)->Get(0, &value) || value == NULL) { + DCHECK(false) << "Input arguments are not a non-empty list."; + } + return value; +} + +bool GetListAndIntegerValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list, + int* int_value) { + Value* value = GetListAndFirstValue(input_list_args, list); + DCHECK(value != NULL && value->IsType(Value::TYPE_INTEGER)); + if (value == NULL || !value->IsType(Value::TYPE_INTEGER)) { + return false; + } + return value->GetAsInteger(int_value); +} + +DictionaryValue* GetListAndDictionaryValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + Value* value = GetListAndFirstValue(input_list_args, list); + DCHECK(value != NULL && value->IsType(Value::TYPE_DICTIONARY)); + if (value == NULL || !value->IsType(Value::TYPE_DICTIONARY)) { + return NULL; + } + return static_cast<DictionaryValue*>(value); +} + +bool GetListAndDictIntValue(const std::string& input_list_args, + const char* dict_value_key_name, + scoped_ptr<ListValue>* list, + int* int_value) { + DictionaryValue* dict = GetListAndDictionaryValue(input_list_args, list); + if (dict == NULL) + return false; + return dict->GetInteger(dict_value_key_name, int_value); +} + +} // namespace api_module_util diff --git a/ceee/ie/broker/api_module_util.h b/ceee/ie/broker/api_module_util.h new file mode 100644 index 0000000..e8bc9c2 --- /dev/null +++ b/ceee/ie/broker/api_module_util.h @@ -0,0 +1,101 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utilities used by the API modules. + +#ifndef CEEE_IE_BROKER_API_MODULE_UTIL_H_ +#define CEEE_IE_BROKER_API_MODULE_UTIL_H_ + +#include <string> + +#include "toolband.h" //NOLINT + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "ceee/ie/broker/api_dispatcher.h" + +namespace api_module_util { + +// Helper function to fetch a list from a JSON string. +bool GetListFromJsonString(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string and return its +// first Value. Note that the Value allocation is owned by the list. +// Returns NULL on failures. +Value* GetListAndFirstValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string and return its +// first Value as an integer. Returns false on failures. +bool GetListAndIntegerValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list, + int* int_value); + +// Helper function to fetch a list from a JSON string and return its +// first Value as a DictionaryValue. Note that the DictionaryValue +// allocation is owned by the list. Returns NULL on failures. +DictionaryValue* GetListAndDictionaryValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string, get its +// first Value as a DictionaryValue and extracts an int from the dict using +// the provided key name. Returns false on failures. +bool GetListAndDictIntValue(const std::string& input_list_args, + const char* dict_value_key_name, + scoped_ptr<ListValue>* list, + int* int_value); + +// A function that can be used as a permanent event handler, which converts the +// tab window handle in the input arguments into the corresponding tab ID. +// @tparam tab_id_key_name The key name for the tab ID. +// @param input_args A list of arguments in the form of a JSON string. The first +// argument is a dictionary. It contains the key tab_id_key_name, whose +// corresponding value is the tab window handle. +// @param converted_args On success returns a JSON string, in which the tab +// window handle has been replaced with the actual tab ID; otherwise +// returns input_args. +// @param dispatcher The dispatcher used to query tab IDs using tab window +// handles. +template<const char* tab_id_key_name> +bool ConvertTabIdInDictionary(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + if (converted_args == NULL || dispatcher == NULL) { + NOTREACHED(); + return false; + } + // Fail safe... + *converted_args = input_args; + + scoped_ptr<ListValue> input_list; + DictionaryValue* input_dict = GetListAndDictionaryValue(input_args, + &input_list); + if (input_dict == NULL) { + LOG(ERROR) + << "Failed to get the details object from the list of arguments."; + return false; + } + + int tab_handle = -1; + bool success = input_dict->GetInteger(tab_id_key_name, &tab_handle); + if (!success) { + LOG(ERROR) << "Failed to get " << tab_id_key_name << " property."; + return false; + } + + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + int tab_id = kInvalidChromeSessionId; + if (tab_window != INVALID_HANDLE_VALUE) { + tab_id = dispatcher->GetTabIdFromHandle(tab_window); + } + input_dict->SetInteger(tab_id_key_name, tab_id); + + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; +} + +} // namespace api_module_util + +#endif // CEEE_IE_BROKER_API_MODULE_UTIL_H_ diff --git a/ceee/ie/broker/broker.cc b/ceee/ie/broker/broker.cc new file mode 100644 index 0000000..24ed0da --- /dev/null +++ b/ceee/ie/broker/broker.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ICeeeBroker implementation + +#include "ceee/ie/broker/broker.h" + +#include "base/logging.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/common/ceee_module_util.h" + + +HRESULT CeeeBroker::FinalConstruct() { + // So that we get a pointer to the ExecutorsManager and let tests override it. + executors_manager_ = Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>::get(); + api_dispatcher_ = ProductionApiDispatcher::get(); + return S_OK; +} + +void CeeeBroker::OnAddConnection(bool first_lock) { + if (first_lock) + ceee_module_util::LockModule(); +} + +void CeeeBroker::OnReleaseConnection(bool last_unlock, + bool last_unlock_releases) { + if (last_unlock) + ceee_module_util::UnlockModule(); + IExternalConnectionImpl<CeeeBroker>::OnReleaseConnection( + last_unlock, last_unlock_releases); +} + +STDMETHODIMP CeeeBroker::Execute(BSTR function, BSTR* response) { + // This is DEPRECATED and we should use ChromePostman (see FireEvent). + api_dispatcher_->HandleApiRequest(function, response); + return S_OK; +} + +STDMETHODIMP CeeeBroker::FireEvent(BSTR event_name, BSTR event_args) { + ChromePostman::GetInstance()->FireEvent(event_name, event_args); + return S_OK; +} + +STDMETHODIMP CeeeBroker::RegisterWindowExecutor(long thread_id, + IUnknown* executor) { + // TODO(mad@chromium.org): Add security check here. + return executors_manager_->RegisterWindowExecutor(thread_id, executor); +} + +STDMETHODIMP CeeeBroker::UnregisterExecutor(long thread_id) { + // TODO(mad@chromium.org): Add security check here. + return executors_manager_->RemoveExecutor(thread_id); +} + +STDMETHODIMP CeeeBroker::RegisterTabExecutor(long thread_id, + IUnknown* executor) { + // TODO(mad@chromium.org): Implement the proper manual/secure registration. + return executors_manager_->RegisterTabExecutor(thread_id, executor); +} + +STDMETHODIMP CeeeBroker::SetTabIdForHandle(long tab_id, + CeeeWindowHandle handle) { + // TODO(mad@chromium.org): Add security check here. + DCHECK(tab_id != kInvalidChromeSessionId && + handle != reinterpret_cast<CeeeWindowHandle>(INVALID_HANDLE_VALUE)); + executors_manager_->SetTabIdForHandle(tab_id, reinterpret_cast<HWND>(handle)); + return S_OK; +} diff --git a/ceee/ie/broker/broker.gyp b/ceee/ie/broker/broker.gyp new file mode 100644 index 0000000..422eeac --- /dev/null +++ b/ceee/ie/broker/broker.gyp @@ -0,0 +1,105 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'broker', + 'type': 'static_library', + 'dependencies': [ + '../common/common.gyp:ie_common', + '../common/common.gyp:ie_common_settings', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../../../base/base.gyp:base', + '../../../build/temp_gyp/googleurl.gyp:googleurl', + '../../../ceee/common/common.gyp:initializing_coclass', + '../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + 'api_dispatcher.cc', + 'api_dispatcher.h', + 'api_dispatcher_docs.h', + 'api_module_constants.cc', + 'api_module_constants.h', + 'api_module_util.cc', + 'api_module_util.h', + 'broker.cc', + 'broker.h', + 'broker_docs.h', + 'chrome_postman.cc', + 'chrome_postman.h', + 'common_api_module.cc', + 'common_api_module.h', + 'cookie_api_module.cc', + 'cookie_api_module.h', + 'event_dispatching_docs.h', + 'executors_manager.cc', + 'executors_manager.h', + 'executors_manager_docs.h', + 'infobar_api_module.cc', + 'infobar_api_module.h', + '../common/precompile.cc', + '../common/precompile.h', + 'tab_api_module.cc', + 'tab_api_module.h', + 'webnavigation_api_module.cc', + 'webnavigation_api_module.h', + 'webrequest_api_module.cc', + 'webrequest_api_module.h', + 'window_api_module.cc', + 'window_api_module.h', + 'window_events_funnel.cc', + 'window_events_funnel.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../common/precompile.cc', + 'msvs_precompiled_header': '../common/precompile.h', + }, + }, + }, + { + # IF YOU CHANGE THIS TARGET NAME YOU MUST UPDATE: + # ceee_module_util::kCeeeBrokerModuleName + 'target_name': 'ceee_broker', + 'type': 'executable', + 'sources': [ + 'broker.rgs', + 'broker_module.cc', + 'broker_module.rc', + 'broker_module.rgs', + 'broker_module_util.h', + 'resource.h' + ], + 'dependencies': [ + 'broker', + '../common/common.gyp:ie_common_settings', + '../common/common.gyp:ie_guids', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../../../base/base.gyp:base', + '../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../ceee/common/common.gyp:ceee_common', + '<(DEPTH)/chrome/chrome.gyp:chrome_version_header', + '<(DEPTH)/chrome_frame/chrome_frame.gyp:chrome_frame_ie', # for GUIDs. + ], + 'msvs_settings': { + 'VCLinkerTool': { + 'OutputFile': '$(OutDir)/servers/$(ProjectName).exe', + # Set /SUBSYSTEM:WINDOWS since this is not a command-line program. + 'SubSystem': '2', + }, + }, + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + ] +} diff --git a/ceee/ie/broker/broker.h b/ceee/ie/broker/broker.h new file mode 100644 index 0000000..8132e8a --- /dev/null +++ b/ceee/ie/broker/broker.h @@ -0,0 +1,70 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// ICeeeBroker implementation. + +#ifndef CEEE_IE_BROKER_BROKER_H_ +#define CEEE_IE_BROKER_BROKER_H_ + +#include <atlbase.h> +#include <atlcom.h> + +#include "ceee/ie/broker/resource.h" + +#include "broker_lib.h" // NOLINT + +class ExecutorsManager; +class ApiDispatcher; + +// The CEEE user Broker is a COM object exposed from an executable server +// so that we can call it to execute code in destination threads of other +// processes, even if they run at a hight integrity level. + +// Entry point to execute code in specific destination threads. +class ATL_NO_VTABLE CeeeBroker + : public CComObjectRootEx<CComMultiThreadModel>, + public CComCoClass<CeeeBroker, &CLSID_CeeeBroker>, + public ICeeeBroker, + public ICeeeBrokerRegistrar, + public IExternalConnectionImpl<CeeeBroker> { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_BROKER) + + DECLARE_NOT_AGGREGATABLE(CeeeBroker) + BEGIN_COM_MAP(CeeeBroker) + COM_INTERFACE_ENTRY(IExternalConnection) + COM_INTERFACE_ENTRY(ICeeeBrokerRegistrar) + COM_INTERFACE_ENTRY(ICeeeBroker) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + // To set get our pointer to the ExecutorsManager and let tests override it. + virtual HRESULT FinalConstruct(); + + // @name ICeeeBroker implementation. + // @{ + STDMETHOD(Execute)(BSTR function, BSTR* response); + STDMETHOD(FireEvent)(BSTR event_name, BSTR event_args); + // @} + + // @name ICeeeBrokerRegistrar implementation. + // @{ + STDMETHOD(RegisterWindowExecutor)(long thread_id, IUnknown* executor); + STDMETHOD(RegisterTabExecutor)(long thread_id, IUnknown* executor); + STDMETHOD(UnregisterExecutor)(long thread_id); + STDMETHOD(SetTabIdForHandle)(long tab_id, CeeeWindowHandle handle); + // @} + + // IExternalConnectionImpl overrides + void OnAddConnection(bool first_lock); + void OnReleaseConnection(bool last_unlock, bool last_unlock_releases); + + protected: + // A pointer to single instance objects, or seams set for unittests. + ExecutorsManager * executors_manager_; + ApiDispatcher* api_dispatcher_; +}; + +#endif // CEEE_IE_BROKER_BROKER_H_ diff --git a/ceee/ie/broker/broker.rgs b/ceee/ie/broker/broker.rgs new file mode 100644 index 0000000..65b012b --- /dev/null +++ b/ceee/ie/broker/broker.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove {6D88A70D-2218-4466-BBD6-87AB563811A2} = s 'Google CEEE Broker' { + ForceRemove 'Programmable' + LocalServer32 = s '%MODULE%' + 'TypeLib' = s '{45B783D0-8040-49a6-A719-84E320AAB3C5}' + } + } +} diff --git a/ceee/ie/broker/broker_docs.h b/ceee/ie/broker/broker_docs.h new file mode 100644 index 0000000..5cc1220 --- /dev/null +++ b/ceee/ie/broker/broker_docs.h @@ -0,0 +1,61 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_BROKER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_BROKER_DOCS_H_ + +/** @page BrokerDoc Detailed documentation of the Broker process. + +@section BrokerIntro Introduction + +Since Internet Explorer can have multiple processes and run them at different +integrity level, we use a separate process to take care of integrity level +elevation when needed. This Broker process is also used as the sole +communication point with a Chrome Frame instance for Chrome Extension API +invocation to be executed in IE as well as events coming from IE and going back +to Chrome. + +@subsection ApiInvocation API Invocation + +The Broker must register a Chrome Frame Events sink to receive the private +messages used to redirect Chrome Extension API Invocations. But this has to be +done in a non-blocking single threaded way. So we must create a Single Thread +Apartment (STA) into which an instance of the ChromeFrameHost class will run, so +it can receive the private messages safely. + +Once an API Invocation message is passed to the Broker via the Chrome Frame +Host, the request is simply added to a global queue of API Invocation, waiting +to be processed by a worker thread running in the Multi Thread Apartment (MTA). + +Running the API invocation mostly likely need to execute code in the IE process, +but we can't block the STA for those calls, this is why we need to delegate +these calls to a work thread while the Chrome Frame Host STA can run free. + +Once an API invocation is picked up from the queue, and processed (by making +calls to instances of the CeeeExecutor running in the IE process and managed +by the ExecutorsManager), then the API response can be sent back to Chrome, via +the Chrome Frame Host, in the exact same way that we would send Chrome Extension +Events back to Chrome as described below. + +@subsection EventDispatching Event Dispatching + +When an event occurs in IE, that Chrome Extension might be interested in, the +code running in IE fires an event on the ICeeeBroker interface and one of +the RPC threads, running in the MTA catch it. We process all that is needed +to fill the event data from the RPC thread, and then add the event to the +global queue of events that are to be dispatched to Chrome via the Chrome Frame +Host running in the STA. + +So the Chrome Frame Host thread must wake up when events are added to the queue +and simply pass them to Chrome Frame before going back to sleep. Waiting for +further API invocations, or more events. + +Since the API invocation responses need to go through the Chrome Frame Host +in the exact same way as the events (except for a different target string), we +can use the exact same mechanism to send back API invocation response as we do +for events. + +**/ + +#endif // CEEE_IE_BROKER_BROKER_DOCS_H_ diff --git a/ceee/ie/broker/broker_lib.idl b/ceee/ie/broker/broker_lib.idl new file mode 100644 index 0000000..ddb2ece --- /dev/null +++ b/ceee/ie/broker/broker_lib.idl @@ -0,0 +1,103 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Interface and object declarations for CEEE IE user broker. +import "exdisp.idl"; +import "oaidl.idl"; +import "ocidl.idl"; + +typedef long CeeeWindowHandle; + +[ + object, + uuid(A767CE3D-D65B-458f-B66A-CDB10B1F2DF6), + nonextensible, + helpstring("ICeeeBroker Interface"), + pointer_default(unique), + oleautomation +] +// Can execute code in specific threads of other process, even at higher +// integrity level than the calling process. + +// Allows the execution of code in destination threads. +interface ICeeeBroker : IUnknown { + // This method is DEPRECATED. + // Execute the requested message in the appropriate process/thread. + // + // @param function The JSON encoded function message to process. + // @param response Where to return the JSON encoded response. + HRESULT Execute([in] BSTR function, [out] BSTR* response); + + // Fire the given event message to Chrome Frame and potentially use it + // to complete asynchronous extension API execution. + // + // @param event_name The name of the event to fire. + // @param event_args The JSON encoded event arguments. + HRESULT FireEvent([in] BSTR event_name, [in] BSTR event_args); +} + +[ + object, + uuid(1821043a-a496-4d0e-9b50-1e0599237016), + nonextensible, + helpstring("ICeeeBrokerRegistrar Interface"), + pointer_default(unique), + oleautomation +] +// Access to the broker registrar to un/register a CeeeExecutor or a +// Chrome Frame Host running in an STA. +interface ICeeeBrokerRegistrar : IUnknown { + // Register the Window Executor @p executor to be used for the given + // @p thread_id. + // + // @param thread_id The identifier of the thread where @p executor runs. + // @param executor The executor to register. It must expose the + // ICeeeWindowExecutor interface. + HRESULT RegisterWindowExecutor([in] long thread_id, [in] IUnknown* executor); + + // TODO(mad@chromium.org): Implement the proper manual/secure + // registration. Register a tab executor. This version bypasses the + // checks to make sure the broker is the one which initiated the + // registration. + // + // @param thread_id The identifier of the thread where @p executor runs. + // @param executor The executor to register. It must expose the + // ICeeeTabExecutor interface. + HRESULT RegisterTabExecutor([in] long thread_id, [in] IUnknown* executor); + + // Unregister @p executor for the given @p thread_id. + // + // @param thread_id The identifier of the thread for which we want to + // unregister the executor of. + HRESULT UnregisterExecutor([in] long thread_id); + + // TODO(hansl@google.com): Remove this and implement it in + // RegisterTabExecutor. Make sure the Tab isn't registered before a TabId + // is available. + // Link a tab_id with a related BHO handle. There is a strict one to one + // relation between tab_ids and handles. + // + // @param tab_id The Chrome TabId related to the tab. + // @param handle The HWND of the BHO for this TabId. + HRESULT SetTabIdForHandle([in] long tab_id, [in] CeeeWindowHandle handle); +}; + +[ + uuid(45B783D0-8040-49a6-A719-84E320AAB3C5), + version(1.0), + helpstring("CEEE Broker 1.0 Type Library") +] +library CeeeBrokerLib { + interface ICeeeBrokerRegistrar; + interface ICeeePostman; + + [ + uuid(6D88A70D-2218-4466-BBD6-87AB563811A2), + helpstring("CEEE Broker Class") + ] + coclass CeeeBroker { + [default] interface ICeeeBroker; + }; +}; diff --git a/ceee/ie/broker/broker_module.cc b/ceee/ie/broker/broker_module.cc new file mode 100644 index 0000000..38b8529 --- /dev/null +++ b/ceee/ie/broker/broker_module.cc @@ -0,0 +1,194 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of ATL module object for EXE module. + +#include <atlbase.h> +#include <atlhost.h> + +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "base/logging_win.h" +#include "ceee/ie/broker/broker.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/broker/resource.h" +#include "ceee/ie/broker/window_events_funnel.h" +#include "ceee/ie/common/crash_reporter.h" +#include "ceee/common/com_utils.h" +#include "chrome/common/url_constants.h" + +namespace { + +const wchar_t kLogFileName[] = L"CeeeBroker.log"; + +// {6E3D6168-1DD2-4edb-A183-584C2C66E96D} +const GUID kCeeeBrokerLogProviderName = + { 0x6e3d6168, 0x1dd2, 0x4edb, + { 0xa1, 0x83, 0x58, 0x4c, 0x2c, 0x66, 0xe9, 0x6d } }; + +} // namespace + +// Object entries go here instead of with each object, so that we can keep +// the objects in a lib, and also to decrease the amount of magic. +OBJECT_ENTRY_AUTO(__uuidof(CeeeBroker), CeeeBroker) + +class CeeeBrokerModule : public CAtlExeModuleT<CeeeBrokerModule> { + public: + CeeeBrokerModule(); + ~CeeeBrokerModule(); + + DECLARE_LIBID(LIBID_CeeeBrokerLib) + static HRESULT WINAPI UpdateRegistryAppId(BOOL register) throw(); + + // We have our own version so that we can explicitly specify + // that we want to be in a multi threaded apartment. + static HRESULT InitializeCom(); + + // Prevent COM objects we don't own to control our lock count. + // To properly manage our lifespan, yet still be able to control the + // lifespan of the ChromePostman's thread, we must only rely on the + // CeeeBroker implementation of the IExternalConnection interface + // as well as the ExecutorsManager map content to decide when to die. + virtual LONG Lock() { + return 1; + } + virtual LONG Unlock() { + return 1; + } + + // We prevent access to the module lock count from objects we don't + // own by overriding the Un/Lock methods above. But when we want to + // access the module lock count, we do it from here, and bypass our + // override. These are the entry points that only our code accesses. + LONG LockModule() { + return CAtlExeModuleT<CeeeBrokerModule>::Lock(); + } + LONG UnlockModule() { + return CAtlExeModuleT<CeeeBrokerModule>::Unlock(); + } + + HRESULT PostMessageLoop(); + HRESULT PreMessageLoop(int show); + private: + // We maintain a postman COM object on the stack so that we can + // properly initialize and terminate it at the right time. + CComObjectStackEx<ChromePostman> chrome_postman_; + CrashReporter crash_reporter_; + base::AtExitManager at_exit_; +}; + +CeeeBrokerModule module; + +extern "C" int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int nShowCmd) { + return module.WinMain(nShowCmd); +} + +CeeeBrokerModule::CeeeBrokerModule() + : crash_reporter_(L"ceee_broker") { + TRACE_EVENT_BEGIN("ceee.broker", this, ""); + + wchar_t logfile_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(logfile_path), logfile_path); + ::PathAppend(logfile_path, kLogFileName); + + // It seems we're obliged to initialize the current command line + // before initializing logging. + CommandLine::Init(0, NULL); + + logging::InitLogging( + logfile_path, + logging::LOG_TO_BOTH_FILE_AND_SYSTEM_DEBUG_LOG, + logging::LOCK_LOG_FILE, + logging::APPEND_TO_OLD_LOG_FILE); + + // Initialize ETW logging. + logging::LogEventProvider::Initialize(kCeeeBrokerLogProviderName); + + // Initialize control hosting. + BOOL initialized = AtlAxWinInit(); + DCHECK(initialized); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + crash_reporter_.InitializeCrashReporting(false); +} + +HRESULT CeeeBrokerModule::InitializeCom() { + HRESULT hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (FAILED(hr)) + return hr; + + // We need to initialize security before setting global options. + hr = ::CoInitializeSecurity(NULL, + -1, + NULL, + NULL, + // Clients must identify. + RPC_C_IMP_LEVEL_IDENTIFY, + // And we identify. + RPC_C_IMP_LEVEL_IDENTIFY, + NULL, + // We don't want low integrity to be able to + // instantiate arbitrary objects in our process. + EOAC_NO_CUSTOM_MARSHAL, + NULL); + DCHECK(SUCCEEDED(hr)); + + // Ensure the marshaling machinery doesn't eat our crashes. + CComPtr<IGlobalOptions> options; + hr = options.CoCreateInstance(CLSID_GlobalOptions); + if (SUCCEEDED(hr)) { + hr = options->Set(COMGLB_EXCEPTION_HANDLING, COMGLB_EXCEPTION_DONOT_HANDLE); + } + DLOG_IF(WARNING, FAILED(hr)) << "IGlobalOptions::Set failed " + << com::LogHr(hr); + + // The above is best-effort, don't bail on error. + return S_OK; +} + +HRESULT CeeeBrokerModule::PreMessageLoop(int show) { + // It's important to initialize the postman BEFORE we make the Broker + // and the event funnel available, since we may get requests to execute + // API invocation or Fire events before the postman is ready to handle them. + chrome_postman_.Init(); + WindowEventsFunnel::Initialize(); + return CAtlExeModuleT<CeeeBrokerModule>::PreMessageLoop(show); +} + +HRESULT CeeeBrokerModule::PostMessageLoop() { + HRESULT hr = CAtlExeModuleT<CeeeBrokerModule>::PostMessageLoop(); + Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>()->Terminate(); + WindowEventsFunnel::Terminate(); + chrome_postman_.Term(); + return hr; +} + +CeeeBrokerModule::~CeeeBrokerModule() { + crash_reporter_.ShutdownCrashReporting(); + logging::CloseLogFile(); + + TRACE_EVENT_END("ceee.broker", this, ""); +} + +HRESULT WINAPI CeeeBrokerModule::UpdateRegistryAppId(BOOL reg) throw() { + return com::ModuleRegistrationWithoutAppid(IDR_BROKER_MODULE, reg); +} + +namespace ceee_module_util { + +LONG LockModule() { + return module.LockModule(); +} + +LONG UnlockModule() { + return module.UnlockModule(); +} + +} // namespace diff --git a/ceee/ie/broker/broker_module.rc b/ceee/ie/broker/broker_module.rc new file mode 100644 index 0000000..43af9fe --- /dev/null +++ b/ceee/ie/broker/broker_module.rc @@ -0,0 +1,104 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Microsoft Visual C++ generated resource script. +// +#include "ceee/ie/broker/resource.h" +#include "version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +#error Don't open this in the GUI, it'll be massacred on save. +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION CHROME_VERSION + PRODUCTVERSION CHROME_VERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Google Inc." + VALUE "FileDescription", "Internet Explorer User Broker for Google CEEE" + VALUE "FileVersion", CHROME_VERSION_STRING + VALUE "LegalCopyright", COPYRIGHT_STRING + VALUE "InternalName", "ceee_broker.exe" + VALUE "OriginalFilename", "ceee_broker.exe" + VALUE "ProductName", "Google Chrome Extensions Execution Environment" + VALUE "ProductVersion", CHROME_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +IDR_BROKER_MODULE REGISTRY "broker_module.rgs" +IDR_BROKER REGISTRY "broker.rgs" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PROJNAME "ceee_broker" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +1 TYPELIB "broker_lib.tlb" + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/ceee/ie/broker/broker_module.rgs b/ceee/ie/broker/broker_module.rgs new file mode 100644 index 0000000..dfd0d75 --- /dev/null +++ b/ceee/ie/broker/broker_module.rgs @@ -0,0 +1,17 @@ +HKLM { + NoRemove SOFTWARE { + NoRemove Microsoft { + NoRemove 'Internet Explorer' { + NoRemove 'Low Rights' { + NoRemove ElevationPolicy { + ForceRemove {F2DE0DE9-098E-48a7-92C2-53B89A272BF4} { + val Policy = d 3 + val AppName = s '%MODULE_BASENAME%' + val AppPath = s '%MODULE_PATH%' + } + } + } + } + } + } +} diff --git a/ceee/ie/broker/broker_module_util.h b/ceee/ie/broker/broker_module_util.h new file mode 100644 index 0000000..effcd99 --- /dev/null +++ b/ceee/ie/broker/broker_module_util.h @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Utility functions to be called on the Broker Module object. + +#ifndef CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ +#define CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ + +namespace ceee_module_util { + +// Locks the module to prevent it from being terminated. +// +// @return The new module ref count, but only for debugging purposes. +LONG LockModule(); + +// Unlocks the module to allow it to be terminated appropriately. +// +// @return The new module ref count, but only for debugging purposes. +LONG UnlockModule(); + +} // namespace + +#endif // CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ diff --git a/ceee/ie/broker/broker_unittest.cc b/ceee/ie/broker/broker_unittest.cc new file mode 100644 index 0000000..aec7453 --- /dev/null +++ b/ceee/ie/broker/broker_unittest.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for user broker. + +#include "base/logging.h" +#include "ceee/ie/broker/broker.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::Return; + +// Lets us test protected members and mock certain calls. +class MockExecutorsManager : public ExecutorsManager { + public: + // true for no thread. + MockExecutorsManager() : ExecutorsManager(true) { + } + MOCK_METHOD2(RegisterWindowExecutor, HRESULT(ThreadId thread_id, + IUnknown* executor)); + MOCK_METHOD2(RegisterTabExecutor, HRESULT(ThreadId thread_id, + IUnknown* executor)); + MOCK_METHOD1(RemoveExecutor, HRESULT(ThreadId thread_id)); +}; + +class TestBroker: public CeeeBroker { + public: + // Prevents the instantiation of the ExecutorsManager, ApiDispatcher + // singletons. + HRESULT FinalConstruct() { + return S_OK; + } + // Set our own ExecutorsManager mock. + void SetExecutorsManager(ExecutorsManager* executors_manager) { + executors_manager_ = executors_manager; + } + + // Set our own ApiDispatcher mock. + void SetApiDisptacher(ApiDispatcher* api_dispatcher) { + api_dispatcher_ = api_dispatcher; + } +}; + +TEST(CeeeBroker, All) { + CComObject<TestBroker>* broker = NULL; + CComObject<TestBroker>::CreateInstance(&broker); + CComPtr<TestBroker> broker_keeper(broker); + + MockExecutorsManager executors_manager; + broker->SetExecutorsManager(&executors_manager); + + testing::MockApiDispatcher api_dispatcher; + broker->SetApiDisptacher(&api_dispatcher); + + EXPECT_CALL(api_dispatcher, HandleApiRequest(NULL, NULL)).Times(1); + EXPECT_EQ(S_OK, broker->Execute(NULL, NULL)); + + EXPECT_CALL(executors_manager, RegisterWindowExecutor(0, NULL)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->RegisterWindowExecutor(0, NULL)); + + EXPECT_CALL(executors_manager, RegisterTabExecutor(0, NULL)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->RegisterTabExecutor(0, NULL)); + + EXPECT_CALL(executors_manager, RemoveExecutor(0)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->UnregisterExecutor(0)); +} + +} // namespace diff --git a/ceee/ie/broker/chrome_postman.cc b/ceee/ie/broker/chrome_postman.cc new file mode 100644 index 0000000..59f2ba7 --- /dev/null +++ b/ceee/ie/broker/chrome_postman.cc @@ -0,0 +1,300 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ChromePostman implementation. +#include "ceee/ie/broker/chrome_postman.h" + +#include "base/string_util.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/common/api_registration.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/common/url_constants.h" +#include "ceee/common/com_utils.h" + +namespace ext = extension_automation_constants; + +namespace { + // The maximum number of times we should try to start Frame. + const unsigned int kMaxFrameResetCount = 5; +} + +class ChromeFrameMessageTask : public Task { + public: + explicit ChromeFrameMessageTask( + ChromePostman::ChromePostmanThread* thread_object, + BSTR message, BSTR target) + : thread_object_(thread_object), + message_(message), + target_(target) { + } + + virtual void Run() { + thread_object_->PostMessage(message_, target_); + } + private: + ChromePostman::ChromePostmanThread* thread_object_; + CComBSTR message_; + CComBSTR target_; +}; + + +class ChromeFrameResetTask : public Task { + public: + explicit ChromeFrameResetTask( + ChromePostman::ChromePostmanThread* thread_object) + : thread_object_(thread_object) { + } + + virtual void Run() { + thread_object_->ResetChromeFrame(); + } + private: + ChromePostman::ChromePostmanThread* thread_object_; +}; + + +class ApiExecutionTask : public Task { + public: + explicit ApiExecutionTask(BSTR message) : message_(message) {} + + virtual void Run() { + ProductionApiDispatcher::get()->HandleApiRequest(message_, NULL); + } + private: + CComBSTR message_; +}; + +class FireEventTask : public Task { + public: + FireEventTask(BSTR event_name, BSTR event_args) + : event_name_(event_name), event_args_(event_args) {} + + virtual void Run() { + ProductionApiDispatcher::get()->FireEvent(event_name_, event_args_); + } + private: + CComBSTR event_name_; + CComBSTR event_args_; +}; + + +ChromePostman* ChromePostman::single_instance_ = NULL; +ChromePostman::ChromePostman() { + DCHECK(single_instance_ == NULL); + single_instance_ = this; + frame_reset_count_ = 0; +} + +ChromePostman::~ChromePostman() { + DCHECK(single_instance_ == this); + single_instance_ = NULL; +} + +void ChromePostman::Init() { + // The postman thread must be a UI thread so that it can pump windows + // messages and allow COM to handle cross apartment calls. + + chrome_postman_thread_.StartWithOptions( + base::Thread::Options(MessageLoop::TYPE_UI, 0)); + api_worker_thread_.Start(); +} + +void ChromePostman::Term() { + api_worker_thread_.Stop(); + chrome_postman_thread_.Stop(); +} + +void ChromePostman::PostMessage(BSTR message, BSTR target) { + MessageLoop* message_loop = chrome_postman_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask( + FROM_HERE, new ChromeFrameMessageTask(&chrome_postman_thread_, + message, target)); + } else { + LOG(ERROR) << "Trying to post a message before the postman thread is" + "completely initialized and ready."; + } +} + +void ChromePostman::FireEvent(BSTR event_name, BSTR event_args) { + MessageLoop* message_loop = api_worker_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, + new FireEventTask(event_name, event_args)); + } else { + LOG(ERROR) << "Trying to post a message before the API worker thread is" + "completely initialized and ready."; + } +} + +HRESULT ChromePostman::OnCfReadyStateChanged(LONG state) { + if (state == READYSTATE_COMPLETE) { + // If the page is fully loaded, we reset the count to 0 to ensure we restart + // it if it's the user's fault (no max count). + DLOG(INFO) << "frame_reset_count_ reset"; + frame_reset_count_ = 0; + } + return S_OK; +} + +HRESULT ChromePostman::OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target) { + if (CComBSTR(target) == ext::kAutomationRequestTarget) { + MessageLoop* message_loop = api_worker_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, new ApiExecutionTask(msg)); + } else { + LOG(ERROR) << "Trying to post a message before the API worker thread is" + "completely initialized and ready."; + } + } + return S_OK; +} + +HRESULT ChromePostman::OnCfExtensionReady(BSTR path, int response) { + return S_OK; +} + +HRESULT ChromePostman::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* tab_delimited_paths) { + return S_OK; +} + +HRESULT ChromePostman::OnCfGetExtensionApisToAutomate(BSTR* functions_enabled) { + DCHECK(functions_enabled != NULL); + std::vector<std::string> function_names; +#define REGISTER_API_FUNCTION(func) \ + function_names.push_back(func##Function::function_name()) + REGISTER_ALL_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // There is a special case with CreateWindow that is explained in the + // class comments. + function_names.push_back(CreateWindowFunction::function_name()); + std::string function_names_delim = JoinString(function_names, ','); + *functions_enabled = CComBSTR(function_names_delim.c_str()).Detach(); + + // CF is asking for the list of extension APIs to automate so the + // automation channel is ready. Set a dummy URL so we get the OnLoad + // callback. + // + // The current function call should come to us on the COM thread so we can + // just call the ChromeFrameHost directly from here. + CComPtr<IChromeFrameHost> host; + chrome_postman_thread_.GetHost(&host); + return host->SetUrl(CComBSTR(chrome::kAboutBlankURL)); + + return S_OK; +} + +HRESULT ChromePostman::OnCfChannelError() { + MessageLoop* message_loop = chrome_postman_thread_.message_loop(); + if (message_loop) { + frame_reset_count_++; + LOG(INFO) << "frame_reset_count_ = " << frame_reset_count_; + + // No use staying alive if Chrome Frame can't start. + CHECK(frame_reset_count_ < kMaxFrameResetCount) + << "Trying to reset Chrome Frame too many times already. Something's " + "wrong."; + + message_loop->PostTask(FROM_HERE, + new ChromeFrameResetTask(&chrome_postman_thread_)); + } else { + LOG(ERROR) << "Trying to reset Chrome Frame before the postman thread is" + "completely initialized and ready."; + } + return S_OK; +} + +ChromePostman::ChromePostmanThread::ChromePostmanThread() + : base::Thread("ChromePostman") { +} + +void ChromePostman::ChromePostmanThread::Init() { + HRESULT hr = ::CoInitializeEx(0, COINIT_APARTMENTTHREADED); + DCHECK(SUCCEEDED(hr)) << "Can't Init COM??? " << com::LogHr(hr); + + hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); +} + +HRESULT ChromePostman::ChromePostmanThread::InitializeChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + HRESULT hr = CreateChromeFrameHost(); + DCHECK(SUCCEEDED(hr) && chrome_frame_host_ != NULL); + if (FAILED(hr) || chrome_frame_host_ == NULL) { + LOG(ERROR) << "Failed to get chrome frame host " << com::LogHr(hr); + return com::AlwaysError(hr); + } + + DCHECK(ChromePostman::GetInstance() != NULL); + chrome_frame_host_->SetEventSink(ChromePostman::GetInstance()); + chrome_frame_host_->SetChromeProfileName( + ceee_module_util::GetBrokerProfileNameForIe()); + hr = chrome_frame_host_->StartChromeFrame(); + DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to start chrome frame " << com::LogHr(hr); + return hr; + } + return hr; +} + +HRESULT ChromePostman::ChromePostmanThread::CreateChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + return ChromeFrameHost::CreateInitializedIID(IID_IChromeFrameHost, + &chrome_frame_host_); +} + +void ChromePostman::ChromePostmanThread::CleanUp() { + TeardownChromeFrameHost(); + ::CoUninitialize(); +} + +void ChromePostman::ChromePostmanThread::TeardownChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + if (chrome_frame_host_) { + chrome_frame_host_->SetEventSink(NULL); + HRESULT hr = chrome_frame_host_->TearDown(); + DCHECK(SUCCEEDED(hr)) << "ChromeFrameHost TearDown failed " << + com::LogHr(hr); + chrome_frame_host_.Release(); + } +} + +void ChromePostman::ChromePostmanThread::ResetChromeFrame() { + TeardownChromeFrameHost(); + HRESULT hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); +} + +void ChromePostman::ChromePostmanThread::PostMessage(BSTR message, + BSTR target) { + DCHECK(thread_id() == ::GetCurrentThreadId()); + HRESULT hr = chrome_frame_host_->PostMessage(message, target); + DCHECK(SUCCEEDED(hr)) << "ChromeFrameHost PostMessage failed " << + com::LogHr(hr); +} + +void ChromePostman::ChromePostmanThread::GetHost(IChromeFrameHost** host) { + DCHECK(thread_id() == ::GetCurrentThreadId()); + chrome_frame_host_.CopyTo(host); +} + +ChromePostman::ApiInvocationWorkerThread::ApiInvocationWorkerThread() + : base::Thread("ApiInvocationWorker") { +} + +void ChromePostman::ApiInvocationWorkerThread::Init() { + ::CoInitializeEx(0, COINIT_MULTITHREADED); + ProductionApiDispatcher::get()->SetApiInvocationThreadId( + ::GetCurrentThreadId()); +} + +void ChromePostman::ApiInvocationWorkerThread::CleanUp() { + ::CoUninitialize(); + ProductionApiDispatcher::get()->SetApiInvocationThreadId(0); +} diff --git a/ceee/ie/broker/chrome_postman.h b/ceee/ie/broker/chrome_postman.h new file mode 100644 index 0000000..538abd7 --- /dev/null +++ b/ceee/ie/broker/chrome_postman.h @@ -0,0 +1,133 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Broke Postman implementation. + +#ifndef CEEE_IE_BROKER_CHROME_POSTMAN_H_ +#define CEEE_IE_BROKER_CHROME_POSTMAN_H_ + +#include "base/singleton.h" +#include "base/thread.h" +#include "ceee/ie/common/chrome_frame_host.h" + +#include "broker_lib.h" // NOLINT + +// The Broker Postman singleton object wrapping the ChromeFrameHost +// class so that it can receive API invocation from Chrome Frame, and also +// dispatch response and events to it. +// +// Since Chrome Frame must be access from a Single Thread Apartment, this +// object must only call it in an STA, so the PostMessage method must post a +// task to do so in the STA thread. +// +// But when Chrome Frame calls into this object, we can't block and thus +// the API invocations must be queued so that the API Dispatcher can +// fetch them from the queue, handle them appropriately in the MTA and +// post back the asynchronous response to Chrome Frame via our +// PostMessage method. + +class ChromePostman + : public CComObjectRootEx<CComMultiThreadModel>, + public IChromeFrameHostEvents { + public: + BEGIN_COM_MAP(ChromePostman) + END_COM_MAP() + + ChromePostman(); + virtual ~ChromePostman(); + + // This posts a tasks to our STA thread so that it posts the given message + // to Chrome Frame. + virtual void PostMessage(BSTR message, BSTR target); + + // This posts a tasks to the Api Invocation thread to fire the given event. + virtual void FireEvent(BSTR event_name, BSTR event_args); + + // This creates both an STA and an MTA threads. We must make sure we only call + // Chrome Frame from this STA. And since we can't block Chrome Frame we use + // the MTA thread to executes API Invocation we receive from Chrome Frame. + // We also create and initialize Chrome Framer from here. + virtual void Init(); + + // To cleanly terminate the threads, and our hooks into Chrome Frame. + virtual void Term(); + + // Returns our single instance held by the module. + static ChromePostman* GetInstance() { return single_instance_; } + + class ChromePostmanThread : public base::Thread { + public: + ChromePostmanThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + + // Posts the message to our instance of Chrome Frame. + // THIS CAN ONLY BE CALLED FROM OUR THREAD, and we DCHECK it. + void PostMessage(BSTR message, BSTR target); + + // Retrieves the Chrome Frame host; should only be called from the + // postman thread. + void GetHost(IChromeFrameHost** host); + + // Resets Chrome Frame by tearing it down and restarting it. + // We use this when we receive a channel error meaning Chrome has died. + void ResetChromeFrame(); + + protected: + // Creates and initializes the chrome frame host. + // CAN ONLY BE CALLED FROM THE STA! + HRESULT InitializeChromeFrameHost(); + + // Isolate the creation of the host so we can overload it to mock + // the Chrome Frame Host in our tests. + // CAN ONLY BE CALLED FROM THE STA! + virtual HRESULT CreateChromeFrameHost(); + + // Tears down the Chrome Frame host. + void TeardownChromeFrameHost(); + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<IChromeFrameHost> chrome_frame_host_; + }; + + protected: + // Messages received from Chrome Frame are sent to the API dispatcher via + // a task posted to our MTA thread. + HRESULT OnCfReadyStateChanged(LONG state); + HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target); + HRESULT OnCfExtensionReady(BSTR path, int response); + HRESULT OnCfGetEnabledExtensionsComplete(SAFEARRAY* tab_delimited_paths); + HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled); + HRESULT OnCfChannelError(); + + class ApiInvocationWorkerThread : public base::Thread { + public: + ApiInvocationWorkerThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + }; + + // The STA in which we run. + ChromePostmanThread chrome_postman_thread_; + + // The MTA thread to which we post API Invocations and Fired Events. + ApiInvocationWorkerThread api_worker_thread_; + + // The number of times we tried to launch ChromeFrame. + int frame_reset_count_; + + // "in the end, there should be only one!" :-) + static ChromePostman* single_instance_; +}; + +#endif // CEEE_IE_BROKER_CHROME_POSTMAN_H_ diff --git a/ceee/ie/broker/common_api_module.cc b/ceee/ie/broker/common_api_module.cc new file mode 100644 index 0000000..096003b --- /dev/null +++ b/ceee/ie/broker/common_api_module.cc @@ -0,0 +1,146 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementations for common functions for various API implementation modules. + +#include "ceee/ie/broker/common_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> // Must be included AFTER base. + +#include "base/string_number_conversions.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + +namespace ext = extension_tabs_module_constants; + +namespace common_api { + +bool CommonApiResult::IsTabWindowClass(HWND window) { + return window_utils::IsWindowClass(window, windows::kIeTabWindowClass); +} + +bool CommonApiResult::IsIeFrameClass(HWND window) { + return window_utils::IsWindowClass(window, windows::kIeFrameWindowClass); +} + +HWND CommonApiResult::TopIeWindow() { + HWND window = NULL; + if (window_utils::FindDescendentWindow(NULL, windows::kIeFrameWindowClass, + true, &window)) { + return window; + } + NOTREACHED() << "How come we can't find a Top IE Window???"; + return NULL; +} + +void CommonApiResult::SetResultFromWindowInfo( + HWND window, const CeeeWindowInfo& window_info, bool populate_tabs) { + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + dict->SetInteger(ext::kIdKey, window_id); + dict->SetBoolean(ext::kFocusedKey, window_info.focused != FALSE); + + dict->SetInteger(ext::kLeftKey, window_info.rect.left); + dict->SetInteger(ext::kTopKey, window_info.rect.top); + dict->SetInteger(ext::kWidthKey, window_info.rect.right - + window_info.rect.left); + dict->SetInteger(ext::kHeightKey, window_info.rect.bottom - + window_info.rect.top); + if (populate_tabs) { + DCHECK(window_info.tab_list != NULL); + Value* tab_list_value = CreateTabList(window_info.tab_list); + // DCHECK yet continue if we get a NULL tab list since we may get here + // before it is available. + DCHECK(tab_list_value != NULL); + if (!tab_list_value) + tab_list_value = Value::CreateNullValue(); + dict->Set(ext::kTabsKey, tab_list_value); + } + + dict->SetBoolean(ext::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + + // TODO(mad@chromium.org): for now, always setting to "normal" since we don't + // yet have a way to tell if the window is a popup or not. + dict->SetString(ext::kWindowTypeKey, ext::kWindowTypeValueNormal); + + DCHECK(value_ == NULL); + value_.reset(dict.release()); +} + +Value* CommonApiResult::CreateTabList(BSTR tab_list) { + // No need for a request_id. + tab_api::GetAllTabsInWindowResult result(kNoRequestId); + result.Execute(tab_list); + const Value* tab_list_value = result.value(); + // No DCHECK as this will happen if we try to update a window + // that has been closed. + if (tab_list_value) { + return tab_list_value->DeepCopy(); + } + LOG(WARNING) << "Can't get info for tab_ids: '" << tab_list << "'."; + return NULL; +} + +bool CommonApiResult::CreateWindowValue(HWND window, bool populate_tabs) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + if (window_utils::WindowHasNoThread(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + if (!IsIeFrameClass(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get window info."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + common_api::WindowInfo window_info; + HRESULT hr = executor->GetWindow(populate_tabs, &window_info); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't get info for window: " << std::hex << window << + ". " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + SetResultFromWindowInfo(window, window_info, populate_tabs); + return true; +} + +WindowInfo::WindowInfo() { + focused = FALSE; + rect.bottom = -1; + rect.top = -1; + rect.left = -1; + rect.right = -1; + tab_list = NULL; +} + +WindowInfo::~WindowInfo() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(tab_list); +} + +} // namespace common_api diff --git a/ceee/ie/broker/common_api_module.h b/ceee/ie/broker/common_api_module.h new file mode 100644 index 0000000..c4b6961 --- /dev/null +++ b/ceee/ie/broker/common_api_module.h @@ -0,0 +1,71 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common functions for various API implementation modules. + +#ifndef CEEE_IE_BROKER_COMMON_API_MODULE_H_ +#define CEEE_IE_BROKER_COMMON_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "toolband.h" // NOLINT + +namespace common_api { + +class CommonApiResult : public ApiDispatcher::InvocationResult { + public: + explicit CommonApiResult(int request_id) + : ApiDispatcher::InvocationResult(request_id) { + } + + // Returns true if the given window is an IE "server" window, i.e. a tab. + static bool IsTabWindowClass(HWND window); + + // Returns true if the given window is a top-level IE window. + static bool IsIeFrameClass(HWND window); + + // Returns the IE frame window at the top of the Z-order. This will generally + // be the last window used or the new window just created. + // @return The HWND of the top IE window. + static HWND TopIeWindow(); + + // Build the result_ value from the provided window info. It will set the + // value if it is currently NULL, otherwise it assumes it is a ListValue and + // adds a new Value to it. + // @param window The window handle + // @param window_info The info about the window to create a new value for. + // @param populate_tabs To specify if we want to populate the tabs info. + virtual void SetResultFromWindowInfo(HWND window, + const CeeeWindowInfo& window_info, + bool populate_tabs); + + // Creates a list value of all tabs in the given list. + // @param tab_list A list of HWND and index of the tabs for which we want to + // create a value JSON encoded as a list of (id, index) pairs. + // @Return A ListValue containing the individual Values for each tab info. + virtual Value* CreateTabList(BSTR tab_list); + + // Creates a value for the given window, populating tabs if requested. + // Sets value_ with the appropriate value content, or resets it in case of + // errors. Also calls PostError() if there is an error and returns false. + // The @p window parameter contrasts with CreateTabValue because we most + // often use this function with HWND gotten without Ids (ie. from + // TopIeWindow()). This was not the case with CreateTabValue. + // @param window The identifier of the window for which to create the value. + // @param populate_tabs To specify if we want to populate the tabs info. + virtual bool CreateWindowValue(HWND window, bool populate_tabs); +}; + +// Helper class to handle the data cleanup. +class WindowInfo : public CeeeWindowInfo { + public: + WindowInfo(); + ~WindowInfo(); +}; + +} // namespace common_api + +#endif // CEEE_IE_BROKER_COMMON_API_MODULE_H_ diff --git a/ceee/ie/broker/cookie_api_module.cc b/ceee/ie/broker/cookie_api_module.cc new file mode 100644 index 0000000..2e7d59b --- /dev/null +++ b/ceee/ie/broker/cookie_api_module.cc @@ -0,0 +1,450 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation. + +#include "ceee/ie/broker/cookie_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" + +namespace keys = extension_cookies_api_constants; + +namespace cookie_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); }\ + while (false) + REGISTER_COOKIE_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // Now register the permanent event handler. + dispatcher->RegisterPermanentEventHandler(keys::kOnChanged, + CookieChanged::EventHandler); +} + +bool CookieApiResult::CreateCookieValue(const CookieInfo& cookie_info) { + scoped_ptr<DictionaryValue> cookie(new DictionaryValue()); + cookie->SetString(keys::kNameKey, com::ToString(cookie_info.name)); + cookie->SetString(keys::kValueKey, com::ToString(cookie_info.value)); + cookie->SetString(keys::kDomainKey, com::ToString(cookie_info.domain)); + cookie->SetBoolean(keys::kHostOnlyKey, cookie_info.host_only == TRUE); + cookie->SetString(keys::kPathKey, com::ToString(cookie_info.path)); + cookie->SetBoolean(keys::kSecureKey, cookie_info.secure == TRUE); + cookie->SetBoolean(keys::kHttpOnlyKey, cookie_info.http_only == TRUE); + cookie->SetBoolean(keys::kSessionKey, cookie_info.session == TRUE); + if (cookie_info.session == FALSE) + cookie->SetReal(keys::kExpirationDateKey, cookie_info.expiration_date); + cookie->SetString(keys::kStoreIdKey, com::ToString(cookie_info.store_id)); + DCHECK(value() == NULL); + set_value(cookie.release()); + return true; +} + +HRESULT CookieApiResult::GetCookieInfo( + const std::string& url, const std::string& name, HWND window, + CookieInfo* cookie_info) { + // Get a tab window child of the cookie store window, so that we can properly + // access session cookies. + HWND tab_window = NULL; + if (!window_utils::FindDescendentWindow( + window, windows::kIeTabWindowClass, false, &tab_window) || + tab_window == NULL) { + PostError("Failed to get tab window for a given cookie store."); + return E_FAIL; + } + + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(tab_window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get cookie info."; + PostError("Internal Error while getting cookie info."); + return E_FAIL; + } + + HRESULT hr = executor->GetCookie( + CComBSTR(url.data()), CComBSTR(name.data()), cookie_info); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get cookie info." << com::LogHr(hr); + PostError("Internal Error while getting cookie info."); + } + return hr; +} + +bool CookieApiResult::CreateCookieStoreValue(DWORD process_id, + const WindowSet& windows) { + scoped_ptr<DictionaryValue> cookie_store(new DictionaryValue()); + std::ostringstream store_id_stream; + store_id_stream << process_id; + // For IE CEEE, we use a string representation of the IE process ID as the + // cookie store ID. + cookie_store->SetString(keys::kIdKey, store_id_stream.str()); + DCHECK(windows.size()); + if (windows.size() == 0) { + PostError(api_module_constants::kInternalErrorError); + return false; + } + WindowSet::const_iterator iter = windows.begin(); + // First register the cookie store once per process. + if (FAILED(RegisterCookieStore(*iter))) + return false; + // Now collect all tab IDs from each frame window into a single list. + scoped_ptr<ListValue> tab_ids(new ListValue()); + for (; iter != windows.end(); ++iter) { + if (!AppendToTabIdList(*iter, tab_ids.get())) { + PostError(api_module_constants::kInternalErrorError); + return false; + } + } + cookie_store->Set(keys::kTabIdsKey, tab_ids.release()); + set_value(cookie_store.release()); + PostResult(); + return true; +} + +HRESULT CookieApiResult::RegisterCookieStore(HWND window) { + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to register a cookie store."; + PostError(api_module_constants::kInternalErrorError); + return E_FAIL; + } + HRESULT hr = executor->RegisterCookieStore(); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't register cookie store. " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + } + return hr; +} + +HRESULT CookieApiResult::CookieStoreIsRegistered(HWND window) { + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to register a cookie store."; + PostError(api_module_constants::kInternalErrorError); + return E_FAIL; + } + HRESULT hr = executor->CookieStoreIsRegistered(); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Error accessing cookie store. " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + } + return hr; +} + +bool CookieApiResult::AppendToTabIdList(HWND window, ListValue* tab_ids) { + CComPtr<ICeeeWindowExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get window tabs."; + return false; + } + CComBSTR tab_ids_string; + HRESULT hr = executor->GetTabs(&tab_ids_string); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't get tabs for window: " << std::hex << window << + ". " << com::LogHr(hr); + return false; + } + scoped_ptr<ListValue> tabs_list; + if (!api_module_util::GetListFromJsonString(CW2A(tab_ids_string).m_psz, + &tabs_list)) { + NOTREACHED() << "Invalid tabs list BSTR: " << tab_ids_string; + return false; + } + size_t num_values = tabs_list->GetSize(); + if (num_values % 2 != 0) { + // Values should come in pairs, one for the id and another one for the + // index. + NOTREACHED() << "Invalid tabs list BSTR: " << tab_ids_string; + return false; + } + for (size_t i = 0; i < num_values; i += 2) { + int tab_id = 0; + if (tabs_list->GetInteger(i, &tab_id)) + tab_ids->Append(Value::CreateIntegerValue(tab_id)); + } + return true; +} + +void CookieApiResult::FindAllProcessesAndWindows( + ProcessWindowMap* all_windows) { + DCHECK(all_windows); + WindowSet ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + if (ie_frame_windows.empty()) + return; + + WindowSet::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + DWORD process_id = 0; + // Skip over windows with no thread. + if (::GetWindowThreadProcessId(*iter, &process_id) == 0) + continue; + DCHECK(process_id); + + if (process_id != 0) + (*all_windows)[process_id].insert(*iter); + } +} + +HWND CookieApiResult::GetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + DWORD store_process_id = 0; + std::istringstream store_id_stream(store_id); + store_id_stream >> store_process_id; + if (!store_process_id) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidStoreIdError, store_id)); + return NULL; + } + + WindowSet ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + + WindowSet::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + DWORD process_id = 0; + if (::GetWindowThreadProcessId(*iter, &process_id) != 0 && + process_id == store_process_id) { + if (allow_unregistered_store) + return *iter; + HRESULT hr = CookieStoreIsRegistered(*iter); + // If the above call failed, an error has already been posted. + if (FAILED(hr)) { + return NULL; + } else if (hr == S_OK) { + return *iter; + } + } + } + // Matching window not found. + PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidStoreIdError, store_id)); + return NULL; +} + +void GetCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + + DictionaryValue* details; + if (!args.GetDictionary(0, &details)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + std::string url; + if (!details->GetString(keys::kUrlKey, &url)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + if (!GURL(url).is_valid()) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidUrlError, url)); + return; + } + // TODO(cindylau@chromium.org): Add extension host permissions + // checks against the URL. + + std::string name; + if (!details->GetString(keys::kNameKey, &name)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + HWND cookie_store_window = NULL; + if (details->HasKey(keys::kStoreIdKey)) { + std::string store_id; + if (!details->GetString(keys::kStoreIdKey, &store_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + cookie_store_window = result->GetWindowFromStoreId(store_id, false); + // If no window was found, an error has already been posted. + if (!cookie_store_window) + return; + } else { + // The store ID was unspecified or isn't a registered cookie store + // ID. Use the current execution context's cookie store by default. + // TODO(cindylau@chromium.org): We currently don't have a way to + // get the current execution context, so we are using the top IE + // window for now. + cookie_store_window = window_api::WindowApiResult::TopIeWindow(); + } + DCHECK(cookie_store_window); + + CookieInfo cookie_info; + HRESULT hr = result->GetCookieInfo(url, name, cookie_store_window, + &cookie_info); + // If the call failed, an error has already been posted. + if (FAILED(hr)) + return; + if (hr == S_OK) { + DCHECK(WideToASCII(com::ToString(cookie_info.name)) == name); + if (!result->CreateCookieValue(cookie_info)) + return; + } + result->PostResult(); +} + +void GetAllCookies::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void SetCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void RemoveCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void GetAllCookieStores::Execute(const ListValue& args, int request_id) { + scoped_ptr<IterativeCookieApiResult> result(CreateApiResult(request_id)); + + // TODO(cindylau@chromium.org): Restrict incognito (InPrivate) + // windows if incognito is not enabled for the extension. Right now + // CEEE has no mechanism to discover the extension's + // incognito-enabled setting, so adding support here is premature. + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + if (all_windows.empty()) { + result->FlushAllPosts(); + return; + } + + CookieApiResult::ProcessWindowMap::const_iterator iter = all_windows.begin(); + for (; iter != all_windows.end(); ++iter) { + bool created_ok = result->CreateCookieStoreValue(iter->first, iter->second); + LOG_IF(WARNING, !created_ok) << "Could not create cookie store value:" + << result->LastError(); + } + + if (result->IsEmpty()) // This is an error! + result->PostError(keys::kNoCookieStoreFoundError); + + result->FlushAllPosts(); +} + +// Static wrapper for the real event handler implementation. +bool CookieChanged::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + CookieChanged event_handler; + return event_handler.EventHandlerImpl(input_args, converted_args); +} + +// Handles a cookies.onChanged event by verifying that the store ID is +// registered. If not, this function registers the store ID. +bool CookieChanged::EventHandlerImpl(const std::string& input_args, + std::string* converted_args) { + DCHECK(converted_args); + // We don't need to modify the arguments, we only need to verify that the + // store ID is registered. + *converted_args = input_args; + + // Get the cookie info from the input arguments. + scoped_ptr<Value> input_val(base::JSONReader::Read(input_args, true)); + DCHECK(input_val.get() != NULL); + if (input_val == NULL) { + LOG(ERROR) << "Invalid Arguments sent to CookieChangedEventHandler()"; + return false; + } + + ListValue* input_list = static_cast<ListValue*>(input_val.get()); + DCHECK(input_list && input_list->GetSize() == 1); + + DictionaryValue* changeInfo = NULL; + bool success = input_list->GetDictionary(0, &changeInfo); + DictionaryValue* cookie = NULL; + if (success && changeInfo && changeInfo->HasKey(keys::kCookieKey)) + success = changeInfo->GetDictionary(keys::kCookieKey, &cookie); + if (!success || cookie == NULL) { + NOTREACHED() << "Failed to get the cookie value from the list of args."; + return false; + } + std::string store_id; + if (cookie->HasKey(keys::kStoreIdKey)) + success = cookie->GetString(keys::kStoreIdKey, &store_id); + if (!success || store_id.size() == 0) { + NOTREACHED() << "Failed to get the store ID value from the cookie arg."; + return false; + } + + scoped_ptr<CookieApiResult> api_result(CreateApiResult()); + HWND store_window = api_result->GetWindowFromStoreId(store_id, true); + if (store_window == NULL) { + // Error was already logged by GetWindowFromStoreId. + return false; + } + HRESULT is_registered = api_result->CookieStoreIsRegistered(store_window); + if (FAILED(is_registered)) { + // Error was already logged by CookieStoreIsRegistered. + return false; + } + if (is_registered == S_FALSE) { + // The store ID has not yet been queried by the user; register it here + // before exposing it to the user. + is_registered = api_result->RegisterCookieStore(store_window); + } + if (is_registered != S_OK) { + // Any errors should have already been logged by RegisterCookieStore. + return false; + } + + return true; +} + +CookieInfo::CookieInfo() { + name = NULL; + value = NULL; + domain = NULL; + host_only = false; + path = NULL; + secure = false; + http_only = false; + session = false; + expiration_date = 0; + store_id = NULL; +} + +CookieInfo::~CookieInfo() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(name); + ::SysFreeString(value); + ::SysFreeString(domain); + ::SysFreeString(path); + ::SysFreeString(store_id); +} + +} // namespace cookie_api diff --git a/ceee/ie/broker/cookie_api_module.h b/ceee/ie/broker/cookie_api_module.h new file mode 100644 index 0000000..ceace34 --- /dev/null +++ b/ceee/ie/broker/cookie_api_module.h @@ -0,0 +1,149 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation. + +#ifndef CEEE_IE_BROKER_COOKIE_API_MODULE_H_ +#define CEEE_IE_BROKER_COOKIE_API_MODULE_H_ + +#include <list> +#include <map> +#include <set> +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "toolband.h" //NOLINT + +namespace cookie_api { + +class CookieApiResult; +typedef ApiResultCreator<CookieApiResult> CookieApiResultCreator; + +// Registers all Cookie API Invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +// Helper class to handle the data cleanup. +class CookieInfo : public CeeeCookieInfo { + public: + CookieInfo(); + ~CookieInfo(); + private: + DISALLOW_COPY_AND_ASSIGN(CookieInfo); +}; + +class CookieApiResult : public ApiDispatcher::InvocationResult { + public: + typedef std::set<HWND> WindowSet; + typedef std::map<DWORD, WindowSet> ProcessWindowMap; + + explicit CookieApiResult(int request_id) + : ApiDispatcher::InvocationResult(request_id) { + } + + // Creates a value object with the information for a cookie and assigns the + // API result value with it. + // @return True on success. Returns false and also calls + // ApiDispatcher::InvocationResult::PostError on failure. + virtual bool CreateCookieValue(const CookieInfo& info); + + // Gets information for the cookie with the given URL and name, using the + // cookie store corresponding to the given window. + virtual HRESULT GetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info); + + // Constructs a cookie store value given the IE process ID and a set of all + // IEFrame windows for that process. As defined by the chrome.cookies + // API, a cookie store value consists of a string ID and a list of all tab + // IDs belonging to that cookie store. Assigns the API result value with the + // newly constructed cookie store object. + // @return True on success. Returns false and also calls + // ApiDispatcher::InvocationResult::PostError on failure. + virtual bool CreateCookieStoreValue(DWORD process_id, + const WindowSet& windows); + + // Finds an IEFrame window belonging to the process associated with the given + // cookie store ID. + // If allow_unregistered_store is true, the function succeeds even if the + // given store ID has not been registered. If it's false, an unregistered + // store ID will result in a failure and PostError. + // @return A window associated with the given cookie store, or NULL on error. + // Will also call ApiDispatcher::InvocationResult::PostError + // on failure to find a window. + virtual HWND GetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store); + + // Registers the cookie store for the process corresponding to the given + // window. + // @return S_OK on success, failure code on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual HRESULT RegisterCookieStore(HWND window); + + // Checks whether the given window's process is a registered cookie store. + // @return S_OK if the cookie store is registered, S_FALSE if not. Returns + // a failure code on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual HRESULT CookieStoreIsRegistered(HWND window); + + // Finds all IEFrame windows and puts them in a map of HWND sets keyed by + // process ID. + static void FindAllProcessesAndWindows(ProcessWindowMap* all_windows); + + private: + // Retrieves all tab IDs from the given window and adds them to the given tab + // ID list. + // @return false on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + bool AppendToTabIdList(HWND window, ListValue* tab_ids); +}; + +typedef IterativeApiResult<CookieApiResult> IterativeCookieApiResult; + +class GetCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class GetAllCookies : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class SetCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class RemoveCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class GetAllCookieStores : public ApiResultCreator<IterativeCookieApiResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Permanent event handler for cookies.onChanged. We define a class for the +// event in order to ease unit testing of the CookieApiResult used by the +// event handler implementation. +class CookieChanged { + public: + bool EventHandlerImpl(const std::string& input_args, + std::string* converted_args); + + // Static wrapper for the event handler implementation. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); + protected: + virtual ~CookieChanged() {} + + // Returns allocated memory; the caller is responsible for + // freeing it. + virtual CookieApiResult* CreateApiResult() { + return new CookieApiResult(CookieApiResult::kNoRequestId); + } +}; + +} // namespace cookie_api + +#endif // CEEE_IE_BROKER_COOKIE_API_MODULE_H_ diff --git a/ceee/ie/broker/cookie_api_module_unittest.cc b/ceee/ie/broker/cookie_api_module_unittest.cc new file mode 100644 index 0000000..00e28c8 --- /dev/null +++ b/ceee/ie/broker/cookie_api_module_unittest.cc @@ -0,0 +1,777 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/string_util.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace keys = extension_cookies_api_constants; + +namespace { + +using cookie_api::CookieApiResult; +using cookie_api::CookieChanged; +using cookie_api::CookieInfo; +using cookie_api::GetCookie; +using cookie_api::GetAllCookies; +using cookie_api::SetCookie; +using cookie_api::RemoveCookie; +using cookie_api::GetAllCookieStores; +using cookie_api::IterativeCookieApiResult; +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::Invoke; +using testing::InstanceCountMixin; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +using window_api::WindowApiResult; + +const int kRequestId = 21; +const int kThreadId = 1; +const int kNumWindows = 4; +const int kNumProcesses = 3; +const HWND kAllWindows[kNumWindows] = {(HWND)1, (HWND)17, (HWND)5, (HWND)8}; +const HWND kBadWindows[] = {(HWND)23, (HWND)2}; +const DWORD kWindowProcesses[kNumWindows] = {2, 3, 2, 7}; +const DWORD kAllProcesses[kNumProcesses] = {2, 3, 7}; +const HANDLE kProcessHandles[kNumProcesses] + = {HANDLE(0xFF0002), HANDLE(0xFF0003), HANDLE(0xFF0007)}; + +class MockCookieApiResult : public CookieApiResult { + public: + explicit MockCookieApiResult(int request_id) : CookieApiResult(request_id) {} + + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + MOCK_METHOD4(GetCookieInfo, HRESULT(const std::string&, const std::string&, + HWND, CookieInfo*)); + MOCK_METHOD2(CreateCookieStoreValue, bool(DWORD, const WindowSet&)); + MOCK_METHOD1(RegisterCookieStore, HRESULT(HWND)); + MOCK_METHOD1(CookieStoreIsRegistered, HRESULT(HWND)); + MOCK_METHOD2(GetWindowFromStoreId, HWND(const std::string&, bool)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + HRESULT CallGetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info) { + return CookieApiResult::GetCookieInfo(url, name, window, cookie_info); + } + + bool CallCreateCookieStoreValue(DWORD process_id, const WindowSet& windows) { + return CookieApiResult::CreateCookieStoreValue(process_id, windows); + } + + HWND CallGetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + return CookieApiResult::GetWindowFromStoreId( + store_id, allow_unregistered_store); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +// Mocks the iterative version of result. Unlike in the mock of the 'straight' +// version, here PostResult and Post Error are not mocked because they serve +// to accumulate result, which can be examined by standard means. +class MockIterativeCookieApiResult : public IterativeCookieApiResult { + public: + explicit MockIterativeCookieApiResult(int request_id) + : IterativeCookieApiResult(request_id) {} + + MOCK_METHOD0(CallRealPostResult, void()); + MOCK_METHOD1(CallRealPostError, void(const std::string&)); + MOCK_METHOD4(GetCookieInfo, HRESULT(const std::string&, const std::string&, + HWND, CookieInfo*)); + MOCK_METHOD2(CreateCookieStoreValue, bool(DWORD, const WindowSet&)); + MOCK_METHOD1(RegisterCookieStore, HRESULT(HWND)); + MOCK_METHOD1(CookieStoreIsRegistered, HRESULT(HWND)); + MOCK_METHOD2(GetWindowFromStoreId, HWND(const std::string&, bool)); + + virtual void PostError(const std::string& error) { + ++error_counter_; + IterativeCookieApiResult::PostError(error); + } + + virtual void PostResult() { + ++success_counter_; + IterativeCookieApiResult::PostResult(); + } + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + HRESULT CallGetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info) { + return CookieApiResult::GetCookieInfo(url, name, window, cookie_info); + } + + bool CallCreateCookieStoreValue(DWORD process_id, const WindowSet& windows) { + return CookieApiResult::CreateCookieStoreValue(process_id, windows); + } + + static HRESULT MockGetTabs(BSTR* tab_list) { + // The string is not retained since the calling object takes overship. + *tab_list = SysAllocString(L"[27, 1]"); + return S_OK; + } + + HWND CallGetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + return CookieApiResult::GetWindowFromStoreId( + store_id, allow_unregistered_store); + } + + static void ResetCounters() { + success_counter_ = 0; + error_counter_ = 0; + } + + static int success_counter() { + return success_counter_; + } + + static int error_counter() { + return error_counter_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + static int success_counter_; + static int error_counter_; +}; + +int MockIterativeCookieApiResult::success_counter_ = 0; +int MockIterativeCookieApiResult::error_counter_ = 0; + +class MockCookieChanged : public CookieChanged { + public: + void AllocateApiResult() { + api_result_.reset(new MockCookieApiResult(CookieApiResult::kNoRequestId)); + } + + virtual CookieApiResult* CreateApiResult() { + DCHECK(api_result_.get() != NULL); + return api_result_.release(); + } + + scoped_ptr<MockCookieApiResult> api_result_; +}; + +// Mock static functions defined in CookieApiResult. +MOCK_STATIC_CLASS_BEGIN(MockCookieApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockCookieApiResultStatics) + MOCK_STATIC_INIT2(CookieApiResult::FindAllProcessesAndWindows, + FindAllProcessesAndWindows); + MOCK_STATIC_INIT_END() + MOCK_STATIC1(void, , FindAllProcessesAndWindows, + CookieApiResult::ProcessWindowMap*); +MOCK_STATIC_CLASS_END(MockCookieApiResultStatics) + +// Mock static functions defined in WindowApiResult. +MOCK_STATIC_CLASS_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT2(WindowApiResult::TopIeWindow, + TopIeWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(HWND, , TopIeWindow); +MOCK_STATIC_CLASS_END(MockWindowApiResultStatics) + +class CookieApiTests: public testing::Test { + protected: + template <class T> void ExpectInvalidArguments( + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + T> >& invocation, + const ListValue& args) { + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq( + api_module_constants::kInvalidArgumentsError))).Times(1); + invocation.Execute(args, kRequestId); + } + + StrictMock<testing::MockUser32> user32_; +}; + +TEST_F(CookieApiTests, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, + RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(5)); + cookie_api::RegisterInvocations(&disp); +} + +TEST_F(CookieApiTests, CreateCookieValue) { + CookieInfo cookie_info; + cookie_info.name = ::SysAllocString(L"foo"); + cookie_info.value = ::SysAllocString(L"bar"); + cookie_info.domain = ::SysAllocString(L"google.com"); + cookie_info.host_only = FALSE; + cookie_info.path = ::SysAllocString(L"/testpath"); + cookie_info.secure = TRUE; + cookie_info.session = TRUE; + cookie_info.expiration_date = 0; + cookie_info.store_id = ::SysAllocString(L"test_store_id"); + + CookieApiResult result(CookieApiResult::kNoRequestId); + EXPECT_TRUE(result.CreateCookieValue(cookie_info)); + EXPECT_EQ(Value::TYPE_DICTIONARY, result.value()->GetType()); + const DictionaryValue* cookie = + reinterpret_cast<const DictionaryValue*>(result.value()); + + std::string string_value; + bool boolean_value; + EXPECT_TRUE(cookie->GetString(keys::kNameKey, &string_value)); + EXPECT_EQ("foo", string_value); + EXPECT_TRUE(cookie->GetString(keys::kValueKey, &string_value)); + EXPECT_EQ("bar", string_value); + EXPECT_TRUE(cookie->GetString(keys::kDomainKey, &string_value)); + EXPECT_EQ("google.com", string_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kHostOnlyKey, &boolean_value)); + EXPECT_FALSE(boolean_value); + EXPECT_TRUE(cookie->GetString(keys::kPathKey, &string_value)); + EXPECT_EQ("/testpath", string_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kSecureKey, &boolean_value)); + EXPECT_TRUE(boolean_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kHttpOnlyKey, &boolean_value)); + EXPECT_FALSE(boolean_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kSessionKey, &boolean_value)); + EXPECT_TRUE(boolean_value); + EXPECT_TRUE(cookie->GetString(keys::kStoreIdKey, &string_value)); + EXPECT_EQ("test_store_id", string_value); + EXPECT_FALSE(cookie->HasKey(keys::kExpirationDateKey)); +} + +TEST_F(CookieApiTests, GetCookieInfo) { + testing::LogDisabler no_dchecks; + MockCookieApiResult result(CookieApiResult::kNoRequestId); + HWND test_frame_window = reinterpret_cast<HWND>(1); + HWND test_tab_window = reinterpret_cast<HWND>(2); + StrictMock<testing::MockWindowUtils> window_utils; + + // Test no tab windows found. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)) + .WillOnce(Return(false)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Test invalid tab window. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillOnce( + DoAll(SetArgumentPointee<3>(HWND(NULL)), Return(true))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillRepeatedly( + DoAll(SetArgumentPointee<3>(test_tab_window), Return(true))); + + // Test failed executor access. + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(test_tab_window, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Test executor. + testing::MockCookieExecutor* mock_cookie_executor; + CComPtr<ICeeeCookieExecutor> mock_cookie_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockCookieExecutor::CreateInitialized( + &mock_cookie_executor, &mock_cookie_executor_keeper)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(test_tab_window, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_cookie_executor_keeper.p), + AddRef(mock_cookie_executor_keeper.p))); + // Failing Executor. + // The executor classes are already strict from their base class impl. + EXPECT_CALL(*mock_cookie_executor, GetCookie(_, _, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Success. + EXPECT_CALL(*mock_cookie_executor, GetCookie(_, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(result, PostError(_)).Times(0); + EXPECT_EQ(S_OK, result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); +} + +TEST_F(CookieApiTests, GetWindowFromStoreId) { + MockCookieApiResult result(CookieApiResult::kNoRequestId); + + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, + result.CallGetWindowFromStoreId("test_store_id", false)); + + std::set<HWND> empty_window_set; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(empty_window_set)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + std::set<HWND> test_windows(kAllWindows, kAllWindows + kNumWindows); + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillRepeatedly(SetArgumentPointee<1>(test_windows)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(DoAll(SetArgumentPointee<1>(1), + Return(kThreadId))); + // Test unregistered cookie store. + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillRepeatedly(Return(S_FALSE)); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", true)); + + // Test registered cookie store. + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", false)); + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", true)); +} + +TEST_F(CookieApiTests, CreateCookieStoreValue) { + testing::LogDisabler no_dchecks; + MockCookieApiResult result(CookieApiResult::kNoRequestId); + CookieApiResult::WindowSet windows; + + // Test empty window set. + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test failed cookie store registration. + windows.insert(kAllWindows[0]); + EXPECT_CALL(result, RegisterCookieStore(kAllWindows[0])). + WillOnce(Return(E_FAIL)); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test failed executor access. + EXPECT_CALL(result, RegisterCookieStore(kAllWindows[0])). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test executor. + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + // Failing Executor. + // The executor classes are already strict from their base class impl. + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test success. + BSTR tab_ids = SysAllocString(L"[27, 1]"); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_ids), Return(S_OK))); + EXPECT_CALL(result, PostResult()).Times(1); + EXPECT_TRUE(result.CallCreateCookieStoreValue(2, windows)); + + ASSERT_TRUE(result.value() != NULL && + result.value()->IsType(Value::TYPE_DICTIONARY)); + const DictionaryValue* cookie_store = + static_cast<const DictionaryValue*>(result.value()); + std::string id; + EXPECT_TRUE(cookie_store->GetString(keys::kIdKey, &id)); + EXPECT_EQ(std::string("2"), id); + ListValue* tab_list; + EXPECT_TRUE(cookie_store->GetList(keys::kTabIdsKey, &tab_list)); + EXPECT_EQ(1, tab_list->GetSize()); + int tab; + EXPECT_TRUE(tab_list->GetInteger(0, &tab)); + EXPECT_EQ(27, tab); + // The cookie_store takes ownership of this pointer, so there's no need to + // free it. + tab_ids = NULL; +} + +TEST_F(CookieApiTests, FindAllProcessesAndWindowsNoWindows) { + std::set<HWND> empty_window_set; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(empty_window_set)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).Times(0); + + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + EXPECT_EQ(0, all_windows.size()); +} + +TEST_F(CookieApiTests, FindAllProcessesAndWindowsMultipleProcesses) { + testing::LogDisabler no_dchecks; + + std::set<HWND> test_windows(kAllWindows, kAllWindows + kNumWindows); + test_windows.insert(kBadWindows[0]); + test_windows.insert(kBadWindows[1]); + StrictMock<testing::MockWindowUtils> window_utils; + + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(test_windows)); + for (int i = 0; i < kNumWindows; ++i) { + EXPECT_CALL(user32_, GetWindowThreadProcessId(kAllWindows[i], _)). + WillOnce(DoAll(SetArgumentPointee<1>(kWindowProcesses[i]), + Return(kThreadId))); + } + // Test that threads and processes with ID 0 don't get added. + EXPECT_CALL(user32_, GetWindowThreadProcessId(kBadWindows[0], _)). + WillOnce(Return(0)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(kBadWindows[1], _)). + WillOnce(DoAll(SetArgumentPointee<1>(0), Return(kThreadId))); + + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + EXPECT_EQ(kNumProcesses, all_windows.size()); + + CookieApiResult::WindowSet& window_set = all_windows[kAllProcesses[0]]; + EXPECT_EQ(2, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[0]) != window_set.end()); + EXPECT_TRUE(window_set.find(kAllWindows[2]) != window_set.end()); + + window_set = all_windows[kAllProcesses[1]]; + EXPECT_EQ(1, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[1]) != window_set.end()); + + window_set = all_windows[kAllProcesses[2]]; + EXPECT_EQ(1, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[3]) != window_set.end()); +} + +TEST_F(CookieApiTests, GetCookiesInvalidArgumentsResultInErrors) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetCookie> > invocation; + // First test that required arguments are enforced. + ExpectInvalidArguments(invocation, args); + DictionaryValue* details = new DictionaryValue(); + args.Append(details); + ExpectInvalidArguments(invocation, args); + // TODO(cindylau@chromium.org): Check for invalid URL error. + details->SetString(keys::kUrlKey, "helloworld"); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidUrlError, "helloworld")))). + Times(1); + invocation.Execute(args, kRequestId); + details->SetString(keys::kUrlKey, "http://www.foobar.com"); + ExpectInvalidArguments(invocation, args); + details->SetString(keys::kNameKey, "foo"); + details->SetInteger(keys::kStoreIdKey, 1); + ExpectInvalidArguments(invocation, args); + + // GetWindowFromStoreId fails. + std::string store_id("test_cookie_store"); + details->SetString(keys::kStoreIdKey, store_id); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetWindowFromStoreId(StrEq(store_id), false)). + WillOnce(Return((HWND)NULL)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(0); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, GetCookiesTopIeWindowIsDefaultCookieStore) { + // TODO(cindylau@chromium.org): The expected behavior here will + // change; we should use the current execution context's cookie + // store, not the top window. + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetCookie> > invocation; + + ListValue args; + DictionaryValue* details = new DictionaryValue(); + args.Append(details); + details->SetString(keys::kUrlKey, "http://www.foobar.com"); + details->SetString(keys::kNameKey, "foo"); + + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics window_statics; + EXPECT_CALL(window_statics, TopIeWindow()).WillOnce(Return(HWND(42))); + EXPECT_CALL(*invocation.invocation_result_, GetCookieInfo(_, _, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, GetAllCookieStores) { + testing::LogDisabler no_dchecks; + + MockCookieApiResultStatics result_statics; + CookieApiResult::ProcessWindowMap all_windows; + ListValue args; + + StrictMock<MockApiInvocation<IterativeCookieApiResult, + MockIterativeCookieApiResult, + GetAllCookieStores> > invocation; + + // First test the trivial case of no cookie stores. One call to success and + // no calls to error function. + EXPECT_CALL(result_statics, FindAllProcessesAndWindows(_)). + WillOnce(SetArgumentPointee<0>(all_windows)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + + // Now test cases with multiple windows. + for (int i = 0; i < kNumWindows; ++i) { + all_windows[kWindowProcesses[i]].insert(kAllWindows[i]); + } + EXPECT_CALL(result_statics, FindAllProcessesAndWindows(_)). + WillRepeatedly(SetArgumentPointee<0>(all_windows)); + + // Test error case: can't access a single cookie store. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + CreateCookieStoreValue(_, _)). + WillRepeatedly(Return(false)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(0); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(args, kRequestId); + + // Test error case: accessing dispatcher fails each time and so an error is + // reported. Count errors and make sure everything is reported as error. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(0); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), 0); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), all_windows.size()); + + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper)); + + // Now test the case of multiple open windows/processes. Each invocation is + // successful and a result is produced. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillRepeatedly(Invoke(MockIterativeCookieApiResult::MockGetTabs)); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), + all_windows.size()); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), 0); + + // Now test the case of multiple open windows/processes. One invocation + // fails, but everything else will be OK. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillRepeatedly(Invoke(MockIterativeCookieApiResult::MockGetTabs)); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), + all_windows.size() - 1); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), 1); +} + +TEST_F(CookieApiTests, GetAllCookiesNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetAllCookies> > invocation; + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, SetCookieNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + SetCookie> > invocation; + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, RemoveCookieNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + RemoveCookie> > invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, CookieChangedEventHandler) { + testing::LogDisabler no_dchecks; + MockCookieChanged cookie_changed; + std::string converted_args; + // Empty args. + std::string input_args = ""; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + // Invalid args. + input_args = "[false, {hello]"; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + input_args = "[3]"; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + // Valid args. + input_args = "[{\"removed\": false, \"cookie\": {\"storeId\": \"1\"}}]"; + + // Invalid store ID. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(NULL))); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Cookie store access errors. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(E_FAIL)); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_FALSE)); + EXPECT_CALL(*cookie_changed.api_result_, + RegisterCookieStore(HWND(5))).WillOnce(Return(E_FAIL)); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Registered cookie store. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_OK)); + EXPECT_EQ(true, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Unregistered cookie store. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_FALSE)); + EXPECT_CALL(*cookie_changed.api_result_, + RegisterCookieStore(HWND(5))).WillOnce(Return(S_OK)); + EXPECT_EQ(true, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); +} + +} // namespace diff --git a/ceee/ie/broker/event_dispatching_docs.h b/ceee/ie/broker/event_dispatching_docs.h new file mode 100644 index 0000000..254821b --- /dev/null +++ b/ceee/ie/broker/event_dispatching_docs.h @@ -0,0 +1,262 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ + +/** @page EventDispatchingDoc Detailed documentation of CEEE Event Dispatching. + +@section EventDispatchingIntro Introduction + +<a href="http://code.google.com/chrome/extensions/index.html">Chrome extensions +</a> can register to be notified of specific events like +<a href="http://code.google.com/chrome/extensions/windows.html#event-onCreated"> +window creation</a> or +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onUpdated"> +tab update</a>. + +So CEEE needs to let Chrome know when these events occure in IE. To do so, we +unfortunately need to be hooked in a few different places. So in the same way +that the Chrome Extensions API implementation was described in @ref +ApiDispatcherDoc, this page describes all the notifications one by one and how +we implement their broadcasting. + +Note that we use the EventFunnel class to properly package the arguments to be +sent with the events, and then send them to the Broker via the +ICeeeBroker::FireEvent method. + +@section WindowsEvents Windows Events + +There are three +<a href="http://code.google.com/chrome/extensions/windows.html#events">Windows +events</a> that can be sent to Chrome extensions, +<a href="http://code.google.com/chrome/extensions/windows.html#event-onCreated"> +onCreated</a>, <a href= +"http://code.google.com/chrome/extensions/windows.html#event-onFocusChanged"> +onFocusChanged</a>, and +<a href="http://code.google.com/chrome/extensions/windows.html#event-onRemoved"> +onRemoved</a>. + +These notifications are for top level windows only, so we can get notifications +from the OS by using the WH_SHELL hook (as described on +<a href="http://msdn.microsoft.com/en-us/library/ms644991(VS.85).aspx"> +MSDN</a>). So we need to implement a hook function in the ceee_ie.dll so that +it can be injected in the system process and be called for the following +events: +<table> +<tr><td>HSHELL_WINDOWACTIVATED</td><td>Handle to the activated window.</td></tr> +<tr><td>HSHELL_WINDOWCREATED</td><td>Handle to the created window.</td></tr> +<tr><td>HSHELL_WINDOWDESTROYED</td><td>Handle to the destroyed window.</td></tr> +</table> + +Then we must redirect those via Chrome Frame (more details in the @ref +ChromeFrameDispatching section). In the case of Windows Events, we must relay +the notifications to the Broker process via the ICeeeBrokerNotification +(TBD) interface since they get handled by an injected ceee_ie.dll. + +@subsection onCreated + +The onCreated event needs to send (as arguments to the notification message) +the same information about the Window as the one returned by the windows.create +API (and as described in the @ref ApiDispatcherDoc and on the <a href= +"http://code.google.com/chrome/extensions/windows.html#type-Window"> Chrome +Extensions documentation</a>). + +@subsection onFocusChanged + +All that is needed here is to send the window identifier of the newly focused +window to the listener. + +@subsection onRemoved + +All that is needed here is to send the window identifier of the removed window +to the listener. + +@section TabsEvents Tabs Events + +There are seven +<a href="http://code.google.com/chrome/extensions/tabs.html#events">Tabs +events</a> that can be sent to Chrome extensions, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onAttached"> +onAttached</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onCreated"> +onCreated</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onDetached"> +onDetached</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onMoved"> +onMoved</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onRemoved"> +onRemoved</a>, <a href= +"http://code.google.com/chrome/extensions/tabs.html#event-onSelectionChanged" +>onSelectionChanged</a>, and +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onUpdated"> +onUpdated</a>. + +Since IE can't move tabs between windows (yet), CEEE won't fire the +attached/detached notifications. The tab created event can be fired when an +instance of the CEEE BHO is created and associated to a tab. The move, remove +and selected tab events will most likely need to be caught from IE's +TabWindowManager somehow (TBD). And finally, the update tab event can be +fired when the instance of the BHO attached to the given tab receives a +notification that the tab content was updated (via a series of notification from +the <a href="http://msdn.microsoft.com/en-us/library/aa752085(VS.85).aspx"> +WebBrowser</a> or the <a href= +"http://msdn.microsoft.com/en-us/library/aa752574(VS.85).aspx">document</a>). + +@note As a side note, here's an email from Siggi about tricks for this: + +@note <em>2009/10/16 Sigurour Asgeirsson <siggi@google.com></em> + +@note I think we should be able to derive all the window events from tab +activity, if we e.g. have the BHO register each tab with its HWND/ID, then we +can derive the association to parent window from there, which means we have +created/destroyed on first registration and last unregistration for a frame +window. + +@note I'm pretty sure we can derive the focus event from events fired on the +top-level browser associated with a BHO as well. + +@subsection onAttached + +Not implemented on IE. + +@subsection onDetached + +Not implemented on IE. + +@subsection onMoved + +TBD: We need to find a way to get the tab move notification from the +TabWindowManager... somehow... (e.g., patch the move method, but we would +rather find a notification coming from there instead). + +We could also see if we can get win32 notifications of focus or window +visibility or things of the like. + +Once we do, the arguments that must be sent with this notification are the +identifier of the tab, and a dictionary of moveInfo that contains the +"windowId", the "fromIndex" and the "toIndex" fields, which are all integers. + +@subsection onRemoved + +TBD: We need to find a way to get the tab removed notification from the +TabWindowManager... somehow... + +We could also use the destruction of the BHO as a hint that the tab is being +removed. + +Once we do, the argument that must be sent with this notification is just the +identifier of the tab. + +@subsection onSelectionChanged + +TBD: We need to find a way to get the tab selection changed notification from +the TabWindowManager... somehow... + +We could also see if we can get win32 notifications of focus or window +visibility or things of the like. + +Once we do, the arguments that must be sent with this notification are the +identifier of the tab that just got selected, and a dictionary of selectInfo +that only contains the "windowId" field. + +@subsection onCreated + +When a new BHO instance is created and attached to a tab, it can send this +notification directly to the Chrome Frame it is attached to. We will most likely +change this direct call to Chrome Frame once we implement the asynchronous +callback to Chrome Extension for the tabs.create API implementation for IE. At +that point, we should use the same ICeeeBrokerNotification (TBD) interface +as we plan on using for the @link WindowsEvents Windows Events @endlink. + +The onCreated event needs to send to the extensions registered as listeners, +the same information about the Tab as the one returned by the tabs.create +API (and as described in the @ref ApiDispatcherDoc and on the <a href= +"http://code.google.com/chrome/extensions/tabs.html#type-Tab"> Chrome +Extensions documentation</a>). + +@subsection onUpdated + +When a new BHO instance receives a <a href= +"http://msdn.microsoft.com/en-us/library/aa768334(VS.85).aspx">NavigateComplete2 +</a> notification from the WebBrowser, it must fire the onUpdated notification +for the tab it is attached to. It must also fire it for the ready state change +notifications it would get from the document. There's an edge case for when +IE refreshes the page, which can be caught when the frame ready state drops from +complete to loading, so we must also handle this case to send an onUpdated +notification for the tab. + +It gets trickier when the page has embedded frames. These independent frames +will all have their own ready state, and changing the ready state (by +refreshing) a frame in the middle of the hierarchy, will change its own ready +state from complete, to loading, and then to interactive, and then the child +frames will go from uninitialized, to loading and then to interactive. The +innermost child will also go all the way to complete, and then its immediate +parent will get from interactive to complete, and then the next ancestor, all +the way to where the refresh started, but not further (so the outermost frame's +ready state will not change unless it is the one that triggered the refresh). + +From the Chrome Extension's point of view, there should be only two events sent, +the initial one that goes from complete to loading, and the final one to go +from loading to complete. Since this doesn't necessary happen on the topmost +frame, we must listen to all the frames ready state changes, but we must be +able to compute the state of the whole page, so that we only send one +notification that would go from complete to loading at the beginning of the +refresh, and then only one that will go from loading to complete, once the +page is completely refreshed. + +To do so, we need to propagate the state changes upward in the hierarchy and +parse the whole tree to confirm the page state, which is loading unless all +the frames are complete, in which case it will be complete. To minimize the +number of times we need to parse the hierarchy, only the top frame needs to +do the parsing. Also, frames don't need to propagate up their children's state +unless they are, themselves (the parent) in the complete state. If they are not, +then their own parent (or themselves if its the root) know that the overall +state isn't complete. + +When we fire the onUpdated notification we must provide the identifier of the +tab and dictionary that contains the status field, which can be either "loading" +or "complete", as well as an optional URL field that the tab is set to show +(it is only provided if it changed). + +@section BookmarksEvents Bookmarks Events + +TBD + +http://code.google.com/chrome/extensions/bookmarks.html#events + +@section ExtensionsEvents Extensions Events + +This is already available... more details TBD... + +http://code.google.com/chrome/extensions/extension.html#events + +@section ChromeFrameDispatching Dispatching to Chrome via the Chrome Frame + +To simplify our access to (and reception of notifications from) Chrome Frame, +we have a ChromeFrameHost class that implements all we need. It takes care of +notification registration, and have virtual methods for all of them, and it +also has a PostMessage method to allow calls to be dispatched to Chrome via +the automation layer and Chrome Frame. + +Events are such notifications that need to be sent, so the places where we +intercept IE notifications can use a ChromeFrameHost object, package the +event information in a JSON encoded string and pass it to the +ChromeFrameHost::PostMessage method. The JSON data must be a list with two +entries, the first one is the event name (e.g., "windows.onRemoved" or +"tabs.onUpdated") and the other one is the arguments for this event (e.g., +a single int representing the id of the window to remove or for the +tabs.onUpdated event, a list of two values, the first one being an int for the +tab identifier, and the other one is a dictionary with a mandatory field for the +"status", which value can be "loading" or "complete", and an optional one for +the "url" which is a string reprensenting the URL to which the tab is being +navigated to). + +The target of the message must be kAutomationBrowserEventRequestTarget (which is +"__priv_evtreq") and the origin, must, of course, be kAutomationOrigin (which is +"__priv_xtapi"). + +**/ + +#endif // CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ diff --git a/ceee/ie/broker/executors_manager.cc b/ceee/ie/broker/executors_manager.cc new file mode 100644 index 0000000..5d87467 --- /dev/null +++ b/ceee/ie/broker/executors_manager.cc @@ -0,0 +1,453 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ExecutorsManager implementation. + +#include "ceee/ie/broker/executors_manager.h" + +#include "base/logging.h" +#include "ceee/ie/broker/broker_module_util.h" +#include "ceee/common/com_utils.h" + +namespace { + +// The timeout we set before accepting a failure when we wait for events. +const DWORD kTimeOut = 20000; + +// Utility class to ensure the setting of an event before we exit a method. +class AutoSetEvent { + public: + explicit AutoSetEvent(HANDLE event_handle) : event_handle_(event_handle) {} + ~AutoSetEvent() { ::SetEvent(event_handle_); } + private: + HANDLE event_handle_; +}; + +// A cached handle to our current process which we use when we call +// DuplicateHandle. GetCurrentProcess returns a pseudo HANDLE that doesn't +// need to be closed. +const HANDLE kProcessHandle = ::GetCurrentProcess(); + +} // namespace + + +const size_t ExecutorsManager::kTerminationHandleIndexOffset = 0; +const size_t ExecutorsManager::kUpdateHandleIndexOffset = 1; +const size_t ExecutorsManager::kLastHandleIndexOffset = + kUpdateHandleIndexOffset; +const size_t ExecutorsManager::kExtraHandles = kLastHandleIndexOffset + 1; + +ExecutorsManager::ExecutorsManager(bool no_thread) + : update_threads_list_gate_(::CreateEvent(NULL, FALSE, FALSE, NULL)), + // Termination is manual reset. When we're terminated... We're terminated! + termination_gate_(::CreateEvent(NULL, TRUE, FALSE, NULL)) { + DCHECK(update_threads_list_gate_ != NULL); + DCHECK(termination_gate_ != NULL); + + if (!no_thread) { + ThreadStartData thread_start_data; + thread_start_data.me = this; + // Again, manual reset, because when we are started... WE ARE STARTED!!! :-) + thread_start_data.thread_started_gate.Attach(::CreateEvent(NULL, TRUE, + FALSE, NULL)); + DCHECK(thread_start_data.thread_started_gate != NULL); + + // Since we provide the this pointer to the thread, we must make sure to + // keep ALL INITIALIZATION CODE ABOVE THIS LINE!!! + thread_.Attach(::CreateThread(NULL, 0, ThreadProc, &thread_start_data, + 0, 0)); + DCHECK(thread_ != NULL); + + // Make sure the thread is ready before continuing + DWORD result = WaitForSingleObject(thread_start_data.thread_started_gate, + kTimeOut); + DCHECK(result == WAIT_OBJECT_0); + } +} + +HRESULT ExecutorsManager::RegisterTabExecutor(ThreadId thread_id, + IUnknown* executor) { + // We will need to know outside of the lock if the map was empty or not. + // This way we can add a ref to the module for the existence of the map. + bool map_was_empty = false; + { + AutoLock lock(lock_); + map_was_empty = executors_.empty(); + if (!map_was_empty && executors_.find(thread_id) != executors_.end()) { + return S_OK; + } + CHandle thread_handle(::OpenThread(SYNCHRONIZE, FALSE, thread_id)); + if (thread_handle == NULL) { + DCHECK(false) << "Can't Open thread: " << thread_id; + return E_UNEXPECTED; + } + ExecutorInfo& new_executor_info = executors_[thread_id]; + new_executor_info.executor = executor; + new_executor_info.thread_handle = thread_handle; + } // End of lock. + + if (map_was_empty) { + // We go from empty to not empty, + // so lock the module to make sure we stay alive. + ceee_module_util::LockModule(); + } + return S_OK; +} + +HRESULT ExecutorsManager::RegisterWindowExecutor(ThreadId thread_id, + IUnknown* executor) { + // We need to fetch the event handle associated to this thread ID from + // our map in a thread safe way... + CHandle executor_registration_gate; + { + AutoLock lock(lock_); + if (executors_.find(thread_id) != executors_.end()) { + DCHECK(false) << "Unexpected registered thread_id: " << thread_id; + return E_UNEXPECTED; + } + + Tid2Event::iterator iter = pending_registrations_.find(thread_id); + if (iter == pending_registrations_.end() || executor == NULL) { + DCHECK(false) << "Invalid thread_id: " << thread_id << + ", or NULL executor."; + return E_INVALIDARG; + } + // Make sure we use a duplicate handle so that we don't get caught setting + // a dead handle when we exit, in case the other thread wakes up because + // of a (unlikely) double registration. + BOOL success = ::DuplicateHandle( + kProcessHandle, iter->second.m_h, kProcessHandle, + &executor_registration_gate.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } // End of lock. + + // We must make sure to wake up the thread(s) that might be waiting on us. + // But only when we are done. + AutoSetEvent auto_set_event(executor_registration_gate); + + // Try to get a handle to this thread right away so that we can do the rest + // atomically. We need it to wake us up when it dies. + CHandle thread_handle(::OpenThread(SYNCHRONIZE, FALSE, thread_id)); + if (thread_handle == NULL) { + DCHECK(false) << "Can't Open thread: " << thread_id; + return S_FALSE; + } + + // We will need to know outside of the lock if the map was empty or not. + // This way we can add a ref to the module for the existence of the map. + bool map_was_empty = false; + { + AutoLock lock(lock_); + map_was_empty = executors_.empty(); + // We should not get here if we already have an executor for that thread. + DCHECK(executors_.find(thread_id) == executors_.end()); + ExecutorInfo& new_executor_info = executors_[thread_id]; + new_executor_info.executor = executor; + new_executor_info.thread_handle = thread_handle; + } // End of lock. + + if (map_was_empty) { + // We go from empty to not empty, + // so lock the module to make sure we stay alive. + ceee_module_util::LockModule(); + } + + // Update the list of handles that our thread is waiting on. + BOOL success = ::SetEvent(update_threads_list_gate_); + DCHECK(success); + return S_OK; +} + +HRESULT ExecutorsManager::GetExecutor(ThreadId thread_id, HWND window, + REFIID riid, void** executor) { + DCHECK(executor != NULL); + // We may need to wait for either a currently pending + // or own newly created registration of a new executor. + CHandle executor_registration_gate; + + // We need to remember if we must create a new one or not. + // But we must create the executor creator outside of the lock. + bool create_executor = false; + { + AutoLock lock(lock_); + ExecutorsMap::iterator exec_iter = executors_.find(thread_id); + if (exec_iter != executors_.end()) { + // Found it... We're done... That was quick!!! :-) + DCHECK(exec_iter->second.executor != NULL); + return exec_iter->second.executor->QueryInterface(riid, executor); + } + + // Check if we need to wait for a pending registration. + Tid2Event::iterator event_iter = pending_registrations_.find(thread_id); + if (event_iter == pending_registrations_.end()) { + // No pending registration, so we will need to create a new executor. + create_executor = true; + + // Use the thread id as a cookie to only allow known threads to register. + // Also use it to map to a new event we will use to signal the end of this + // registration. We use a manual reset event so that more than one thread + // can wait for it, and once we're done... we're done... period! :-) + executor_registration_gate.Attach(::CreateEvent(NULL, TRUE, FALSE, NULL)); + DCHECK(executor_registration_gate != NULL); + CHandle& new_registration_handle = pending_registrations_[thread_id]; + // Make sure we use a duplicate handle so that we don't get caught waiting + // on a dead handle later, in case other threads wake up before we do and + // close the handle before we wake up. + BOOL success = ::DuplicateHandle( + kProcessHandle, executor_registration_gate, kProcessHandle, + &new_registration_handle.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } else { + // Same comment as above... + BOOL success = ::DuplicateHandle( + kProcessHandle, event_iter->second.m_h, kProcessHandle, + &executor_registration_gate.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } + } // End of lock. + + CComPtr<ICeeeExecutorCreator> executor_creator; + if (create_executor) { + // We need to create an executor creator so that the code setting up + // a Windows Hook in the other process, runs from a DLL that can be + // injected in that other process... WE are running in an executable. + HRESULT hr = GetExecutorCreator(&executor_creator); + DCHECK(SUCCEEDED(hr) && executor_creator != NULL) << + "CoCreating Executor Creator. " << com::LogHr(hr); + hr = executor_creator->CreateWindowExecutor(thread_id, + reinterpret_cast<CeeeWindowHandle>(window)); + if (FAILED(hr)) { + // This could happen if the thread we want to hook to died prematurely. + AutoLock lock(lock_); + pending_registrations_.erase(thread_id); + return hr; + } + } + + // Wait for the registration to complete. + DWORD result = WaitForSingleObject(executor_registration_gate, kTimeOut); + LOG_IF(INFO, result != WAIT_OBJECT_0) << "Registration problem? " << + "Wait Result: " << com::LogWe(result); + + // Let the executor creator know that we got the registration + // and it can tear down what was needed to trigger it. + if (executor_creator != NULL) { + HRESULT hr = executor_creator->Teardown(thread_id); + DCHECK(SUCCEEDED(hr)) << "Tearing down executor creator" << com::LogHr(hr); + } + + // Do our own cleanup and return a reference thread safely... + AutoLock lock(lock_); + pending_registrations_.erase(thread_id); + ExecutorsMap::iterator iter = executors_.find(thread_id); + if (iter == executors_.end()) { + DCHECK(false) << "New executor registration failed."; + return E_UNEXPECTED; + } + + DCHECK(iter->second.executor != NULL); + return iter->second.executor->QueryInterface(riid, executor); +} + +HRESULT ExecutorsManager::RemoveExecutor(ThreadId thread_id) { + // Make sure to Release the executor outside the lock. + CComPtr<IUnknown> dead_executor; + bool map_is_empty = false; + { + AutoLock lock(lock_); + ExecutorsMap::iterator iter = executors_.find(thread_id); + if (iter == executors_.end()) { + return S_FALSE; + } + + dead_executor.Attach(iter->second.executor.Detach()); + executors_.erase(iter); + map_is_empty = executors_.empty(); + } // End of lock. + + if (map_is_empty) { + // We go from not empty to empty, + // so unlock the module it can leave in peace. + ceee_module_util::UnlockModule(); + } + return S_OK; +} + +HRESULT ExecutorsManager::Terminate() { + if (thread_ != NULL) { + // Ask our thread to quit and wait for it to be done. + DWORD result = ::SignalObjectAndWait(termination_gate_, thread_, kTimeOut, + FALSE); + DCHECK(result == WAIT_OBJECT_0); + thread_.Close(); + } + if (!executors_.empty()) { + // TODO(mad@chromium.org): Can this happen??? + NOTREACHED(); + ceee_module_util::UnlockModule(); + } + + executors_.clear(); + update_threads_list_gate_.Close(); + termination_gate_.Close(); + + return S_OK; +} + +void ExecutorsManager::SetTabIdForHandle(long tab_id, HWND handle) { + AutoLock lock(lock_); + DCHECK(tab_id_map_.end() == tab_id_map_.find(tab_id)); + DCHECK(handle_map_.end() == handle_map_.find(handle)); + if (handle == reinterpret_cast<HWND>(INVALID_HANDLE_VALUE) || + tab_id == kInvalidChromeSessionId) { + NOTREACHED(); + return; + } + + tab_id_map_[tab_id] = handle; + handle_map_[handle] = tab_id; +} + +void ExecutorsManager::DeleteTabHandle(HWND handle) { + AutoLock lock(lock_); + HandleMap::iterator handle_it = handle_map_.find(handle); + if(handle_map_.end() != handle_it) { + DCHECK(false); + return; + } + + TabIdMap::iterator tab_id_it = tab_id_map_.find(handle_it->second); + if(tab_id_map_.end() != tab_id_it) { + DCHECK(false); + return; + } + +#ifdef DEBUG + tab_id_map_[handle_it->second] = reinterpret_cast<HWND>(INVALID_HANDLE_VALUE); + handle_map_[handle] = kInvalidChromeSessionId; +#else + tab_id_map_.erase(handle_it->second); + handle_map_.erase(handle); +#endif // DEBUG +} + +HWND ExecutorsManager::GetTabHandleFromId(int tab_id) { + AutoLock lock(lock_); + TabIdMap::const_iterator it = tab_id_map_.find(tab_id); + DCHECK(it != tab_id_map_.end()); + + if (it == tab_id_map_.end()) + return reinterpret_cast<HWND>(INVALID_HANDLE_VALUE); + + // Deleted? I hope not. + DCHECK(it->second != reinterpret_cast<HWND>(INVALID_HANDLE_VALUE)); + return it->second; +} + +int ExecutorsManager::GetTabIdFromHandle(HWND tab_handle) { + AutoLock lock(lock_); + HandleMap::const_iterator it = handle_map_.find(tab_handle); + DCHECK(it != handle_map_.end()); + if (it == handle_map_.end()) + return kInvalidChromeSessionId; + DCHECK(it->second != kInvalidChromeSessionId); // Deleted? I hope not. + return it->second; +} + +HRESULT ExecutorsManager::GetExecutorCreator( + ICeeeExecutorCreator** executor_creator) { + return ::CoCreateInstance(CLSID_CeeeExecutorCreator, NULL, + CLSCTX_INPROC_SERVER, IID_ICeeeExecutorCreator, + reinterpret_cast<void**>(executor_creator)); +} + +size_t ExecutorsManager::GetThreadHandles( + CHandle thread_handles[], ThreadId thread_ids[], size_t num_threads) { + AutoLock lock(lock_); + ExecutorsMap::iterator iter = executors_.begin(); + size_t index = 0; + for (; index < num_threads && iter != executors_.end(); ++index, ++iter) { + DCHECK(thread_handles[index].m_h == NULL); + // We need to duplicate the handle to make sure the caller will not wait + // on a closed handle. + BOOL success = ::DuplicateHandle( + kProcessHandle, iter->second.thread_handle, kProcessHandle, + &thread_handles[index].m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + thread_ids[index] = iter->first; + } + + return index; +} + +DWORD ExecutorsManager::WaitForSingleObject(HANDLE wait_handle, DWORD timeout) { + return ::WaitForSingleObject(wait_handle, timeout); +} + +DWORD ExecutorsManager::WaitForMultipleObjects(DWORD num_handles, + const HANDLE* wait_handles, BOOL wait_all, DWORD timeout) { + return ::WaitForMultipleObjects(num_handles, wait_handles, wait_all, timeout); +} + + +DWORD ExecutorsManager::ThreadProc(LPVOID parameter) { + // We must make sure to join the multi thread apartment so that the executors + // get released properly in the same apartment they were acquired from. + ::CoInitializeEx(NULL, COINIT_MULTITHREADED); + + ThreadStartData* thread_start_data = + reinterpret_cast<ThreadStartData*>(parameter); + DCHECK(thread_start_data != NULL); + ExecutorsManager* me = thread_start_data->me; + DCHECK(me != NULL); + + // Let our parent know that we are old enough now! + ::SetEvent(thread_start_data->thread_started_gate); + // Setting the event will destroy the thread start data living on the stack + // so make sure we don't use it anymore. + thread_start_data = NULL; + + while (true) { + CHandle smart_handles[MAXIMUM_WAIT_OBJECTS]; + HANDLE handles[MAXIMUM_WAIT_OBJECTS]; + ThreadId thread_ids[MAXIMUM_WAIT_OBJECTS]; + // Get as many handles as we can, leaving room for kExtraHandles. + size_t num_threads = me->GetThreadHandles( + smart_handles, thread_ids, MAXIMUM_WAIT_OBJECTS - kExtraHandles); + // Wait function needs an array of raw handles, not smart ones. + for (size_t index = 0; index < num_threads; ++index) + handles[index] = smart_handles[index]; + + // We also need to wait for our termination signal. + handles[num_threads + kTerminationHandleIndexOffset] = + me->termination_gate_; + // As well as a signal warning us to go fetch more thread handles. + handles[num_threads + kUpdateHandleIndexOffset] = + me->update_threads_list_gate_; + + size_t num_handles = num_threads + kExtraHandles; + DWORD result = me->WaitForMultipleObjects(num_handles, handles, FALSE, + INFINITE); + if (result == WAIT_OBJECT_0 + num_threads + + kUpdateHandleIndexOffset) { + // We got a new thread added, + // simply let the loop turn to add it to our watch list. + } else if (result >= WAIT_OBJECT_0 && + result < WAIT_OBJECT_0 + num_threads) { + // One of our threads have died, cleanup time. + me->RemoveExecutor(thread_ids[result - WAIT_OBJECT_0]); + } else if (result == WAIT_OBJECT_0 + num_threads + + kTerminationHandleIndexOffset) { + // we are being terminated, break the cycle. + break; + } else { + DCHECK(result == WAIT_FAILED); + LOG(ERROR) << "ExecutorsManager::ThreadProc " << com::LogWe(); + break; + } + } + // Merci... Bonsoir... + ::CoUninitialize(); + return 1; +} diff --git a/ceee/ie/broker/executors_manager.h b/ceee/ie/broker/executors_manager.h new file mode 100644 index 0000000..3b9ac3b --- /dev/null +++ b/ceee/ie/broker/executors_manager.h @@ -0,0 +1,210 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// ExecutorsManager implementation, an object to keep track of the +// CeeeExecutor objects that were instantiated in destination threads. + +#ifndef CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ +#define CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <map> + +#include "base/lock.h" +#include "base/singleton.h" + +#include "toolband.h" // NOLINT + +// This class is to be used as a single instance for the broker module to +// hold on a map of executor objects per thread that won't go away when +// the instance of the Broker object does. +// +// See the @ref ExecutorsManagerDoc page for more details. + +// Manages a map of destination threads to CeeeExecutor interfaces. +class ExecutorsManager { + public: + // Identifiers for destination threads where to run executors. + typedef DWORD ThreadId; + + // To avoid lint errors, even though we are only virtual for unittests. + virtual ~ExecutorsManager() {} + + // Adds a new executor to the map associated to the given thread_id. + // + // @param thread_id The thread for which we want to register a new executor. + // @param executor The executor we want to register for the given thread_id. + // @return S_OK iff we didn't already have an executor, and we had a pending + // request to add one for that exact same thread. + virtual HRESULT RegisterWindowExecutor(ThreadId thread_id, + IUnknown* executor); + // TODO(mad@chromium.org): Implement the proper manual/secure registration. + // + // @param thread_id The thread for which we want to register a new executor. + // @param executor The executor we want to register for the given thread_id. + // @return S_OK iff we didn't already have an executor, and we had a pending + // request to add one for that exact same thread. + virtual HRESULT RegisterTabExecutor(ThreadId thread_id, IUnknown* executor); + + // Gets the executor associated to the given thread_id. Gets if from the map + // if there was already one in there or create a new one otherwise. + // + // @param thread_id The thread for which we want the executor. + // @param window The window handle for which we want the executor. + // @param riid Which interface is to be returned in @p executor. + // @param executor Where to return the pointer to that executor. + // @return S_OK iff we found an existing or successfully created an executor. + virtual HRESULT GetExecutor(ThreadId thread_id, HWND window, REFIID riid, + void** executor); + + // Removes an executor from our map. + // + // @param thread_id The thread for which we want to remove the executor. + // @return S_OK if we removed the executor or S_FALSE if it wasn't there. + virtual HRESULT RemoveExecutor(ThreadId thread_id); + + // Terminates the usage of the map by freeing our resources. + virtual HRESULT Terminate(); + + // Return a tab handle associated with the id. + // + // @param tab_id The tab identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if tab_id isn't + // found). + virtual HWND GetTabHandleFromId(int tab_id); + + // Return a tab id associated with the HWND. + // + // @param tab_handle The tab HWND. + // @return The corresponding tab id (or 0 if tab_handle isn't found). + virtual int GetTabIdFromHandle(HWND tab_handle); + + // Register the relation between a tab_id and a HWND. + virtual void SetTabIdForHandle(long tab_id, HWND tab_handle); + + // Unregister the HWND and its corresponding tab_id. + virtual void DeleteTabHandle(HWND handle); + + // Traits for Singleton<ExecutorsManager> so that we can pass an argument + // to the constructor. + struct SingletonTraits { + static ExecutorsManager* New() { + return new ExecutorsManager(false); // By default, we want a thread. + } + static void Delete(ExecutorsManager* x) { + delete x; + } + static const bool kRegisterAtExit = true; + }; + + protected: + // The data we pass to start our worker thread. + // THERE IS A COPY OF THIS CLASS IN THE UNITTEST WHICH YOU NEED TO UPDATE IF + // you change this one... + struct ThreadStartData { + ExecutorsManager* me; + CHandle thread_started_gate; + }; + + // A structures holding on the info about an executor and thread it runs in. + struct ExecutorInfo { + ExecutorInfo(IUnknown* new_executor = NULL, HANDLE handle = NULL) + : executor(new_executor), thread_handle(handle) { + } + ExecutorInfo(const ExecutorInfo& executor_info) + : executor(executor_info.executor), + thread_handle(executor_info.thread_handle) { + } + CComPtr<IUnknown> executor; + // mutable so that we can assign/Detach a const copy. + mutable CHandle thread_handle; + }; + + typedef std::map<ThreadId, ExecutorInfo> ExecutorsMap; + typedef std::map<ThreadId, CHandle> Tid2Event; + typedef std::map<int, HWND> TabIdMap; + typedef std::map<HWND, int> HandleMap; + + // The index of the termination event in the array of handles we wait for. + static const size_t kTerminationHandleIndexOffset; + + // The index of the update event in the array of handles we wait for. + static const size_t kUpdateHandleIndexOffset; + + // The index of the last event in the array of handles we wait for. + static const size_t kLastHandleIndexOffset; + + // The number of extra handles we used for the events described above. + static const size_t kExtraHandles; + + // Protected constructor to ensure single instance and initialize some + // members. Set no_thread for testing... + explicit ExecutorsManager(bool no_thread); + + // Creates an executor creator in a virtual method so we can override it in + // Our unit test code. + // + // @param executor_creator Where to return the executor creator. + virtual HRESULT GetExecutorCreator( + ICeeeExecutorCreator** executor_creator); + + // Returns a list of HANDLEs of threads for which we have an executor. + // + // @param thread_handles Where to return at most @p num_threads handles. + // @param thread_ids Where to return at most @p num_threads ThreadIds. + // @param num_threads How many handles can fit in @p thread_handles. + // @return How many handles have been added in @p thread_handles and the same + // ThreadIds have been added to @p thread_ids. + virtual size_t GetThreadHandles(CHandle thread_handles[], + ThreadId thread_ids[], size_t num_threads); + + // A few seams so that we don't have to mock the kernel functions. + virtual DWORD WaitForSingleObject(HANDLE wait_handle, DWORD timeout); + virtual DWORD WaitForMultipleObjects(DWORD num_handles, + const HANDLE* wait_handles, BOOL wait_all, DWORD timeout); + + // The thread procedure that we use to clean up dead threads from the map. + // + // @param thread_data A small structure containing this and an event to + // signal when the thread has finished initializing itself. + static DWORD WINAPI ThreadProc(LPVOID thread_data); + + // The map of executor and their thread handle keyed by thread identifiers. + // Thread protected by lock_. + ExecutorsMap executors_; + + // We remember the thread identifiers for which we are pending a registration + // so that we make sure that we only accept registration that we initiate. + // Also, for each pending registration we must wait on a different event + // per thread_id that we are waiting for the registration of. + // Thread protected by ExecutorsManager::lock_. + Tid2Event pending_registrations_; + + // The mapping between a tab_id and the HWND of the window holding the BHO. + // In DEBUG, this mapping will grow over time since we don't remove it on + // DeleteTabHandle. This is useful for debugging as we know if a mapping has + // been deleted and is invalidly used. + // Thread protected by ExecutorsManager::lock_. + TabIdMap tab_id_map_; + HandleMap handle_map_; + + // The handle to the thread running ThreadProc. + CHandle thread_; + + // Used to signal the thread to reload the list of thread handles. + CHandle update_threads_list_gate_; + + // Used to signal the thread to terminate. + CHandle termination_gate_; + + // To protect the access to the maps (ExecutorsManager::executors_ & + // ExecutorsManager::pending_registrations_ & tab_id_map_/handle_map_). + Lock lock_; + + DISALLOW_EVIL_CONSTRUCTORS(ExecutorsManager); +}; + +#endif // CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ diff --git a/ceee/ie/broker/executors_manager_docs.h b/ceee/ie/broker/executors_manager_docs.h new file mode 100644 index 0000000..3c607a4 --- /dev/null +++ b/ceee/ie/broker/executors_manager_docs.h @@ -0,0 +1,208 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ + +/* @page ExecutorsManagerDoc Detailed documentation of the ExecutorsManager. + +@section ExecutorsManagerIntro Introduction + +The Executors Manager implemented by the ExecutorsManager class, +is used by the CeeeBroker object to instantiate and hold on to +Marshaled proxies of CeeeExecutor COM objects running in other processes. + +The complexity lies in the fact that the Broker could send concurrent requests +to access Executor proxies and the creation of new proxies can't be done +synchronously. + +So the Executors Manager must block on a request for a new Executor, and all +subsequent requests for the same executor until the asynchronous creation of +the Executor is complete. Once the Executor is completely registered in the +Executors Manager, it can be returned right away when requested (though a wait +may need to be done on a lock to access the map of Executors). + +Executors are created to execute code in a given thread (called the +destination thread from now on), so we map the Executor to their associated +destination thread identifier and reuse existing Executors for subsequent +requests to execute in the same destination thread. We also keep an open +handle to the destination thread so that we can wait to it in a separate +Executors Manager @ref ThreadProc "worker thread" and wake up when the +destination thread dies, so that we can clean our map. + +@section PublicMethods Public methods + +The Executors Manager has has 3 public entry points, @ref GetExecutor, +@ref AddExecutor and @ref RemoveExecutor. + +@subsection GetExecutor + +The @link ExecutorsManager::GetExecutor GetExecutor @endlink method returns a +reference to a CeeeExecutor COM object (simply called Executor from now on) +for a given destination thread. It first checks for an existing Executor in +the @link ExecutorsManager::executors_ Executors map@endlink, and if it finds +one, it simply returns it (though it must @link ExecutorsManager::lock_ lock +@endlink the access to the Executors map). + +If the @link ExecutorsManager::executors_ Executors map@endlink doesn't +already have an Executor for the requested +destination thread, we must first check if we already have a @link +ExecutorsManager::pending_registrations_ pending registration @endlink for that +same destination thread. + +As mentioned @ref ExecutorsManagerIntro "above", registration requests can be +pending, since the creation of new Executors can not be done synchronously. + +If there are no existing Executor and no pending registration to create one, +we can create a new pending registration (all of this, properly locked so that +we make sure to only start one pending registration for a given destination +thread). + +The creation of new Executors is done via the usage of the @link +CeeeExecutorCreator Executor Creator @endlink COM object that is exposed by +a DLL so that it can inject a @link CeeeExecutorCreator::hook_ hook +@endlink in the destination thread to create the Executor. Once the hook is +injected, we can only post to the hook (since sending a message creates +a user-synchronous operation that can't get out of the process, so we won't be +able to register the newly created Executor in the Broker). + +Once the Executor Creator posted a message to the hook to instantiate a new +Executor and get it registered in the Broker, the Executors Manager must wait +for the registration to complete. Since the Executors Manager is called by the +Broker, which is instantiated in a multi-threaded apartment, the registration +will happen in a free thread while the thread that made the call to GetExecutor +is blocked, waiting for the pending registration to complete. + +At that point, there may be more than one thread calling GetExecutor for the +same destination thread, so they must all wait on the registration complete +event. Also, since there can be more than one destination thread with pending +registrations for a new Executor, there must be multiple events used to signal +the completion of the registration, one for each destination thread that has a +pending request to create a new executor. So the Executors Manager keeps a +@link ExecutorsManager::pending_registrations_ map of destination thread +identifier to registration completion event@endlink. + +Once the registration of a new Executor is complete (via a call to @ref +RegisterTabExecutor), the event for the destination thread for which the +Executor is being registered is signaled so that any thread waiting for it can +wake up, get the new Executor from the map, and return it to their caller. + +@subsection RegisterExecutor + +The @link ExecutorsManager::RegisterExecutor RegisterExecutor @endlink method is +called by the Broker when it receives a request to register a new Executor for a +given destination thread. So the Executors Manager must first confirm that we +didn't already have an Executor for that destination thread, and also confirm +that we actually have a pending request for that destination thread. + +@note <b>TODO(mad@chromium.org)</b>: We need to change this so that we +allow the BHO and maybe other modules in the future to preemptively +register Executors without a pending request. + +Once this is confirmed, the new Executor is added to the map (again, in a +thread safe way, of course), and the event of the pending request to register +an executor for that destination thread is signaled. We also need to wake up +the @ref ThreadProc "worker thread", waiting on destination thread handles, +(using the @ref update_threads_list_gate_ event) so that the new one can be +added to the list to wait on. + +@subsection RemoveExecutor + +If for some reason, an executor must remove itself from the Executors Manager's +map, it can do so by calling the @link +ICeeeBrokerRegistrar::UnregisterExecutor UnregisterExecutor @endlink method +on the Broker ICeeeBrokerRegistrar interface which delegates to the +Executors Manager's @link ExecutorsManager::RemoveExecutor RemoveExecutor +@endlink method. + +This same method is also called from the Executors Manager's @ref ThreadProc +"worker thread" waiting for the death of the destination thread for which we +have an executor in the map. When the thread dies, we remove the associated +executor from the map. + +@subsection Terminate + +The @link ExecutorsManager::Terminate Terminate @endlink method is to be called +when we are done with the Executors Manager, and it can clean up and release +all its resources (including stopping the @ref ThreadProc "worker thread" used +to wait for the destination threads to die, by setting the @ref +termination_gate_ event). + +@section PrivateMethods Private methods + +We use a few private methods to help with the implementation of the public +methods described @ref PublicMethods "above". + +@subsection GetThreadHandles + +@link ExecutorsManager::GetThreadHandles GetThreadHandles @endlink parses +through our @link ExecutorsManager::executors_ Executors map @endlink to return +the list of handles that the @ref ThreadProc "worker thread" has to wait for. + +The caller must provide two pre-allocated arrays, one for thread handles and the +other one for thread identifiers. So we may not be able to return info for +all the threads if there isn't enough room. This is OK since the only caller is +the worker thread procedure (described @ref ThreadProc "below") and it can't +wait for more than a specific number of handles anyway. The arrays must be kept +in sync, so that index i in one of them refers to the same thread as index i in +the other. + +@subsection ThreadProc + +The @link ExecutorsManager::ThreadProc ThreadProc @endlink is the worker thread +procedure that is used to get the list of destination threads for +which we have an executor in the @link ExecutorsManager::executors_ Executors +map@endlink, and then wait on them to know when they die. + +We also need to wait on two events, @ref termination_gate_ for the termination +of the worker thread (set in the Terminate public method described @ref +Terminate "above") and @ref update_threads_list_gate_ warning us that there are +new threads that we must wait on (set in the RegisterExecutor public method +described @ref RegisterExecutor "above"). + +@section PrivateDataMembers Private data members + +@subsection executors_ + +@link ExecutorsManager::executors_ executors_ @endlink is a map from a +destination thread identifier to a couple made of an Executor interface and a +thread handle. We need to keep an opened thread handle so that we can wait on +it until it dies. + +@subsection pending_registrations_ + +@link ExecutorsManager::pending_registrations_ pending_registrations_ @endlink +is another map of destination thread identifier, but this one holds on to +synchronization events that we will set once the registration of an executor +for the given destination thread is complete. Since there may be more than one +thread waiting on this signal, and we use the signal only once, we create it as +a manual reset event. + +@subsection thread_ + +Of course, we need to keep a @link ExecutorsManager::thread_ handle @endlink on +our worker thread. + +@subsection update_threads_list_gate_ + +This @link ExecutorsManager::update_threads_list_gate_ event @endlink is used +to wake the worker thread so that it refreshes its list of thread handles that +it waits on. It is set from the RegisterExecutor public method described +@ref RegisterExecutor "above". + +@subsection termination_gate_ + +This other @link ExecutorsManager::termination_gate_ event @endlink is set from +the Terminate public method described @ref Terminate "above" to notify the +worker thread to terminate itself. + +@subsection lock_ + +Since the Executors Manager can be called from many different threads, it must +protect the access to its data (and atomize certain operations) with this @link +ExecutorsManager::lock_ lock @endlink. + +*/ + +#endif // CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ diff --git a/ceee/ie/broker/executors_manager_unittest.cc b/ceee/ie/broker/executors_manager_unittest.cc new file mode 100644 index 0000000..47c6275 --- /dev/null +++ b/ceee/ie/broker/executors_manager_unittest.cc @@ -0,0 +1,664 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for executors manager. + +#include <vector> + +#include "base/logging.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/nt_internals.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::DoAll; +using testing::InvokeWithoutArgs; +using testing::Return; +using testing::SetArgumentPointee; + +// We mock the IUnknown interface to make sure we properly AddRef and +// Release the executors in the manager's map. +// TODO(mad@chromium.org): replace this with the MockExecutorIUnknown +// in mock_broker_and_friends.h. +class MockExecutor : public IUnknown { + public: + MockExecutor() : ref_count_(0) {} + STDMETHOD_(ULONG, AddRef)() { return ++ref_count_; } + STDMETHOD_(ULONG, Release)() { + EXPECT_GT(ref_count_, 0UL); + return --ref_count_; + } + ULONG ref_count() const { return ref_count_; } + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, + HRESULT(REFIID, void**)); + private: + ULONG ref_count_; +}; + +// We also need to mock the creator or executors for 1) making sure it is +// called properly, and 2) for it to return our mock executor. +class MockExecutorCreator: public ICeeeExecutorCreator { + public: + // No need to mock AddRef, it is not called. + STDMETHOD_(ULONG, AddRef)() { return 1; } + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + STDMETHOD (QueryInterface)(REFIID, LPVOID*) { return S_OK; } + + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, CreateWindowExecutor, + HRESULT(long, CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Teardown, HRESULT(long)); +}; + +// We need to override some virtual functions of the executors manager and +// also provide public access to some protected methods and data. +class TestingExecutorsManager : public ExecutorsManager { + public: + TestingExecutorsManager() + : ExecutorsManager(true), // no thread. + current_executor_(NULL), + current_handle_(NULL), + current_thread_id_(kThreadId) { + } + + ~TestingExecutorsManager() { + nt_internals::PUBLIC_OBJECT_BASIC_INFORMATION pobi; + ULONG length = 0; + for (size_t index = 0; index < opened_handles_.size(); ++index) { + nt_internals::NtQueryObject(opened_handles_[index].first, + nt_internals::ObjectBasicInformation, &pobi, sizeof(pobi), &length); + EXPECT_EQ(length, sizeof(pobi)); + EXPECT_EQ(1UL, pobi.HandleCount); + if (1UL != pobi.HandleCount) { + printf("Leaked handle for: %s\n", opened_handles_[index].second); + } + EXPECT_TRUE(::CloseHandle(opened_handles_[index].first)); + } + } + + MOCK_METHOD2(WaitForSingleObject, DWORD(HANDLE, DWORD)); + MOCK_METHOD4(WaitForMultipleObjects, + DWORD(DWORD, const HANDLE*, BOOL, DWORD)); + + HANDLE GetNewHandle(const char* info) { + // Some tests depend on the handle to be manual reset. + opened_handles_.push_back(std::pair<HANDLE, const char*>( + ::CreateEvent(NULL, TRUE, FALSE, NULL), info)); + HANDLE returned_handle = NULL; + EXPECT_TRUE(::DuplicateHandle(::GetCurrentProcess(), + opened_handles_.back().first, ::GetCurrentProcess(), &returned_handle, + 0, FALSE, DUPLICATE_SAME_ACCESS)); + return returned_handle; + } + + // This is a seem in the base class allowing us ot use our own creator. + HRESULT GetExecutorCreator(ICeeeExecutorCreator** executor_creator) { + *executor_creator = &executor_creator_; + return S_OK; + } + // provide public access so we can test the method. + using ExecutorsManager::GetThreadHandles; + // Wrap the call to ThreadPRoc by first creating the necessary info. + DWORD CallThreadProc() { + // This struct is declared in the unnamed namespace of the implementation. + struct ThreadStartData { + ExecutorsManager* me; + CHandle thread_started_gate; + } thread_start_data; + thread_start_data.me = this; + thread_start_data.thread_started_gate.Attach(GetNewHandle("TSD")); + DCHECK(thread_start_data.thread_started_gate != NULL); + return ExecutorsManager::ThreadProc(&thread_start_data); + } + + // Access to protected handles. + HANDLE GetUpdateHandle() { + return update_threads_list_gate_.m_h; + } + HANDLE GetTerminateHandle() { + return termination_gate_.m_h; + } + + // Fake a pending registration, to properly test that case. + void AddPendingRegistration(HANDLE thread_handle, + ThreadId thread_id = kThreadId) { + pending_registrations_[thread_id].Attach(thread_handle); + } + void RemovePendingRegistration(ThreadId thread_id = kThreadId) { + pending_registrations_.erase(thread_id); + } + + // This method is to be Invoked by the Mock of WaitForXXXObject[s]. + void RegisterExecutorOnWait() { + executors_[current_thread_id_] = ExecutorInfo(current_executor_, + current_handle_); + } + + // Public access to the executors_ map. + void FakeRegisterExecutor(HANDLE handle, + IUnknown* executor = NULL, + ThreadId thread_id = kThreadId) { + executors_[thread_id] = ExecutorInfo(executor, handle); + } + size_t GetNumExecutors() { + return executors_.size(); + } + bool IsExecutorRegistered(ThreadId thread_id = kThreadId) { + return (executors_.find(thread_id) != executors_.end()); + } + + IUnknown* current_executor_; + HANDLE current_handle_; + ThreadId current_thread_id_; + MockExecutorCreator executor_creator_; + static const ThreadId kThreadId; + static const HWND kWindowHwnd; + static const CeeeWindowHandle kWindowHandle; + + // Publicize these protected static const values. + using ExecutorsManager::kTerminationHandleIndexOffset; + using ExecutorsManager::kUpdateHandleIndexOffset; + + std::vector<std::pair<HANDLE, const char*>> opened_handles_; +}; + +const TestingExecutorsManager::ThreadId TestingExecutorsManager::kThreadId = 42; +const HWND TestingExecutorsManager::kWindowHwnd = reinterpret_cast<HWND>(93); +const CeeeWindowHandle TestingExecutorsManager::kWindowHandle + = reinterpret_cast<CeeeWindowHandle>(kWindowHwnd); + +class ExecutorsManagerTests: public testing::Test { + public: + ExecutorsManagerTests() : initial_handle_count_(0) { + } + // This is not reliable enough, but we keep it here so that we can enable + // it from time to time to make sure everything is OK. + /* + virtual void SetUp() { + // This is called from the threadproc and changes the handles count if we + // don't call it first. + ASSERT_HRESULT_SUCCEEDED(::CoInitializeEx(NULL, COINIT_MULTITHREADED)); + + // Acquire the number of handles in the process. + ::GetProcessHandleCount(::GetCurrentProcess(), &initial_handle_count_); + printf("Initial Count: %d\n", initial_handle_count_); + } + + virtual void TearDown() { + // Make sure the number of handles in the process didn't change. + DWORD new_handle_count = 0; + ::GetProcessHandleCount(::GetCurrentProcess(), &new_handle_count); + EXPECT_EQ(initial_handle_count_, new_handle_count); + printf("Final Count: %d\n", new_handle_count); + + ::CoUninitialize(); + } + */ + private: + DWORD initial_handle_count_; +}; + +TEST_F(ExecutorsManagerTests, RegisterExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Invalid arguments. + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, NULL)); + + MockExecutor executor; + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingRegistration1")); + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, NULL)); + + // Dead thread... + testing::MockKernel32 kernel32; + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return((HANDLE)NULL)); + EXPECT_EQ(S_FALSE, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + // Failed, already registered. + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("FakeExec1"), &executor); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + EXPECT_EQ(E_UNEXPECTED, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(0, executor.ref_count()); + + // Success!!! + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce( + Return(executors_manager.GetNewHandle("OpenThread"))); + EXPECT_EQ(S_OK, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + executors_manager.RemovePendingRegistration(); + EXPECT_EQ(S_OK, executors_manager.Terminate()); + EXPECT_EQ(0, executor.ref_count()); +} + +TEST_F(ExecutorsManagerTests, RegisterTabExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Already registered. + MockExecutor executor; + HANDLE new_handle = executors_manager.GetNewHandle("FakeExec1"); + executors_manager.FakeRegisterExecutor(new_handle, &executor); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + EXPECT_EQ(S_OK, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(0, executor.ref_count()); + + // Dead thread... + testing::MockKernel32 kernel32; + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return((HANDLE)NULL)); + EXPECT_EQ(E_UNEXPECTED, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + // Success!!! + new_handle = executors_manager.GetNewHandle("OpenThread"); + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return(new_handle)); + EXPECT_EQ(S_OK, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + EXPECT_EQ(S_OK, executors_manager.Terminate()); + EXPECT_EQ(0, executor.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); +} + +TEST_F(ExecutorsManagerTests, GetExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Already in the map. + MockExecutor executor; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("FakeExec1"), &executor); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + IUnknown* result_executor = NULL; + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + // Since our mocked QI doesn't addref, ref count shouldn't have changed. + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + + // Fail Executor creation. + result_executor = NULL; + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_EQ(E_FAIL, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Fail registration. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(Return(WAIT_OBJECT_0)); + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, + Teardown(TestingExecutorsManager::kThreadId)).WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_EQ(E_UNEXPECTED, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Pending registration fail. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(Return(WAIT_OBJECT_0)); + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingReg1")); + + EXPECT_EQ(E_UNEXPECTED, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Success Creating new. + executors_manager.current_executor_ = &executor; + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(WAIT_OBJECT_0))); + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, + Teardown(TestingExecutorsManager::kThreadId)).WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + EXPECT_GT(executor.ref_count(), 0UL); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + + // Success with pending registration. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(WAIT_OBJECT_0))); + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingReg2")); + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + EXPECT_EQ(S_OK, executors_manager.Terminate()); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); +} + +TEST_F(ExecutorsManagerTests, RemoveExecutor) { + TestingExecutorsManager executors_manager; + + // Nothing to remove... + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor(1)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + + // Success. + MockExecutor executor1; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake0"), &executor1); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + + // Multiple values, removed one at a time... + MockExecutor executor2; + MockExecutor executor3; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake1"), &executor1, 1); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake2"), &executor2, 2); + EXPECT_EQ(2, executors_manager.GetNumExecutors()); + EXPECT_GT(executor2.ref_count(), 0UL); + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake3"), &executor3, 3); + EXPECT_EQ(3, executors_manager.GetNumExecutors()); + EXPECT_GT(executor3.ref_count(), 0UL); + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(2)); + EXPECT_EQ(2, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(2)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(1)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(3)); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor2.ref_count()); + EXPECT_GT(executor3.ref_count(), 0UL); + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(3)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(3)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(1)); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor3.ref_count()); + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(1)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(1)); + EXPECT_EQ(0, executor1.ref_count()); +} + +// Since the returned handle is a duplicate, making sure we have the +// appropriate handle is a little tricky. +void VerifyIsSameHandle(HANDLE handle1, HANDLE handle2) { + // First make sure neither is set. + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle2, 0)); + + // Now check that setting one also sets the other. + ::SetEvent(handle1); + EXPECT_EQ(WAIT_OBJECT_0, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_OBJECT_0, ::WaitForSingleObject(handle2, 0)); + + // Manual reset. + ::ResetEvent(handle1); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle2, 0)); +} + +TEST_F(ExecutorsManagerTests, GetThreadHandles) { + TestingExecutorsManager executors_manager; + + // We need to work with invalid, and one other valid value for thread info. + static const TestingExecutorsManager::ThreadId kInvalidThreadId = 0xF0F0F0F0; + static const TestingExecutorsManager::ThreadId kThreadIdA = 0xAAAAAAAA; + static const TestingExecutorsManager::ThreadId kThreadIdB = 0xBBBBBBBB; + + // As for the thread proc code, we work with arrays of handles and thread ids + // on the stack. + CHandle thread_handles[MAXIMUM_WAIT_OBJECTS]; + TestingExecutorsManager::ThreadId thread_ids[MAXIMUM_WAIT_OBJECTS]; + + // Make sure the arrays are not touched. + thread_ids[0] = kInvalidThreadId; + size_t num_threads = executors_manager.GetThreadHandles( + thread_handles, thread_ids, MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(0, num_threads); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[0]); + EXPECT_EQ(kInvalidThreadId, thread_ids[0]); + + // Make sure only the first index will get affected. + thread_ids[1] = kInvalidThreadId; + HANDLE thread_handle_a = executors_manager.GetNewHandle("A"); + executors_manager.FakeRegisterExecutor(thread_handle_a, NULL, kThreadIdA); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(1, num_threads); + VerifyIsSameHandle(thread_handle_a, thread_handles[0]); + EXPECT_EQ(kThreadIdA, thread_ids[0]); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[1]); + EXPECT_EQ(kInvalidThreadId, thread_ids[1]); + // Need to close active handles since GetThreadHandles expect NULL m_h. + thread_handles[0].Close(); + + // Now expect 2 values and make sure the third one isn't affected. + thread_ids[0] = kInvalidThreadId; + thread_ids[2] = kInvalidThreadId; // We asserted above that index 1 is OK. + HANDLE thread_handle_b = executors_manager.GetNewHandle("B"); + executors_manager.FakeRegisterExecutor(thread_handle_b, NULL, kThreadIdB); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(2, num_threads); + // We can't be sure of the order in which the map returns the elements. + if (thread_ids[0] == kThreadIdA) { + VerifyIsSameHandle(thread_handle_a, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[1]); + VerifyIsSameHandle(thread_handle_b, thread_handles[1]); + } else { + VerifyIsSameHandle(thread_handle_b, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[0]); + VerifyIsSameHandle(thread_handle_a, thread_handles[1]); + EXPECT_EQ(kThreadIdA, thread_ids[1]); + } + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[2]); + EXPECT_EQ(kInvalidThreadId, thread_ids[2]); + thread_handles[0].Close(); + thread_handles[1].Close(); + + // Now remove threads and make sure they won't be returned again. + thread_ids[0] = kInvalidThreadId; + thread_ids[1] = kInvalidThreadId; // Asserted index 2 invalid already. + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(kThreadIdA)); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(1, num_threads); + VerifyIsSameHandle(thread_handle_b, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[0]); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[1]); + EXPECT_EQ(kInvalidThreadId, thread_ids[1]); + thread_handles[0].Close(); + + // Now remove the last one... + thread_ids[0] = kInvalidThreadId; + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(kThreadIdB)); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(0, num_threads); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[0]); + EXPECT_EQ(kInvalidThreadId, thread_ids[0]); +} + +TEST_F(ExecutorsManagerTests, ThreadProc) { + testing::LogDisabler no_logs; + TestingExecutorsManager executors_manager; + + // Register an object and make sure we wake up to update our list. + MockExecutor executor1; + executors_manager.current_executor_ = &executor1; + executors_manager.current_handle_ = executors_manager.GetNewHandle("1"); + EXPECT_CALL(executors_manager, WaitForMultipleObjects(2, _, FALSE, INFINITE)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(TestingExecutorsManager::kUpdateHandleIndexOffset))); + EXPECT_CALL(executors_manager, WaitForMultipleObjects(3, _, FALSE, INFINITE)). + WillOnce(Return( + TestingExecutorsManager::kTerminationHandleIndexOffset + 1)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); + + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + + // Add another destination thread. + static const TestingExecutorsManager::ThreadId kOtherThreadId = 14; + MockExecutor executor2; + executors_manager.current_executor_ = &executor2; + executors_manager.current_handle_ = executors_manager.GetNewHandle("2"); + executors_manager.current_thread_id_ = kOtherThreadId; + // Number of handles and return index offset needs to be increased by 1 + // because we already have a destination thread in our map. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(3, _, FALSE, INFINITE)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), Return( + TestingExecutorsManager::kUpdateHandleIndexOffset + 1))).WillOnce( + Return(TestingExecutorsManager::kTerminationHandleIndexOffset + 1)); + // Now that we have another destination thread in the map, + // we wait for 4 handles. We return 1 to remove one of them, and wait will + // be called with three handles again, the mock above will be called for the + // second time with 3 handles and will return the proper termination offset. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(4, _, FALSE, INFINITE)). + WillOnce(Return(1)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + // We can't tell which one is at position 1 in the array returned by + // GetThreadHandles since they are fetched from an unsorted map. + EXPECT_TRUE(executors_manager.IsExecutorRegistered(kOtherThreadId) || + executors_manager.IsExecutorRegistered( + TestingExecutorsManager::kThreadId)); + if (executors_manager.IsExecutorRegistered(kOtherThreadId)) { + // We kept the other executor in the map. + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_GT(executor2.ref_count(), 0UL); + } else { + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor2.ref_count()); + } + + // Cleanup. + executors_manager.Terminate(); + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_EQ(0, executor2.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + + // Test the thread failure path. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(2, _, FALSE, INFINITE)). + WillOnce(Return(WAIT_FAILED)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); +} + +} // namespace diff --git a/ceee/ie/broker/infobar_api_module.cc b/ceee/ie/broker/infobar_api_module.cc new file mode 100644 index 0000000..f857b4a --- /dev/null +++ b/ceee/ie/broker/infobar_api_module.cc @@ -0,0 +1,99 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Infobar API implementation. + +#include "ceee/ie/broker/infobar_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> // Must be included AFTER base. + +#include "base/string_number_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_infobar_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" + +namespace ext = extension_infobar_module_constants; + +namespace infobar_api { + +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); } while (false) + REGISTER_INFOBAR_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION +} + +void ShowInfoBar::Execute(const ListValue& args, int request_id) { + scoped_ptr<InfobarApiResult> result(CreateApiResult(request_id)); + + // Get the first parameter (object details). + DictionaryValue* dict; + if (!args.GetDictionary(0, &dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // The dictionary should have both tabId and path properties. + int tab_id; + std::string path; + if (!dict->GetInteger(ext::kTabId, &tab_id) || + !dict->GetString(ext::kHtmlPath, &path)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id)).c_str()); + return; + } + + CComPtr<ICeeeInfobarExecutor> executor; + GetDispatcher()->GetExecutor(tab_window, IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to show infobar."; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + CeeeWindowHandle window_handle; + HRESULT hr = executor->ShowInfobar(CComBSTR(path.data()), &window_handle); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't show infobar. " << com::LogHr(hr) << " path=" << path; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // Store the window information returned by ShowInfobar(). + result->CreateWindowValue(dispatcher->GetWindowHandleFromId(window_handle), + false); + + // Now wait until the document is complete before responding. + dispatcher->RegisterEphemeralEventHandler(kOnDocumentCompleteEventName, + ShowInfoBar::ContinueExecution, result.release()); +} + +HRESULT ShowInfoBar::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<InfobarApiResult> result( + static_cast<InfobarApiResult*>(user_data)); + result->PostResult(); + return S_OK; +} + +} // namespace infobar_api diff --git a/ceee/ie/broker/infobar_api_module.h b/ceee/ie/broker/infobar_api_module.h new file mode 100644 index 0000000..53b3f65 --- /dev/null +++ b/ceee/ie/broker/infobar_api_module.h @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Infobar API implementation. + +#ifndef CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ +#define CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +namespace infobar_api { + +class InfobarApiResult; +typedef ApiResultCreator<InfobarApiResult> InfobarApiResultCreator; + +// Registers all Infobar API invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class InfobarApiResult : public common_api::CommonApiResult { + public: + explicit InfobarApiResult(int request_id) + : common_api::CommonApiResult(request_id) {} +}; + +typedef IterativeApiResult<InfobarApiResult> IterativeInfobarApiResult; + +class ShowInfoBar : public InfobarApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // We need to wait for the infobar browser ready event to complete the + // result response. + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ diff --git a/ceee/ie/broker/infobar_api_module_unittest.cc b/ceee/ie/broker/infobar_api_module_unittest.cc new file mode 100644 index 0000000..d9bce10 --- /dev/null +++ b/ceee/ie/broker/infobar_api_module_unittest.cc @@ -0,0 +1,144 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "ceee/ie/broker/infobar_api_module.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_infobar_module_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext = extension_infobar_module_constants; + +namespace { + +using infobar_api::InfobarApiResult; +using infobar_api::RegisterInvocations; +using infobar_api::ShowInfoBar; + +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +const int kGoodTabWindowId = 99; +const int kGoodFrameWindowId = 88; +const int kRequestId = 43; +const char kHtmlPath[] = "/infobar/test.html"; + +const HWND kGoodTabWindow = (HWND)(kGoodTabWindowId + 1); +const HWND kGoodFrameWindow = (HWND)(kGoodFrameWindowId + 1); + +TEST(InfobarApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(1)); + RegisterInvocations(&disp); +} + +class MockInfobarApiResult : public InfobarApiResult { + public: + explicit MockInfobarApiResult(int request_id) + : InfobarApiResult(request_id) {} + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +class InfobarApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockInfobarExecutor::CreateInitialized( + &mock_infobar_executor_, &mock_infobar_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_infobar_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + void AlwaysMockGetInfobarExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_infobar_executor_keeper_.p), + AddRef(mock_infobar_executor_keeper_.p))); + } + + void MockGetInfobarExecutorOnce(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(mock_infobar_executor_keeper_.p), + AddRef(mock_infobar_executor_keeper_.p))); + } + + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockInfobarExecutor* mock_infobar_executor_; + + private: + // To control the life span of the tab executor. + CComPtr<ICeeeInfobarExecutor> mock_infobar_executor_keeper_; +}; + +TEST_F(InfobarApiTests, ShowInfoBarNoErrors) { + // TODO(vadimb@google.com): Make the implementation work. +#if 0 + testing::LogDisabler no_dchecks; + + DictionaryValue dict; + dict.Set(ext::kTabId, Value::CreateIntegerValue(kGoodTabWindowId)); + dict.Set(ext::kHtmlPath, Value::CreateStringValue(std::string(kHtmlPath))); + ListValue good_args; + ASSERT_TRUE(good_args.Set(0, dict.DeepCopy())); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + + StrictMock<MockApiInvocation<InfobarApiResult, MockInfobarApiResult, + ShowInfoBar> > invocation; + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kGoodFrameWindowId)). + WillRepeatedly(Return(kGoodFrameWindow)); + MockGetInfobarExecutorOnce(&invocation.mock_api_dispatcher_, kGoodTabWindow); + CComBSTR html_path(kHtmlPath); + CeeeWindowHandle window_handle = + reinterpret_cast<CeeeWindowHandle>(kGoodFrameWindow); + EXPECT_CALL(*mock_infobar_executor_, ShowInfobar(StrEq(html_path.m_str), _)). + WillOnce(DoAll(SetArgumentPointee<1>(window_handle), Return(S_OK))); + + invocation.AllocateNewResult(kRequestId); + + invocation.Execute(good_args, kRequestId); +#endif +} + +} // namespace diff --git a/ceee/ie/broker/resource.h b/ceee/ie/broker/resource.h new file mode 100644 index 0000000..60c6977 --- /dev/null +++ b/ceee/ie/broker/resource.h @@ -0,0 +1,31 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Resource constants for user broker executable. + +#ifndef CEEE_IE_BROKER_RESOURCE_H_ +#define CEEE_IE_BROKER_RESOURCE_H_ + + +// {{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by broker_module.rc +// +#define IDS_PROJNAME 100 +#define IDR_BROKER_MODULE 101 +#define IDR_BROKER 102 + + +// Next default values for new objects +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 201 +#define _APS_NEXT_COMMAND_VALUE 32768 +#define _APS_NEXT_CONTROL_VALUE 201 +#define _APS_NEXT_SYMED_VALUE 103 +#endif +#endif + + +#endif // CEEE_IE_BROKER_RESOURCE_H_ diff --git a/ceee/ie/broker/tab_api_module.cc b/ceee/ie/broker/tab_api_module.cc new file mode 100644 index 0000000..c75e2c1 --- /dev/null +++ b/ceee/ie/broker/tab_api_module.cc @@ -0,0 +1,1185 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation. +// +// Tab IDs are the window handle of the "TabWindowClass" window class +// of the whole tab. +// +// To find the chrome.window.* "window ID" we can just get the top-level parent +// window of the tab window. +// +// TODO(joi@chromium.org) Figure out what to do in IE6 (which has no tabs). + +#include "ceee/ie/broker/tab_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/values.h" +#include "base/utf_string_conversions.h" +#include "base/win/windows_version.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/common/api_registration.h" +#include "ceee/ie/common/constants.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "chrome/common/url_constants.h" +#include "googleurl/src/gurl.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; +namespace keys = extension_automation_constants; + +namespace tab_api { + +namespace { + +// bb3147348 +// Convert the tab_id parameter (always the first one) and verify that the +// event received has the right number of parameters. +// NumParam is the number of parameters we expect from events_funnel. +// AddTabParam if true, we add a Tab object to the converted_args. +template<int NumParam, bool AddTabParam> +bool ConvertTabIdEventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + *converted_args = input_args; + + // Get the tab ID from the input arguments. + scoped_ptr<ListValue> input_list; + if (!api_module_util::GetListFromJsonString(input_args, &input_list)) { + NOTREACHED() << "Invalid Arguments sent to event."; + return false; + } + + if (input_list == NULL || input_list->GetSize() != NumParam) { + NOTREACHED() << "Invalid Number of Arguments sent to event."; + return false; + } + + int tab_handle = -1; + bool success = input_list->GetInteger(0, &tab_handle); + DCHECK(success) << "Failed getting the tab_id value from the list of args."; + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + input_list->Set(0, Value::CreateIntegerValue(tab_id)); + + if (AddTabParam) { + TabApiResult result(TabApiResult::kNoRequestId); + // Don't DCHECK here since we have cases where the tab died beforehand. + if (!result.CreateTabValue(tab_id, -1)) { + LOG(ERROR) << "Failed to create a value for tab: " << std::hex << tab_id; + return false; + } + + input_list->Append(result.value()->DeepCopy()); + } + + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; +} + +bool CeeeUnmapTabEventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + int tab_handle = reinterpret_cast<int>(INVALID_HANDLE_VALUE); + + scoped_ptr<ListValue> input_list; + if (!api_module_util::GetListAndIntegerValue(input_args, &input_list, + &tab_handle) || + tab_handle == kInvalidChromeSessionId) { + NOTREACHED() << "An invalid argument was passed to UnmapTab"; + return false; + } +#ifdef DEBUG + int tab_id = kInvalidChromeSessionId; + input_list->GetInteger(1, &tab_id); + DCHECK(tab_id == dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle))); +#endif // DEBUG + + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + dispatcher->DeleteTabHandle(tab_window); + return false; +} + +} + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); }\ + while (false) + REGISTER_TAB_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // Registers our private events. + dispatcher->RegisterPermanentEventHandler( + ceee_event_names::kCeeeOnTabUnmapped, CeeeUnmapTabEventHandler); + + // And now register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabCreated, + CreateTab::EventHandler); + + // For OnTabUpdate, we receive 2 from events_funnel, and add a Tab Parameter. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabUpdated, + ConvertTabIdEventHandler<2, true>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabAttached, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabDetached, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabMoved, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabRemoved, + ConvertTabIdEventHandler<1, false>); + dispatcher->RegisterPermanentEventHandler( + ext_event_names::kOnTabSelectionChanged, + ConvertTabIdEventHandler<2, false>); +} + +bool TabApiResult::CreateTabValue(int tab_id, long index) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + + if (window_utils::WindowHasNoThread(tab_window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + + if (!IsTabWindowClass(tab_window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get tab info."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + TabInfo tab_info; + HRESULT hr = executor->GetTabInfo(&tab_info); + if (FAILED(hr)) { + LOG(WARNING) << "Executor failed to get tab info." << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + + scoped_ptr<DictionaryValue> result(new DictionaryValue()); + result->SetInteger(ext::kIdKey, tab_id); + + // The window ID is just the window handle of the frame window, which is the + // top-level ancestor of this window. + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + if (frame_window == tab_window || + !window_utils::IsWindowClass(frame_window, + windows::kIeFrameWindowClass)) { + // If we couldn't get a valid parent frame window, then it must be because + // the frame window (and the tab then) has been closed by now or it lives + // under the hidden IE window. + DCHECK(!::IsWindow(tab_window) || window_utils::IsWindowClass(frame_window, + windows::kHiddenIeFrameWindowClass)); + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + result->SetInteger(ext::kWindowIdKey, frame_window_id); + + // Only the currently selected tab has the VS_VISIBLE style. + result->SetBoolean(ext::kSelectedKey, TRUE == ::IsWindowVisible(tab_window)); + + result->SetString(ext::kUrlKey, com::ToString(tab_info.url)); + result->SetString(ext::kTitleKey, com::ToString(tab_info.title)); + + std::string status = ext::kStatusValueComplete; + if (tab_info.status == kCeeeTabStatusLoading) + status = ext::kStatusValueLoading; + else + DCHECK(tab_info.status == kCeeeTabStatusComplete) << "New Status???"; + + result->SetString(ext::kStatusKey, status); + + if (tab_info.fav_icon_url != NULL) { + result->SetString(ext::kFavIconUrlKey, + com::ToString(tab_info.fav_icon_url)); + } + + // When enumerating all tabs, we already have the index + // so we can save an IPC call. + if (index == -1) { + // We need another executor to get the index from the frame window thread. + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get tab index."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + hr = executor->GetTabIndex(reinterpret_cast<CeeeWindowHandle>(tab_window), + &index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get tab info for tab: " << std::hex << tab_id << + ". " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + } + result->SetInteger(ext::kIndexKey, static_cast<int>(index)); + + result->SetBoolean(ext::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + + if (value_ == NULL) { + value_.reset(result.release()); + } else { + DCHECK(value_->IsType(Value::TYPE_LIST)); + ListValue* list = reinterpret_cast<ListValue*>(value_.get()); + list->Append(result.release()); + } + return true; +} + +bool TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + const DictionaryValue& input_dict, + const Value* saved_window_value, + HWND* tab_window, + ApiDispatcher* dispatcher) { + int tab_id = 0; + bool success = input_dict.GetInteger(ext::kIdKey, &tab_id); + DCHECK(success && tab_id != 0) << "The input_dict MUST have a tab ID!!!"; + DCHECK(dispatcher != NULL); + HWND input_tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (tab_window != NULL) + *tab_window = input_tab_window; + + if (saved_window_value == NULL) + return true; + + DCHECK(saved_window_value->IsType(Value::TYPE_INTEGER)); + int saved_window_id = 0; + success = saved_window_value->GetAsInteger(&saved_window_id); + DCHECK(success && saved_window_id != 0); + + HWND frame_window = NULL; + int frame_window_id = 0; + if (!input_dict.GetInteger(ext::kWindowIdKey, &frame_window_id)) { + // If the parent window is not specified, it is easy to fetch it ourselves. + frame_window = window_utils::GetTopLevelParent(input_tab_window); + frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + DCHECK_NE(0, frame_window_id); + } else { + frame_window = dispatcher->GetWindowHandleFromId(frame_window_id); + DCHECK_EQ(window_utils::GetTopLevelParent(input_tab_window), frame_window); + } + + return frame_window_id == saved_window_id; +} + +bool GetIntegerFromValue( + const Value& value, const char* key_name, int* out_value) { + switch (value.GetType()) { + case Value::TYPE_INTEGER: { + return value.GetAsInteger(out_value); + } + case Value::TYPE_DICTIONARY: { + const DictionaryValue* dict = static_cast<const DictionaryValue*>(&value); + if (dict->HasKey(key_name)) + return dict->GetInteger(key_name, out_value); + *out_value = 0; + return true; + } + case Value::TYPE_LIST: { + const ListValue* args_list = static_cast<const ListValue*>(&value); + Value* anonymous_value = NULL; + if (!args_list->Get(0, &anonymous_value)) { + // If given an empty list value, we return 0 so that the frame window is + // fetched. + *out_value = 0; + return true; + } + DCHECK(anonymous_value != NULL); + return GetIntegerFromValue(*anonymous_value, key_name, out_value); + } + case Value::TYPE_NULL: { + // If given an empty list value, we return 0 so that the frame window is + // fetched. + *out_value = 0; + return true; + } + default: { + return false; + } + } +} + +HWND TabApiResult::GetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified) { + int window_id = 0; + if (!GetIntegerFromValue(args, ext::kWindowIdKey, &window_id)) { + NOTREACHED() << "Invalid Arguments."; + return NULL; + } + + HWND frame_window = NULL; + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + if (window_id != 0) + frame_window = dispatcher->GetWindowHandleFromId(window_id); + + if (!frame_window) { + // TODO(mad@chromium.org): We currently don't have access to the + // actual 'current' window from the point of view of the extension + // API caller. Use one of the top windows for now. bb2255140 + window_utils::FindDescendentWindow(NULL, windows::kIeFrameWindowClass, + true, &frame_window); + if (specified != NULL) + *specified = false; + } else { + if (specified != NULL) + *specified = true; + } + + if (!frame_window) { + return NULL; + } + + if (!window_utils::IsWindowClass(frame_window, windows::kIeFrameWindowClass)) + return NULL; + + return frame_window; +} + +void GetTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = kInvalidChromeSessionId; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + // -1 when we don't know the index. + if (result->CreateTabValue(tab_id, -1)) { + // CreateTabValue called PostError if it returned false. + result->PostResult(); + } +} + +void GetSelectedTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + + bool specified = false; + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(args, + &specified); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + // The selected tab is the only visible "TabWindowClass" window + // that is a child of the frame window. Enumerate child windows to find it, + // and fill in the value_ when we do. + HWND selected_tab = NULL; + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + if (!window_utils::FindDescendentWindow( + frame_window, windows::kIeTabWindowClass, true, &selected_tab)) { + if (specified) { + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + // We remember the frame window if it was specified so that we only + // react asynchronously to new tabs created in the same frame window. + result->SetValue(ext::kWindowIdKey, + Value::CreateIntegerValue(frame_window_id)); + } + DCHECK(dispatcher != NULL); + dispatcher->RegisterEphemeralEventHandler( + ext_event_names::kOnTabCreated, + GetSelectedTab::ContinueExecution, + // We don't want to destroy the result in the scoped_ptr when we pass + // it as user_data to GetSelectedTab::ContinueExecution(). + result.release()); + } else { + int tab_id = dispatcher->GetTabIdFromHandle(selected_tab); + DCHECK(tab_id != kInvalidChromeSessionId); + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); + } +} + +HRESULT GetSelectedTab::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(dispatcher != NULL); + DCHECK(user_data != NULL); + + // Any tab is good for us, so relaunch the search for a selected tab + // by using the frame window of the newly created tab. + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + scoped_ptr<ListValue> args_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &args_list); + if (input_dict == NULL) { + DCHECK(false) << "Event arguments are not a list with a dictionary in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + HWND tab_window = NULL; + if (!TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow(*input_dict, + result->GetValue(ext::kWindowIdKey), &tab_window, dispatcher)) { + // These are not the droids you are looking for. :-) + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + // We must reset the value and start from scratch in CreateTabValue. + // TODO(mad@chromium.org): We might be able to save a few steps if + // we support adding to existing value... Maybe... + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + result->set_value(NULL); + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); + return S_OK; +} + +bool GetAllTabsInWindowResult::Execute(BSTR tab_handles) { + // This is a list of tab_handles as it comes from the executor, not Chrome. + DCHECK(tab_handles); + scoped_ptr<ListValue> tabs_list; + if (!api_module_util::GetListFromJsonString(CW2A(tab_handles).m_psz, + &tabs_list)) { + NOTREACHED() << "Invalid tabs list BSTR: " << tab_handles; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + size_t num_values = tabs_list->GetSize(); + if (num_values % 2 != 0) { + // Values should come in pairs, one for the id and another one for the + // index. + NOTREACHED() << "Invalid tabs list BSTR: " << tab_handles; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + + // This will get populated by the calls to CreateTabValue in the loop below. + value_.reset(new ListValue()); + num_values /= 2; + for (size_t index = 0; index < num_values; ++index) { + int tab_handle = 0; + tabs_list->GetInteger(index * 2, &tab_handle); + int tab_index = -1; + tabs_list->GetInteger(index * 2 + 1, &tab_index); + int tab_id = dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle)); + DCHECK(tab_id != kInvalidChromeSessionId); + if (!CreateTabValue(tab_id, tab_index)) { + return false; + } + } + return true; +} + +void GetAllTabsInWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<GetAllTabsInWindowResult> result(CreateApiResult(request_id)); + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(args, NULL); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get list of tabs."; + result->PostError("Internal Error while getting all tabs in window."); + return; + } + + long num_tabs = 0; + CComBSTR tab_handles; + HRESULT hr = executor->GetTabs(&tab_handles); + if (FAILED(hr)) { + DCHECK(tab_handles == NULL); + LOG(ERROR) << "Failed to get list of tabs for window: " << std::hex << + frame_window << ". " << com::LogHr(hr); + result->PostError("Internal Error while getting all tabs in window."); + return; + } + + // Execute posted an error if it returns false. + if (result->Execute(tab_handles)) + result->PostResult(); +} + +void UpdateTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = 0; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + if (window_utils::WindowHasNoThread(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + DictionaryValue* update_props = NULL; + if (!args.GetDictionary(1, &update_props)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + if (update_props->HasKey(ext::kUrlKey)) { + std::wstring url; + if (!update_props->GetString(ext::kUrlKey, &url)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to navigate tab."; + result->PostError("Internal error trying to update tab."); + return; + } + HRESULT hr = executor->Navigate(CComBSTR(url.c_str()), 0, + CComBSTR(L"_top")); + // Don't DCHECK here, see the comment at the bottom of + // CeeeExecutor::Navigate(). + if (FAILED(hr)) { + LOG(ERROR) << "Failed to navigate tab: " << std::hex << tab_id << + " to " << url << ". " << com::LogHr(hr); + result->PostError("Internal error trying to update tab."); + return; + } + } + + if (update_props->HasKey(ext::kSelectedKey)) { + bool selected = false; + if (!update_props->GetBoolean(ext::kSelectedKey, &selected)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // We only take action if the user wants to select the tab; this function + // does not actually let you deselect a tab. + if (selected) { + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(window_utils::GetTopLevelParent(tab_window), + IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get a frame executor to select tab."; + result->PostError("Internal error trying to select tab."); + return; + } + HRESULT hr = frame_executor->SelectTab( + reinterpret_cast<CeeeWindowHandle>(tab_window)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to select tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal error trying to select tab."); + return; + } + } + } + // TODO(mad@chromium.org): Check if we need to wait for the + // tabs.onUpdated event to make sure that the update was fully + // completed (e.g., Navigate above is async). + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); +} + +void RemoveTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(window_utils::GetTopLevelParent(tab_window), + IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get a frame executor to select tab."; + result->PostError("Internal error trying to select tab."); + return; + } + HRESULT hr = frame_executor->RemoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to remove tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal error trying to remove tab."); + return; + } + + // Now we must wait for the tab removal to be completely done before + // posting the response back to Chrome Frame. + // And we remember the tab identifier so that we can recognize the event. + result->SetValue(ext::kTabIdKey, Value::CreateIntegerValue(tab_id)); + dispatcher->RegisterEphemeralEventHandler(ext_event_names::kOnTabRemoved, + RemoveTab::ContinueExecution, + result.release()); +} + +HRESULT RemoveTab::ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + + scoped_ptr<ListValue> args_list; + int tab_id = 0; + if (!api_module_util::GetListAndIntegerValue(input_args, &args_list, + &tab_id)) { + NOTREACHED() << "Event arguments are not a list with an integer in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + const Value* saved_tab_value = result->GetValue(ext::kTabIdKey); + DCHECK(saved_tab_value != NULL && + saved_tab_value->IsType(Value::TYPE_INTEGER)); + int saved_tab_id = 0; + bool success = saved_tab_value->GetAsInteger(&saved_tab_id); + DCHECK(success && saved_tab_id != 0); + if (saved_tab_id == tab_id) { + // The tabs.remove callback doesn't have any arguments. + result->set_value(NULL); + result->PostResult(); + return S_OK; + } else { + // release doesn't destroy result, we need to keep it for next try. + result.release(); + return S_FALSE; // S_FALSE keeps us in the queue. + } +} + +void CreateTab::Execute(const ListValue& args, int request_id) { + // TODO(joi@chromium.org) Handle setting remaining tab properties + // ('title' and 'favIconUrl') if/when CE adds them (this is per a + // TODO for rafaelw@chromium.org in the extensions code). + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + DictionaryValue* input_dict = NULL; + if (!args.GetDictionary(0, &input_dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + bool specified = false; + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(*input_dict, + &specified); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + // In case the frame window wasn't specified, we must remember it for later + // use when we react to events below. + if (specified) { + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + result->SetValue( + ext::kWindowIdKey, Value::CreateIntegerValue(frame_window_id)); + } + + std::string url_string(chrome::kAboutBlankURL); // default if no URL provided + if (input_dict->HasKey(ext::kUrlKey)) { + if (!input_dict->GetString(ext::kUrlKey, &url_string)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + GURL url(url_string); + if (!url.is_valid()) { + // TODO(joi@chromium.org) See if we can support absolute paths in IE (see + // extension_tabs_module.cc, AbsolutePath function and its uses) + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kInvalidUrlError, url_string)); + return; + } + // Remember the URL, we will use it to recognize the event below. + result->SetValue(ext::kUrlKey, Value::CreateStringValue(url_string)); + } + + bool selected = true; + if (input_dict->HasKey(ext::kSelectedKey)) { + if (!input_dict->GetBoolean(ext::kSelectedKey, &selected)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + } + + if (input_dict->HasKey(ext::kIndexKey)) { + int index = -1; + if (!input_dict->GetInteger(ext::kIndexKey, &index)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + result->SetValue(ext::kIndexKey, Value::CreateIntegerValue(index)); + } + + // We will have some work pending, even after we completed the tab creation, + // because the tab creation itself is asynchronous and we must wait for it + // to complete before we can post the complete result. + + // UNFORTUNATELY, this scheme doesn't work in protected mode for some reason. + // So bb2284073 & bb2492252 might still occur there. + std::wstring url_wstring = UTF8ToWide(url_string); + if (base::win::GetVersion() < base::win::VERSION_VISTA) { + CComPtr<IWebBrowser2> browser; + HRESULT hr = ie_util::GetWebBrowserForTopLevelIeHwnd( + frame_window, NULL, &browser); + DCHECK(SUCCEEDED(hr)) << "Can't get the browser for window: " << + frame_window; + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + long flags = selected ? navOpenInNewTab : navOpenInBackgroundTab; + hr = browser->Navigate(CComBSTR(url_wstring.c_str()), + &CComVariant(flags), + &CComVariant(L"_blank"), + &CComVariant(), // Post data + &CComVariant()); // Headers + DCHECK(SUCCEEDED(hr)) << "Failed to create tab. " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError("Internal error while trying to create tab."); + return; + } + } else { + // To create a new tab, we find an existing tab in the desired window (there + // is always at least one), and use it to navigate to a new tab. + HWND existing_tab = NULL; + bool success = window_utils::FindDescendentWindow( + frame_window, windows::kIeTabWindowClass, false, &existing_tab); + DCHECK(success && existing_tab != NULL) << + "Can't find an existing tab for" << frame_window; + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(existing_tab, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to create a tab."; + result->PostError("Internal error while trying to create tab."); + return; + } + + long flags = selected ? navOpenInNewTab : navOpenInBackgroundTab; + HRESULT hr = executor->Navigate(CComBSTR(url_wstring.c_str()), + flags, CComBSTR(L"_blank")); + // We can DCHECK here because navigating to a new tab shouldn't fail as + // described in the comment at the bottom of CeeeExecutor::Navigate(). + DCHECK(SUCCEEDED(hr)) << "Failed to create tab. " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError("Internal error while trying to create tab."); + return; + } + } + + // And now we must wait for the new tab to be created before we can respond. + dispatcher->RegisterEphemeralEventHandler( + ext_event_names::kOnTabCreated, + CreateTab::ContinueExecution, + // We don't want to destroy the result in the scoped_ptr when we pass + // it as user_data to CreateTab::ContinueExecution(). + result.release()); +} + +HRESULT CreateTab::ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + // Check if it has been created with the same info we were created for. + scoped_ptr<ListValue> args_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &args_list); + if (input_dict == NULL) { + DCHECK(false) << "Event arguments are not a list with a dictionary in it."; + result->PostError(api_module_constants::kInvalidArgumentsError); + return E_INVALIDARG; + } + + HWND tab_window = NULL; + if (!TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow(*input_dict, + result->GetValue(ext::kWindowIdKey), &tab_window, dispatcher)) { + // These are not the droids you are looking for. :-) + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + std::string event_url; + bool success = input_dict->GetString(ext::kUrlKey, &event_url); + DCHECK(success) << "The event MUST send a URL!!!"; + // if we didn't specify a URL, we should have navigated to about blank. + std::string requested_url(chrome::kAboutBlankURL); + // Ignore failures here, we fall back to the default about blank. + const Value* url_value = result->GetValue(ext::kUrlKey); + DCHECK(url_value != NULL && url_value->IsType(Value::TYPE_STRING)); + if (url_value != NULL && url_value->IsType(Value::TYPE_STRING)) { + bool success = url_value->GetAsString(&requested_url); + DCHECK(success) << "url_value->GetAsString() Failed!"; + } + + if (GURL(event_url) != GURL(requested_url)) { + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + // We can't rely on selected, since it may have changed if + // another tab creation was made before we got to broadcast the completion + // of this one, so we will assume this one is ours. + + // Now move the tab to desired index if specified, we couldn't do it until we + // had a tab_id. + long destination_index = -1; + const Value* index_value = result->GetValue(ext::kIndexKey); + if (index_value != NULL) { + DCHECK(index_value->IsType(Value::TYPE_INTEGER)); + int destination_index_int = -1; + bool success = index_value->GetAsInteger(&destination_index_int); + DCHECK(success) << "index_value->GetAsInteger()"; + + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(frame_window, __uuidof(ICeeeWindowExecutor), + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal error while trying to move created tab."); + return E_UNEXPECTED; + } + + destination_index = static_cast<long>(destination_index_int); + HRESULT hr = frame_executor->MoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window), destination_index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to move tab: " << std::hex << tab_window << ". " << + com::LogHr(hr); + result->PostError("Internal error while trying to move created tab."); + return E_UNEXPECTED; + } + } + + // We must reset current state before calling CreateTabValue. + result->set_value(NULL); + // TODO(mad@chromium.org): Do we need to go through CreateTabValue? + // Maybe we already have enough info available to create the + // response??? + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + if (result->CreateTabValue(tab_id, destination_index)) + result->PostResult(); + return S_OK; +} + +bool CreateTab::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + *converted_args = input_args; + + scoped_ptr<ListValue> input_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &input_list); + if (input_dict == NULL) { + DCHECK(false) << "Input arguments are not a list with a dictionary in it."; + return false; + } + + // Check if we got the index, this would mean we already have all we need. + int int_value = -1; + if (input_dict->GetInteger(ext::kIndexKey, &int_value)) { + // We should also have all other non-optional values + DCHECK(input_dict->GetInteger(ext::kWindowIdKey, &int_value)); + bool bool_value = false; + DCHECK(input_dict->GetBoolean(ext::kSelectedKey, &bool_value)); + return false; + } + + // Get the complete tab info from the tab_handle coming from IE + int tab_handle = reinterpret_cast<int>(INVALID_HANDLE_VALUE); + bool success = input_dict->GetInteger(ext::kIdKey, &tab_handle); + DCHECK(success) << "Couldn't get the tab ID key from the input args."; + int tab_id = dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle)); + DCHECK(tab_id != kInvalidChromeSessionId); + + TabApiResult result(TabApiResult::kNoRequestId); + if (result.CreateTabValue(tab_id, -1)) { + input_list->Set(0, result.value()->DeepCopy()); + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; + } else { + // Don't DCHECK, this can happen if we close the window while tabs are + // being created. + // TODO(mad@chromium.org): Find a way to DCHECK that the window is + // actually closing. + return false; + } +} + +void MoveTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = 0; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + DictionaryValue* update_props = NULL; + if (!args.GetDictionary(1, &update_props)) { + NOTREACHED() << "Can't get update properties from dictionary argument"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + if (update_props->HasKey(ext::kWindowIdKey)) { + // TODO(joi@chromium.org) Move to shared constants file + result->PostError("Moving tabs between windows is not supported."); + return; + } + + int new_index = -1; + if (!update_props->GetInteger(ext::kIndexKey, &new_index)) { + NOTREACHED() << "Can't get tab index from update properties."; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal Error while trying to move tab."); + return; + } + + HRESULT hr = frame_executor->MoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window), new_index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to move tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to move tab."); + return; + } + if (result->CreateTabValue(tab_id, new_index)) + result->PostResult(); +} + +ApiDispatcher::InvocationResult* TabsInsertCode::ExecuteImpl( + const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr) { + scoped_ptr<ApiDispatcher::InvocationResult> result( + CreateApiResult(request_id)); + // TODO(ericdingle@chromium.org): This needs to support when NULL is + // sent in as the first parameter. + if (!args.GetInteger(0, tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return NULL; + } + + DictionaryValue* dict; + if (!args.GetDictionary(1, &dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return NULL; + } + + // The dictionary should have either a code property or a file property, + // but not both. + std::string code; + std::string file; + if (dict->HasKey(ext::kCodeKey) && dict->HasKey(ext::kFileKey)) { + result->PostError(ext::kMoreThanOneValuesError); + return NULL; + } else if (dict->HasKey(ext::kCodeKey)) { + dict->GetString(ext::kCodeKey, &code); + } else if (dict->HasKey(ext::kFileKey)) { + dict->GetString(ext::kFileKey, &file); + } else { + result->PostError(ext::kNoCodeOrFileToExecuteError); + return NULL; + } + + // All frames is optional. If not specified, the default value is false. + bool all_frames; + if (!dict->GetBoolean(ext::kAllFramesKey, &all_frames)) + all_frames = false; + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(*tab_id); + if (!TabApiResult::IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(*tab_id))); + return NULL; + } + + CComPtr<ICeeeTabExecutor> tab_executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&tab_executor)); + if (tab_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal Error while trying to insert code in tab."); + return NULL; + } + + *hr = tab_executor->InsertCode(CComBSTR(code.c_str()), + CComBSTR(file.c_str()), + all_frames, + type); + return result.release(); +} + +void TabsExecuteScript::Execute(const ListValue& args, int request_id) { + int tab_id; + HRESULT hr = S_OK; + scoped_ptr<ApiDispatcher::InvocationResult> result( + TabsInsertCode::ExecuteImpl( + args, request_id, kCeeeTabCodeTypeJs, &tab_id, &hr)); + if (result.get() == NULL) + return; + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to execute script in tab: " << + std::hex << + tab_id << + ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to execute script in tab."); + } else { + result->PostResult(); + } +} + +void TabsInsertCSS::Execute(const ListValue& args, int request_id) { + int tab_id; + HRESULT hr = S_OK; + scoped_ptr<ApiDispatcher::InvocationResult> result( + TabsInsertCode::ExecuteImpl( + args, request_id, kCeeeTabCodeTypeCss, &tab_id, &hr)); + if (result.get() == NULL) + return; + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to insert CSS in tab: " << + std::hex << + tab_id << + ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to insert CSS in tab."); + } else { + result->PostResult(); + } +} + +TabInfo::TabInfo() { + url = NULL; + title = NULL; + status = kCeeeTabStatusLoading; + fav_icon_url = NULL; +} + +TabInfo::~TabInfo() { + Clear(); +} + +void TabInfo::Clear() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(url); + url = NULL; + ::SysFreeString(title); + title = NULL; + ::SysFreeString(fav_icon_url); + fav_icon_url = NULL; + + status = kCeeeTabStatusLoading; +} + +} // namespace tab_api diff --git a/ceee/ie/broker/tab_api_module.h b/ceee/ie/broker/tab_api_module.h new file mode 100644 index 0000000..50c0f69 --- /dev/null +++ b/ceee/ie/broker/tab_api_module.h @@ -0,0 +1,148 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation. + +#ifndef CEEE_IE_BROKER_TAB_API_MODULE_H_ +#define CEEE_IE_BROKER_TAB_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +#include "toolband.h" //NOLINT + +namespace tab_api { + +class TabApiResult; +typedef ApiResultCreator<TabApiResult> TabApiResultCreator; + +// Registers all Tab API Invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class TabApiResult : public common_api::CommonApiResult { + public: + explicit TabApiResult(int request_id) + : common_api::CommonApiResult(request_id) { + } + + // Retrieves the frame window to use from the arguments provided, or the + // current frame window if none was specified in arguments. + // Will set @p specified to true if the window was specified and @p specified + // isn't NULL. It can be NULL if caller doesn't need to know. + virtual HWND GetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified); + + // Creates a value object with the information for a tab, as specified + // by the API definition. + // @param tab_id The ID of the tab we want the create a value for. + // @param index The index of the tab if already known, -1 otherwise. + // @return true for success, false for failure and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual bool CreateTabValue(int tab_id, long index); + + // Check if saved_dict has a specified frame window and if so compares it + // to the frame window that is contained in the given input_dict or the + // grand parent of the tab window found in the input_dict and returned in + // tab_window. + static bool IsTabFromSameOrUnspecifiedFrameWindow( + const DictionaryValue& input_dict, + const Value* saved_window_value, + HWND* tab_window, + ApiDispatcher* dispatcher); +}; + +class GetTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetSelectedTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +class GetAllTabsInWindowResult : public TabApiResult { + public: + explicit GetAllTabsInWindowResult(int request_id) : TabApiResult(request_id) { + } + + // Populates the result with all tabs in the given JSON encoded list. + virtual bool Execute(BSTR tab_handles); +}; + +class GetAllTabsInWindow + : public ApiResultCreator<GetAllTabsInWindowResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class UpdateTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class RemoveTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +class CreateTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // We need to wait for the new tab to complete the result response. + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); + // And we also have a PermanentEventHandler too... + // To properly convert and complete the event arguments. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class MoveTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class TabsInsertCode : public ApiResultCreator<> { + protected: + // Will return NULL after calling PostError on the result upon failure. + ApiDispatcher::InvocationResult* ExecuteImpl(const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr); +}; + +class TabsExecuteScript : public TabsInsertCode { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class TabsInsertCSS : public TabsInsertCode { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Helper class to handle the data cleanup. +class TabInfo : public CeeeTabInfo { + public: + TabInfo(); + ~TabInfo(); + // Useful for reuse in unit tests. + void Clear(); +}; + +} // namespace tab_api + +#endif // CEEE_IE_BROKER_TAB_API_MODULE_H_ diff --git a/ceee/ie/broker/tab_api_module_unittest.cc b/ceee/ie/broker/tab_api_module_unittest.cc new file mode 100644 index 0000000..8f1f4df --- /dev/null +++ b/ceee/ie/broker/tab_api_module_unittest.cc @@ -0,0 +1,1452 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/json/json_writer.h" +#include "base/json/json_reader.h" +#include "base/win/windows_version.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +namespace { + +using tab_api::CreateTab; +using tab_api::GetAllTabsInWindow; +using tab_api::GetAllTabsInWindowResult; +using tab_api::GetSelectedTab; +using tab_api::GetTab; +using tab_api::MoveTab; +using tab_api::RegisterInvocations; +using tab_api::RemoveTab; +using tab_api::TabApiResult; +using tab_api::TabsInsertCode; +using tab_api::UpdateTab; + +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::CopyInterfaceToArgument; +using testing::CopyStringToArgument; +using testing::Gt; +using testing::InstanceCountMixin; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::Sequence; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::VariantPointerEq; + +const int kGoodTabWindowId = 99; +const int kGoodFrameWindowId = 88; +const int kBadWindowId = 666; + +const CeeeWindowHandle kGoodTabWindowHandle = kGoodTabWindowId + 1; +const CeeeWindowHandle kGoodFrameWindowHandle = kGoodFrameWindowId + 1; +const CeeeWindowHandle kBadWindowHandle = kBadWindowId + 1; + +const HWND kGoodTabWindow = reinterpret_cast<HWND>(kGoodTabWindowHandle); +const HWND kGoodFrameWindow = reinterpret_cast<HWND>(kGoodFrameWindowHandle); +const HWND kBadWindow = reinterpret_cast<HWND>(kBadWindowHandle); + +const int kTabIndex = 26; +const wchar_t kClassName[] = L"TabWindowClass"; +const int kRequestId = 43; + +TEST(TabApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(7)); + RegisterInvocations(&disp); +} + +class MockTabApiResult : public TabApiResult { + public: + explicit MockTabApiResult(int request_id) : TabApiResult(request_id) {} + MOCK_METHOD2(GetSpecifiedOrCurrentFrameWindow, HWND(const Value&, bool*)); + MOCK_METHOD2(CreateTabValue, bool(int, long)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + // Need to overload to bypass the mock. + HWND CallGetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified) { + return TabApiResult::GetSpecifiedOrCurrentFrameWindow(args, specified); + } + bool CallCreateTabValue(int tab_id, long index) { + return TabApiResult::CreateTabValue(tab_id, index); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +// TODO(mad@chromium.org) TODO(hansl@google.com): Unify tests between +// {window,tab}_module_api. Create a base class for executor mocker +// tests, since we now have two very similar classes. Consider adding +// the cookie and infobar API tests too. +class TabApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockTabExecutor::CreateInitialized( + &mock_tab_executor_, &mock_tab_executor_keeper_)); + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor_, &mock_window_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_window_executor_keeper_.Release(); + mock_tab_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + void AlwaysMockGetTabExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void MockGetTabExecutorOnce(MockApiDispatcher* api_dispatcher, HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void MockGetWindowExecutorOnce(MockApiDispatcher* api_dispatcher, + HWND window) { + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + void AlwaysMockGetWindowExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockTabExecutor* mock_tab_executor_; + testing::MockWindowExecutor* mock_window_executor_; + + private: + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); + }; + // We should never get to the postman, we mock all the calls getting there. + // So we simply instantiate it strict and it will register itself as the + // one and only singleton to use all the time. + CComObjectStackEx< StrictMock< MockChromePostman > > postman_; + // To control the life span of the tab executor. + CComPtr<ICeeeTabExecutor> mock_tab_executor_keeper_; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; +}; + +TEST_F(TabApiTests, CreateTabValueErrorHandling) { + testing::LogDisabler no_dchecks; + + // Window with no thread. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillOnce(Return(true)); + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kGoodFrameWindow)). + WillRepeatedly(Return(kGoodFrameWindowId)); + + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Not an IE Frame. + invocation_result.set_value(NULL); + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillRepeatedly(Return(false)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(false)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // No Executor for the tab window. + invocation_result.set_value(NULL); + EXPECT_CALL(window_utils, GetTopLevelParent(_)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(user32_, IsWindowVisible(_)).WillRepeatedly(Return(FALSE)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Failing executor for the tab info. + invocation_result.set_value(NULL); + MockGetTabExecutorOnce(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // No Executor for the frame window. + // We only mock once at a time so that the executor for the tab doesn't get + // confused by the one for the frame window, or vice versa. + invocation_result.set_value(NULL); + MockGetTabExecutorOnce(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Failing executor for the tab index. + invocation_result.set_value(NULL); + AlwaysMockGetWindowExecutor(&invocation_result.mock_api_dispatcher_, + kGoodFrameWindow); + AlwaysMockGetTabExecutor(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_window_executor_, GetTabIndex(kGoodTabWindowHandle, + NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); +} + +TEST_F(TabApiTests, CreateTabValue) { + // Setup all we need from other for success. + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillRepeatedly(Return(false)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, GetTopLevelParent(_)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // Try a few different combination or parameters. + struct { + bool window_selected; + const char* status; + const char* fav_icon_url; + const char* title; + const char* url; + int index; + } tests[] = { + { false, ext::kStatusValueLoading, NULL, "Mine", "http://icietla", 1 }, + { false, ext::kStatusValueComplete, "Here", "Yours", "https://secure", 32 }, + { true, ext::kStatusValueLoading, "There", "There's", "Just a string", -1 }, + { true, ext::kStatusValueComplete, NULL, "Unknown", "Boo!!!", -1 }, + }; + + for (int i = 0; i < arraysize(tests); ++i) { + EXPECT_CALL(user32_, IsWindowVisible(_)).WillOnce( + Return(tests[i].window_selected ? TRUE : FALSE)); + CeeeTabInfo tab_info; + // The allocation within the tab_info will be freed by the caller of + // GetTabInfo which we mock below... + tab_info.url = ::SysAllocString(CA2W(tests[i].url).m_psz); + tab_info.title = ::SysAllocString(CA2W(tests[i].title).m_psz); + + if (strcmp(tests[i].status, ext::kStatusValueLoading) == 0) + tab_info.status = kCeeeTabStatusLoading; + else + tab_info.status = kCeeeTabStatusComplete; + + if (tests[i].fav_icon_url) { + tab_info.fav_icon_url = + ::SysAllocString(CA2W(tests[i].fav_icon_url).m_psz); + } else { + tab_info.fav_icon_url = NULL; + } + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + AlwaysMockGetTabExecutor(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + AlwaysMockGetWindowExecutor(&invocation_result.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_info), Return(S_OK))); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kGoodFrameWindow)). + WillOnce(Return(kGoodFrameWindowId)); + + if (tests[i].index == -1) { + EXPECT_CALL(*mock_window_executor_, GetTabIndex(kGoodTabWindowHandle, + NotNull())).WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), + Return(S_OK))); + } + if (i % 2 == 0) + invocation_result.set_value(new ListValue); + EXPECT_TRUE(invocation_result.CallCreateTabValue( + kGoodTabWindowId, tests[i].index)); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + const Value* result = NULL; + if (i % 2 == 0) { + EXPECT_TRUE(invocation_result.value()->IsType(Value::TYPE_LIST)); + const ListValue* list = static_cast<const ListValue*>( + invocation_result.value()); + Value * non_const_result = NULL; + EXPECT_TRUE(list->Get(0, &non_const_result)); + result = non_const_result; + } else { + result = invocation_result.value(); + } + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + const DictionaryValue* dict = static_cast<const DictionaryValue*>(result); + + int tab_id; + EXPECT_TRUE(dict->GetInteger(ext::kIdKey, &tab_id)); + EXPECT_EQ(kGoodTabWindowId, tab_id); + + int window_id; + EXPECT_TRUE(dict->GetInteger(ext::kWindowIdKey, &window_id)); + EXPECT_EQ(kGoodFrameWindowId, window_id); + + bool selected = false; + EXPECT_TRUE(dict->GetBoolean(ext::kSelectedKey, &selected)); + EXPECT_EQ(tests[i].window_selected, selected); + + std::string url; + EXPECT_TRUE(dict->GetString(ext::kUrlKey, &url)); + EXPECT_STREQ(tests[i].url, url.c_str()); + + std::string title; + EXPECT_TRUE(dict->GetString(ext::kTitleKey, &title)); + EXPECT_STREQ(tests[i].title, title.c_str()); + + std::string status; + EXPECT_TRUE(dict->GetString(ext::kStatusKey, &status)); + EXPECT_STREQ(tests[i].status, status.c_str()); + + std::string fav_icon_url; + if (tests[i].fav_icon_url == NULL) { + EXPECT_FALSE(dict->GetString(ext::kFavIconUrlKey, &fav_icon_url)); + } else { + EXPECT_TRUE(dict->GetString(ext::kFavIconUrlKey, &fav_icon_url)); + EXPECT_STREQ(tests[i].fav_icon_url, fav_icon_url.c_str()); + } + + int index = -1; + EXPECT_TRUE(dict->GetInteger(ext::kIndexKey, &index)); + if (tests[i].index == -1) + EXPECT_EQ(kTabIndex, index); + else + EXPECT_EQ(tests[i].index, index); + } +} + +TEST(TabApi, IsTabWindowClassWithNull) { + testing::LogDisabler no_dchecks; + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(NULL)); +} + +TEST(TabApi, IsTabWindowClassWithNonWindow) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillRepeatedly(Return(FALSE)); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, IsTabWindowClassWrongClassName) { + const wchar_t kBadClassName[] = L"BadWindowClass"; + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kBadClassName), + Return(arraysize(kBadClassName)))); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, IsTabWindowClassStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_TRUE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, GetSpecifiedOrCurrentFrameWindow) { + testing::LogDisabler no_dchecks; + + StrictMock<testing::MockWindowUtils> window_utils; + scoped_ptr<Value> bad_args(Value::CreateRealValue(4.7)); + DictionaryValue bad_dict_args; // no window ID key. + DictionaryValue good_dict_args; + int window1_id = 77; + HWND window1 = reinterpret_cast<HWND>(window1_id); + good_dict_args.SetInteger(ext::kWindowIdKey, window1_id); + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + + // First, fail because no method finds us a window. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)) + .WillOnce(Return(false)); + HWND null_window = NULL; + EXPECT_EQ(null_window, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), NULL)); + + // Then, fail because we find a window that's not a window. + HWND window2 = reinterpret_cast<HWND>(88); + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillRepeatedly( + DoAll(SetArgumentPointee<3>(window2), Return(true))); + EXPECT_CALL(window_utils, IsWindowClass(window2, _)).WillOnce(Return(false)); + EXPECT_EQ(null_window, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), NULL)); + + // From now on, all windows are valid. + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + + // So succeed for once - we're using null args so we get the + // window from the FindDescendentWindow. + bool specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), &specified)); + EXPECT_FALSE(specified); + + ListValue good_list_with_null; + ASSERT_TRUE(good_list_with_null.Set(0, Value::CreateNullValue())); + specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_null, &specified)); + EXPECT_FALSE(specified); + + ListValue good_list_with_null_dict; + ASSERT_TRUE(good_list_with_null_dict.Set(0, new DictionaryValue())); + specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_null_dict, &specified)); + EXPECT_FALSE(specified); + + // From now on, we can expect those to always return consistent values. + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(window1_id)).WillRepeatedly(Return(window1)); + + // Get window from good args. + scoped_ptr<Value> good_int_args(Value::CreateIntegerValue(kGoodTabWindowId)); + ListValue good_list_with_int; + ASSERT_TRUE(good_list_with_int.Set(0, Value::CreateIntegerValue(window1_id))); + ListValue good_list_with_dict; + ASSERT_TRUE(good_list_with_dict.Set(0, good_dict_args.DeepCopy())); + + EXPECT_EQ(kGoodTabWindow, invocation_result. + CallGetSpecifiedOrCurrentFrameWindow(*good_int_args.get(), &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_dict_args, &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_int, &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_dict, &specified)); + EXPECT_TRUE(specified); +} + +TEST_F(TabApiTests, GetTab) { + testing::LogDisabler no_dchecks; + + ListValue good_args; + ASSERT_TRUE(good_args.Set(0, Value::CreateIntegerValue(kGoodTabWindowId))); + ListValue bad_window; + ASSERT_TRUE(bad_window.Set(0, Value::CreateIntegerValue(kBadWindowId))); + + // Mocking IsTabWindowClass. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, IsWindowClass(kBadWindow, _)). + WillRepeatedly(Return(false)); + + StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, GetTab> > + invocation; + invocation.AllocateNewResult(kRequestId); + + // Start with success. + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // No more successful calls. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kBadWindowId)).WillOnce(Return(kBadWindow)); + invocation.Execute(bad_window, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(TabApiTests, GetSelectedTab) { + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, GetSelectedTab> > + invocation; + // No frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce( + Return(static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Success + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(true), + Return(kGoodFrameWindow))); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow( + kGoodFrameWindow, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<3>(kGoodTabWindow), Return(true))); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + invocation.Execute(ListValue(), kRequestId); + + // TODO(mad@chromium.org): Try the async case. +} + +class MockGetAllTabsInWindowResult : public GetAllTabsInWindowResult { + public: + explicit MockGetAllTabsInWindowResult(int request_id) + : GetAllTabsInWindowResult(request_id) {} + MOCK_METHOD2(CreateTabValue, bool(int, long)); + MOCK_METHOD2(GetSpecifiedOrCurrentFrameWindow, HWND(const Value&, bool*)); + MOCK_METHOD1(Execute, bool(BSTR)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + bool CallExecute(BSTR tabs) { + return GetAllTabsInWindowResult::Execute(tabs); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +TEST_F(TabApiTests, GetAllTabsInWindowResult) { + testing::LogDisabler no_dchecks; + StrictMock<MockGetAllTabsInWindowResult> invocation_result( + TabApiResult::kNoRequestId); + + // Start with a few failure cases. + // Not a proper JSON string. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"Bla bla bla"); + invocation_result.set_value(NULL); + + // Proper JSON string, not being a list. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"{}"); + invocation_result.set_value(NULL); + + // Wrong number of elements in the list. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"[23]"); + invocation_result.set_value(NULL); + + // Empty is valid yet doesn't get CreateTabValue called. + EXPECT_TRUE(invocation_result.CallExecute(L"[]")); + invocation_result.set_value(NULL); + + // Successes. + int index = 24; + EXPECT_CALL(invocation_result, CreateTabValue(kGoodTabWindowId, index)). + WillOnce(Return(true)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + { + std::wostringstream args; + args << L"[" << kGoodTabWindowHandle << L"," << index << L"]"; + EXPECT_TRUE(invocation_result.CallExecute((BSTR)(args.str().c_str()))); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + invocation_result.set_value(NULL); + } + + { + std::wostringstream args; + Sequence seq; + struct id_map_struct { + int id; + HWND handle; + int value; + } const test_id_map[] = { + { 1, reinterpret_cast<HWND>(2), 3 }, + { 4, reinterpret_cast<HWND>(5), 6 }, + { 7, reinterpret_cast<HWND>(8), 9 }, + { 0 } + }; + + // Build the JSON list from the above test map. We also want to expect the + // correct underlying calls. + args << L"["; + for (int i = 0; test_id_map[i].id != 0; ++i) { + const id_map_struct& item = test_id_map[i]; + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabIdFromHandle(item.handle)).WillOnce(Return(item.id)); + EXPECT_CALL(invocation_result, CreateTabValue(item.id, item.value)). + InSequence(seq).WillOnce(Return(true)); + if (i) + args << L","; + args << reinterpret_cast<int>(item.handle) << L"," << item.value; + } + args << L"]"; + EXPECT_TRUE(invocation_result.CallExecute((BSTR)(args.str().c_str()))); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + } +} + +TEST_F(TabApiTests, GetAllTabsInWindow) { + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<GetAllTabsInWindowResult, + MockGetAllTabsInWindowResult, GetAllTabsInWindow>> invocation; + + // No frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce( + Return(static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // No Executor. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // Failing Executor. + // The executor classes are already strict from their base class impl. + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Successes. + invocation.AllocateNewResult(kRequestId); + BSTR tab_ids = ::SysAllocString(L""); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_ids), Return(S_OK))); + EXPECT_CALL(*invocation.invocation_result_, Execute(tab_ids)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); + // Execute freed tab_ids... + tab_ids = NULL; +} + +// TODO(mad@chromium.org): Test the event handling. +class MockUpdateTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + UpdateTab> > { +}; + +TEST_F(TabApiTests, UpdateTab) { + ListValue bad_window; + bad_window.Append(Value::CreateIntegerValue(kBadWindowId)); + + ListValue wrong_second_arg; + wrong_second_arg.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_second_arg.Append(Value::CreateStringValue(L"Angus")); + + ListValue wrong_url_type; + DictionaryValue* wrong_url_type_dictionary = new DictionaryValue(); + wrong_url_type_dictionary->SetInteger(ext::kUrlKey, 1); + wrong_url_type.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_url_type.Append(wrong_url_type_dictionary); + + ListValue wrong_selected_type; + DictionaryValue* wrong_selected_type_dictionary = new DictionaryValue(); + wrong_selected_type_dictionary->SetString(ext::kSelectedKey, L"yes"); + wrong_selected_type.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_selected_type.Append(wrong_selected_type_dictionary); + + ListValue good_url; + DictionaryValue* good_url_dictionary = new DictionaryValue(); + good_url_dictionary->SetString(ext::kUrlKey, "http://google.com"); + good_url.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + good_url.Append(good_url_dictionary); + + ListValue selected_true; + DictionaryValue* selected_true_dictionary = new DictionaryValue(); + selected_true_dictionary->SetBoolean(ext::kSelectedKey, true); + selected_true.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + selected_true.Append(selected_true_dictionary); + + testing::LogDisabler no_dchecks; + + MockUpdateTab invocation; + ListValue empty_list; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(empty_list, kRequestId); + + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kBadWindowId)).WillRepeatedly(Return(kBadWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + + // Not an IeServerClass. + invocation.AllocateNewResult(kRequestId); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kBadWindow, _)). + WillOnce(Return(false)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(bad_window, kRequestId); + + // Window has no thread. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, WindowHasNoThread(kBadWindow)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(bad_window, kRequestId); + + EXPECT_CALL(window_utils, WindowHasNoThread(_)).WillRepeatedly(Return(false)); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_second_arg, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_url_type, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_selected_type, kRequestId); + + // Can't get an executor for Navigate. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_url, kRequestId); + + // Failing Executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_url, kRequestId); + + // Can't get an executor for SelectTab. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(selected_true, kRequestId); + + // Failing Executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, SelectTab(kGoodTabWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(selected_true, kRequestId); + + // Successful Navigate. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(good_url, kRequestId); + + // Successful SelectTab. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, SelectTab(kGoodTabWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(selected_true, kRequestId); +} + +class MockRemoveTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + RemoveTab> > { +}; + +TEST_F(TabApiTests, RemoveTabExecute) { + testing::LogDisabler no_dchecks; + + MockRemoveTab invocation; + ListValue list_args; + + // Bad arguments. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_args, kRequestId); + + // Not a IeServerClass. + invocation.AllocateNewResult(kRequestId); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(false)); + + list_args.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Fail to get the executor. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Failing executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, RemoveTab(kGoodTabWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Success. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, RemoveTab(kGoodTabWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnTabRemoved), + RemoveTab::ContinueExecution, + invocation.invocation_result_.get())); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + // This will cause the invocation result to be released at the end of the + // test since the success case purposely does not delete it. + scoped_ptr<ApiDispatcher::InvocationResult> result( + invocation.invocation_result_.get()); + invocation.Execute(list_args, kRequestId); +} + +TEST_F(TabApiTests, RemoveTabContinueExecution) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockRemoveTab invocation; + + ListValue list_value; + list_value.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + std::string input_args; + base::JSONWriter::Write(&list_value, false, &input_args); + + // Bad arguments. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + HRESULT hr = invocation.CallContinueExecution(""); + EXPECT_HRESULT_FAILED(hr); + + // Mismatched tab IDs. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kTabIdKey, Value::CreateIntegerValue(1234)); + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kTabIdKey, Value::CreateIntegerValue(kGoodTabWindowId)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +// Mock the CoCreateInstance call that is used to create a new IE window. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetWebBrowserForTopLevelIeHwnd, + GetWebBrowserForTopLevelIeHwnd); + MOCK_STATIC_INIT_END() + MOCK_STATIC3(HRESULT, , GetWebBrowserForTopLevelIeHwnd, + HWND, IWebBrowser2*, IWebBrowser2**); +MOCK_STATIC_CLASS_END(MockIeUtil) + +// Mock static calls to TabApiResult. +MOCK_STATIC_CLASS_BEGIN(MockStaticTabApiResult) + MOCK_STATIC_INIT_BEGIN(MockStaticTabApiResult) + MOCK_STATIC_INIT2(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow, + IsTabFromSameOrUnspecifiedFrameWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC4(bool, , IsTabFromSameOrUnspecifiedFrameWindow, + const DictionaryValue&, const Value*, HWND*, ApiDispatcher*); +MOCK_STATIC_CLASS_END(MockStaticTabApiResult) + +// TODO(mad@chromium.org): Test the asynchronicity and the event handling. +class MockCreateTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + CreateTab> > { +}; + +TEST_F(TabApiTests, CreateTabExecute) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockCreateTab invocation; + + ListValue list_value; + DictionaryValue* list_dict = new DictionaryValue; + // list_dict will be freed by ListValue, yet we need to keep it around + // to set values into it. + list_value.Append(list_dict); + + // No Frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce(Return( + static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Can't get Executor/IWebBrowser2. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + StrictMock<MockIeUtil> mock_ie_util; + StrictMock<testing::MockWindowUtils> mock_window_utils; + bool pre_vista = base::win::GetVersion() < base::win::VERSION_VISTA; + if (pre_vista) { + EXPECT_CALL(mock_ie_util, GetWebBrowserForTopLevelIeHwnd( + kGoodFrameWindow, _, NotNull())).WillOnce(Return(E_FAIL)); + } else { + EXPECT_CALL(mock_window_utils, FindDescendentWindow( + kGoodFrameWindow, _, _, NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<3>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + } + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Navigate Fails. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance( + &browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + if (pre_vista) { + EXPECT_CALL(mock_ie_util, GetWebBrowserForTopLevelIeHwnd( + kGoodFrameWindow, _, NotNull())).WillRepeatedly(DoAll( + CopyInterfaceToArgument<2>(browser_keeper.p), Return(S_OK))); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + } else { + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(E_FAIL)); + } + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Success! + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + if (pre_vista) { + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(S_OK)); + } else { + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(S_OK)); + } + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnTabCreated), + CreateTab::ContinueExecution, + invocation.invocation_result_.get())); + + // This will cause the invocation result to be released at the end of the + // test since the success case purposely does not delete it. + scoped_ptr<ApiDispatcher::InvocationResult> result( + invocation.invocation_result_.get()); + invocation.Execute(list_value, kRequestId); +} + +TEST_F(TabApiTests, CreateTabContinueExecution) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockCreateTab invocation; + const char kUrl[] = "http://url/"; + const int kIndex = 0; + + ListValue list_value; + DictionaryValue* dict_value = new DictionaryValue; + list_value.Append(dict_value); + dict_value->SetInteger(ext::kWindowIdKey, kGoodTabWindowId); + dict_value->SetString(ext::kUrlKey, kUrl); + dict_value->SetInteger(ext::kIndexKey, kIndex); + + std::string input_args; + base::JSONWriter::Write(&list_value, false, &input_args); + + // No input dictionary. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + HRESULT hr = invocation.CallContinueExecution(""); + EXPECT_HRESULT_FAILED(hr); + + // IsTabFromSameOrUnspecifiedFrameWindow returns false. + invocation.AllocateNewResult(kRequestId); + StrictMock<MockStaticTabApiResult> tab_api_result; + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(Return(false)); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Mistmatched URLs. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kUrlKey, Value::CreateStringValue("http://other/")); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(Return(true)); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success with no index. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kUrlKey, Value::CreateStringValue(kUrl)); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success with an index. + invocation.AllocateNewResult(kRequestId); + ApiDispatcher::InvocationResult* result = invocation.invocation_result_.get(); + result->SetValue(ext::kUrlKey, Value::CreateStringValue(kUrl)); + result->SetValue(ext::kIndexKey, Value::CreateIntegerValue(kIndex)); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + + StrictMock<testing::MockWindowUtils> window_utils; + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillOnce(Return(kGoodTabWindow)); + + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kIndex)). + WillOnce(Return(S_OK)); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, kIndex)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +class MockMoveTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + MoveTab> > { +}; + +TEST_F(TabApiTests, MoveTab) { + testing::LogDisabler no_dchecks; + + // Empty list is not valid. + StrictMock<testing::MockWindowUtils> window_utils; + MockMoveTab invocation; + ListValue good_args; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // First entry should be an Integer. + invocation.AllocateNewResult(kRequestId); + DictionaryValue* good_args_dict = new DictionaryValue(); + good_args.Append(good_args_dict); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Good arg format, but not an IE Frame. + invocation.AllocateNewResult(kRequestId); + EXPECT_TRUE(good_args.Insert(0, Value::CreateIntegerValue(kGoodTabWindowId))); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillOnce(Return(false)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Wrong second list value. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_TRUE(good_args.Insert(1, Value::CreateNullValue())); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Unsupported window id key. + invocation.AllocateNewResult(kRequestId); + good_args.Remove(1, NULL); + good_args_dict->SetInteger(ext::kWindowIdKey, kBadWindowId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Wrong format for index key + invocation.AllocateNewResult(kRequestId); + good_args_dict->Remove(ext::kWindowIdKey, NULL); + good_args_dict->SetBoolean(ext::kIndexKey, false); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Can't get an executor + invocation.AllocateNewResult(kRequestId); + good_args_dict->SetInteger(ext::kIndexKey, kTabIndex); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Get the Executor to fail + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kTabIndex)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Success + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kTabIndex)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, kTabIndex)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); +} + +class MockTabsInsertCode + : public StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, + TabsInsertCode> > { + public: + // Overloaded to change the type of the return value for easier testing. + StrictMock<MockTabApiResult>* CallExecuteImpl(const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr) { + return static_cast<StrictMock<MockTabApiResult>*>(ExecuteImpl( + args, request_id, type, tab_id, hr)); + } + // Declare this so that this is not an abstract class. + virtual void Execute(const ListValue& args, int request_id) {} +}; + +TEST_F(TabApiTests, TabsInsertCode) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + int tab_id; + HRESULT hr; + + // Empty list is not valid. + MockTabsInsertCode invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + ListValue(), kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // First parameter should be an Integer. + ListValue good_args; + DictionaryValue* good_args_dict = new DictionaryValue(); + good_args.Append(good_args_dict); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but no values in dictionary. + EXPECT_TRUE(good_args.Insert(0, Value::CreateIntegerValue(kGoodTabWindowId))); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but both code and file values in dictionary. + good_args_dict->SetString(ext::kCodeKey, "alert(5);"); + good_args_dict->SetString(ext::kFileKey, "test.js"); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but not an IE Frame. + good_args_dict->Remove(ext::kFileKey, NULL); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillOnce(Return(false)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Can't get an executor. + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Executor failure, all_frames defaulted to false. + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, + InsertCode(_, _, false, kCeeeTabCodeTypeCss)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + scoped_ptr<StrictMock<MockTabApiResult>> result(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + EXPECT_EQ(kGoodTabWindowId, tab_id); + EXPECT_EQ(E_FAIL, hr); + + // Success, all_frames defaulted to false. + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, + InsertCode(_, _, false, kCeeeTabCodeTypeCss)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + result.reset(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + EXPECT_EQ(tab_id, kGoodTabWindowId); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success, set all_frames to true. + good_args_dict->SetBoolean(ext::kAllFramesKey, true); + EXPECT_CALL(*mock_tab_executor_, InsertCode(_, _, true, + kCeeeTabCodeTypeJs)).WillOnce(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + result.reset(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeJs, &tab_id, &hr)); + EXPECT_EQ(tab_id, kGoodTabWindowId); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(TabApiTests, IsTabFromSameOrUnspecifiedFrameWindow) { + // We need a mock for this now. + StrictMock<MockApiDispatcher> mock_api_dispatcher; + + // We expect these calls repeatedly + EXPECT_CALL(mock_api_dispatcher, GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(mock_api_dispatcher, GetWindowIdFromHandle(kGoodFrameWindow)). + WillRepeatedly(Return(kGoodFrameWindowId)); + EXPECT_CALL(mock_api_dispatcher, GetWindowHandleFromId(kGoodFrameWindowId)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // We always need a kIdKey value in the input_dict. + DictionaryValue input_dict; + input_dict.SetInteger(ext::kIdKey, kGoodTabWindowId); + // Start with no saved dict, so any input value is good. + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, NULL, NULL, &mock_api_dispatcher)); + // Also test that we are properly returned the input value. + HWND tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, NULL, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // Now check with the same value found as a grand parent. + FundamentalValue saved_window(kGoodFrameWindowId); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // Now check with the same value provided in the input_dict. + input_dict.SetInteger(ext::kWindowIdKey, kGoodFrameWindowId); + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // And now check the cases where they differ. + FundamentalValue other_saved_window(kGoodFrameWindowId + 1); + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + input_dict.Remove(ext::kWindowIdKey, NULL); + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); +} + +} // namespace diff --git a/ceee/ie/broker/webnavigation_api_module.cc b/ceee/ie/broker/webnavigation_api_module.cc new file mode 100644 index 0000000..e35bc51 --- /dev/null +++ b/ceee/ie/broker/webnavigation_api_module.cc @@ -0,0 +1,40 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebNavigation implementation that needs to reside on the broker +// side. + +#include "ceee/ie/broker/webnavigation_api_module.h" + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_util.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" + +namespace ext = extension_webnavigation_api_constants; + +namespace webnavigation_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { + // Register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeNavigate, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeRetarget, + api_module_util::ConvertTabIdInDictionary<ext::kSourceTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnCommitted, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnCompleted, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnDOMContentLoaded, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnErrorOccurred, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); +} + +} // namespace webnavigation_api diff --git a/ceee/ie/broker/webnavigation_api_module.h b/ceee/ie/broker/webnavigation_api_module.h new file mode 100644 index 0000000..5f7c24e --- /dev/null +++ b/ceee/ie/broker/webnavigation_api_module.h @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebNavigation implementation that needs to reside on the broker +// side. + +#ifndef CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ +#define CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ + +class ApiDispatcher; + +namespace webnavigation_api { + +// Registers permanent event handlers to convert tab window handles in event +// arguments into actual tab IDs. +void RegisterInvocations(ApiDispatcher* dispatcher); + +} // namespace webnavigation_api + +#endif // CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ diff --git a/ceee/ie/broker/webrequest_api_module.cc b/ceee/ie/broker/webrequest_api_module.cc new file mode 100644 index 0000000..77c1177 --- /dev/null +++ b/ceee/ie/broker/webrequest_api_module.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebRequest implementation that needs to reside on the broker +// side. + +#include "ceee/ie/broker/webrequest_api_module.h" + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_util.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" + +namespace ext = extension_webrequest_api_constants; + +namespace webrequest_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { + // Register the permanent event handler. + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeRequest, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); +} + +} // namespace webrequest_api diff --git a/ceee/ie/broker/webrequest_api_module.h b/ceee/ie/broker/webrequest_api_module.h new file mode 100644 index 0000000..9ace96c --- /dev/null +++ b/ceee/ie/broker/webrequest_api_module.h @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebRequest implementation that needs to reside on the broker +// side. + +#ifndef CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ +#define CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ + +class ApiDispatcher; + +namespace webrequest_api { + +// Registers permanent event handlers to convert tab window handles in event +// arguments into actual tab IDs. +void RegisterInvocations(ApiDispatcher* dispatcher); + +} // namespace webrequest_api + +#endif // CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ diff --git a/ceee/ie/broker/window_api_module.cc b/ceee/ie/broker/window_api_module.cc new file mode 100644 index 0000000..2115163 --- /dev/null +++ b/ceee/ie/broker/window_api_module.cc @@ -0,0 +1,639 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation. +// +// Window IDs are the HWND of the top-level frame window of IE. + +#include "ceee/ie/broker/window_api_module.h" + +#include <atlbase.h> +#include <iepmapi.h> +#include <sddl.h> +#include <set> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/url_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "googleurl/src/gurl.h" + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +namespace window_api { + +// We sometimes need to wait for window creation and removal to be completed +// even though we receive an event about them, these events sometimes arrive +// before the creation/removal is really totally completed. +const int kMaxDelayMs = 5000; // 5 seconds may be needed on slow machines. +const int kDelayMs = 50; + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); } while (false) + REGISTER_WINDOW_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + dispatcher->RegisterInvocation(CreateWindowFunction::function_name(), + NewApiInvocation< CreateWindowFunc >); + // And now register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnWindowRemoved, + RemoveWindow::EventHandler); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnWindowCreated, + CreateWindowFunc::EventHandler); +} + +bool WindowApiResult::UpdateWindowRect(HWND window, + const DictionaryValue* window_props) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + if (window_utils::WindowHasNoThread(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + if (!IsIeFrameClass(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + // Unspecified entries are set to -1 to let the executor know not to change + // them. + long left = -1; + long top = -1; + long width = -1; + long height = -1; + + if (window_props) { + int dim; + if (window_props->HasKey(ext::kLeftKey)) { + if (!window_props->GetInteger(ext::kLeftKey, &dim)) { + PostError("bad request"); + return false; + } + left = dim; + } + + if (window_props->HasKey(ext::kTopKey)) { + if (!window_props->GetInteger(ext::kTopKey, &dim)) { + PostError("bad request"); + return false; + } + top = dim; + } + + if (window_props->HasKey(ext::kWidthKey)) { + if (!window_props->GetInteger(ext::kWidthKey, &dim) || dim < 0) { + PostError("bad request"); + return false; + } + width = dim; + } + + if (window_props->HasKey(ext::kHeightKey)) { + if (!window_props->GetInteger(ext::kHeightKey, &dim) || dim < 0) { + PostError("bad request"); + return false; + } + height = dim; + } + } + + common_api::WindowInfo window_info; + if (left != -1 || top != -1 || width != -1 || height != -1) { + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to update window."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + HRESULT hr = executor->UpdateWindow(left, top, width, height, &window_info); + if (FAILED(hr)) { + LOG(ERROR) << "Couldn't update window: " << std::hex << window << ". " << + com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + SetResultFromWindowInfo(window, window_info, false); + return true; + } else { + return CreateWindowValue(window, false); + } +} + +void GetWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + if (!args.GetInteger(0, &window_id)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // CreateWindowValue validates the HWND. + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (result->CreateWindowValue(window, false)) + result->PostResult(); +} + +void GetCurrentWindow::Execute(const ListValue& args, int request_id) { + // TODO(mad@chromium.org): We currently don't have access to the + // actual 'current' window from the point of view of the extension + // API caller. Use the top window for now. bb2255140 + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + if (result->CreateWindowValue(result->TopIeWindow(), false)) + result->PostResult(); +} + +void GetLastFocusedWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + if (result->CreateWindowValue(result->TopIeWindow(), false)) + result->PostResult(); +} + +bool CreateWindowFunc::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + // We don't need to modify anything in the arguments, we just want to delay. + *converted_args = input_args; + + scoped_ptr<ListValue> args_list; + int window_id = 0; + if (!api_module_util::GetListAndDictIntValue( + input_args, ext::kIdKey, &args_list, &window_id)) { + NOTREACHED() << "Event arguments wasn't a dictionary with an ID in it. " << + input_args; + return false; + } + + // The hook may call us before the window is completely created, so we + // must delay the execution until the first tab is completely created. + // TODO(mad@chromium.org): Find a way to do this without blocking + // all other ApiDispatching. + HWND window = dispatcher->GetWindowHandleFromId(window_id); + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && + ::IsWindow(window) && + !window_utils::FindDescendentWindow( + window, windows::kIeTabWindowClass, false, NULL)) { + ::SleepEx(kDelayMs, TRUE); // TRUE = Alertable. + waited_ms += kDelayMs; + } + // We don't DCHECK if the window died, but we must still return false + // if it did, so that we don't broadcast the event back to Chrome. + DCHECK(waited_ms < kMaxDelayMs || !::IsWindow(window)); + return waited_ms < kMaxDelayMs && ::IsWindow(window); +} + +void CreateWindowFunc::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + scoped_ptr<DictionaryValue> input_dict; + // The input dictionary is optional; if not provided, the args may be + // either an empty list or a list with a single null value element. + if (args.GetSize() > 0) { + Value* first_arg = NULL; + if (!args.Get(0, &first_arg) || first_arg == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + if (first_arg->GetType() != Value::TYPE_NULL) { + DictionaryValue* args_dict = NULL; + if (!args.GetDictionary(0, &args_dict) || args_dict == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + // Remember the arguments so that we can use them later. + input_dict.reset(static_cast<DictionaryValue*>(args_dict->DeepCopy())); + } + } + + // Look for optional url. + scoped_ptr<GURL> spec(new GURL(chrome::kAboutBlankURL)); + std::string url_input; + if (input_dict.get() != NULL) { + if (input_dict->HasKey(ext::kUrlKey)) { + if (!input_dict->GetString(ext::kUrlKey, &url_input)) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + spec.reset(new GURL(url_input)); + if (!spec->is_valid()) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kInvalidUrlError, url_input).c_str()); + return; + } + } + } + + // There are many ways to create new IE windows, but they don't all behave + // the same depending on the IE version, OS or even settings. The most + // reliable way we found was to CoCreate an instance of IE, but navigating + // this instance doesn't behave exactly the same on all platforms. + // + // The main problem is with protected mode. If an instance of IE is launched + // with protected mode on, and we navigate to a URL that doesn't need + // protection (or vice versa), then the navigation will need to occur in + // another browser, not the one that was launched in the inappropriate + // protected mode. + // + // On IE7 if you CoCreate an instance of IE from a process running at an + // elevated integrity level as our Broker is, you get an unprotected mode + // IE, and if you navigate to a URL that needs protected mode, it will either + // create a new IE with the proper protected mode, or re-use an existing one + // if one is already opened. + // + // On IE8 the current process' integrity level is not taken into account + // when CoCreating a new IE, it relies on the CLSID which is the regular one + // (CLSID_InternetExplorer) for protected mode and a new one used for running + // at medium integrity level (CLSID_InternetExplorerMedium). Also, if you + // would then navigate to a URL that has a different protected mode than the + // one used for the CoCreate, then, a new window will always be created, an + // existing one will never be used. + // + // The other alternatives we looked at was to + // 1) Explicitly take the IWebBrowser2 interface of an existing IE. + // But this can cause the navigate to create a new tab instead + // of a new window (even if we specified navOpenInNewWindow) if + // the tab settings specify that popups should be opened in a + // new tab as opposed to a new window. + // 2) Use the IELaunchURL API (available in iepmapi.h). + // Except from the fact that it always creates a set of new + // IE processes in their own individual session this would + // behave exactly as we would like... But the price is too high. + // And it isn't available on XP... + // 3) Still use CoCreateInstance but don't rely on the knowledge of the + // current OS or IE version but rely on notifications sent to the + // DWebBrowser2 events thingy. Experimenting with these was not trivial + // and in some case lead to a non-deterministic way of identifying if + // a new window had been created or not. + + // We need to know whether we will navigate to a URL that needs protected + // mode enabled or not. On earlier versions of the OS (e.g., pre-Vista), + // this call simply fails and thus act as if we didn't need protected mode. + std::wstring url = UTF8ToWide(spec->spec()); + HRESULT protected_mode_url_hr = ::IEIsProtectedModeURL(url.c_str()); + + // We default to CLSID_InternetExplorer and CLSCTX_ALL but we may need to + // switch to CLSID_InternetExplorerMedium or add CLSCTX_ENABLE_CLOAKING later. + DWORD class_context = CLSCTX_ALL; + CLSID ie_clsid = CLSID_InternetExplorer; + bool lowered_integrity_level = false; + bool impersonating = false; + if (protected_mode_url_hr != S_OK) { // S_FALSE is returned for no. + // When we don't need protected mode, we need to explicitly + // request that IE8 starts the medium integrity version. + if (ie_util::GetIeVersion() == ie_util::IEVERSION_IE8) + ie_clsid = CLSID_InternetExplorerMedium; + } else if (ie_util::GetIeVersion() == ie_util::IEVERSION_IE7) { + // IE7 in protected mode needs to be started at lower integrity level + // than the broker process. So we must enable cloaking and temporary bring + // down the integrity level of this thread. + class_context |= CLSCTX_ENABLE_CLOAKING; + // We would have liked to use the CAccessToken class from ATL but it doesn't + // support the integrity level impersonation, just the owner, primary group + // and DACL. So we do it ourselves. + if (!::ImpersonateSelf(SecurityImpersonation)) { + DCHECK(false) << com::LogWe(); + return; + } + // Remember that we successfully impersonated, so we can RevertToSelf. + impersonating = true; + // This call fails on XP, so we don't look at the error, we just log it and + // remember our success so we can revert it later. Specifying NULL for the + // thread pointer means that we want to affect the current thread. + HRESULT hr_lowered_integrity_level = + process_utils_win::SetThreadIntegrityLevel(NULL, SDDL_ML_LOW); + lowered_integrity_level = SUCCEEDED(hr_lowered_integrity_level); + DLOG_IF(WARNING, !lowered_integrity_level) << + "SetThreadIntegrityLevelLow" << com::LogHr(hr_lowered_integrity_level); + } + + // Now we can create a new web browser and be sure it will be the one that is + // kept (as well as its window) once we navigate. + CComPtr<IWebBrowser2> web_browser; + HRESULT hr = web_browser.CoCreateInstance(ie_clsid, NULL, class_context); + DCHECK(SUCCEEDED(hr)) << "Can't CoCreate IE! " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // And now we can bring back the integrity level to where it was. + if (lowered_integrity_level) { + HRESULT hr_integrity_level = + process_utils_win::ResetThreadIntegrityLevel(NULL); + DCHECK(SUCCEEDED(hr_integrity_level)) << "Failed to bring back thread " << + "integrity level! " << com::LogHr(hr_integrity_level); + LOG_IF(WARNING, FAILED(hr)) << "ResetThreadIntegrityLevel(NULL) " << + com::LogHr(hr_integrity_level); + } + + // And stop impersonating. + if (impersonating) { + BOOL success = ::RevertToSelf(); + DCHECK(success) << "Failed to stop impersonating! " << com::LogWe(); + LOG_IF(WARNING, !success) << "RevertToSelf() " << com::LogWe(); + } + + // We need the HWND to create the window value to fill the info needed + // by the callback, and also, to potentially resize and position it. + HWND web_browserhwnd = NULL; + hr = web_browser->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&web_browserhwnd)); + DCHECK(SUCCEEDED(hr)) << "Can't get HWND!" << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + if (input_dict.get() == NULL) { + // No arguments, so no need to popupize, resize or reposition. + if (!result->CreateWindowValue(web_browserhwnd, false)) { + // CreateWindowValue will have posted the error if any. + return; + } + } else { + // Popupize if needed. + std::string window_type; + if (input_dict->GetString(ext::kWindowTypeKey, &window_type) && + window_type == ext::kWindowTypeValuePopup) { + HRESULT hr = web_browser->put_AddressBar(VARIANT_FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed to hide address bar. " << com::LogHr(hr); + hr = web_browser->put_StatusBar(VARIANT_FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed to hide status bar. " << com::LogHr(hr); + hr = web_browser->put_ToolBar(FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed put_ToolBar. " << com::LogHr(hr); + } + // Reposition and Resize if needed. + if (!result->UpdateWindowRect(web_browserhwnd, input_dict.get())) { + // UpdateWindowRect will have posted the error if any. + return; + } + } + + // Now we can Navigate to the requested url. + hr = web_browser->Navigate(CComBSTR(url.c_str()), + &CComVariant(), // unused flags + &CComVariant(L"_top"), // Target frame + &CComVariant(), // Unused POST DATA + &CComVariant()); // Unused Headers + DCHECK(SUCCEEDED(hr)) << "Can't Navigate IE to " << url << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // A CoCreated IE is not visible until we ask it to be. + hr = web_browser->put_Visible(VARIANT_TRUE); + DCHECK(SUCCEEDED(hr)) << "put_Visible: " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + result->PostResult(); +} + +void UpdateWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + DictionaryValue* update_props; + if (!args.GetInteger(0, &window_id) || + !args.GetDictionary(1, &update_props)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (!result->IsIeFrameClass(window)) { + LOG(WARNING) << "Extension trying to access non-IE or dying window: " << + std::hex << window_id; + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id)).c_str()); + return; + } + + if (result->UpdateWindowRect(window, update_props)) + result->PostResult(); +} + +void RemoveWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + if (!args.GetInteger(0, &window_id)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (!result->IsIeFrameClass(window)) { + LOG(WARNING) << "Extension trying to access non-IE or dying window: " << + std::hex << window_id; + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return; + } + + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to remove window."; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + HRESULT hr = executor->RemoveWindow(); + if (FAILED(hr)) { + LOG(ERROR) << "Executor failed to remove window: " << std::hex << + window_id << ". " << com::LogHr(hr); + result->PostError("Internal Error trying to close window."); + return; + } + + // S_FALSE is returned when there are no tabs to close, e.g., the initial + // tab has not finished loading by the time we are asked to close the window. + if (hr == S_FALSE) { + LOG(WARNING) << "Failed to get window manager to close the window, " + "trying WM_CLOSE instead." << com::LogHr(hr); + // We fall back to this to be slightly more robust, but this approach has + // the drawback that it shows a pop-up dialog with a message like "do you + // want to close N tabs" if there is more than one tab. And we may need + // to try a few times because we have seen cases where SendMessage didn't + // return 0 because the window couldn't process the message for some reason. + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && ::SendMessage(window, WM_CLOSE, 0, 0)) { + ::SleepEx(kDelayMs, TRUE); // Alertable. + waited_ms += kDelayMs; + } + + DCHECK(waited_ms < kMaxDelayMs); + if (waited_ms >= kMaxDelayMs) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + } + + // Now we must wait for the window removal to be completely done before + // posting the response back to Chrome Frame. + // And we remember the window identifier so that we can recognize the event. + result->SetValue(ext::kWindowIdKey, Value::CreateIntegerValue(window_id)); + DCHECK(dispatcher != NULL); + dispatcher->RegisterEphemeralEventHandler(ext_event_names::kOnWindowRemoved, + RemoveWindow::ContinueExecution, + result.release()); +} + +HRESULT RemoveWindow::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(dispatcher != NULL); + DCHECK(user_data != NULL); + + scoped_ptr<WindowApiResult> result(static_cast<WindowApiResult*>(user_data)); + + scoped_ptr<ListValue> args_list; + int window_id = 0; + if (!api_module_util::GetListAndIntegerValue(input_args, &args_list, + &window_id)) { + NOTREACHED() << "Event arguments are not a list with an integer in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + const Value* saved_window_value = result->GetValue(ext::kWindowIdKey); + DCHECK(saved_window_value != NULL && + saved_window_value->IsType(Value::TYPE_INTEGER)); + int saved_window_id = 0; + bool success = saved_window_value->GetAsInteger(&saved_window_id); + DCHECK(success && saved_window_id != 0); + if (saved_window_id == window_id) { + // The windows.remove callback doesn't have any arguments. + result->set_value(NULL); + result->PostResult(); + return S_OK; + } else { + // release doesn't destroy result, we need to keep it for next try. + result.release(); + return S_FALSE; // S_FALSE keeps us in the queue. + } +} + +bool RemoveWindow::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + // We don't need to modify anything in the arguments, we just want to delay. + *converted_args = input_args; + + scoped_ptr<Value> input_args_val(base::JSONReader::Read(input_args, true)); + DCHECK(input_args_val.get() != NULL && + input_args_val->IsType(Value::TYPE_LIST)); + if (input_args_val.get() == NULL || !input_args_val->IsType(Value::TYPE_LIST)) + return false; + + int window_id = 0; + bool success = static_cast<ListValue*>(input_args_val.get())->GetInteger( + 0, &window_id); + DCHECK(success) << "Couldn't get an int window Id from input args."; + if (!success) + return false; + // The hook may call us before the window has completely disappeared so we + // must delay the execution until the window is completely gone. + // TODO(mad@chromium.org): Find a way to do this without blocking + // all other ApiDispatching. + HWND window = dispatcher->GetWindowHandleFromId(window_id); + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && ::IsWindow(window)) { + ::SleepEx(kDelayMs, TRUE); // Alertable. + waited_ms += kDelayMs; + } + DCHECK(waited_ms < kMaxDelayMs); + if (waited_ms < kMaxDelayMs) { + return true; + } else { + return false; + } +} + +void GetAllWindows::Execute(const ListValue& args, int request_id) { + scoped_ptr<IterativeWindowApiResult> result(CreateApiResult(request_id)); + bool populate_tabs = false; + // The input dictionary is optional; if not provided, the args may be + // either an empty list or a list with a single null value element. + if (args.GetSize() > 0) { + Value* first_arg = NULL; + if (!args.Get(0, &first_arg) || first_arg == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + } else if (first_arg->GetType() != Value::TYPE_NULL) { + DictionaryValue* dict = NULL; + if (!args.GetDictionary(0, &dict) || + (dict->HasKey(ext::kPopulateKey) && + !dict->GetBoolean(ext::kPopulateKey, &populate_tabs))) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + } + } + + if (result->AllFailed()) { + result->FlushAllPosts(); + return; + } + } + + std::set<HWND> ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + if (ie_frame_windows.empty()) { + result->FlushAllPosts(); + return; + } + + std::set<HWND>::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + if (result->CreateWindowValue(*iter, populate_tabs)) + result->PostResult(); + } + + DCHECK(!result->IsEmpty()); + if (result->IsEmpty()) // This is an error! + result->PostError(api_module_constants::kInternalErrorError); + + result->FlushAllPosts(); +} + +} // namespace window_api diff --git a/ceee/ie/broker/window_api_module.h b/ceee/ie/broker/window_api_module.h new file mode 100644 index 0000000..6aed95f --- /dev/null +++ b/ceee/ie/broker/window_api_module.h @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation. + +#ifndef CEEE_IE_BROKER_WINDOW_API_MODULE_H_ +#define CEEE_IE_BROKER_WINDOW_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +#include "toolband.h" // NOLINT + +namespace window_api { + +class WindowApiResult; +typedef ApiResultCreator<WindowApiResult> WindowApiResultCreator; + +// Registers all Window API invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class WindowApiResult : public common_api::CommonApiResult { + public: + explicit WindowApiResult(int request_id) + : common_api::CommonApiResult(request_id) {} + + // Updates the position of the given window based on the arguments given and + // sets the result value appropriately. Calls PostError() if there is an + // error and returns false. + // @param window The window to update. + // @param window_rect The arguments for the window update, a DictionaryValue + // containing the left, top, width, height to update the window with. + virtual bool UpdateWindowRect(HWND window, + const DictionaryValue* window_rect); +}; + +typedef IterativeApiResult<WindowApiResult> IterativeWindowApiResult; + +class GetWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetCurrentWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetLastFocusedWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Unfortunately winuser.h uses a #define for CreateWindow to use either +// the ASCII or Wide char version, so it replaces the Constructor declaration +// with CreateWindowW() and fails compilation if we use CreateWindow as a +// class name. So we must have another class name and use a special case to +// specify the name of the function we replace in Chrome in RegisterInvocations. +class CreateWindowFunc : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // public so that the RegisterInvocations can see it. + // Simply reacts to an OnWindowCreated event and waits for the window to + // be completely created before letting the ApiDispatcher broadcast the event. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class UpdateWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class RemoveWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class GetAllWindows : public ApiResultCreator<IterativeWindowApiResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +} // namespace window_api + +#endif // CEEE_IE_BROKER_WINDOW_API_MODULE_H_ diff --git a/ceee/ie/broker/window_api_module_unittest.cc b/ceee/ie/broker/window_api_module_unittest.cc new file mode 100644 index 0000000..04f67ef --- /dev/null +++ b/ceee/ie/broker/window_api_module_unittest.cc @@ -0,0 +1,937 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include <set> + +#include "base/scoped_ptr.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::AddRef; +using testing::AnyNumber; +using testing::AtLeast; +using testing::CopyStringToArgument; +using testing::DoAll; +using testing::Gt; +using testing::InstanceCountMixin; +using testing::Invoke; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::Sequence; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +using window_api::WindowApiResult; +using window_api::IterativeWindowApiResult; + +namespace keys = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +const wchar_t kClassName[] = L"IEFrame"; +const int kRequestId = 12; +const int kWindowId = 23; +const HWND kWindowHwnd = (HWND)34; +const int kRandomWindowId = 45; +const HWND kRandomWindowHwnd = (HWND)56; + +TEST(WindowApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(7)); + window_api::RegisterInvocations(&disp); +} + +TEST(WindowApi, IsIeFrameClassWithNull) { + testing::LogDisabler no_dchecks; + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(NULL)); +} + +TEST(WindowApi, IsIeFrameClassWithNonWindow) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillRepeatedly(Return(FALSE)); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, IsIeFrameClassWrongClassName) { + const wchar_t kClassName[] = L"IEFrames"; // note 's', making it invalid. + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, IsIeFrameClassStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_TRUE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, TopIeWindowNeverFound) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, NotNull())). + WillOnce(Return(false)); + EXPECT_EQ(NULL, WindowApiResult::TopIeWindow()); +} + +TEST(WindowApi, TopIeWindowStraightline) { + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<3>(kRandomWindowHwnd), Return(true))); + EXPECT_EQ(kRandomWindowHwnd, WindowApiResult::TopIeWindow()); +} + +class MockWindowApiResult + : public WindowApiResult, + public InstanceCountMixin<MockWindowApiResult> { + public: + explicit MockWindowApiResult(int request_id) + : WindowApiResult(request_id) { + } + MOCK_METHOD1(CreateTabList, Value*(BSTR)); + MOCK_METHOD3(SetResultFromWindowInfo, void(HWND, const CeeeWindowInfo&, + bool)); + MOCK_METHOD2(UpdateWindowRect, bool(HWND, const DictionaryValue*)); + MOCK_METHOD2(CreateWindowValue, bool(HWND, bool)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + void CallSetResultFromWindowInfo(HWND window, const CeeeWindowInfo& info, + bool pop) { + WindowApiResult::SetResultFromWindowInfo(window, info, pop); + } + bool CallCreateWindowValue(HWND window, bool pop) { + return WindowApiResult::CreateWindowValue(window, pop); + } + Value* CallCreateTabList(BSTR tabs) { + return WindowApiResult::CreateTabList(tabs); + } + bool CallUpdateWindowRect(HWND window, const DictionaryValue* dict_value) { + return WindowApiResult::UpdateWindowRect(window, dict_value); + } + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +class MockIterativeWindowApiResult + : public IterativeWindowApiResult, + public InstanceCountMixin<MockIterativeWindowApiResult> { + public: + explicit MockIterativeWindowApiResult(int request_id) + : IterativeWindowApiResult(request_id) {} + + MOCK_METHOD0(CallRealPostResult, void()); + MOCK_METHOD1(CallRealPostError, void(const std::string&)); + MOCK_METHOD1(CreateTabList, Value*(BSTR)); + MOCK_METHOD3(SetResultFromWindowInfo, void(HWND, const CeeeWindowInfo&, + bool)); + MOCK_METHOD2(UpdateWindowRect, bool(HWND, const DictionaryValue*)); + MOCK_METHOD2(CreateWindowValue, bool(HWND, bool)); + + virtual void PostError(const std::string& error) { + ++error_counter_; + IterativeWindowApiResult::PostError(error); + } + + virtual void PostResult() { + ++success_counter_; + IterativeWindowApiResult::PostResult(); + } + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + static void ResetCounters() { + success_counter_ = 0; + error_counter_ = 0; + } + + static int success_counter() { + return success_counter_; + } + + static int error_counter() { + return error_counter_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + static int success_counter_; + static int error_counter_; +}; + +int MockIterativeWindowApiResult::success_counter_ = 0; +int MockIterativeWindowApiResult::error_counter_ = 0; + +// Mock static functions defined in WindowApiResult. +MOCK_STATIC_CLASS_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT2(WindowApiResult::TopIeWindow, + TopIeWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(HWND, , TopIeWindow); +MOCK_STATIC_CLASS_END(MockWindowApiResultStatics) + +class WindowApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor_, &mock_window_executor_keeper_)); + EXPECT_HRESULT_SUCCEEDED(testing::MockTabExecutor::CreateInitialized( + &mock_tab_executor_, &mock_tab_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_window_executor_keeper_.Release(); + mock_tab_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + protected: + void MockGetWindowExecutor(MockApiDispatcher* executor_owner) { + EXPECT_CALL(*executor_owner, GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + void MockGetTabExecutor(MockApiDispatcher* executor_owner) { + EXPECT_CALL(*executor_owner, GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void WindowAlwaysHasThread() { + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).WillRepeatedly( + Return(1)); // We only need to return a non-0 value. + } + + void MockIsIeFrameClass() { + // This is to mock the static call to IsIeFrameClass. + EXPECT_CALL(user32_, IsWindow(NotNull())).WillRepeatedly(Return(TRUE)); + EXPECT_CALL(user32_, GetClassName(NotNull(), NotNull(), Gt(0))). + WillRepeatedly(DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + } + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockWindowExecutor* mock_window_executor_; + testing::MockTabExecutor* mock_tab_executor_; + // To control the life span of the executors. + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; + CComPtr<ICeeeTabExecutor> mock_tab_executor_keeper_; + + private: + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); + }; + // We should never get to the postman, we mock all the calls getting there. + // So we simply instantiate it strict and it will register itself as the + // one and only singleton to use all the time. + CComObjectStackEx<StrictMock<MockChromePostman>> postman_; +}; + +TEST_F(WindowApiTests, CreateTabList) { + // We only check that we can properly handle an empty list since we can't + // easily mock the tab_api::GetAllTabsInWindow declared on the stack. + // We validate it more completely in the tabs api unittest anyway. + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + scoped_ptr<Value> returned_value(invocation_result.CreateTabList(L"[]")); + EXPECT_NE(static_cast<Value*>(NULL), returned_value.get()); + EXPECT_TRUE(returned_value->IsType(Value::TYPE_LIST)); + EXPECT_EQ(0, static_cast<ListValue*>(returned_value.get())->GetSize()); + + // Also test the failure path. + testing::LogDisabler no_dchecks; + returned_value.reset(invocation_result.CreateTabList(L"")); + EXPECT_EQ(NULL, returned_value.get()); +} + +// Mock IeIsInPrivateBrowsing. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIEIsInPrivateBrowsing, + GetIEIsInPrivateBrowsing); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetIEIsInPrivateBrowsing); +MOCK_STATIC_CLASS_END(MockIeUtil) + +TEST_F(WindowApiTests, SetResultFromWindowInfo) { + testing::LogDisabler no_dchecks; + + // Standard test without tabs list populate nor an internal list. + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + CeeeWindowInfo window_info; + window_info.rect.left = 1; + window_info.rect.right = 2; + window_info.rect.top = 3; + window_info.rect.bottom = 4; + window_info.tab_list = NULL; + + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + + StrictMock<MockIeUtil> mock_ie_util; + EXPECT_CALL(mock_ie_util, GetIEIsInPrivateBrowsing()).WillRepeatedly( + Return(false)); + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, + false); + const Value * result = invocation_result.value(); + EXPECT_NE(static_cast<Value*>(NULL), result); + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + DictionaryValue dict; + dict.SetInteger(keys::kIdKey, kWindowId); + dict.SetBoolean(keys::kFocusedKey, window_info.focused != FALSE); + + dict.SetInteger(keys::kLeftKey, window_info.rect.left); + dict.SetInteger(keys::kTopKey, window_info.rect.top); + dict.SetInteger(keys::kWidthKey, window_info.rect.right - + window_info.rect.left); + dict.SetInteger(keys::kHeightKey, window_info.rect.bottom - + window_info.rect.top); + + dict.SetBoolean(keys::kIncognitoKey, false); + + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we are not handling popups or app windows in IE yet. + dict.SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + EXPECT_TRUE(dict.Equals(result)); + + invocation_result.set_value(NULL); + + // Now make sure we call CreateTablist, yet still succeed if it returns NULL. + int tab_list = 42; + EXPECT_CALL(invocation_result, CreateTabList((BSTR)tab_list)). + WillOnce(Return(static_cast<Value*>(NULL))); + window_info.tab_list = (BSTR)tab_list; + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, true); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + + invocation_result.set_value(NULL); + + // And now a successful run with CreateTablist. + EXPECT_CALL(invocation_result, CreateTabList((BSTR)tab_list)). + WillOnce(Return(Value::CreateIntegerValue(tab_list))); + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, true); + result = invocation_result.value(); + EXPECT_NE(static_cast<Value*>(NULL), result); + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + dict.Set(keys::kTabsKey, Value::CreateIntegerValue(tab_list)); + EXPECT_TRUE(dict.Equals(result)); +} + +TEST_F(WindowApiTests, CreateWindowValue) { + testing::LogDisabler no_dchecks; + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(Return(0)); + + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + + // Fail because the window is not associated to a thread. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now fail because the window is not an IE Frame. + WindowAlwaysHasThread(); + invocation_result.set_value(NULL); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now fail because we can't get an executor. + MockIsIeFrameClass(); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now get the executor to fail. + MockGetWindowExecutor(&invocation_result.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, GetWindow(FALSE, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now go all the way. + EXPECT_CALL(*mock_window_executor_, GetWindow(FALSE, NotNull())). + WillOnce(Return(S_OK)); + EXPECT_CALL(invocation_result, SetResultFromWindowInfo(_, _, _)).Times(1); + EXPECT_TRUE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); +} + +TEST_F(WindowApiTests, UpdateWindowRect) { + testing::LogDisabler no_dchecks; + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kRandomWindowHwnd)). + WillRepeatedly(Return(kRandomWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillRepeatedly(Return(kRandomWindowHwnd)); + + // Window has no thread. + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).WillRepeatedly( + Return(0)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Window is not an IEFrame. + WindowAlwaysHasThread(); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now try an empty dictionary (which should not cause an error). + MockIsIeFrameClass(); + EXPECT_CALL(invocation_result, CreateWindowValue(_, false)). + WillOnce(Return(true)); + EXPECT_TRUE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + + // Now try a dictionary with invalid dictionary values. + DictionaryValue bad_values; + bad_values.Set(keys::kLeftKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kLeftKey, Value::CreateIntegerValue(43)); + bad_values.Set(keys::kTopKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kTopKey, Value::CreateIntegerValue(44)); + bad_values.Set(keys::kWidthKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kWidthKey, Value::CreateIntegerValue(45)); + bad_values.Set(keys::kHeightKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now make sure the values get properly propagated. + // But start by failing the GetExecutor. + scoped_ptr<DictionaryValue> good_values(static_cast<DictionaryValue*>( + bad_values.DeepCopy())); + good_values->Set(keys::kHeightKey, Value::CreateIntegerValue(46)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kRandomWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now get the executor to fail. + MockGetWindowExecutor(&invocation_result.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, UpdateWindow(43, 44, 45, 46, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now go all the way, + EXPECT_CALL(invocation_result, SetResultFromWindowInfo(_, _, _)).Times(1); + EXPECT_CALL(*mock_window_executor_, UpdateWindow(43, 44, 45, 46, _)). + WillOnce(Return(S_OK)); + EXPECT_TRUE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); +} + +template <class BaseClass> +class MockWindowInvocation + : public MockApiInvocation<WindowApiResult, MockWindowApiResult, + BaseClass> { +}; + +template <class BaseClass> +class MockIterativeWindowInvocation + : public MockApiInvocation<IterativeWindowApiResult, + MockIterativeWindowApiResult, + BaseClass> { +}; + +TEST_F(WindowApiTests, GetWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::GetWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + + // Bad args failure. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(WindowApiTests, GetWindowStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::GetWindow>> invocation; + MockIsIeFrameClass(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + ListValue args_list; + args_list.Append(Value::CreateIntegerValue(kRandomWindowId)); + invocation.Execute(args_list, kRequestId); +} + +TEST_F(WindowApiTests, GetCurrentWindowStraightline) { + testing::LogDisabler no_dchecks; + + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(TRUE)); + + StrictMock<MockWindowInvocation<window_api::GetCurrentWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics result_statics; + EXPECT_CALL(result_statics, TopIeWindow()).WillOnce(Return( + kRandomWindowHwnd)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(WindowApiTests, GetLastFocusedWindowStraightline) { + testing::LogDisabler no_dchecks; // don't care about NULL pointers. + + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(TRUE)); + + StrictMock<MockWindowInvocation<window_api::GetLastFocusedWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics result_statics; + EXPECT_CALL(result_statics, TopIeWindow()).WillOnce(Return( + kRandomWindowHwnd)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +// Mock the CoCreateInstance call that is used to create a new IE window. +MOCK_STATIC_CLASS_BEGIN(MockIeWindowCreation) + MOCK_STATIC_INIT_BEGIN(MockIeWindowCreation) + MOCK_STATIC_INIT(CoCreateInstance); + MOCK_STATIC_INIT_END() + + MOCK_STATIC5(HRESULT, CALLBACK, CoCreateInstance, REFCLSID, LPUNKNOWN, + DWORD, REFIID, LPVOID*); +MOCK_STATIC_CLASS_END(MockIeWindowCreation) + +TEST_F(WindowApiTests, CreateWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::CreateWindowFunc>> invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + ListValue wrong_type; + wrong_type.Append(Value::CreateBooleanValue(false)); + invocation.Execute(wrong_type, kRequestId); + + invocation.AllocateNewResult(kRequestId); + ListValue args_list; + DictionaryValue* dict_value = new DictionaryValue; + args_list.Append(dict_value); + // Wrong data type... + dict_value->SetInteger(keys::kUrlKey, rand()); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + // InvalidUrl + dict_value->SetString(keys::kUrlKey, "ht@tp://www.google.com/"); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + // Valid dictionary... + dict_value->SetString(keys::kUrlKey, "http://ossum.the.magnificent.com/"); + dict_value->SetInteger(keys::kLeftKey, 21); // Just to force the rect access. + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)).Times(AnyNumber()); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + MockIeWindowCreation mock_ie_create; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillRepeatedly(DoAll(SetArgumentPointee<4>(browser_keeper.p), + AddRef(browser_keeper.p), Return(S_OK))); + + EXPECT_CALL(*browser, get_HWND(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(0), Return(S_OK))); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(false)); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*browser, put_Visible(VARIANT_TRUE)).WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); +} + +TEST_F(WindowApiTests, CreateWindowStraightline) { + testing::LogDisabler no_dchecks; + + ListValue args1; + DictionaryValue* dict1 = new DictionaryValue; + args1.Append(dict1); + dict1->SetString(keys::kUrlKey, "http://ossum.the.magnificent.com/"); + dict1->SetInteger(keys::kLeftKey, 21); // Just to force the rect access. + + ListValue args2; + DictionaryValue* dict2 = new DictionaryValue; + args2.Append(dict2); + dict2->SetString(keys::kUrlKey, "http://ossum.the.omnipotent.com/"); + dict2->SetString(keys::kWindowTypeKey, keys::kWindowTypeValuePopup); + + ListValue args3; + args3.Append(Value::CreateNullValue()); + + ListValue empty_args; + + const wchar_t* about_blank = L"about:blank"; + struct { + const ListValue* args; + const wchar_t* expected_url; + const bool popup; + } test_data[] = { + { &args1, L"http://ossum.the.magnificent.com/", false }, + { &args2, L"http://ossum.the.omnipotent.com/", true }, + { &args3, about_blank, false }, + { &empty_args, about_blank, false }, + }; + + typedef StrictMock<MockWindowInvocation<window_api::CreateWindowFunc>> + MockCreateWindowFunc; + + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + MockIeWindowCreation mock_ie_create; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillRepeatedly(DoAll(SetArgumentPointee<4>(browser_keeper.p), + AddRef(browser_keeper.p), Return(S_OK))); + EXPECT_CALL(*browser, get_HWND(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(0), Return(S_OK))); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*browser, put_Visible(VARIANT_TRUE)).WillRepeatedly(Return(S_OK)); + + for (int i = 0; i < arraysize(test_data); ++i) { + MockCreateWindowFunc invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + Value* first_arg = NULL; + if (!test_data[i].args->Get(0, &first_arg) || + first_arg->GetType() == Value::TYPE_NULL) { + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue(_, _)). + WillOnce(Return(true)); + invocation.Execute(*test_data[i].args, kRequestId); + } else { + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + if (test_data[i].popup) { + EXPECT_CALL(*browser, put_AddressBar(VARIANT_FALSE)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*browser, put_StatusBar(VARIANT_FALSE)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*browser, put_ToolBar(FALSE)).WillOnce(Return(S_OK)); + } + invocation.Execute(*test_data[i].args, kRequestId); + } + } +} + +TEST_F(WindowApiTests, UpdateWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::UpdateWindow>> + invocation; + invocation.AllocateNewResult(kRequestId); + + ListValue root; + root.Append(Value::CreateIntegerValue(kRandomWindowId)); + // Too few values in list. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(root, kRequestId); + + invocation.AllocateNewResult(kRequestId); + + Value* wrong_type = Value::CreateStringValue(L"The Answer"); + root.Append(wrong_type); // root takes ownership. + // Right number of values, wrong type of second value. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(root, kRequestId); + + invocation.AllocateNewResult(kRequestId); + + root.Remove(*wrong_type); + root.Append(new DictionaryValue()); // root takes ownership. + // Right values, but not IE Frame. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(FALSE)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(root, kRequestId); +} + +TEST_F(WindowApiTests, UpdateWindowStraightline) { + testing::LogDisabler no_dchecks; + + ListValue root; + root.Append(Value::CreateIntegerValue(kRandomWindowId)); + DictionaryValue* args = new DictionaryValue(); // root takes ownership. + root.Append(args); + + MockIsIeFrameClass(); + + StrictMock<MockWindowInvocation<window_api::UpdateWindow>> + invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect( + kRandomWindowHwnd, args)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(root, kRequestId); +} + +class MockRemoveWindow + : public StrictMock<MockWindowInvocation<window_api::RemoveWindow>> { +}; + +TEST_F(WindowApiTests, RemoveWindowErrorHandling) { + testing::LogDisabler no_dchecks; + + // Wrong argument type. + MockRemoveWindow invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Not a valid window. + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + ListValue good_args; + good_args.Append(Value::CreateIntegerValue(kRandomWindowId)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); + + // No executor. + MockIsIeFrameClass(); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kRandomWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); + + // Executor fails. + MockGetWindowExecutor(&invocation.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, RemoveWindow()). + WillOnce(Return(E_FAIL)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); +} + +TEST_F(WindowApiTests, RemoveWindowStraightline) { + testing::LogDisabler no_dchecks; + + MockIsIeFrameClass(); + MockRemoveWindow invocation; + ListValue good_args; + good_args.Append(Value::CreateIntegerValue(kRandomWindowId)); + MockGetWindowExecutor(&invocation.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, RemoveWindow()). + WillOnce(Return(S_OK)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnWindowRemoved), + window_api::RemoveWindow::ContinueExecution, + invocation.invocation_result_.get())).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + ApiDispatcher::InvocationResult* result = invocation.invocation_result_.get(); + invocation.Execute(good_args, kRequestId); + + std::ostringstream args; + args << "[" << kRandomWindowId << "]"; + invocation.ContinueExecution(args.str(), result, invocation.GetDispatcher()); +} + +TEST_F(WindowApiTests, GetAllWindowsErrorHandling) { + testing::LogDisabler no_dchecks; + ListValue bad_args; + DictionaryValue* bad_dict = new DictionaryValue(); + bad_dict->SetInteger(keys::kPopulateKey, 42); + bad_args.Append(bad_dict); + StrictMock<MockIterativeWindowInvocation<window_api::GetAllWindows> > + invocation; + invocation.AllocateNewResult(kRequestId); + // Using a strict mock ensures that FindTopLevelWindows won't get called. + MockIterativeWindowApiResult::ResetCounters(); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(bad_args, kRequestId); + EXPECT_EQ(MockIterativeWindowApiResult::error_counter(), 1); +} + +TEST_F(WindowApiTests, GetAllWindowsStraightline) { + testing::LogDisabler no_dchecks; + + ListValue empty_args; + ListValue null_args; + null_args.Append(Value::CreateNullValue()); + ListValue true_args; + DictionaryValue* dict_value = new DictionaryValue; + // The list will take ownership + ASSERT_TRUE(true_args.Set(0, dict_value)); + dict_value->SetBoolean(keys::kPopulateKey, true); + struct { + const ListValue* value; + bool populate; + } values[] = { + { &empty_args, false }, + { &null_args, false }, + { &true_args, true } + }; + + std::set<HWND> ie_hwnds; + static const HWND kWindow1 = reinterpret_cast<HWND>(1); + ie_hwnds.insert(kWindow1); + static const HWND kWindow2 = reinterpret_cast<HWND>(2); + ie_hwnds.insert(kWindow2); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, NotNull())).WillRepeatedly( + SetArgumentPointee<1>(ie_hwnds)); + + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(Return(1)); + + for (int i = 0; i < arraysize(values); ++i) { + StrictMock<MockIterativeWindowInvocation<window_api::GetAllWindows> > + invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kWindow1, values[i].populate)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kWindow2, values[i].populate)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + MockIterativeWindowApiResult::ResetCounters(); + invocation.Execute(*values[i].value, kRequestId); + EXPECT_EQ(MockIterativeWindowApiResult::error_counter(), 0); + EXPECT_EQ(MockIterativeWindowApiResult::success_counter(), ie_hwnds.size()); + } +} + +} // namespace diff --git a/ceee/ie/broker/window_events_funnel.cc b/ceee/ie/broker/window_events_funnel.cc new file mode 100644 index 0000000..4300dcc --- /dev/null +++ b/ceee/ie/broker/window_events_funnel.cc @@ -0,0 +1,154 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events to be sent to Chrome. + +#include "ceee/ie/broker/window_events_funnel.h" + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { +// The Shell Hook uses a registered message to communicate with registered +// windows. +const UINT kShellHookMessage = ::RegisterWindowMessage(L"SHELLHOOK"); + +// The HWND, Class and WindowProc for the Registered Shell Hook Window. +HWND shell_hook_window = NULL; +const wchar_t kShellHookWindowClassName[] = L"CeeeShellHookWindow"; +LRESULT CALLBACK WindowEventsFunnelWindowProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + if (message == kShellHookMessage) { + switch (wparam) { + case HSHELL_WINDOWCREATED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnCreated(lparam); + } + break; + } + case HSHELL_WINDOWDESTROYED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnRemoved(lparam); + } + break; + } + case HSHELL_WINDOWACTIVATED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnFocusChanged(lparam); + } + break; + } + } + } + return ::DefWindowProc(hwnd, message, wparam, lparam); +} +} + +void WindowEventsFunnel::Initialize() { + if (shell_hook_window) + return; + + WNDCLASSEX shell_window_hook_class = {0}; + shell_window_hook_class.cbSize = sizeof(WNDCLASSEX); + shell_window_hook_class.lpfnWndProc = WindowEventsFunnelWindowProc; + shell_window_hook_class.hInstance = GetModuleHandle(NULL); + shell_window_hook_class.lpszClassName = kShellHookWindowClassName; + + ATOM class_registration = ::RegisterClassEx(&shell_window_hook_class); + DCHECK(class_registration != NULL) << + "Couldn't register Shell Hook Window class!" << com::LogWe(); + if (!class_registration) + return; + + shell_hook_window = ::CreateWindow( + reinterpret_cast<wchar_t*>(class_registration), + L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); + DCHECK(shell_hook_window != NULL) << "Couldn't Create Shell Hook Window!" << + com::LogWe(); + if (shell_hook_window) { + BOOL success = ::RegisterShellHookWindow(shell_hook_window); + DCHECK(success) << "Couldn't register shell hook window!" << com::LogWe(); + } +} + +void WindowEventsFunnel::Terminate() { + if (shell_hook_window) { + BOOL success = ::DeregisterShellHookWindow(shell_hook_window); + DCHECK(success) << "Couldn't unregister shell hook window!" << com::LogWe(); + success = ::DestroyWindow(shell_hook_window); + shell_hook_window = NULL; + DCHECK(success) << "Couldn't destroy shell hook window!" << com::LogWe(); + } +} + +HRESULT WindowEventsFunnel::SendEvent(const char* event_name, + const Value& event_args) { + // Event arguments are always stored in a list. + std::string event_args_str; + if (event_args.IsType(Value::TYPE_LIST)) { + base::JSONWriter::Write(&event_args, false, &event_args_str); + } else { + ListValue list; + list.Append(event_args.DeepCopy()); + base::JSONWriter::Write(&list, false, &event_args_str); + } + + DCHECK(ChromePostman::GetInstance() != NULL); + ChromePostman::GetInstance()->FireEvent(CComBSTR(event_name), + CComBSTR(event_args_str.c_str())); + return S_OK; +} + +HRESULT WindowEventsFunnel::OnCreated(int window_id) { + HWND window = reinterpret_cast<HWND>(window_id); + RECT window_rect; + if (!::GetWindowRect(window, &window_rect)) { + DWORD we = ::GetLastError(); + DCHECK(false) << "GetWindowRect failed " << com::LogWe(we); + return HRESULT_FROM_WIN32(we); + } + + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + dict->SetInteger(keys::kIdKey, window_id); + dict->SetBoolean(keys::kFocusedKey, + (window == window_utils::GetTopLevelParent(::GetForegroundWindow()))); + dict->SetInteger(keys::kLeftKey, window_rect.left); + dict->SetInteger(keys::kTopKey, window_rect.top); + dict->SetInteger(keys::kWidthKey, window_rect.right - window_rect.left); + dict->SetInteger(keys::kHeightKey, window_rect.bottom - window_rect.top); + dict->SetBoolean(keys::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we don't yet have a way to tell if the window is a popup or not. + dict->SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + + return SendEvent(ext_event_names::kOnWindowCreated, *dict.get()); +} + +HRESULT WindowEventsFunnel::OnFocusChanged(int window_id) { + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + return SendEvent(ext_event_names::kOnWindowFocusedChanged, *args.get()); +} + +HRESULT WindowEventsFunnel::OnRemoved(int window_id) { + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + return SendEvent(ext_event_names::kOnWindowRemoved, *args.get()); +} diff --git a/ceee/ie/broker/window_events_funnel.h b/ceee/ie/broker/window_events_funnel.h new file mode 100644 index 0000000..0278959 --- /dev/null +++ b/ceee/ie/broker/window_events_funnel.h @@ -0,0 +1,52 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Window Events. + +#ifndef CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ +#define CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ + +#include <wtypes.h> // For HRESULT and others... + +#include "base/basictypes.h" + + +// Fwd. +class Value; + + +// Implements a set of methods to send window related events to the Chrome. +class WindowEventsFunnel { + public: + WindowEventsFunnel() {} + virtual ~WindowEventsFunnel() {} + + // Un/Register our window to receive Shell Hook Notification Messages. + static void Initialize(); + static void Terminate(); + + // Sends the windows.onCreated event to Chrome. + // @param window_id The identifier of the window that was created. + virtual HRESULT OnCreated(int window_id); + + // Sends the windows.onFocusChanged event to Chrome. + // @param window_id The identifier of the window that received the focus. + virtual HRESULT OnFocusChanged(int window_id); + + // Sends the windows.onRemoved event to Chrome. + // @param window_id The identifier of the window that was removed. + virtual HRESULT OnRemoved(int window_id); + + protected: + // Send the given event to Chrome. + // @param event_name The name of the event. + // @param event_args The arguments to be sent with the event. + // protected virtual for testability... + virtual HRESULT SendEvent(const char* event_name, + const Value& event_args); + private: + DISALLOW_COPY_AND_ASSIGN(WindowEventsFunnel); +}; + +#endif // CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/broker/window_events_funnel_unittest.cc b/ceee/ie/broker/window_events_funnel_unittest.cc new file mode 100644 index 0000000..d116bbd --- /dev/null +++ b/ceee/ie/broker/window_events_funnel_unittest.cc @@ -0,0 +1,156 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WindowEventsFunnel. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/json/json_writer.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/window_events_funnel.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { + +using testing::_; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWindowEventsFunnel : public WindowEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); + HRESULT CallSendEvent(const char* name, const Value& value) { + return WindowEventsFunnel::SendEvent(name, value); + } +}; + +TEST(WindowEventsFunnelTest, SendEvent) { + TestWindowEventsFunnel events_funnel; + + static const char* kEventName = "MADness"; + DictionaryValue event_args; + event_args.SetInteger("Answer to the Ultimate Question of Life," + "the Universe, and Everything", 42); + event_args.SetString("AYBABTU", "All your base are belong to us"); + event_args.SetReal("www.unrealtournament.com", 3.0); + + ListValue args_list; + args_list.Append(event_args.DeepCopy()); + + std::string event_args_str; + base::JSONWriter::Write(&args_list, false, &event_args_str); + + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(FireEvent, void(BSTR, BSTR)); + }; + // Simply instantiating the postman will register it as the + // one and only singleton to use all the time. + CComObjectStackEx<testing::StrictMock<MockChromePostman>> postman; + EXPECT_CALL(postman, FireEvent( + StrEq(CComBSTR(kEventName).m_str), + StrEq(CComBSTR(event_args_str.c_str()).m_str))).Times(1); + EXPECT_HRESULT_SUCCEEDED(events_funnel.CallSendEvent(kEventName, event_args)); +} + +// Mock IeIsInPrivateBrowsing. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIEIsInPrivateBrowsing, + GetIEIsInPrivateBrowsing); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetIEIsInPrivateBrowsing); +MOCK_STATIC_CLASS_END(MockIeUtil) + +TEST(WindowEventsFunnelTest, OnWindowCreated) { + testing::LogDisabler no_dchecks; + TestWindowEventsFunnel window_events_funnel; + + int window_id = 42; + HWND window = reinterpret_cast<HWND>(window_id); + + testing::MockUser32 user32; + EXPECT_CALL(user32, GetWindowRect(window, NotNull())) + .WillOnce(Return(FALSE)); + EXPECT_CALL(window_events_funnel, SendEvent(_, _)).Times(0); + // We return the HRESULT conversion of GetLastError. + ::SetLastError(ERROR_INVALID_ACCESS); + EXPECT_HRESULT_FAILED(window_events_funnel.OnCreated(window_id)); + ::SetLastError(ERROR_SUCCESS); + + RECT window_rect = {1, 2, 3, 4}; + EXPECT_CALL(user32, GetWindowRect(window, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + EXPECT_CALL(user32, GetForegroundWindow()).WillOnce(Return((HWND)NULL)); + testing::MockWindowUtils mock_windows_utils; + EXPECT_CALL(mock_windows_utils, GetTopLevelParent((HWND)NULL)) + .WillOnce(Return(window)); + + StrictMock<MockIeUtil> mock_ie_util; + EXPECT_CALL(mock_ie_util, GetIEIsInPrivateBrowsing()).WillRepeatedly( + Return(false)); + + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + dict->SetInteger(keys::kIdKey, window_id); + dict->SetBoolean(keys::kFocusedKey, true); + dict->SetInteger(keys::kLeftKey, window_rect.left); + dict->SetInteger(keys::kTopKey, window_rect.top); + dict->SetInteger(keys::kWidthKey, window_rect.right - window_rect.left); + dict->SetInteger(keys::kHeightKey, window_rect.bottom - window_rect.top); + dict->SetBoolean(keys::kIncognitoKey, false); + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we are not handling popups or app windows in IE yet. + dict->SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + + EXPECT_CALL(window_events_funnel, SendEvent(StrEq( + ext_event_names::kOnWindowCreated), ValuesEqual(dict.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnCreated(window_id)); +} + +TEST(WindowEventsFunnelTest, OnFocusChanged) { + TestWindowEventsFunnel window_events_funnel; + int window_id = 42; + + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + EXPECT_CALL(window_events_funnel, SendEvent(StrEq( + ext_event_names::kOnWindowFocusedChanged), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnFocusChanged(window_id)); +} + +TEST(WindowEventsFunnelTest, OnWindowRemoved) { + TestWindowEventsFunnel window_events_funnel; + int window_id = 42; + + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + EXPECT_CALL(window_events_funnel, SendEvent( + StrEq(ext_event_names::kOnWindowRemoved), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnRemoved(window_id)); +} + +} // namespace diff --git a/ceee/ie/common/api_registration.h b/ceee/ie/common/api_registration.h new file mode 100644 index 0000000..26dad37 --- /dev/null +++ b/ceee/ie/common/api_registration.h @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Lists the APIs to be registered, and in which modules to register them. +// This is to give us a single place to specify the list of APIs to handle +// (in the broker) and to redirect (in each Chrome Frame host - which is not +// necessarily in the broker and thus wouldn't have a ProductionApiDispatcher +// object available to query for registered functions). + +#ifndef CEEE_IE_COMMON_API_REGISTRATION_H_ +#define CEEE_IE_COMMON_API_REGISTRATION_H_ + +#include "chrome/browser/extensions/execute_code_in_tab_function.h" +#include "chrome/browser/extensions/extension_cookies_api.h" +#include "chrome/browser/extensions/extension_infobar_module.h" +#include "chrome/browser/extensions/extension_tabs_module.h" + +#ifdef REGISTER_API_FUNCTION +#error Must change the name of the REGISTER_API_FUNCTION macro in this file +#endif // REGISTER_API_FUNCTION + +// You must define a REGISTER_API_FUNCTION macro in the compilation unit where +// you include this file, before invoking any of the REGISTER_XYZ_API_FUNCTIONS +// macros. +// +// In the compilation unit that registers function groups with the production +// API dispatcher, define PRODUCTION_API_DISPATCHER and your own definition of +// each of these macros. This may seem a bit roundabout, it's just done this +// way to ensure that if you add function groups to this file and forget to +// update the production API dispatcher you'll get a compilation error. + +#ifndef PRODUCTION_API_DISPATCHER + +// Registers the chrome.tab.* functions we handle. +#define REGISTER_TAB_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetTab); \ + REGISTER_API_FUNCTION(GetSelectedTab); \ + REGISTER_API_FUNCTION(GetAllTabsInWindow); \ + REGISTER_API_FUNCTION(CreateTab); \ + REGISTER_API_FUNCTION(UpdateTab); \ + REGISTER_API_FUNCTION(MoveTab); \ + REGISTER_API_FUNCTION(TabsExecuteScript); \ + REGISTER_API_FUNCTION(TabsInsertCSS); \ + REGISTER_API_FUNCTION(RemoveTab) + +// Registers the chrome.window.* functions we handle. +// We don't have CreateWindow in here because of a compilation limitation. +// See the comment in the windows_api::CreateWindowFunc class for more details. +#define REGISTER_WINDOW_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetWindow); \ + REGISTER_API_FUNCTION(GetCurrentWindow); \ + REGISTER_API_FUNCTION(GetLastFocusedWindow); \ + REGISTER_API_FUNCTION(UpdateWindow); \ + REGISTER_API_FUNCTION(RemoveWindow); \ + REGISTER_API_FUNCTION(GetAllWindows) + +// Registers the chrome.cookies.* functions we handle. +#define REGISTER_COOKIE_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetCookie); \ + REGISTER_API_FUNCTION(GetAllCookies); \ + REGISTER_API_FUNCTION(SetCookie); \ + REGISTER_API_FUNCTION(RemoveCookie); \ + REGISTER_API_FUNCTION(GetAllCookieStores) + +// Registers the chrome.experimental.infobars.* functions we handle. +#define REGISTER_INFOBAR_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(ShowInfoBar) + +// Although we don't need to handle any chrome.experimental.webNavigation.* +// functions, we use this macro to register permanent event handlers when +// PRODUCTION_API_DISPATCHER is defined. +#define REGISTER_WEBNAVIGATION_API_FUNCTIONS() + +// Although we don't need to handle any chrome.experimental.webRequest.* +// functions, we use this macro to register permanent event handlers when +// PRODUCTION_API_DISPATCHER is defined. +#define REGISTER_WEBREQUEST_API_FUNCTIONS() + +// Add new tab function groups before this line. +#endif // PRODUCTION_API_DISPATCHER + +// Call this to register all functions. If you don't define +// PRODUCTION_API_DISPATCHER it will simply cause REGISTER_FUNCTION to be called +// for all functions. Otherwise it will cause your custom implementation of +// REGISTER_XYZ_API_FUNCTIONS to be called for each of the function groups +// above. +#define REGISTER_ALL_API_FUNCTIONS() \ + REGISTER_TAB_API_FUNCTIONS(); \ + REGISTER_WINDOW_API_FUNCTIONS(); \ + REGISTER_COOKIE_API_FUNCTIONS(); \ + REGISTER_INFOBAR_API_FUNCTIONS(); \ + REGISTER_WEBNAVIGATION_API_FUNCTIONS(); \ + REGISTER_WEBREQUEST_API_FUNCTIONS() + +#endif // CEEE_IE_COMMON_API_REGISTRATION_H_ diff --git a/ceee/ie/common/ceee_module_util.cc b/ceee/ie/common/ceee_module_util.cc new file mode 100644 index 0000000..0b9335e --- /dev/null +++ b/ceee/ie/common/ceee_module_util.cc @@ -0,0 +1,279 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CEEE module-wide utilities. + +#include "ceee/ie/common/ceee_module_util.h" + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "base/win/registry.h" +#include "ceee/common/process_utils_win.h" +#include "chrome/installer/util/google_update_constants.h" + +namespace { + +const wchar_t* kRegistryPath = L"SOFTWARE\\Google\\CEEE"; +const wchar_t* kRegistryValueToolbandIsHidden = L"toolband_is_hidden"; +const wchar_t* kRegistryValueToolbandPlaced = L"toolband_placed"; +const wchar_t* kRegistryValueCrxInstalledPath = L"crx_installed_path"; +const wchar_t* kRegistryValueCrxInstalledTime = L"crx_installed_time"; + +// Global state needed by the BHO and the +// toolband, to indicate whether ShowDW calls should affect +// registry tracking of the user's visibility preference. A +// non-zero value indicates that the calls should be ignored. +LONG g_ignore_show_dw_changes = 0; + +bool GetCeeeRegistryBoolean(const wchar_t* key, bool default_value) { + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + LOG_IF(ERROR, !hkcu.Valid()) << "Could not open reg key: " << kRegistryPath; + + DWORD dword_value_representation = 0; + DWORD size = sizeof(dword_value_representation); + DWORD type = REG_DWORD; + + if (!hkcu.Valid() || + !hkcu.ReadValue(key, &dword_value_representation, &size, &type)) { + return default_value; + } + + return dword_value_representation != 0; +} + +void SetCeeeRegistryBoolean(const wchar_t* key, bool assign_value) { + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_WRITE); + LOG_IF(ERROR, !hkcu.Valid()) << "Could not open reg key: " << kRegistryPath; + + DWORD dword_value_representation = assign_value ? 1 : 0; + bool write_result = hkcu.WriteValue(key, &dword_value_representation, + sizeof(dword_value_representation), + REG_DWORD); + + LOG_IF(ERROR, !write_result) << "Failed to write a registry key: " << key; +} + +} // anonymous namespace + +namespace ceee_module_util { + + +// The name of the profile we want ChromeFrame to use (for Internet Explorer). +const wchar_t kChromeProfileName[] = L"iexplore"; + +// The name of the profile we want ChromeFrame to use (for Internet Explorer) +// in case when explorer is 'Run as Administrator'. +const wchar_t kChromeProfileNameForAdmin[] = L"iexplore_admin"; + +const wchar_t kChromeProfileNameForFirefox[] = L"ceee_ff"; +const wchar_t kInternetExplorerModuleName[] = L"iexplore.exe"; +const wchar_t kCeeeBrokerModuleName[] = L"ceee_broker.exe"; + +std::wstring GetExtensionPath() { + const wchar_t* kRegistryValue = L"crx_path"; + + // We first check HKEY_CURRENT_USER, then HKEY_LOCAL_MACHINE. This is to + // let individual users override the machine-wide setting. The machine-wide + // setting is needed as our installer runs as system. + // + // On 64-bit Windows, there are separate versions of the registry for 32-bit + // applications and 64-bit applications. When you manually set things in the + // registry, you may have set them using the 32-bit regedit program + // (generally at c:\windows\SysWOW64\regedit.exe) or the 64-bit version, + // confusingly named regedt32.exe (at c:\windows\system32\regedt32.exe). + // + // You need to make sure you set the version of the registry corresponding to + // the way this executable is compiled. + // + // If the registry values are not set, then attempt to locate the extension + // by convention: that is, in a folder called + // "[32-bit program files]\Google\CEEE\Extensions", look for the + // first valid directory and use that. Eventually, when we support more + // than one extension, we can load/install all directories and/or crx files + // found here. + std::wstring crx_path; + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, kRegistryPath, KEY_READ); + + base::win::RegKey* keys[] = { &hkcu, &hklm }; + for (int i = 0; i < arraysize(keys); ++i) { + if (keys[i]->Valid() && keys[i]->ReadValue(kRegistryValue, &crx_path)) + break; + } + + if (crx_path.size() == 0u) { + FilePath file_path; + PathService::Get(base::DIR_PROGRAM_FILES, &file_path); + + file_path = file_path.Append(L"Google").Append(L"CEEE"). + Append(L"Extensions"); + if (!file_path.empty()) { + // First check for a .crx file (we prefer the .crx) + file_util::FileEnumerator e( + file_path, false, file_util::FileEnumerator::FILES); + for (FilePath ext = e.Next(); !ext.empty(); ext = e.Next()) { + if (ext.Extension() == L".crx") { + crx_path = ext.value(); + break; + } + } + + if (crx_path.size() == 0u) { + // Use the first directory instead, in the hope that it is an + // exploded extension directory. + file_util::FileEnumerator e( + file_path, false, file_util::FileEnumerator::DIRECTORIES); + FilePath directory = e.Next(); + if (!directory.empty()) { + crx_path = directory.value(); + } + } + } + } + + // Don't DCHECK here - it's an expected case that no .crx file is provided + // for installation along with CEEE. + LOG_IF(WARNING, crx_path.empty()) << "No CRX found to install."; + return crx_path; +} + +FilePath GetInstalledExtensionPath() { + std::wstring crx_path; + + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + // Default value is good enough for us. + hkcu.ReadValue(kRegistryValueCrxInstalledPath, &crx_path); + + return FilePath(crx_path); +} + +base::Time GetInstalledExtensionTime() { + int64 crx_time = 0; + DWORD size = sizeof(crx_time); + + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + // Default value is good enough for us. + hkcu.ReadValue(kRegistryValueCrxInstalledTime, &crx_time, &size, NULL); + + return base::Time::FromInternalValue(crx_time); +} + +bool NeedToInstallExtension() { + std::wstring path = GetExtensionPath(); + if (path.empty()) { + // No extension to install, so no need to install the extension. + return false; + } + + const FilePath crx_path(path); + if (IsCrxOrEmpty(crx_path.value())) { + if (crx_path == GetInstalledExtensionPath()) { + base::Time installed_time; + base::PlatformFileInfo extension_info; + const bool success = file_util::GetFileInfo(crx_path, &extension_info); + // If the call above didn't succeed, assume we need to install. + return !success || + extension_info.last_modified > GetInstalledExtensionTime(); + } + + return true; + } + + return false; +} + +void SetInstalledExtensionPath(const FilePath& path) { + base::PlatformFileInfo extension_info; + const bool success = file_util::GetFileInfo(path, &extension_info); + const int64 crx_time = success ? + extension_info.last_modified.ToInternalValue() : + base::Time::Now().ToInternalValue(); + + base::win::RegKey key(HKEY_CURRENT_USER, kRegistryPath, KEY_WRITE); + bool write_result = key.WriteValue(kRegistryValueCrxInstalledTime, + &crx_time, + sizeof(crx_time), + REG_QWORD); + DCHECK(write_result); + write_result = key.WriteValue(kRegistryValueCrxInstalledPath, + path.value().c_str()); + DCHECK(write_result); +} + +bool IsCrxOrEmpty(const std::wstring& path) { + return (path.empty() || + (path.substr(std::max(path.size() - 4, 0u)) == L".crx")); +} + +void SetOptionToolbandIsHidden(bool is_hidden) { + SetCeeeRegistryBoolean(kRegistryValueToolbandIsHidden, is_hidden); +} + +bool GetOptionToolbandIsHidden() { + return GetCeeeRegistryBoolean(kRegistryValueToolbandIsHidden, false); +} + +void SetOptionToolbandForceReposition(bool reposition_next_time) { + SetCeeeRegistryBoolean(kRegistryValueToolbandPlaced, !reposition_next_time); +} + +bool GetOptionToolbandForceReposition() { + return !GetCeeeRegistryBoolean(kRegistryValueToolbandPlaced, false); +} + +void SetIgnoreShowDWChanges(bool ignore) { + if (ignore) { + ::InterlockedIncrement(&g_ignore_show_dw_changes); + } else { + ::InterlockedDecrement(&g_ignore_show_dw_changes); + } +} + +bool GetIgnoreShowDWChanges() { + return ::InterlockedExchangeAdd(&g_ignore_show_dw_changes, 0) != 0; +} + +const wchar_t* GetBrokerProfileNameForIe() { + bool running_as_admin = false; + HRESULT hr = + process_utils_win::IsCurrentProcessUacElevated(&running_as_admin); + DCHECK(SUCCEEDED(hr)); + + // Profile name for 'runas' mode has to be different because this mode spawns + // its own copy of ceee_broker. To avoid scrambling user data, we run it in + // different profile. In the unlikely event of even failing to retrieve info, + // run as normal user. + return (FAILED(hr) || !running_as_admin) ? kChromeProfileName + : kChromeProfileNameForAdmin; +} + +// TODO(vitalybuka@google.com) : remove this code and use BrowserDistribution. +// BrowserDistribution requires modification to know about CEEE (bb3136374). +std::wstring GetCromeFrameClientStateKey() { + static const wchar_t kChromeFrameGuid[] = + L"{8BA986DA-5100-405E-AA35-86F34A02ACBF}"; + std::wstring key(google_update::kRegPathClientState); + key.append(L"\\"); + key.append(kChromeFrameGuid); + return key; +} + +// TODO(vitalybuka@google.com) : remove this code and use +// GoogleUpdateSettings::GetCollectStatsConsent() code. +// BrowserDistribution requires modification to know about CEEE (bb3136374). +bool GetCollectStatsConsent() { + std::wstring reg_path = GetCromeFrameClientStateKey(); + base::win::RegKey key(HKEY_CURRENT_USER, reg_path.c_str(), KEY_READ); + DWORD value; + if (!key.ReadValueDW(google_update::kRegUsageStatsField, &value)) { + base::win::RegKey hklm_key(HKEY_LOCAL_MACHINE, reg_path.c_str(), KEY_READ); + if (!hklm_key.ReadValueDW(google_update::kRegUsageStatsField, &value)) + return false; + } + return (1 == value); +} + +} // namespace ceee_module_util diff --git a/ceee/ie/common/ceee_module_util.h b/ceee/ie/common/ceee_module_util.h new file mode 100644 index 0000000..e7b2f30 --- /dev/null +++ b/ceee/ie/common/ceee_module_util.h @@ -0,0 +1,101 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CEEE module-wide utilities. + +#ifndef CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ +#define CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ + +#include <wtypes.h> +#include <string> + +#include "base/file_path.h" +#include "base/time.h" + +namespace ceee_module_util { + +void AddRefModuleWorkerThread(); +void ReleaseModuleWorkerThread(); + +// Fires an event to the broker, so that the call can be made with an +// instance of a broker proxy that was CoCreated in the worker thread. +void FireEventToBroker(const std::string& event_name, + const std::string& event_args); + +void Lock(); +void Unlock(); + +LONG LockModule(); +LONG UnlockModule(); + +class AutoLock { + public: + AutoLock() { + Lock(); + } + ~AutoLock() { + Unlock(); + } +}; + +// Gets the path of a .crx that should be installed at the same time as +// CEEE is installed. May be empty, meaning no .crx files need to be +// installed. +// +// If the returned path does not have a .crx extension, it should be +// assumed to be an exploded extension directory rather than a .crx +std::wstring GetExtensionPath(); + +// Return the path and time of the last installation that occurred. +FilePath GetInstalledExtensionPath(); +base::Time GetInstalledExtensionTime(); + +// Return true if the extension is outdated compared to the registry +bool NeedToInstallExtension(); + +// Install an extension and sets the right registry values +void SetInstalledExtensionPath(const FilePath& path); + +// Returns true if the given path ends with .crx or is empty (as an +// unset "what to load/install" preference means we should expect +// an extension to already be installed). +bool IsCrxOrEmpty(const std::wstring& path); + +// Stores/reads a registry entry that tracks whether the user has made the +// toolband visible or hidden. +void SetOptionToolbandIsHidden(bool isHidden); +bool GetOptionToolbandIsHidden(); + +// Stores/reads a registry entry that tracks whether intial (after setup) +// positioning of the toolband has been completed. +void SetOptionToolbandForceReposition(bool reposition_next_time); +bool GetOptionToolbandForceReposition(); + +// Indicates whether ShowDW calls should affect registry tracking of the +// user's visibility preference. +void SetIgnoreShowDWChanges(bool ignore); +bool GetIgnoreShowDWChanges(); + +// Chooses between kChromeProfileName and kChromeProfileNameForAdmin depending +// on process properties (run as admin or not). +const wchar_t* GetBrokerProfileNameForIe(); + +// Returns true if Chrome Frame is allowed to send usage stats. +bool GetCollectStatsConsent(); + +// Returns Google Update ClientState registry key for ChromeFrame. +std::wstring GetCromeFrameClientStateKey(); + +// The name of the profile that is used by the Firefox CEEE. +extern const wchar_t kChromeProfileNameForFirefox[]; + +// The name of the Internet Explorer executable. +extern const wchar_t kInternetExplorerModuleName[]; + +// The name of the CEEE Broker executable. +extern const wchar_t kCeeeBrokerModuleName[]; + +} // namespace + +#endif // CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ diff --git a/ceee/ie/common/ceee_module_util_unittest.cc b/ceee/ie/common/ceee_module_util_unittest.cc new file mode 100644 index 0000000..67c0dd1 --- /dev/null +++ b/ceee/ie/common/ceee_module_util_unittest.cc @@ -0,0 +1,287 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for the CEEE module-wide utilities. + +#include "ceee/ie/common/ceee_module_util.h" + +#include <wtypes.h> +#include <string> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "base/win/registry.h" +#include "base/string_util.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/installer/util/google_update_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::DoAll; +using testing::NotNull; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::Return; + +const wchar_t* kReplacementRoot = + L"Software\\Google\\InstallUtilUnittest"; +const wchar_t* kHKCUReplacement = + L"Software\\Google\\InstallUtilUnittest\\HKCU"; +const wchar_t* kHKLMReplacement = + L"Software\\Google\\InstallUtilUnittest\\HKLM"; + +MOCK_STATIC_CLASS_BEGIN(MockProcessWinUtils) + MOCK_STATIC_INIT_BEGIN(MockProcessWinUtils) + MOCK_STATIC_INIT2(process_utils_win::IsCurrentProcessUacElevated, + IsCurrentProcessUacElevated); + MOCK_STATIC_INIT_END() + + MOCK_STATIC1(HRESULT, , IsCurrentProcessUacElevated, bool*); +MOCK_STATIC_CLASS_END(MockProcessWinUtils) + + +class CeeeModuleUtilTest : public testing::Test { + protected: + static const DWORD kFalse = 0; + static const DWORD kTrue = 1; + static const DWORD kInvalid = 5; + + virtual void SetUp() { + // Wipe the keys we redirect to. + // This gives us a stable run, even in the presence of previous + // crashes or failures. + LSTATUS err = SHDeleteKey(HKEY_CURRENT_USER, kReplacementRoot); + EXPECT_TRUE(err == ERROR_SUCCESS || err == ERROR_FILE_NOT_FOUND); + + // Create the keys we're redirecting HKCU and HKLM to. + ASSERT_TRUE(hkcu_.Create(HKEY_CURRENT_USER, kHKCUReplacement, KEY_READ)); + ASSERT_TRUE(hklm_.Create(HKEY_CURRENT_USER, kHKLMReplacement, KEY_READ)); + + // And do the switcharoo. + ASSERT_EQ(ERROR_SUCCESS, + ::RegOverridePredefKey(HKEY_CURRENT_USER, hkcu_.Handle())); + ASSERT_EQ(ERROR_SUCCESS, + ::RegOverridePredefKey(HKEY_LOCAL_MACHINE, hklm_.Handle())); + } + + virtual void TearDown() { + // Undo the redirection. + EXPECT_EQ(ERROR_SUCCESS, ::RegOverridePredefKey(HKEY_CURRENT_USER, NULL)); + EXPECT_EQ(ERROR_SUCCESS, ::RegOverridePredefKey(HKEY_LOCAL_MACHINE, NULL)); + + // Close our handles and delete the temp keys we redirected to. + hkcu_.Close(); + hklm_.Close(); + EXPECT_EQ(ERROR_SUCCESS, SHDeleteKey(HKEY_CURRENT_USER, kReplacementRoot)); + } + + base::win::RegKey hkcu_; + base::win::RegKey hklm_; +}; + +// Mock PathService::Get that is used to look up files. +MOCK_STATIC_CLASS_BEGIN(MockPathService) + MOCK_STATIC_INIT_BEGIN(MockPathService) + bool (*func_ptr)(int, FilePath*) = PathService::Get; + MOCK_STATIC_INIT2(func_ptr, Get); + MOCK_STATIC_INIT_END() + + MOCK_STATIC2(bool, , Get, int, FilePath*); +MOCK_STATIC_CLASS_END(MockPathService); + +// Empty registry case (without crx) +TEST_F(CeeeModuleUtilTest, ExtensionPathTestNoRegistry) { + namespace cmu = ceee_module_util; + + StrictMock<MockPathService> mock_path; + FilePath temp_path; + ASSERT_TRUE(file_util::CreateNewTempDirectory(L"CeeeModuleUtilTest", + &temp_path)); + + EXPECT_CALL(mock_path, Get(base::DIR_PROGRAM_FILES, NotNull())). + WillOnce(DoAll( + SetArgumentPointee<1>(temp_path), + Return(true))); + EXPECT_EQ(std::wstring(), cmu::GetExtensionPath()); + + FilePath full_path = temp_path.Append(L"Google"). + Append(L"CEEE").Append(L"Extensions"); + FilePath crx_path = full_path.Append(L"testing.crx"); + ASSERT_TRUE(file_util::CreateDirectory(full_path)); + FilePath temp_file_path; + ASSERT_TRUE(file_util::CreateTemporaryFileInDir(full_path, &temp_file_path)); + ASSERT_TRUE(file_util::Move(temp_file_path, crx_path)); + EXPECT_CALL(mock_path, Get(base::DIR_PROGRAM_FILES, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(temp_path), Return(true))); + EXPECT_EQ(crx_path.value(), cmu::GetExtensionPath()); + + // Clean up. + file_util::Delete(temp_path, true); +} + +TEST_F(CeeeModuleUtilTest, ExtensionPathTest) { + namespace cmu = ceee_module_util; + + // The FilePath::Get method shouldn't be called if we take the value + // from the registry. + StrictMock<MockPathService> mock_path; + + // Creates a registry key + base::win::RegKey hkcu(HKEY_CURRENT_USER, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + + // Create a random string so we can compare the result with it. + // Ideally, this would be a really random string, not some arbitrary constant. + std::wstring path = L"asdfqwerasdfqwer"; + const wchar_t* kRegistryValueName = L"crx_path"; + + hklm.WriteValue(kRegistryValueName, path.c_str()); + hklm.Close(); + EXPECT_EQ(path, cmu::GetExtensionPath()); + + // Change the random string and verify we get the new one if we set it + // to HKCU + path += L"_HKCU"; + hkcu.WriteValue(kRegistryValueName, path.c_str()); + hkcu.Close(); + EXPECT_EQ(path, cmu::GetExtensionPath()); +} + +TEST_F(CeeeModuleUtilTest, NeedToInstallExtension) { + namespace cmu = ceee_module_util; + + // Create our registry key and a temporary file + base::win::RegKey hkcu(HKEY_CURRENT_USER, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + FilePath temp_path; + ASSERT_TRUE(file_util::CreateNewTempDirectory(L"CeeeModuleUtilTest", + &temp_path)); + FilePath crx_path = temp_path.Append(L"temp.crx"); + FilePath temp_file_path; + ASSERT_TRUE(file_util::CreateTemporaryFileInDir(temp_path, &temp_file_path)); + ASSERT_TRUE(file_util::Move(temp_file_path, crx_path)); + + const wchar_t* kRegistryCrxPathValueName = L"crx_path"; + const wchar_t* kRegistryValueCrxInstalledPath = L"crx_installed_path"; + const wchar_t* kRegistryValueCrxInstalledTime = L"crx_installed_time"; + + hkcu.WriteValue(kRegistryCrxPathValueName, crx_path.value().c_str()); + + // Those should return empty values + // We use EXPECT_TRUE instead of EXPECT_EQ because there's no operator<< + // declared for FilePath and base::Time so we cannot output the + // expected/received values. + EXPECT_TRUE(FilePath() == cmu::GetInstalledExtensionPath()); + EXPECT_TRUE(base::Time() == cmu::GetInstalledExtensionTime()); + + // Now we should need to install, since the keys aren't there yet. + EXPECT_TRUE(cmu::NeedToInstallExtension()); + + // And if we update the install info, we shouldn't need to anymore. + cmu::SetInstalledExtensionPath(crx_path); + EXPECT_FALSE(cmu::NeedToInstallExtension()); + + // We get the installed path and time and verify them against our values. + base::PlatformFileInfo crx_info; + ASSERT_TRUE(file_util::GetFileInfo(crx_path, &crx_info)); + EXPECT_TRUE(crx_path == cmu::GetInstalledExtensionPath()); + EXPECT_TRUE(crx_info.last_modified == cmu::GetInstalledExtensionTime()); + + // Finally, if we update the file time, we should be able to install again. + // But we must make sure to cross the 10ms boundary which is the granularity + // of the FILETIME, so sleep for 20ms to be on the safe side. + ::Sleep(20); + ASSERT_TRUE(file_util::SetLastModifiedTime(crx_path, base::Time::Now())); + EXPECT_TRUE(cmu::NeedToInstallExtension()); +} + +TEST(CeeeModuleUtil, GetBrokerProfileNameForIe) { + MockProcessWinUtils mock_query_admin; + + EXPECT_CALL(mock_query_admin, IsCurrentProcessUacElevated(_)). + WillOnce(DoAll(SetArgumentPointee<0>(false), Return(S_OK))); + const wchar_t* profile_user = + ceee_module_util::GetBrokerProfileNameForIe(); + + EXPECT_CALL(mock_query_admin, IsCurrentProcessUacElevated(_)). + WillOnce(DoAll(SetArgumentPointee<0>(true), Return(S_OK))); + const wchar_t* profile_admin = + ceee_module_util::GetBrokerProfileNameForIe(); + + ASSERT_STRNE(profile_user, profile_admin); +} + +TEST(CeeeModuleUtil, GetCromeFrameClientStateKey) { + EXPECT_EQ(L"Software\\Google\\Update\\ClientState\\" + L"{8BA986DA-5100-405E-AA35-86F34A02ACBF}", + ceee_module_util::GetCromeFrameClientStateKey()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentFromHkcu) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hkcu(HKEY_CURRENT_USER, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kTrue)); + EXPECT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kInvalid)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.DeleteValue(google_update::kRegUsageStatsField)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentFromHklm) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kTrue)); + EXPECT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kInvalid)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.DeleteValue(google_update::kRegUsageStatsField)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentHkcuBeforeHklm) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hkcu(HKEY_CURRENT_USER, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kTrue)); + ASSERT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.DeleteValue(google_update::kRegUsageStatsField)); + ASSERT_TRUE(hklm.DeleteValue(google_update::kRegUsageStatsField)); +} + +} // namespace diff --git a/ceee/ie/common/chrome_frame_host.cc b/ceee/ie/common/chrome_frame_host.cc new file mode 100644 index 0000000..907b1f9 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host.cc @@ -0,0 +1,373 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ChromeFrameHost implementation. +#include "ceee/ie/common/chrome_frame_host.h" + +#include <algorithm> +#include <vector> + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/common/chrome_switches.h" + +#include "toolband.h" // NOLINT + +namespace ext = extension_automation_constants; + + +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_bstr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BSTR } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_variantptr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BYREF | VT_VARIANT } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_bstrarray_= + { CC_STDCALL, VT_EMPTY, 1, { VT_ARRAY | VT_BSTR } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_void_= + { CC_STDCALL, VT_EMPTY, 0, { } }; + +// {AFA3E2CF-2C8E-4546-8CD0-A2D93759A4DE} +extern const GUID IID_IChromeFrameHost = + { 0xafa3e2cf, 0x2c8e, 0x4546, + { 0x8c, 0xd0, 0xa2, 0xd9, 0x37, 0x59, 0xa4, 0xde } }; + +ChromeFrameHost::ChromeFrameHost() + : document_loaded_(false), origin_(ext::kAutomationOrigin) { + LOG(INFO) << "Create ChromeFrameHost(" << this << ")"; +} + +ChromeFrameHost::~ChromeFrameHost() { + LOG(INFO) << "Destroy ChromeFrameHost(" << this << ")"; +} + +HRESULT ChromeFrameHost::FinalConstruct() { + return S_OK; +} + +void ChromeFrameHost::FinalRelease() { +} + +STDMETHODIMP ChromeFrameHost::GetWantsPrivileged(boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::GetChromeProfileName(BSTR* profile_name) { + return chrome_profile_name_.CopyTo(profile_name); +} + +STDMETHODIMP ChromeFrameHost::GetExtensionApisToAutomate( + BSTR* functions_enabled) { + DCHECK(functions_enabled != NULL); + HRESULT hr = S_FALSE; + if (event_sink_ != NULL) { + hr = event_sink_->OnCfGetExtensionApisToAutomate(functions_enabled); +#ifndef NDEBUG + if (*functions_enabled != NULL) { + // Only one chrome frame host is allowed to return a list of functions + // to enable automation on, so make sure we are the one and only. + std::wstring event_name(L"google-ceee-apiautomation!"); + + DCHECK(chrome_profile_name_ != NULL); + if (chrome_profile_name_ != NULL) + event_name += chrome_profile_name_; + + std::replace(event_name.begin(), event_name.end(), '\\', '!'); + std::transform( + event_name.begin(), event_name.end(), event_name.begin(), tolower); + automating_extension_api_.Set( + ::CreateEvent(NULL, TRUE, TRUE, event_name.c_str())); + DWORD we = ::GetLastError(); + DCHECK(automating_extension_api_ != NULL && + we != ERROR_ALREADY_EXISTS && + we != ERROR_ACCESS_DENIED); + } +#endif // NDEBUG + } + return hr; +} + +HRESULT ChromeFrameHost::Initialize() { + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::TearDown() { + if (IsWindow()) { + // TearDown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + + chrome_frame_.Release(); +#ifndef NDEBUG + automating_extension_api_.Close(); +#endif + return S_OK; +} + +STDMETHODIMP_(void) ChromeFrameHost::SetEventSink( + IChromeFrameHostEvents* event_sink) { + event_sink_ = event_sink; +} + +HRESULT ChromeFrameHost::InstallExtension(BSTR crx_path) { + if (chrome_frame_) { + return chrome_frame_->installExtension(crx_path); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::LoadExtension(BSTR extension_dir) { + if (chrome_frame_) { + return chrome_frame_->installExtension(extension_dir); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::GetEnabledExtensions() { + if (chrome_frame_) { + return chrome_frame_->getEnabledExtensions(); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::GetSessionId(int *session_id) { + if (chrome_frame_) { + CComQIPtr<IChromeFrameInternal> chrome_frame_internal_(chrome_frame_); + if (chrome_frame_internal_) + return chrome_frame_internal_->getSessionId(session_id); + else + return kInvalidChromeSessionId; + } + NOTREACHED(); + return E_UNEXPECTED; +} + +void ChromeFrameHost::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +HRESULT ChromeFrameHost::SetChildSite(IUnknown* child) { + if (child == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + CComPtr<IObjectWithSite> child_site; + hr = child->QueryInterface(&child_site); + if (SUCCEEDED(hr)) + hr = child_site->SetSite(GetUnknown()); + + return hr; +} + +LRESULT ChromeFrameHost::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + return 0; +} + +HRESULT ChromeFrameHost::SetUrl(BSTR url) { + HRESULT hr = chrome_frame_->put_src(url); + DCHECK(SUCCEEDED(hr)) << "Failed to navigate Chrome Frame: " << + com::LogHr(hr); + return hr; +} + +STDMETHODIMP ChromeFrameHost::StartChromeFrame() { + DCHECK(!IsWindow()); + + // Create a message window to host our control. + if (NULL == Create(HWND_MESSAGE)) + return E_FAIL; + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CreateActiveXHost(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create ActiveX host window: " << com::LogHr(hr); + return hr; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = CreateChromeFrame(&chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed create Chrome Frame: " << com::LogHr(hr); + return hr; + } + + // And attach it to our window. This causes the host to subclass + // our window and attach itself to it. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host" << com::LogHr(hr); + return hr; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink: " << com::LogHr(hr); + return hr; + } + + return hr; +} + +HRESULT ChromeFrameHost::CreateActiveXHost(IAxWinHostWindow** host) { + return CAxHostWindow::CreateInstance(host); +} + +HRESULT ChromeFrameHost::CreateChromeFrame(IChromeFrame** chrome_frame) { + CComPtr<IChromeFrame> new_cf; + HRESULT hr = new_cf.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (SUCCEEDED(hr)) + hr = new_cf.CopyTo(chrome_frame); + + return hr; +} + +STDMETHODIMP ChromeFrameHost::PostMessage(BSTR message, BSTR target) { + if (!document_loaded_) { + PostedMessage posted_message = { message, target }; + posted_messages_.push_back(posted_message); + return S_FALSE; + } + + HRESULT hr = chrome_frame_->postPrivateMessage(message, origin_, target); + + return hr; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfLoad(IDispatch* event) { + DLOG(INFO) << "OnCfLoad"; + if (document_loaded_) { + // If we were already loaded, our list should be empty. + DCHECK(posted_messages_.empty()); + return; + } + document_loaded_ = true; + + // Flush all posted messages. + PostedMessageList::iterator it(posted_messages_.begin()); + for (; it != posted_messages_.end(); ++it) { + HRESULT hr = chrome_frame_->postPrivateMessage(it->message, origin_, + it->target); + DCHECK(SUCCEEDED(hr)) << "postPrivateMessage failed with: " << + com::LogHr(hr); + } + posted_messages_.clear(); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfLoadError(IDispatch* event) { + DLOG(ERROR) << "OnCfLoadError"; + DCHECK(false) << "OnCfLoadError"; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfExtensionReady(BSTR path, + int response) { + DLOG(INFO) << "OnCfExtensionReady: " << path << ", " << response; + // Early exit if there's no sink. + if (event_sink_ == NULL) + return; + event_sink_->OnCfExtensionReady(path, response); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* extension_directories) { + DLOG(INFO) << "OnCfGetEnabledExtensionsComplete"; + if (event_sink_) + event_sink_->OnCfGetEnabledExtensionsComplete(extension_directories); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfChannelError() { + DCHECK(false) << "OnCfChannelError means that Chrome has Crashed!"; + if (event_sink_) + event_sink_->OnCfChannelError(); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfMessage(IDispatch* event) { + DLOG(INFO) << "OnCfMessage"; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfReadyStateChanged(LONG state) { + DLOG(INFO) << "OnCfReadyStateChanged(" << state << ")"; + if (event_sink_) + event_sink_->OnCfReadyStateChanged(state); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfPrivateMessage(IDispatch* event, + BSTR target) { + // Early exit if there's no sink. + if (event_sink_ == NULL) + return; + + // Make sure that the message has a "data" member and get it. This should + // be a JSON-encoded command to execute. + CComDispatchDriver event_dispatch(event); + + CComVariant origin; + HRESULT hr = event_dispatch.GetPropertyByName(L"origin", &origin); + DCHECK(SUCCEEDED(hr) && origin.vt == VT_BSTR); + if (FAILED(hr) || origin.vt != VT_BSTR) { + NOTREACHED() << "No origin on event"; + return; + } + + CComVariant data; + hr = event_dispatch.GetPropertyByName(L"data", &data); + DCHECK(SUCCEEDED(hr) && data.vt == VT_BSTR); + if (FAILED(hr) || data.vt != VT_BSTR) { + NOTREACHED() << "No data on event"; + return; + } + + // Forward to the sink. + event_sink_->OnCfPrivateMessage(V_BSTR(&data), V_BSTR(&origin), target); +} + +STDMETHODIMP_(void) ChromeFrameHost::SetChromeProfileName( + const wchar_t* chrome_profile_name) { + chrome_profile_name_ = chrome_profile_name; + DLOG(INFO) << "Assigned profile name " << chrome_profile_name_; +} diff --git a/ceee/ie/common/chrome_frame_host.h b/ceee/ie/common/chrome_frame_host.h new file mode 100644 index 0000000..ef43499 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host.h @@ -0,0 +1,237 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE ChromeFrameHost implementation. +#ifndef CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ +#define CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ + +#include <atlbase.h> +#include <atlwin.h> +#include <atlcrack.h> +#include <list> + +#include "base/basictypes.h" +#include "base/scoped_handle.h" +#include "ceee/common/initializing_coclass.h" +#include "chrome_tab.h" // NOLINT + +// fwd. +struct IAxWinHostWindow; + +class IChromeFrameHostEvents: public IUnknown { + public: + virtual HRESULT OnCfReadyStateChanged(LONG state) = 0; + virtual HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target) = 0; + virtual HRESULT OnCfExtensionReady(BSTR path, int response) = 0; + virtual HRESULT OnCfGetEnabledExtensionsComplete(SAFEARRAY* base_dirs) = 0; + virtual HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled) = 0; + virtual HRESULT OnCfChannelError() = 0; +}; + +// This is the interface the chrome frame host presents to its consumers. +extern const GUID IID_IChromeFrameHost; +class IChromeFrameHost: public IUnknown { + public: + // Set the name of the profile we want Chrome Frame to use. + // @param chrome_profile_name The name of the profile to use. + STDMETHOD_(void, SetChromeProfileName)( + const wchar_t* chrome_profile_name) = 0; + + // Set the URL we want to navigate Chrome Frame to once it is ready. + // @param url The URL to navigate to. + STDMETHOD(SetUrl)(BSTR url) = 0; + + // Creates and initializes our Chrome Frame instance. + STDMETHOD(StartChromeFrame)() = 0; + + // Posts a message through Chrome Frame, or enqueues it if + // Chrome Frame is not yet ready and @p queueable is true. + // @param message The message to post to Chrome Frame. + // @param target Where we want the message to be posted within Chrome. + STDMETHOD(PostMessage)(BSTR message, BSTR target) = 0; + + // Tears down an initialized ChromeFrameHost. + STDMETHOD(TearDown)() = 0; + + // Sets the event sink for this ChromeFrameHost. + STDMETHOD_(void, SetEventSink)(IChromeFrameHostEvents* event_sink) = 0; + + // Installs the given extension. Results come back via + // IChromeFrameHostEvents::OnCfExtensionReady. + STDMETHOD(InstallExtension)(BSTR crx_path) = 0; + + // Loads the given exploded extension directory. Results come back via + // IChromeFrameHostEvents::OnCfExtensionReady. + STDMETHOD(LoadExtension)(BSTR extension_dir) = 0; + + // Initiates a request for installed extensions. Results come back via + // IChromeFrameHostEvents::OnCfGetEnabledExtensionsComplete. + STDMETHOD(GetEnabledExtensions)() = 0; + + // Retrieves the session_id used by Chrome for the CF tab. Will return S_FALSE + // if the session id is not yet available. + // The session_id is the id used for the Tab javascript object. + STDMETHOD(GetSessionId)(int* session_id) = 0; +}; + +class ATL_NO_VTABLE ChromeFrameHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ChromeFrameHost>, + public IServiceProviderImpl<ChromeFrameHost>, + public IChromeFrameHost, + public IChromeFramePrivileged, + public IDispEventSimpleImpl<0, + ChromeFrameHost, + &DIID_DIChromeFrameEvents>, + public CWindowImpl<ChromeFrameHost> { + public: + typedef IDispEventSimpleImpl<0, + ChromeFrameHost, + &DIID_DIChromeFrameEvents> ChromeFrameEvents; + ChromeFrameHost(); + ~ChromeFrameHost(); + + BEGIN_COM_MAP(ChromeFrameHost) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY_IID(IID_IChromeFrameHost, IChromeFrameHost) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(ChromeFrameHost) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + END_SERVICE_MAP() + + BEGIN_SINK_MAP(ChromeFrameHost) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONLOAD, + OnCfLoad, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONLOADERROR, + OnCfLoadError, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONMESSAGE, + OnCfMessage, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONPRIVATEMESSAGE, + OnCfPrivateMessage, &handler_type_idispatch_bstr_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + OnCfGetEnabledExtensionsComplete, &handler_type_bstrarray_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONCHANNELERROR, + OnCfChannelError, &handler_type_void_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + HRESULT Initialize(); + + HRESULT FinalConstruct(); + void FinalRelease(); + + BEGIN_MSG_MAP(ChromeFrameHost) + MSG_WM_CREATE(OnCreate) + END_MSG_MAP() + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean* wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR* args); + STDMETHOD(GetChromeProfileName)(BSTR* args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR* functions_enabled); + // @} + + // @name ChromeFrame event handlers + // @{ + STDMETHOD_(void, OnCfLoad)(IDispatch* event); + STDMETHOD_(void, OnCfLoadError)(IDispatch* event); + STDMETHOD_(void, OnCfMessage)(IDispatch* event); + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfPrivateMessage)(IDispatch *event, BSTR target); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfGetEnabledExtensionsComplete)( + SAFEARRAY* extension_directories); + STDMETHOD_(void, OnCfChannelError)(void); + // @} + + // @name IChromeFrameHost implementation. + // @{ + STDMETHOD_(void, SetChromeProfileName)(const wchar_t* chrome_profile_name); + STDMETHOD(SetUrl)(BSTR url); + STDMETHOD(StartChromeFrame)(); + STDMETHOD(PostMessage)(BSTR message, BSTR target); + STDMETHOD(TearDown)(); + STDMETHOD_(void, SetEventSink)(IChromeFrameHostEvents* event_sink); + STDMETHOD(InstallExtension)(BSTR crx_path); + STDMETHOD(LoadExtension)(BSTR extension_dir); + STDMETHOD(GetEnabledExtensions)(); + STDMETHOD(GetSessionId)(int* session_id); + // @} + + protected: + virtual HRESULT CreateActiveXHost(IAxWinHostWindow** host); + virtual HRESULT CreateChromeFrame(IChromeFrame** chrome_frame); + + // Our window maintains a refcount on us for the duration of its lifetime. + // The self-reference is managed with those two methods. + virtual void OnFinalMessage(HWND window); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + private: + struct PostedMessage { + CComBSTR message; + CComBSTR target; + }; + typedef std::list<PostedMessage> PostedMessageList; + + // Set us as site for child. + HRESULT SetChildSite(IUnknown* child); + + // Our Chrome Frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // The Chrome profile we ask to connect with. + CComBSTR chrome_profile_name_; + + // Messages posted before Chrome Frame has loaded have to be enqueued, + // to ensure that the toolband extension has propertly loaded and + // initialized before we attempt to post messages at it. + // This list stores such messages until Chrome Frame reports the + // document loaded. + PostedMessageList posted_messages_; + + // True iff Chrome Frame has reported a document loaded event. + // TODO(mad@chromium.org): Use a three states variable to take the + // error case into account. + bool document_loaded_; + +#ifndef NDEBUG + // We use a cross process event to make sure there is only one chrome frame + // host that returns ExtensionApisToAutomate... But only needed for a DCHECK. + ScopedHandle automating_extension_api_; +#endif + + // A cached BSTR for the posted messages origin (which is kAutomationOrigin). + CComBSTR origin_; + + // Our event sink. + CComPtr<IChromeFrameHostEvents> event_sink_; + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_; + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_idispatch_bstr_; + static _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_bstrarray_; + static _ATL_FUNC_INFO handler_type_void_; + + DISALLOW_COPY_AND_ASSIGN(ChromeFrameHost); +}; + +#endif // CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ diff --git a/ceee/ie/common/chrome_frame_host_unittest.cc b/ceee/ie/common/chrome_frame_host_unittest.cc new file mode 100644 index 0000000..f7cf592 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host_unittest.cc @@ -0,0 +1,607 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for chrome frame host. +#include "ceee/ie/common/chrome_frame_host.h" + +#include "base/string_util.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::FireEvent; + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Eq; +using testing::Field; +using testing::Invoke; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::MockDispatchEx; + +class MockChromeFrameHost : public ChromeFrameHost { + public: + // Mock the creator functions to control them. + MOCK_METHOD1(CreateChromeFrame, HRESULT(IChromeFrame** chrome_frame)); + // And the event handlers to test event subscription. + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfLoad, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfLoadError, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfMessage, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfReadyStateChanged, void(LONG)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, + OnCfPrivateMessage, + void(IDispatch* event, BSTR origin)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, + OnCfExtensionReady, + void(BSTR path, int response)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfGetEnabledExtensionsComplete, + void(SAFEARRAY* extensions)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, OnCfChannelError, void(void)); +}; + +class TestChromeFrameHost + : public StrictMock<MockChromeFrameHost>, + public InitializingCoClass<TestChromeFrameHost> { + public: + using InitializingCoClass<TestChromeFrameHost>::CreateInitialized; + + TestChromeFrameHost() : active_x_host_creation_error_(S_OK) { + ++instance_count_; + } + ~TestChromeFrameHost() { + --instance_count_; + } + + void set_active_x_host_creation_error(HRESULT hr) { + active_x_host_creation_error_ = hr; + } + + HRESULT Initialize(TestChromeFrameHost** self) { + *self = this; + return S_OK; + } + + + HRESULT CreateActiveXHost(IAxWinHostWindow** host) { + if (FAILED(active_x_host_creation_error_)) + return active_x_host_creation_error_; + + return ChromeFrameHost::CreateActiveXHost(host); + } + + // For some of the mocks, we want to test the original behavior. + void CallOnCfLoad(IDispatch* event) { + ChromeFrameHost::OnCfLoad(event); + } + void CallOnCfReadyStateChanged(LONG state) { + ChromeFrameHost::OnCfReadyStateChanged(state); + } + void CallOnCfPrivateMessage(IDispatch* event, BSTR origin) { + ChromeFrameHost::OnCfPrivateMessage(event, origin); + } + void CallOnCfExtensionReady(BSTR path, int response) { + ChromeFrameHost::OnCfExtensionReady(path, response); + } + void CallOnCfGetEnabledExtensionsComplete(SAFEARRAY* enabled_extensions) { + ChromeFrameHost::OnCfGetEnabledExtensionsComplete(enabled_extensions); + } + void CallOnCfChannelError() { + ChromeFrameHost::OnCfChannelError(); + } + + public: + HRESULT active_x_host_creation_error_; + + static size_t instance_count_; +}; + +size_t TestChromeFrameHost::instance_count_ = 0; + +class IChromeFrameImpl : public IDispatchImpl<IChromeFrame, + &IID_IChromeFrame, + &LIBID_ChromeTabLib> { + public: + // @name IChromeFrame implementation + // @{ + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_src, HRESULT(BSTR *src)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_src, HRESULT(BSTR src)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, postMessage, HRESULT( + BSTR message, VARIANT target)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onload, HRESULT( + VARIANT *onload_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onload, HRESULT( + VARIANT onload_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onloaderror, HRESULT( + VARIANT *onerror_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onloaderror, HRESULT( + VARIANT onerror_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onmessage, HRESULT( + VARIANT *onmessage_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onmessage, HRESULT( + VARIANT onmessage_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_readyState, HRESULT( + LONG *ready_state)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, addEventListener, HRESULT( + BSTR event_type, IDispatch *listener, VARIANT use_capture)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, removeEventListener, HRESULT( + BSTR event_type, IDispatch *listener, VARIANT use_capture)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_version, HRESULT(BSTR *version)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, postPrivateMessage, HRESULT( + BSTR message, BSTR origin, BSTR target)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_useChromeNetwork, HRESULT( + VARIANT_BOOL *pVal)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_useChromeNetwork, HRESULT( + VARIANT_BOOL newVal)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, installExtension, HRESULT( + BSTR crx_path)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, loadExtension, HRESULT( + BSTR path)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, getEnabledExtensions, HRESULT()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, registerBhoIfNeeded, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, getSessionId, HRESULT(int*)); + // @} +}; + +class MockChromeFrame + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass< StrictMock<MockChromeFrame> >, + public IObjectWithSiteImpl<MockChromeFrame>, + public StrictMock<IChromeFrameImpl>, + public IConnectionPointContainerImpl<MockChromeFrame>, + public IConnectionPointImpl<MockChromeFrame, &DIID_DIChromeFrameEvents> { + public: + BEGIN_COM_MAP(MockChromeFrame) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IChromeFrame) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(MockChromeFrame) + CONNECTION_POINT_ENTRY(DIID_DIChromeFrameEvents) + END_CONNECTION_POINT_MAP() + + MockChromeFrame() : no_events_(false) { + ++instance_count_; + } + ~MockChromeFrame() { + --instance_count_; + } + + void set_no_events(bool no_events) { + no_events_ = no_events; + } + + HRESULT Initialize(MockChromeFrame** self) { + *self = this; + return S_OK; + } + + // Override from IConnectionPointContainerImpl + STDMETHOD(FindConnectionPoint)(REFIID iid, IConnectionPoint** cp) { + typedef IConnectionPointContainerImpl<MockChromeFrame> CPC; + + if (iid == DIID_DIChromeFrameEvents && no_events_) + return CONNECT_E_NOCONNECTION; + + return CPC::FindConnectionPoint(iid, cp); + } + + typedef IConnectionPointImpl<MockChromeFrame, &DIID_DIChromeFrameEvents> CP; + void FireEvent1(DISPID id, IDispatch* event) { + return FireEvent(static_cast<CP*>(this), id, 1, &CComVariant(event)); + } + + void FireCfLoad(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONLOAD, event); + } + + void FireCfLoadError(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONLOADERROR, event); + } + + void FireCfMessage(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONMESSAGE, event); + } + + void FireCfReadyStateChanged(LONG ready_state) { + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONREADYSTATECHANGED, + 1, + &CComVariant(ready_state)); + } + + void FireCfPrivateMessage(IDispatch* event, BSTR origin) { + CComVariant args[] = { origin, event }; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONPRIVATEMESSAGE, + arraysize(args), + args); + } + + void FireCfExtensionReady(BSTR path, int response) { + CComVariant args[] = { response , path}; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONEXTENSIONREADY, + arraysize(args), + args); + } + + void FireCfGetEnabledExtensionsComplete(SAFEARRAY* array) { + VARIANT args[] = { { VT_ARRAY | VT_BSTR } }; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + arraysize(args), + args); + } + + void FireCfChannelError() { + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONCHANNELERROR, + 0, + NULL); + } + + public: + // Quench our event sink. + bool no_events_; + + static size_t instance_count_; +}; + +size_t MockChromeFrame::instance_count_ = 0; + +class IChromeFrameHostEventsMockImpl : public IChromeFrameHostEvents { + public: + MOCK_METHOD3(OnCfPrivateMessage, HRESULT(BSTR, BSTR, BSTR)); + MOCK_METHOD2(OnCfExtensionReady, HRESULT(BSTR, int)); + MOCK_METHOD1(OnCfGetEnabledExtensionsComplete, + HRESULT(SAFEARRAY* extensions)); + MOCK_METHOD1(OnCfGetExtensionApisToAutomate, + HRESULT(BSTR* enabled_functions)); + MOCK_METHOD1(OnCfReadyStateChanged, HRESULT(LONG)); + MOCK_METHOD0(OnCfChannelError, HRESULT(void)); +}; + +class MockChromeFrameHostEvents + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockChromeFrameHostEvents>, + public StrictMock<IChromeFrameHostEventsMockImpl> { + public: + BEGIN_COM_MAP(MockChromeFrameHostEvents) + COM_INTERFACE_ENTRY(IUnknown) + END_COM_MAP() + + HRESULT Initialize(MockChromeFrameHostEvents** self) { + *self = this; + return S_OK; + } +}; + +class ChromeFrameHostTest: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + TestChromeFrameHost::CreateInitialized(&host_, &host_keeper_)); + } + + virtual void TearDown() { + if (host_) + host_->TearDown(); + host_ = NULL; + host_keeper_.Release(); + + chrome_frame_ = NULL; + chrome_frame_keeper_ = NULL; + + ASSERT_EQ(0, TestChromeFrameHost::instance_count_); + ASSERT_EQ(0, MockChromeFrame::instance_count_); + } + + void ExpectCreateChromeFrame(HRESULT hr) { + if (SUCCEEDED(hr)) { + ASSERT_HRESULT_SUCCEEDED( + MockChromeFrame::CreateInitialized(&chrome_frame_, + &chrome_frame_keeper_)); + EXPECT_CALL(*host_, CreateChromeFrame(_)). + WillRepeatedly( + DoAll( + CopyInterfaceToArgument<0>(chrome_frame_keeper_), + Return(S_OK))); + } else { + EXPECT_CALL(*host_, CreateChromeFrame(_)). + WillRepeatedly(Return(hr)); + } + } + + public: + TestChromeFrameHost* host_; + CComPtr<IUnknown> host_keeper_; + + MockChromeFrame* chrome_frame_; + CComPtr<IChromeFrame> chrome_frame_keeper_; + + // Quench logging for all tests. + testing::LogDisabler no_dchecks_; +}; + +TEST_F(ChromeFrameHostTest, StartChromeFrameSuccess) { + ExpectCreateChromeFrame(S_OK); + + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnActiveXHostCreationFailure) { + ExpectCreateChromeFrame(S_OK); + + host_->set_active_x_host_creation_error(E_OUTOFMEMORY); + ASSERT_EQ(E_OUTOFMEMORY, host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnAdviseFailure) { + ExpectCreateChromeFrame(S_OK); + + chrome_frame_->set_no_events(true); + ASSERT_HRESULT_FAILED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnCreationFailure) { + ExpectCreateChromeFrame(E_OUTOFMEMORY); + + ASSERT_HRESULT_FAILED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, ChromeFramePrivilegedInServiceProviderChain) { + ExpectCreateChromeFrame(S_OK); + + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Get the service provider on our mock Chrome frame. + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + chrome_frame_->GetSite(IID_IServiceProvider, + reinterpret_cast<void**>(&sp))); + + CComPtr<IChromeFramePrivileged> priv; + ASSERT_HRESULT_SUCCEEDED(sp->QueryService(SID_ChromeFramePrivileged, + IID_IChromeFramePrivileged, + reinterpret_cast<void**>(&priv))); + ASSERT_TRUE(priv != NULL); + + boolean wants_priv = FALSE; + ASSERT_HRESULT_SUCCEEDED(priv->GetWantsPrivileged(&wants_priv)); + ASSERT_TRUE(wants_priv); + + CComBSTR profile_name; + ASSERT_HRESULT_SUCCEEDED(priv->GetChromeProfileName(&profile_name)); + ASSERT_TRUE(profile_name == NULL); + + static const wchar_t* kProfileName = L"iexplore"; + host_->SetChromeProfileName(kProfileName); + ASSERT_HRESULT_SUCCEEDED(priv->GetChromeProfileName(&profile_name)); + ASSERT_STREQ(kProfileName, profile_name); +} + +TEST_F(ChromeFrameHostTest, PostMessage) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Make sure we properly queue before the document gets loaded. + CComBSTR queue_it_1("queue_it_1"); + CComBSTR queue_it_2("queue_it_2"); + CComBSTR target_1("target_1"); + CComBSTR target_2("target_2"); + + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_1, target_1)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_2, target_2)); + + // Only the queued messages should be posted. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_1.m_str), + _, StrEq(target_1.m_str))).WillOnce(Return(S_OK)); + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_2.m_str), + _, StrEq(target_2.m_str))).WillOnce(Return(S_OK)); + + MockDispatchEx* event; + CComDispatchDriver event_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&event, + &event_keeper)); + host_->CallOnCfLoad(event); + + // Nothing left to be posted once we have loaded. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + host_->CallOnCfLoad(event); + + // Messages should go directly to Chrome Frame now, whether they are + // queueable or not. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_1.m_str), + _, StrEq(target_1.m_str))).WillOnce(Return(S_OK)); + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_2.m_str), + _, StrEq(target_2.m_str))).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_1, target_1)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_2, target_2)); + + // Nothing left to be posted once we have loaded. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + host_->CallOnCfLoad(event); +} + +TEST_F(ChromeFrameHostTest, OnCfReadyStateChanged) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Should call event sink on all states, but only once + // event sink is set. + host_->CallOnCfReadyStateChanged(READYSTATE_UNINITIALIZED); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADING); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADED); + host_->CallOnCfReadyStateChanged(READYSTATE_INTERACTIVE); + host_->CallOnCfReadyStateChanged(READYSTATE_COMPLETE); + + MockChromeFrameHostEvents* event_sink; + CComPtr<IUnknown> event_sink_sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockChromeFrameHostEvents>::CreateInitialized( + &event_sink, &event_sink_sp)); + + host_->SetEventSink(event_sink); + EXPECT_CALL(*event_sink, OnCfReadyStateChanged(_)).Times(5); + host_->CallOnCfReadyStateChanged(READYSTATE_UNINITIALIZED); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADING); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADED); + host_->CallOnCfReadyStateChanged(READYSTATE_INTERACTIVE); + host_->CallOnCfReadyStateChanged(READYSTATE_COMPLETE); +} + +TEST_F(ChromeFrameHostTest, ChromeFrameEventsCaptured) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Create a handy-dandy dispatch mock object. + MockDispatchEx* event; + CComDispatchDriver event_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&event, + &event_keeper)); + + EXPECT_CALL(*host_, OnCfLoad(event)).Times(1); + chrome_frame_->FireCfLoad(event); + + EXPECT_CALL(*host_, OnCfLoadError(event)).Times(1); + chrome_frame_->FireCfLoadError(event); + + EXPECT_CALL(*host_, OnCfMessage(event)).Times(1); + chrome_frame_->FireCfMessage(event); + + EXPECT_CALL(*host_, OnCfReadyStateChanged(READYSTATE_LOADING)).Times(1); + chrome_frame_->FireCfReadyStateChanged(READYSTATE_LOADING); + + EXPECT_CALL(*host_, OnCfReadyStateChanged(READYSTATE_COMPLETE)).Times(1); + chrome_frame_->FireCfReadyStateChanged(READYSTATE_COMPLETE); + + static const wchar_t* kOrigin = L"From Russia with Love"; + EXPECT_CALL(*host_, OnCfPrivateMessage(event, StrEq(kOrigin))).Times(1); + chrome_frame_->FireCfPrivateMessage(event, CComBSTR(kOrigin)); + + static const wchar_t* kPath = L"they all lead to Rome"; + EXPECT_CALL(*host_, OnCfExtensionReady(StrEq(kPath), 42)).Times(1); + chrome_frame_->FireCfExtensionReady(CComBSTR(kPath), 42); + + EXPECT_CALL(*host_, OnCfGetEnabledExtensionsComplete(_)).Times(1); + chrome_frame_->FireCfGetEnabledExtensionsComplete(NULL); + + EXPECT_CALL(*host_, OnCfChannelError()).Times(1); + chrome_frame_->FireCfChannelError(); +} + +// Used to compare two arrays of strings, as when GetIDsOfNames is called +MATCHER_P(SingleEntryLPOLESTRArraysEqual, single_entry, "") { + return std::wstring(single_entry) == arg[0]; +} + +TEST_F(ChromeFrameHostTest, EventSync) { + // Make sure all calls are safe without an event sink. + BSTR functions_enabled = NULL; + CComQIPtr<IChromeFramePrivileged> host_privileged(host_); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + EXPECT_EQ(NULL, functions_enabled); + + host_->CallOnCfExtensionReady(L"", 0); + host_->CallOnCfGetEnabledExtensionsComplete(NULL); + host_->CallOnCfPrivateMessage(NULL, NULL); + host_->CallOnCfPrivateMessage(NULL, NULL); + host_->CallOnCfChannelError(); + + // Now make sure the calls are properly propagated to the event sync. + MockChromeFrameHostEvents* event_sink; + CComPtr<IUnknown> event_sink_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockChromeFrameHostEvents>::CreateInitialized( + &event_sink, &event_sink_keeper)); + // We cheat a bit here because there is no IID to QI for + // IChromeFrameHostEvents. + host_->SetEventSink(event_sink); + + static const BSTR kCheatCode = L"Cheat code"; + EXPECT_CALL(*event_sink, OnCfGetExtensionApisToAutomate(NotNull())).WillOnce( + DoAll(SetArgumentPointee<0>(kCheatCode), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + DCHECK_EQ(kCheatCode, functions_enabled); + + // Other calls should be OK, as long as the string isn't set to a non-NULL + // value. + EXPECT_CALL(*event_sink, OnCfGetExtensionApisToAutomate(NotNull())).WillOnce( + DoAll(SetArgumentPointee<0>(static_cast<LPOLESTR>(0)), Return(S_FALSE))); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + + EXPECT_CALL(*event_sink, OnCfExtensionReady(StrEq(L""), 0)).Times(1); + host_->CallOnCfExtensionReady(L"", 0); + + EXPECT_CALL(*event_sink, OnCfGetEnabledExtensionsComplete(NULL)).Times(1); + host_->CallOnCfGetEnabledExtensionsComplete(NULL); + + EXPECT_CALL(*event_sink, OnCfChannelError()).Times(1); + host_->CallOnCfChannelError(); + + MockDispatchEx* mock_dispatch = NULL; + CComDispatchDriver dispatch_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_dispatch, + &dispatch_keeper)); + static LPCOLESTR kOrigin = L"Origin"; + VARIANT origin; + origin.vt = VT_BSTR; + // This will be freed by the calling API. If we free it here, we're going to + // end up with a double-free. + origin.bstrVal = SysAllocString(kOrigin); + DISPID origin_dispid = 42; + EXPECT_CALL(*mock_dispatch, GetIDsOfNames(_, + SingleEntryLPOLESTRArraysEqual(L"origin"), 1, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<4>(origin_dispid), Return(S_OK))); + EXPECT_CALL(*mock_dispatch, Invoke(origin_dispid, _, _, DISPATCH_PROPERTYGET, + Field(&DISPPARAMS::cArgs, Eq(0)), _, _, _)).WillOnce(DoAll( + SetArgumentPointee<5>(origin), Return(S_OK))); + + static LPCOLESTR kData = L"Data"; + VARIANT data; + data.vt = VT_BSTR; + // This will be freed by the calling API. If we free it here, we're going to + // end up with a double-free. + data.bstrVal = SysAllocString(kData); + DISPID data_dispid = 24; + EXPECT_CALL(*mock_dispatch, GetIDsOfNames(_, + SingleEntryLPOLESTRArraysEqual(L"data"), 1, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<4>(data_dispid), Return(S_OK))); + EXPECT_CALL(*mock_dispatch, Invoke(data_dispid, _, _, DISPATCH_PROPERTYGET, + Field(&DISPPARAMS::cArgs, Eq(0)), _, _, _)).WillOnce(DoAll( + SetArgumentPointee<5>(data), Return(S_OK))); + + static LPOLESTR kTarget = L"Target"; + EXPECT_CALL(*event_sink, OnCfPrivateMessage( + StrEq(kData), StrEq(kOrigin), StrEq(kTarget))).Times(1); + + host_->CallOnCfPrivateMessage(dispatch_keeper, kTarget); + + // Clean the VARIANTs. + ZeroMemory(&origin, sizeof(origin)); + ZeroMemory(&data, sizeof(data)); +} + +} // namespace diff --git a/ceee/ie/common/common.gyp b/ceee/ie/common/common.gyp new file mode 100644 index 0000000..759c901 --- /dev/null +++ b/ceee/ie/common/common.gyp @@ -0,0 +1,124 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'ie_common_settings', + 'type': 'none', + 'direct_dependent_settings': { + 'defines': [ + # TODO(joi@chromium.org) Put into an include somewhere. + '_WIN32_WINDOWS=0x0410', + '_WIN32_IE=0x0600', + '_ATL_CSTRING_EXPLICIT_CONSTRUCTORS', + '_ATL_STATIC_REGISTRY', + '_WTL_NO_CSTRING', + ], + 'include_dirs': [ + '../../../third_party/wtl/include', + ], + }, + }, + { + 'target_name': 'ie_guids', + 'type': 'static_library', + 'dependencies': [ + 'ie_common_settings', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../plugin/toolband/toolband.gyp:chrome_tab_idl', + ], + 'sources': [ + 'ie_guids.cc', + ], + 'include_dirs': [ + '../../..', + ], + }, + { + 'target_name': 'ie_common', + 'type': 'static_library', + 'dependencies': [ + 'ie_common_settings', + '../../../base/base.gyp:base', + '../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../build/temp_gyp/googleurl.gyp:googleurl', + '../../../net/net.gyp:net_base', + '../../../ceee/common/common.gyp:initializing_coclass', + '../../../ceee/common/common.gyp:ceee_common', + '../../../ceee/testing/utils/test_utils.gyp:test_utils', + '../plugin/toolband/toolband.gyp:chrome_tab_idl', + '../plugin/toolband/toolband.gyp:toolband_idl', + ], + 'sources': [ + 'api_registration.h', + 'chrome_frame_host.cc', + 'chrome_frame_host.h', + 'constants.cc', + 'constants.h', + 'crash_reporter.cc', + 'crash_reporter.h', + 'extension_manifest.cc', + 'extension_manifest.h', + 'ie_tab_interfaces.cc', + 'ie_tab_interfaces.h', + 'ie_util.cc', + 'ie_util.h', + 'mock_ie_tab_interfaces.h', + 'precompile.cc', + 'precompile.h', + 'ceee_module_util.cc', + 'ceee_module_util.h', + + # TODO(joi@chromium.org) Refactor to use chrome/common library. + '../../../chrome/browser/automation/extension_automation_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_bookmarks_module_constants.cc', + '../../../chrome/browser/extensions/extension_event_names.cc', + '../../../chrome/browser/extensions/' + 'extension_page_actions_module_constants.cc', + '../../../chrome/browser/extensions/extension_cookies_api_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_infobar_module_constants.cc', + '../../../chrome/browser/extensions/extension_tabs_module_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_webnavigation_api_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_webrequest_api_constants.cc', + '../../../chrome/common/chrome_switches.cc', + '../../../chrome/common/chrome_switches.h', + '../../../chrome/common/url_constants.cc', + '../../../chrome/common/url_constants.h', + '../../../chrome/common/extensions/extension_constants.cc', + '../../../chrome/common/extensions/extension_constants.h', + '../../../chrome/common/extensions/extension_error_utils.cc', + '../../../chrome/common/extensions/extension_error_utils.h', + '../../../chrome/common/extensions/url_pattern.cc', + '../../../chrome/common/extensions/url_pattern.h', + '../../../chrome/common/extensions/user_script.cc', + '../../../chrome/common/extensions/user_script.h', + ], + 'direct_dependent_settings': { + 'include_dirs': [ + # Because we use some of the chrome files above directly, we need + # to specify thess include paths which they depend on. + '../../../skia/config/win', + '../../../third_party/skia/include/config', + ], + }, + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': 'precompile.cc', + 'msvs_precompiled_header': 'precompile.h', + }, + }, + }, + ] +} diff --git a/ceee/ie/common/constants.cc b/ceee/ie/common/constants.cc new file mode 100644 index 0000000..64a1d52 --- /dev/null +++ b/ceee/ie/common/constants.cc @@ -0,0 +1,11 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Shared constants between the different modules of CEEE. + +#include "ceee/ie/common/constants.h" + +namespace ceee_event_names { + const char kCeeeOnTabUnmapped[] = "ceee.OnTabUnmapped"; +} diff --git a/ceee/ie/common/constants.h b/ceee/ie/common/constants.h new file mode 100644 index 0000000..600a589 --- /dev/null +++ b/ceee/ie/common/constants.h @@ -0,0 +1,13 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_COMMON_CONSTANTS_H_ +#define CEEE_IE_COMMON_CONSTANTS_H_ + +namespace ceee_event_names { + // Private messages used by the ApiDispatcher and the funnels. + extern const char kCeeeOnTabUnmapped[]; +} + +#endif // CEEE_IE_COMMON_CONSTANTS_H_ diff --git a/ceee/ie/common/crash_reporter.cc b/ceee/ie/common/crash_reporter.cc new file mode 100644 index 0000000..4ed9b5c --- /dev/null +++ b/ceee/ie/common/crash_reporter.cc @@ -0,0 +1,75 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Definition of IE crash reporter. + +#include "ceee/ie/common/crash_reporter.h" + +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" + +const wchar_t kGoogleUpdatePipeName[] = + L"\\\\.\\pipe\\GoogleCrashServices\\S-1-5-18"; + +CrashReporter::CrashReporter(const wchar_t* component_name) + : exception_handler_(NULL) { + // Initialize the custom data that will be used to identify the client + // when reporting a crash. + // TODO(jeffbailey@google.com): Inherit Chrome's version number. + // (bb3143594). + google_breakpad::CustomInfoEntry ver_entry(L"ver", L"Ver.Goes.Here"); + google_breakpad::CustomInfoEntry prod_entry(L"prod", L"CEEE_IE"); + google_breakpad::CustomInfoEntry plat_entry(L"plat", L"Win32"); + google_breakpad::CustomInfoEntry type_entry(L"ptype", component_name); + google_breakpad::CustomInfoEntry entries[] = { + ver_entry, prod_entry, plat_entry, type_entry }; + + const int num_entries = arraysize(entries); + client_info_entries_.reset( + new google_breakpad::CustomInfoEntry[num_entries]); + + for (int i = 0; i < num_entries; ++i) { + client_info_entries_[i] = entries[i]; + } + + client_info_.entries = client_info_entries_.get(); + client_info_.count = num_entries; +} + +CrashReporter::~CrashReporter() { + DCHECK(exception_handler_ == NULL); +} + +void CrashReporter::InitializeCrashReporting(bool full_dump) { + DCHECK(exception_handler_ == NULL); + + if (!ceee_module_util::GetCollectStatsConsent()) + return; + + wchar_t temp_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(temp_path), temp_path); + if (len == 0) { + LOG(ERROR) << "Failed to instantiate Breakpad exception handler. " << + "Could not get a temp path."; + return; + } + + // Install an exception handler instance here, which should be the lowest + // level in the process. We give it the appropriate pipe name so it can + // talk to the reporting service (e.g. Omaha) to do dump generation and + // send information back to the server. + MINIDUMP_TYPE dump_type = full_dump ? MiniDumpWithFullMemory : MiniDumpNormal; + exception_handler_ = new google_breakpad::ExceptionHandler( + temp_path, NULL, NULL, NULL, + google_breakpad::ExceptionHandler::HANDLER_ALL, dump_type, + kGoogleUpdatePipeName, &client_info_); + + LOG_IF(ERROR, exception_handler_ == NULL) << + "Failed to instantiate Breakpad exception handler."; +} + +void CrashReporter::ShutdownCrashReporting() { + delete exception_handler_; + exception_handler_ = NULL; +} diff --git a/ceee/ie/common/crash_reporter.h b/ceee/ie/common/crash_reporter.h new file mode 100644 index 0000000..1f34370 --- /dev/null +++ b/ceee/ie/common/crash_reporter.h @@ -0,0 +1,41 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of common IE crash reporter. + +#ifndef CEEE_IE_COMMON_CRASH_REPORTER_H_ +#define CEEE_IE_COMMON_CRASH_REPORTER_H_ + +#include "base/scoped_ptr.h" +#include "client/windows/handler/exception_handler.h" + +// A wrapper around Breakpad's ExceptionHandler class for crash reporting using +// Omaha's crash reporting service. +class CrashReporter { + public: + explicit CrashReporter(const wchar_t* component_name); + virtual ~CrashReporter(); + + // Initialize the ExceptionHandler to begin catching and reporting unhandled + // exceptions. + // + // @param full_dump Whether or not to do a full memory dump. + void InitializeCrashReporting(bool full_dump); + + // Halt crash reporting, stopping the ExceptionHandler from catching any + // further unhandled exceptions. + void ShutdownCrashReporting(); + + // TODO(stevet@google.com): Provide a way to trigger a dump send, + // which can be used for debugging. + private: + google_breakpad::CustomClientInfo client_info_; + scoped_array<google_breakpad::CustomInfoEntry> client_info_entries_; + + // Valid after a call to InitializeCrashReporting and invalidated after a call + // to ShutdownCrashReporting. + google_breakpad::ExceptionHandler* exception_handler_; +}; + +#endif // CEEE_IE_COMMON_CRASH_REPORTER_H_ diff --git a/ceee/ie/common/crash_reporter_unittest.cc b/ceee/ie/common/crash_reporter_unittest.cc new file mode 100644 index 0000000..541f581 --- /dev/null +++ b/ceee/ie/common/crash_reporter_unittest.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for the Crash Reporter. + +#include "ceee/ie/common/crash_reporter.h" + +#include "base/logging.h" +#include "base/win/pe_image.h" +#include "ceee/ie/common/ceee_module_util.h" + +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::Return; +using testing::StrictMock; + +// A helper for just extracting the default exception filter. +static LPTOP_LEVEL_EXCEPTION_FILTER GetUnhandledExceptionFilter() { + LPTOP_LEVEL_EXCEPTION_FILTER old_exh = SetUnhandledExceptionFilter(NULL); + EXPECT_EQ(NULL, SetUnhandledExceptionFilter(old_exh)); + return old_exh; +} + +// A helper for just extracting the default invalid parameter handler. +static _invalid_parameter_handler GetInvalidParameterHandler() { + _invalid_parameter_handler old_iph = _set_invalid_parameter_handler(NULL); + EXPECT_EQ(NULL, _set_invalid_parameter_handler(old_iph)); + return old_iph; +} + +// A helper for just extracting the default purecall handler. +static _purecall_handler GetPureCallHandler() { + _purecall_handler old_pch = _set_purecall_handler(NULL); + EXPECT_EQ(NULL, _set_purecall_handler(old_pch)); + return old_pch; +} + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtil) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtil) + MOCK_STATIC_INIT2(ceee_module_util::GetCollectStatsConsent, + GetCollectStatsConsent); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetCollectStatsConsent); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtil) + +// Test that basic exception handling from Breakpad works, by ensuring that +// calling CrashReporter functions correctly replaces default handlers. +TEST(CrashReporterTest, ExceptionHandling) { + base::win::PEImage my_image(reinterpret_cast<HMODULE>(&__ImageBase)); + + // Take the default handlers out and do a quick sanity check. It is needed + // later to ensure it gets replaced by breakpad. + LPTOP_LEVEL_EXCEPTION_FILTER orig_exh = GetUnhandledExceptionFilter(); + EXPECT_TRUE(orig_exh != NULL); + + _invalid_parameter_handler orig_iph = GetInvalidParameterHandler(); + EXPECT_EQ(NULL, orig_iph); + + _purecall_handler orig_pch = GetPureCallHandler(); + EXPECT_EQ(NULL, orig_pch); + + // Initialize and ensure that a new handler has replaced the original + // handler, and that the new handler is within this image/from breakpad. + CrashReporter crash_reporter(L"unittest"); + + StrictMock<MockCeeeModuleUtil> mock; + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(false)); + + crash_reporter.InitializeCrashReporting(false); + EXPECT_EQ(orig_exh, GetUnhandledExceptionFilter()); + crash_reporter.ShutdownCrashReporting(); + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(true)); + crash_reporter.InitializeCrashReporting(false); + + LPTOP_LEVEL_EXCEPTION_FILTER new_exh = GetUnhandledExceptionFilter(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_exh) != NULL); + EXPECT_TRUE(orig_exh != new_exh); + + _invalid_parameter_handler new_iph = GetInvalidParameterHandler(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_iph) != NULL); + EXPECT_TRUE(orig_iph != new_iph); + + _purecall_handler new_pch = GetPureCallHandler(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_pch) != NULL); + EXPECT_TRUE(orig_pch != new_pch); + + // Shut down, and ensure that the original exception handler is replaced + // by breakpad. + crash_reporter.ShutdownCrashReporting(); + + LPTOP_LEVEL_EXCEPTION_FILTER final_exh = GetUnhandledExceptionFilter(); + EXPECT_EQ(orig_exh, final_exh); + + _invalid_parameter_handler final_iph = GetInvalidParameterHandler(); + EXPECT_EQ(orig_iph, final_iph); + + _purecall_handler final_pch = GetPureCallHandler(); + EXPECT_EQ(orig_pch, final_pch); +} + +} // namespace diff --git a/ceee/ie/common/extension_manifest.cc b/ceee/ie/common/extension_manifest.cc new file mode 100644 index 0000000..22d5f4a --- /dev/null +++ b/ceee/ie/common/extension_manifest.cc @@ -0,0 +1,306 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to access Chrome Extension manifest data. + +#include "ceee/ie/common/extension_manifest.h" + +#include "base/base64.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/third_party/nss/blapi.h" +#include "base/third_party/nss/sha256.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/url_constants.h" +#include "net/base/net_util.h" + +namespace ext_keys = extension_manifest_keys; +namespace ext_values = extension_manifest_values; + +const char ExtensionManifest::kManifestFilename[] = "manifest.json"; + +// First 16 bytes of SHA256 hashed public key. +const size_t ExtensionManifest::kIdSize = 16; + +HRESULT ExtensionManifest::ReadManifestFile(const FilePath& extension_path, + bool require_key) { + // TODO(mad@chromium.org): Unbranch (taken from constructor of + // Extension class). + DCHECK(extension_path.IsAbsolute()); +#if defined(OS_WIN) + // Normalize any drive letter to upper-case. We do this for consistency with + // net_utils::FilePathToFileURL(), which does the same thing, to make string + // comparisons simpler. + std::wstring path_str = extension_path.value(); + if (path_str.size() >= 2 && path_str[0] >= L'a' && path_str[0] <= L'z' && + path_str[1] == ':') + path_str[0] += ('A' - 'a'); + + path_ = FilePath(path_str); +#else + path_ = path; +#endif + + // This piece comes from ExtensionsServiceBackend::LoadExtension() + FilePath manifest_path = + extension_path.AppendASCII(ExtensionManifest::kManifestFilename); + std::string json_string; + if (!file_util::ReadFileToString(manifest_path, &json_string)) { + LOG(ERROR) << "Invalid extension path or manifest file: " << + extension_path.value(); + return E_FAIL; + } + + scoped_ptr<Value> value(base::JSONReader::Read(json_string, true)); + if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) { + LOG(ERROR) << "Invalid manifest file"; + return E_FAIL; + } + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + + // And the rest is highly inspired from Extension::InitFromValue() + + // Initialize public key and id. + if (dict->HasKey(ext_keys::kPublicKey)) { + if (!dict->GetString(ext_keys::kPublicKey, &public_key_) || + FAILED(CalculateIdFromPublicKey())) { + public_key_.clear(); + extension_id_.clear(); + LOG(ERROR) << "Invalid public key format"; + return E_FAIL; + } + } else if (require_key) { + LOG(ERROR) << "Required key not found in manifest file"; + return E_FAIL; + } + + // Initialize the URL. + extension_url_ = GURL(std::string(chrome::kExtensionScheme) + + chrome::kStandardSchemeSeparator + extension_id_ + "/"); + + // Initialize content scripts (optional). + // MUST BE DONE AFTER setting up extension_id_, extension_url_ and path_ + if (dict->HasKey(ext_keys::kContentScripts)) { + ListValue* list_value; + if (!dict->GetList(ext_keys::kContentScripts, &list_value)) { + LOG(ERROR) << "Invalid content script JSON value"; + return E_FAIL; + } + + for (size_t i = 0; i < list_value->GetSize(); ++i) { + DictionaryValue* content_script; + if (!list_value->GetDictionary(i, &content_script)) { + LOG(ERROR) << "Invalid content script JSON value"; + return E_FAIL; + } + + UserScript script; + HRESULT hr = LoadUserScriptHelper(content_script, &script); + if (FAILED(hr)) + return hr; + script.set_extension_id(extension_id_); + content_scripts_.push_back(script); + } + } + + // Initialize toolstrips (optional). + toolstrip_file_names_.clear(); + if (dict->HasKey(ext_keys::kToolstrips)) { + ListValue* list_value; + if (!dict->GetList(ext_keys::kToolstrips, &list_value)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + for (size_t i = 0; i < list_value->GetSize(); ++i) { + std::string toolstrip_file_name; + if (!list_value->GetString(i, &toolstrip_file_name)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + toolstrip_file_names_.push_back(toolstrip_file_name); + } + } + // Add code here to read other manifest properties. + return S_OK; +} + +// TODO(mad@chromium.org): Unbranch (taken from Extension class). + +// static +GURL ExtensionManifest::GetResourceUrl(const GURL& extension_url, + const std::string& relative_path) { + DCHECK(extension_url.SchemeIs(chrome::kExtensionScheme)); + DCHECK_EQ("/", extension_url.path()); + + GURL ret_val = GURL(extension_url.spec() + relative_path); + DCHECK(StartsWithASCII(ret_val.spec(), extension_url.spec(), false)); + + return ret_val; +} + +// TODO(mad@chromium.org): Reuse code in common\extensions\extension.cc +HRESULT ExtensionManifest::CalculateIdFromPublicKey() { + std::string public_key_bytes; + if (!base::Base64Decode(public_key_, &public_key_bytes)) + return E_FAIL; + + if (public_key_bytes.length() == 0) + return E_FAIL; + + // SHA256 needs to work with an array of bytes, which we get from a string. + const uint8* ubuf = + reinterpret_cast<const unsigned char*>(public_key_bytes.data()); + SHA256Context ctx; + SHA256_Begin(&ctx); + SHA256_Update(&ctx, ubuf, public_key_bytes.length()); + // We must hash this value to a fixed size array. + uint8 hash[kIdSize]; + SHA256_End(&ctx, hash, NULL, sizeof(hash)); + // To stay in sync with the code in Chrome, we start by converting the bytes + // to a string representing the concatenation of the Hex values of all bytes. + extension_id_ = base::HexEncode(hash, sizeof(hash)); + for (size_t i = 0; i < extension_id_.size(); ++i) { + // And then, for each nibble represented by a single Hex digit, we use + // the value to offset from the letter 'a' to construct the key in the + // limited alphabet used for Chrome extension Ids ['a', 'q']. + int val = -1; + if (base::HexStringToInt(extension_id_.substr(i, 1), &val)) + extension_id_[i] = val + 'a'; + else + extension_id_[i] = 'a'; + } + return S_OK; +} + + +// TODO(mad@chromium.org): Unbranch (taken from the Extension class). +HRESULT ExtensionManifest::LoadUserScriptHelper( + const DictionaryValue* content_script, UserScript* result) { + // run_at + if (content_script->HasKey(ext_keys::kRunAt)) { + std::string run_location; + if (!content_script->GetString(ext_keys::kRunAt, &run_location)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + + if (run_location == ext_values::kRunAtDocumentStart) { + result->set_run_location(UserScript::DOCUMENT_START); + } else if (run_location == ext_values::kRunAtDocumentEnd) { + result->set_run_location(UserScript::DOCUMENT_END); + } else if (run_location == ext_values::kRunAtDocumentIdle) { + result->set_run_location(UserScript::DOCUMENT_IDLE); + } else { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + } + + // all frames + if (content_script->HasKey(ext_keys::kAllFrames)) { + bool all_frames = false; + if (!content_script->GetBoolean(ext_keys::kAllFrames, &all_frames)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + result->set_match_all_frames(all_frames); + } + + // matches + ListValue* matches = NULL; + if (!content_script->GetList(ext_keys::kMatches, &matches)) { + LOG(ERROR) << "Invalid manifest without a matches value"; + return E_FAIL; + } + + if (matches->GetSize() == 0) { + LOG(ERROR) << "Invalid manifest without a matches value"; + return E_FAIL; + } + for (size_t i = 0; i < matches->GetSize(); ++i) { + std::string match_str; + if (!matches->GetString(i, &match_str)) { + LOG(ERROR) << "Invalid matches JSON value"; + return E_FAIL; + } + + URLPattern pattern(UserScript::kValidUserScriptSchemes); + if (pattern.Parse(match_str) != URLPattern::PARSE_SUCCESS) { + LOG(ERROR) << "Invalid matches value"; + return E_FAIL; + } + + result->add_url_pattern(pattern); + } + + // js and css keys + ListValue* js = NULL; + if (content_script->HasKey(ext_keys::kJs) && + !content_script->GetList(ext_keys::kJs, &js)) { + LOG(ERROR) << "Invalid content script JS JSON value"; + return E_FAIL; + } + + ListValue* css = NULL; + if (content_script->HasKey(ext_keys::kCss) && + !content_script->GetList(ext_keys::kCss, &css)) { + LOG(ERROR) << "Invalid content script CSS JSON value"; + return E_FAIL; + } + + // The manifest needs to have at least one js or css user script definition. + if (((js ? js->GetSize() : 0) + (css ? css->GetSize() : 0)) == 0) { + LOG(ERROR) << "Invalid manifest without any content script"; + return E_FAIL; + } + + if (js) { + for (size_t script_index = 0; script_index < js->GetSize(); + ++script_index) { + Value* value; + std::wstring relative; + if (!js->Get(script_index, &value) || !value->GetAsString(&relative)) { + LOG(ERROR) << "Invalid content script JS JSON value"; + return E_FAIL; + } + // TODO(mad@chromium.org): Make GetResourceUrl accept wstring + // too and check with georged@chromium.org who has the same todo + // in chrome\common\extensions\extension.cc + GURL url = GetResourceUrl(WideToUTF8(relative)); + result->js_scripts().push_back( + UserScript::File(path(), FilePath(relative), url)); + // TODO(mad@chromium.org): Verify that the path refers to an + // existing file. + } + } + + if (css) { + for (size_t script_index = 0; script_index < css->GetSize(); + ++script_index) { + Value* value; + std::wstring relative; + if (!css->Get(script_index, &value) || !value->GetAsString(&relative)) { + LOG(ERROR) << "Invalid content script CSS JSON value"; + return E_FAIL; + } + // TODO(mad@chromium.org): Make GetResourceUrl accept wstring + // too and check with georged@chromium.org who has the same todo + // in chrome\common\extensions\extension.cc + GURL url = GetResourceUrl(WideToUTF8(relative)); + result->css_scripts().push_back( + UserScript::File(path(), FilePath(relative), url)); + // TODO(mad@chromium.org): Verify that the path refers to an + // existing file. + } + } + + return S_OK; +} diff --git a/ceee/ie/common/extension_manifest.h b/ceee/ie/common/extension_manifest.h new file mode 100644 index 0000000..11fe165 --- /dev/null +++ b/ceee/ie/common/extension_manifest.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to access Chrome Extension manifest data. + +#ifndef CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ +#define CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ + +#include <wtypes.h> + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/file_path.h" +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + + +class DictionaryValue; + + +// A helper class to read data from a Chrome Extension manifest file. +// TODO(mad@chromium.org): Find a way to reuse code from chrome +// extension classes. +class ExtensionManifest { + public: + ExtensionManifest() {} + ~ExtensionManifest() {} + + // The name of the manifest inside an extension. + static const char kManifestFilename[]; + + // The number of bytes in a legal id. + static const size_t kIdSize; + + // Clears the current content and reset it with the content of the manifest + // file found under the provided Chrome extension folder path. + HRESULT ReadManifestFile(const FilePath& extension_path, bool require_id); + + // Returns the extension ID as read from manifest without any transformation. + const std::string& extension_id() const { + return extension_id_; + } + + // Returns the public key as read from manifest without any transformation. + const std::string& public_key() const { + return public_key_; + } + + // Returns the list of toolstrip file names. + const std::vector<std::string>& GetToolstripFileNames() const { + return toolstrip_file_names_; + } + + const FilePath& path() const { return path_; } + const GURL& extension_url() const { return extension_url_; } + const UserScriptList& content_scripts() const { return content_scripts_; } + + // Returns an absolute url to a resource inside of an extension. The + // |extension_url| argument should be the url() from an Extension object. The + // |relative_path| can be untrusted user input. The returned URL will either + // be invalid() or a child of |extension_url|. + // NOTE: Static so that it can be used from multiple threads. + static GURL GetResourceUrl(const GURL& extension_url, + const std::string& relative_path); + GURL GetResourceUrl(const std::string& relative_path) { + return GetResourceUrl(extension_url(), relative_path); + } + + private: + // Transforms the value public_key_ to set the value of extension_id_ + // using the same algorithm as Chrome. + HRESULT CalculateIdFromPublicKey(); + + // Helper method that loads a UserScript object from a dictionary in the + // content_script list of the manifest. + HRESULT LoadUserScriptHelper(const DictionaryValue* content_script, + UserScript* result); + + // The absolute path to the directory the extension is stored in. + FilePath path_; + + // The base extension url for the extension. + GURL extension_url_; + + // Paths to the content scripts the extension contains. + UserScriptList content_scripts_; + + std::vector<std::string> toolstrip_file_names_; + std::string extension_id_; + std::string public_key_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionManifest); +}; + +#endif // CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ diff --git a/ceee/ie/common/extension_manifest_unittest.cc b/ceee/ie/common/extension_manifest_unittest.cc new file mode 100644 index 0000000..9a3d990 --- /dev/null +++ b/ceee/ie/common/extension_manifest_unittest.cc @@ -0,0 +1,319 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ExtensionManifest. + +#include <atlconv.h> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/extensions/user_script.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext_keys = extension_manifest_keys; +namespace ext_values = extension_manifest_values; + +namespace { + +// Static constants +static const char* kToolstripName1 = "MyToolstrip.html"; +static const char* kToolstripName2 = "YourToolstrip.html"; +static const char* kPublicKeyName = "0123456789ABCDEF0123456789ABCDEF"; +static const char* kUrlPattern1 = "http://madlymad.com/*"; +static const char* kUrlPattern2 = "https://superdave.com/*"; +static const char* kCssPath = "CssPath.css"; +static const wchar_t* kCssWPath = L"CssPath.css"; +static const char* kJsPath1 = "JsPath1.js"; +static const wchar_t* kJsWPath1 = L"JsPath1.js"; +static const char* kJsPath2 = "JsPath2.js"; +static const wchar_t* kJsWPath2 = L"JsPath2.js"; +// This Id value has been computed with a valid version of the code. +// If the algorithm changes, we must update this value. +static const char* kComputedId = "fppgjfcenabdcibneonnejnkdafgjcch"; + +static const char* kExtensionUrl = + "chrome-extension://fppgjfcenabdcibneonnejnkdafgjcch/"; + +// Test fixture to handle the stuff common to all tests. +class ExtensionManifestTest : public testing::Test { + protected: + ExtensionManifestTest() + // We need to remember whether we created the file or not so that we + // can delete it in TearDown because some tests don't create a file. + : created_file_(false) { + } + + // We always use the same temporary file name, so we can make it static. + static void SetUpTestCase() { + EXPECT_TRUE(file_util::GetTempDir(&file_dir_)); + file_path_ = file_dir_.AppendASCII(ExtensionManifest::kManifestFilename); + } + + // This is the common code to write the Json values to the manifest file. + void WriteJsonToFile(const Value& value) { + std::string json_string; + base::JSONWriter::Write(&value, false, &json_string); + + FILE* temp_file = file_util::OpenFile(file_path_, "w"); + EXPECT_TRUE(temp_file != NULL); + created_file_ = true; + + fwrite(json_string.c_str(), json_string.size(), 1, temp_file); + file_util::CloseFile(temp_file); + temp_file = NULL; + } + + // We must delete the file on each tests that created one. + virtual void TearDown() { + if (created_file_) { + EXPECT_TRUE(file_util::Delete(file_path_, false)); + } + } + + protected: + static FilePath file_dir_; + // We keep both the file path and the dir only path to avoid reconstructing it + // all the time. + static FilePath file_path_; + + private: + bool created_file_; +}; + +FilePath ExtensionManifestTest::file_dir_; +FilePath ExtensionManifestTest::file_path_; + + +TEST_F(ExtensionManifestTest, InvalidFileName) { + testing::LogDisabler no_dchecks; + + // This test assumes that there is no manifest file in the current path. + base::PlatformFileInfo dummy; + EXPECT_FALSE(file_util::GetFileInfo(FilePath(L"manifest.json"), &dummy)); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(FilePath(), false)); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile( + FilePath(L"LalaLandBoobooGaloo"), false)); +} + +TEST_F(ExtensionManifestTest, EmptyFile) { + testing::LogDisabler no_dchecks; + + // Value's constructor is protected, so we must go dynamic. + scoped_ptr<Value> value(Value::CreateNullValue()); + WriteJsonToFile(*value); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, InvalidJsonFile) { + testing::LogDisabler no_dchecks; + + ListValue dummy_list; + dummy_list.Append(Value::CreateIntegerValue(42)); + + WriteJsonToFile(dummy_list); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, InvalidPublicKey) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.Set(ext_keys::kPublicKey, + Value::CreateStringValue("Babebibobu")); + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_TRUE(manifest.public_key().empty()); +} + +TEST_F(ExtensionManifestTest, InvalidUserScript) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + ListValue* scripts = new ListValue(); + values.Set(ext_keys::kContentScripts, scripts); + DictionaryValue* script_dict = new DictionaryValue; + scripts->Append(script_dict); + + // Empty scripts are not allowed. + WriteJsonToFile(values); + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + ListValue* matches1 = new ListValue(); + script_dict->Set(ext_keys::kMatches, matches1); + + // Matches must have at least one value. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + matches1->Append(Value::CreateStringValue(kUrlPattern1)); + + // Having a match isn't enough without at least one CSS or JS file. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + ListValue* css = new ListValue(); + script_dict->Set(ext_keys::kCss, css); + + // CSS list must have at least one item. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + script_dict->Remove(ext_keys::kCss, NULL); + ListValue* js = new ListValue(); + script_dict->Set(ext_keys::kJs, js); + + // Same thing for JS. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, EmptyValidJsonFile) { + testing::LogDisabler no_dchecks; + + WriteJsonToFile(DictionaryValue()); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_TRUE(manifest.GetToolstripFileNames().empty()); + EXPECT_STREQ(manifest.path().value().c_str(), file_dir_.value().c_str()); +} + +TEST_F(ExtensionManifestTest, ValidJsonFileWithOneValue) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.SetString("name", "My Name"); + + // We must dynamically allocate since DictionaryValue will free this memory. + ListValue * toolstrips = new ListValue(); + toolstrips->Append(Value::CreateStringValue(kToolstripName1)); + values.Set(ext_keys::kToolstrips, toolstrips); + toolstrips = NULL; + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_FALSE(manifest.GetToolstripFileNames().empty()); + EXPECT_STREQ(manifest.GetToolstripFileNames()[0].c_str(), kToolstripName1); + EXPECT_TRUE(manifest.public_key().empty()); + EXPECT_TRUE(manifest.extension_id().empty()); +} + +TEST_F(ExtensionManifestTest, ValidJsonFileWithManyValues) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.SetString("name", "My Name"); + values.SetString("job", "Your Job"); + values.SetString(ext_keys::kPublicKey, kPublicKeyName); + + ListValue* toolstrips = new ListValue(); + toolstrips->Append(Value::CreateStringValue(kToolstripName1)); + toolstrips->Append(Value::CreateStringValue(kToolstripName2)); + values.Set(ext_keys::kToolstrips, toolstrips); + toolstrips = NULL; + + ListValue* scripts = new ListValue(); + values.Set(ext_keys::kContentScripts, scripts); + DictionaryValue* script_dict1 = new DictionaryValue; + scripts->Append(script_dict1); + script_dict1->SetString(ext_keys::kRunAt, + ext_values::kRunAtDocumentStart); + + ListValue* matches1 = new ListValue(); + script_dict1->Set(ext_keys::kMatches, matches1); + matches1->Append(Value::CreateStringValue(kUrlPattern1)); + + ListValue* css = new ListValue(); + script_dict1->Set(ext_keys::kCss, css); + css->Append(Value::CreateStringValue(kCssPath)); + + DictionaryValue* script_dict2 = new DictionaryValue; + scripts->Append(script_dict2); + + ListValue* matches2 = new ListValue(); + script_dict2->Set(ext_keys::kMatches, matches2); + matches2->Append(Value::CreateStringValue(kUrlPattern1)); + matches2->Append(Value::CreateStringValue(kUrlPattern2)); + + ListValue* js = new ListValue(); + script_dict2->Set(ext_keys::kJs, js); + js->Append(Value::CreateStringValue(kJsPath1)); + js->Append(Value::CreateStringValue(kJsPath2)); + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, true)); + EXPECT_FALSE(manifest.GetToolstripFileNames().empty()); + // Prevent asserts blocking the tests if the test above failed. + if (manifest.GetToolstripFileNames().size() > 1) { + EXPECT_STREQ(manifest.GetToolstripFileNames()[0].c_str(), kToolstripName1); + EXPECT_STREQ(manifest.GetToolstripFileNames()[1].c_str(), kToolstripName2); + } + EXPECT_STREQ(manifest.public_key().c_str(), kPublicKeyName); + EXPECT_STREQ(manifest.extension_id().c_str(), kComputedId); + EXPECT_STREQ(manifest.path().value().c_str(), file_dir_.value().c_str()); + EXPECT_EQ(manifest.extension_url(), GURL(kExtensionUrl)); + + URLPattern url_pattern1(UserScript::kValidUserScriptSchemes); + url_pattern1.Parse(kUrlPattern1); + URLPattern url_pattern2(UserScript::kValidUserScriptSchemes); + url_pattern2.Parse(kUrlPattern2); + + const UserScriptList& script_list = manifest.content_scripts(); + EXPECT_EQ(script_list.size(), 2); + if (script_list.size() == 2) { + const UserScript& script1 = script_list[0]; + EXPECT_EQ(script1.run_location(), + UserScript::DOCUMENT_START); + const std::vector<URLPattern>& url_patterns1 = script1.url_patterns(); + EXPECT_EQ(url_patterns1.size(), 1); + if (url_patterns1.size() == 1) + EXPECT_EQ(url_patterns1[0].GetAsString(), url_pattern1.GetAsString()); + const UserScript::FileList& css_scripts = script1.css_scripts(); + EXPECT_EQ(css_scripts.size(), 1); + if (css_scripts.size() == 1) { + EXPECT_STREQ(css_scripts[0].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(css_scripts[0].relative_path().value().c_str(), kCssWPath); + } + const UserScript& script2 = script_list[1]; + const std::vector<URLPattern>& url_patterns2 = script2.url_patterns(); + EXPECT_EQ(url_patterns2.size(), 2); + if (url_patterns2.size() == 2) { + EXPECT_EQ(url_patterns2[0].GetAsString(), url_pattern1.GetAsString()); + EXPECT_EQ(url_patterns2[1].GetAsString(), url_pattern2.GetAsString()); + } + const UserScript::FileList& js_scripts = script2.js_scripts(); + EXPECT_EQ(js_scripts.size(), 2); + if (js_scripts.size() == 2) { + EXPECT_STREQ(js_scripts[0].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(js_scripts[0].relative_path().value().c_str(), kJsWPath1); + EXPECT_STREQ(js_scripts[1].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(js_scripts[1].relative_path().value().c_str(), kJsWPath2); + } + } +} + +} // namespace diff --git a/ceee/ie/common/ie_guids.cc b/ceee/ie/common/ie_guids.cc new file mode 100644 index 0000000..0babc29 --- /dev/null +++ b/ceee/ie/common/ie_guids.cc @@ -0,0 +1,26 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Pull in all the GUIDs we rely on. +#include "broker_lib.h" // NOLINT +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +#include <initguid.h> // NOLINT + +// Dispex guids - I don't know that they come in any library. +#include <dispex.h> + +// Pull in the tab interface guids also. +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "third_party/activscp/activdbg.h" + +extern "C" { +#include "broker_lib_i.c" // NOLINT +#include "toolband_i.c" // NOLINT + +const GUID IID_IProcessDebugManager = __uuidof(IProcessDebugManager); +const GUID IID_IDebugApplication = __uuidof(IDebugApplication); +const GUID IID_IDebugDocumentHelper = __uuidof(IDebugDocumentHelper); +} // extern "C" diff --git a/ceee/ie/common/ie_tab_interfaces.cc b/ceee/ie/common/ie_tab_interfaces.cc new file mode 100644 index 0000000..eeadd38e --- /dev/null +++ b/ceee/ie/common/ie_tab_interfaces.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ITabWindow and ITabWindowManager interfaces and approaches +// related to them. + +#include "ceee/ie/common/ie_tab_interfaces.h" + +#include <shlguid.h> // SID_SWebBrowserApp + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" + +namespace ie_tab_interfaces { + +HRESULT TabWindowManagerFromFrame(HWND ie_frame, REFIID riid, void** manager) { + DCHECK(ie_frame); + DCHECK(manager && !*manager); + + if (!window_utils::IsWindowThread(ie_frame)) { + LOG(ERROR) << "Can't get tab window manager from frame in other process " + "or thread that created the frame window."; + return E_INVALIDARG; + } + + IDropTarget* drop_target = reinterpret_cast<IDropTarget*>( + ::GetPropW(ie_frame, L"OleDropTargetInterface")); + if (!drop_target) { + NOTREACHED() << "No drop target"; + return E_UNEXPECTED; + } + + CComQIPtr<IServiceProvider> frame_service_provider(drop_target); + if (!frame_service_provider) { + NOTREACHED(); + return E_NOINTERFACE; + } + + return frame_service_provider->QueryService(SID_STabWindowManager, riid, + manager); +} + +} // namespace ie_tab_interfaces diff --git a/ceee/ie/common/ie_tab_interfaces.h b/ceee/ie/common/ie_tab_interfaces.h new file mode 100644 index 0000000..2340692 --- /dev/null +++ b/ceee/ie/common/ie_tab_interfaces.h @@ -0,0 +1,314 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ITabWindow and ITabWindowManager interfaces and approaches +// related to them. + +#ifndef CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ +#define CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ + +#include <atlbase.h> +#include <exdisp.h> // IWebBrowser2 +#include <guiddef.h> // DEFINE_GUID +#include <mshtml.h> // IHTMLDocument2 + +// Service ID to get the tab window manager. +// {122F0301-9AB9-4CBE-B5F6-CEADCF6AA9B7} +DEFINE_GUID(SID_STabWindowManager, + 0x122F0301L, + 0x9AB9, + 0x4CBE, + 0xB5, 0xF6, 0xCE, 0xAD, 0xCF, 0x6A, 0xA9, 0xB7); + +// Adapted from documentation available at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindowmanager.htm +// +// Available in IE7, IE8 and IE9. Retrieve by QueryService for +// SID_STabWindowManager on the IServiceProvider that is the +// IDropTarget you can retrieve from the browser frame window. +// +// The ITabWindowManager interface is NOT the same in IE7, IE8 and +// IE9, and has different IIDs in each. + +interface __declspec(uuid("CAE57FE7-5E06-4804-A285-A985E76708CD")) + ITabWindowManagerIe7 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, long*) PURE; + STDMETHOD(SelectTab)(long tab) PURE; + STDMETHOD(CloseAllTabs)() PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown** active_tab) PURE; + STDMETHOD(GetCount)(long* count) PURE; + // Actually ITabWindow** + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + // Window class of window should be "TabWindowClass" + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actually IBrowserFrame** + STDMETHOD(GetBrowserFrame)(IUnknown** browser_frame) PURE; + STDMETHOD(AddBlankTab)(unsigned long ul, long* l) PURE; + STDMETHOD(AddTabGroup)(LPCITEMIDLIST* pidl, long l, unsigned long ul) PURE; + STDMETHOD(GetCurrentTabGroup)(LPCITEMIDLIST** pidl, long* l1, long* l2) PURE; + STDMETHOD(OpenHomePages)(int flags) PURE; + // @param moving_id ID (as in ITabWindow::GetID) of the tab being moved. + // @param dest_id ID of the tab currently in the desired destination position. + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int) PURE; +}; + +interface __declspec(uuid("9706DA66-D17C-48a5-B42D-39963D174DC0")) + ITabWindowManagerIe8 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, long*) PURE; + STDMETHOD(_AddTabByPosition)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(SelectTab)(long) PURE; + STDMETHOD(CloseAllTabs)() PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown** active_tab) PURE; + STDMETHOD(GetCount)(long* count) PURE; + STDMETHOD(_GetFilteredCount)(void * UNKNOWN_ARGUMENTS) PURE; + // Actually ITabWindow** + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + STDMETHOD(_FilteredIndexFromID)(void * UNKNOWN_ARGUMENTS) PURE; + // Window class of window should be "TabWindowClass" + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actually IBrowserFrame** + STDMETHOD(GetBrowserFrame)(IUnknown** browser_frame) PURE; + STDMETHOD(AddBlankTab)(unsigned long, long*) PURE; + STDMETHOD(_AddBlankTabEx)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(AddTabGroup)(LPCITEMIDLIST* pidl, long, unsigned long) PURE; + STDMETHOD(GetCurrentTabGroup)(LPCITEMIDLIST** pidl, long*, long*) PURE; + STDMETHOD(OpenHomePages)(int flags) PURE; + // @param moving_id ID (as in ITabWindow::GetID) of the tab being moved. + // @param dest_id ID of the tab currently in the desired destination position. + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int) PURE; +}; + +// In IE9 IID and definition of the interface has changed. +interface __declspec(uuid("8059E123-28D5-4C75-A298-664B3720ACAE")) + ITabWindowManagerIe9 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, IUnknown*, long*) PURE; + STDMETHOD(AddTabByPosition)(LPCITEMIDLIST pidl, UINT, ULONG, long, + IUnknown*, DWORD*, long*) PURE; + STDMETHOD(SelectTab)(long) PURE; + STDMETHOD(CloseAllTabs)(void) PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown**) PURE; + STDMETHOD(GetCount)(long*) PURE; + STDMETHOD(GetFilteredCount)(long*, long) PURE; + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + STDMETHOD(FilteredIndexFromID)(long, long, long*) PURE; + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actual IBrowserFrame ** + STDMETHOD(GetBrowserFrame)(IUnknown**) PURE; + STDMETHOD(AddBlankTab)(ULONG, long*) PURE; + STDMETHOD(AddBlankTabEx)(ULONG, DWORD*, long*) PURE; + STDMETHOD(AddTabGroup)(DWORD, long, ULONG) PURE; + STDMETHOD(GetCurrentTabGroup)(DWORD, long*, long*) PURE; + STDMETHOD(OpenHomePages)(int) PURE; + STDMETHOD(RepositionTab)(long, long, int) PURE; + // Actually IClosedTabManager** + STDMETHOD(GetUndoTabManager)(IUnknown**) PURE; + STDMETHOD(GetRestoreTabManager)(IUnknown**) PURE; + // Actually, ITabWindowEvents* (a new interface) + STDMETHOD(AddTabWindowEventHandler)(IUnknown*) PURE; + // Actually, ITabWindowEvents* (a new interface) + STDMETHOD(UnregisterTabWindowEventHandler)(IUnknown*) PURE; + STDMETHOD(CloseTabGroup)(long) PURE; + STDMETHOD(CreateGroupMapping)(long*) PURE; + STDMETHOD(DestroyGroupMapping)(long) PURE; + STDMETHOD(SetDecorationPreference)(DWORD, DWORD*) PURE; + STDMETHOD(FindTabAdjacentToGroup)(long, long, + DWORD, IUnknown**, long*) PURE; + STDMETHOD(GetNewGroupID)(long*) PURE; + STDMETHOD(CloseOldTabIfFailed)(void) PURE; + STDMETHOD(CloseAllTabsExcept)(long) PURE; + STDMETHOD(CloseAllTabsExceptActive)(void) PURE; +}; + +// Adapted from documentation available at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindow.htm +// +// Also available differently in IE7 and IE8. +interface __declspec(uuid("9BAB3405-EE3F-4040-8836-25AA9C2D408E")) + ITabWindowIe7 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe7** tab_manager) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +interface __declspec(uuid("FF18630E-5B18-4A07-8A75-9FD3CE5A2D14")) + ITabWindowIe8 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + // REMOVED from IE7 version: STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +// New version that was introduced between versions 8.0.6001.18928 and +// 8.0.7600.16385. +interface __declspec(uuid("F704B7E0-4760-46ff-BBDB-7439E0A2A814")) + ITabWindowIe8_1 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + // REMOVED from IE7 version: STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetIconWeakReference)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +// Modified interface which appeared in IE9 beta. +interface __declspec(uuid("3927961B-9DB0-4174-B67A-39F34585A692")) + ITabWindowIe9 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(GUID *, ULONG, ULONG, VARIANT *) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(HWND) PURE; + STDMETHOD(OnBrowserCreated)(HWND, HWND, HWND, IDispatch*, + DWORD*, long) PURE; + STDMETHOD(OnNewWindow)(ULONG, ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)(void) PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(SetITBarHolderWindow)(HWND) PURE; + STDMETHOD(EnsureITBar)(int, int) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetIconWeakReference)(HICON**) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(IUnknown**) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetParentComponentHandle)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(IStream *) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; + // Resolved first time here, but existed in earlier versions. + STDMETHOD(GetThumbnailWindow)(HWND**) PURE; + STDMETHOD(GetBrowserWindow)(HWND**) PURE; + STDMETHOD(TransferRecoveryDataForSiteMode)(void) PURE; + STDMETHOD(GetTabGroup)(long*) PURE; + STDMETHOD(GetTabGroupDecoration)(DWORD*) PURE; + STDMETHOD(JoinTabGroup)(long, long) PURE; + STDMETHOD(LeaveTabGroup)(void) PURE; + STDMETHOD(IsParticipatingInTabGroup)(int*) PURE; + STDMETHOD(IsWaitingForGroupRecovery)(int*) PURE; + STDMETHOD(SetWaitingForGroupRecovery)(long, int) PURE; + STDMETHOD(BrowserTabIsHung)(void) PURE; + STDMETHOD(FrameTabWillNotHang)(ULONG, ULONG) PURE; + STDMETHOD(BrowserTabRespondsNow)(ULONG, int, int) PURE; + STDMETHOD(BrowserTabRespondsNow_SetHungAsync)(ULONG, ULONG) PURE; + STDMETHOD(BrowserTabIsPresumedResponsive)(void) PURE; + STDMETHOD(RecoverHungTab)(void) PURE; + STDMETHOD(Duplicate)(void) PURE; + STDMETHOD(ResetBrowserLCIEProxy)(IDispatch*) PURE; + STDMETHOD(SetPendingUrl)(LPCWSTR) PURE; +}; + +namespace ie_tab_interfaces { + +// Retrieves the requested tab manager interface for the specified IEFrame +// window. +// +// @param ie_frame The top-level frame window you wish to manage. +// @param riid The identifier of the requested interface. +// @param manager Returns the IE7 tab window manager on success. +HRESULT TabWindowManagerFromFrame(HWND ie_frame, REFIID riid, void** manager); + +} // namespace ie_tab_interfaces + +#endif // CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ diff --git a/ceee/ie/common/ie_util.cc b/ceee/ie/common/ie_util.cc new file mode 100644 index 0000000..5484898 --- /dev/null +++ b/ceee/ie/common/ie_util.cc @@ -0,0 +1,134 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility functions to interact with IE. + +#include "ceee/ie/common/ie_util.h" + +#include <atlcomcli.h> +#include <exdisp.h> // IWebBrowser2 + +#include "base/logging.h" +#include "base/win/registry.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" + +namespace { + +const wchar_t kIeVersionKey[] = L"SOFTWARE\\Microsoft\\Internet Explorer"; +const wchar_t kIeVersionValue[] = L"Version"; + +HRESULT GetShellWindowsEnum(IEnumVARIANT** enum_windows) { + CComPtr<IShellWindows> shell_windows; + HRESULT hr = shell_windows.CoCreateInstance(CLSID_ShellWindows); + DCHECK(SUCCEEDED(hr)) << "Could not CoCreate ShellWindows. " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + CComPtr<IUnknown> enum_punk; + hr = shell_windows->_NewEnum(&enum_punk); + DCHECK(SUCCEEDED(hr)) << "Could not get Enum IUnknown??? " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + return enum_punk->QueryInterface(IID_IEnumVARIANT, + reinterpret_cast<void**>(enum_windows)); +} + +bool GetIeVersionString(std::wstring* version) { + DCHECK(version != NULL); + if (version == NULL) + return false; + base::win::RegKey key(HKEY_LOCAL_MACHINE, kIeVersionKey, KEY_READ); + DCHECK(key.ValueExists(kIeVersionValue)); + return key.ReadValue(kIeVersionValue, version); +} + +} // namespace + +namespace ie_util { + +HRESULT GetWebBrowserForTopLevelIeHwnd( + HWND window, IWebBrowser2* not_him, IWebBrowser2** browser) { + DCHECK(browser != NULL); + CComPtr<IEnumVARIANT> enum_windows; + HRESULT hr = GetShellWindowsEnum(&enum_windows); + DCHECK(SUCCEEDED(hr)) << "Could not get ShellWindows enum. " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + hr = enum_windows->Reset(); + DCHECK(SUCCEEDED(hr)) << "Can't Reset??? " << com::LogHr(hr); + CComVariant shell_window; + ULONG fetched = 0; + while (SUCCEEDED(enum_windows->Next(1, &shell_window, &fetched)) && + fetched == 1) { + DCHECK(shell_window.vt == VT_DISPATCH); + CComQIPtr<IWebBrowser2> this_browser(shell_window.pdispVal); + if (this_browser != NULL) { + HWND this_window = NULL; + hr = this_browser->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&this_window)); + // This can happen if the browser gets deconnected as we loop. + if (SUCCEEDED(hr) && this_window == window && not_him != this_browser) { + return this_browser.CopyTo(browser); + } + } + shell_window.Clear(); + } + return E_FAIL; +} + +IeVersion GetIeVersion() { + std::wstring ie_version_str; + if (GetIeVersionString(&ie_version_str)) { + int ie_version_num = wcstol( + ie_version_str.substr(0, ie_version_str.find(L'.')).c_str(), NULL, 10); + if (ie_version_num < 6) + return IEVERSION_PRE_IE6; + else if (ie_version_num == 6) + return IEVERSION_IE6; + else if (ie_version_num == 7) + return IEVERSION_IE7; + else if (ie_version_num == 8) + return IEVERSION_IE8; + else if (ie_version_num == 9) + return IEVERSION_IE9; + } + DCHECK(false) << "Failed to get a known IE version!!!"; + return IEVERSION_UNKNOWN; +} + +bool GetIEIsInPrivateBrowsing() { + // TODO(skare@google.com): unify with version in chrome_frame/utils.cc + + // InPrivate flag will not change over process lifetime, so cache it. See: + // http://blogs.msdn.com/ieinternals/archive/2009/06/30/IE8-Privacy-APIs-for-Addons.aspx + static bool inprivate_status_cached = false; + static bool is_inprivate = false; + if (inprivate_status_cached) + return is_inprivate; + + // InPrivate is only supported with IE8+. + if (GetIeVersion() < IEVERSION_IE8) { + inprivate_status_cached = true; + return false; + } + + typedef BOOL (WINAPI* IEIsInPrivateBrowsingPtr)(); + HMODULE ieframe_dll = GetModuleHandle(L"ieframe.dll"); + if (ieframe_dll) { + IEIsInPrivateBrowsingPtr IsInPrivate = + reinterpret_cast<IEIsInPrivateBrowsingPtr>(GetProcAddress( + ieframe_dll, "IEIsInPrivateBrowsing")); + if (IsInPrivate) { + is_inprivate = !!IsInPrivate(); + } + } + + inprivate_status_cached = true; + return is_inprivate; +} + +} // namespace ie_util diff --git a/ceee/ie/common/ie_util.h b/ceee/ie/common/ie_util.h new file mode 100644 index 0000000..5d2a19c --- /dev/null +++ b/ceee/ie/common/ie_util.h @@ -0,0 +1,45 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility functions to interact with IE. + +#ifndef CEEE_IE_COMMON_IE_UTIL_H_ +#define CEEE_IE_COMMON_IE_UTIL_H_ + +#include <wtypes.h> + +#include <set> + +struct IWebBrowser2; + +namespace ie_util { + +// Returns the IWebBrowser2 interface associated to a given top level IE HWND. +// but not the one passed into not_him (which could be NULL), since there are +// some states where there are two web browser assigned to a single HWND. +HRESULT GetWebBrowserForTopLevelIeHwnd( + HWND window, IWebBrowser2* not_him, IWebBrowser2** browser); + +// NOTE: Keep these in order so callers can do things like +// "if (GetIeVersion() > IEVERSION_7) ...". It's OK to change the values, +// though. +enum IeVersion { + IEVERSION_UNKNOWN = -1, + IEVERSION_PRE_IE6 = 0, // Not supported + IEVERSION_IE6 = 1, + IEVERSION_IE7 = 2, + IEVERSION_IE8 = 3, + IEVERSION_IE9 = 4, +}; + +// Returns the IE version. +// Returns true/false for success/failure. +IeVersion GetIeVersion(); + +// Returns true if IE is in InPrivate mode. +bool GetIEIsInPrivateBrowsing(); + +} // namespace ie_util + +#endif // CEEE_IE_COMMON_IE_UTIL_H_ diff --git a/ceee/ie/common/mock_ceee_module_util.h b/ceee/ie/common/mock_ceee_module_util.h new file mode 100644 index 0000000..af70657 --- /dev/null +++ b/ceee/ie/common/mock_ceee_module_util.h @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ceee_module_util mocks. + +#ifndef CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ +#define CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/testing/utils/mock_static.h" + +namespace testing { + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT2(ceee_module_util::SetOptionToolbandIsHidden, + SetOptionToolbandIsHidden); + MOCK_STATIC_INIT2(ceee_module_util::GetOptionToolbandIsHidden, + GetOptionToolbandIsHidden); + MOCK_STATIC_INIT2(ceee_module_util::SetOptionToolbandForceReposition, + SetOptionToolbandForceReposition); + MOCK_STATIC_INIT2(ceee_module_util::GetOptionToolbandForceReposition, + GetOptionToolbandForceReposition); + MOCK_STATIC_INIT2(ceee_module_util::SetIgnoreShowDWChanges, + SetIgnoreShowDWChanges); + MOCK_STATIC_INIT2(ceee_module_util::GetIgnoreShowDWChanges, + GetIgnoreShowDWChanges); + MOCK_STATIC_INIT2(ceee_module_util::GetExtensionPath, + GetExtensionPath); + MOCK_STATIC_INIT_END() + MOCK_STATIC1(void, , SetOptionToolbandIsHidden, bool); + MOCK_STATIC0(bool, , GetOptionToolbandIsHidden); + MOCK_STATIC1(void, , SetOptionToolbandForceReposition, bool); + MOCK_STATIC0(bool, , GetOptionToolbandForceReposition); + MOCK_STATIC1(void, , SetIgnoreShowDWChanges, bool); + MOCK_STATIC0(bool, , GetIgnoreShowDWChanges); + MOCK_STATIC0(std::wstring, , GetExtensionPath); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtils) + +} // namespace testing + +#endif // CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ diff --git a/ceee/ie/common/mock_ie_tab_interfaces.h b/ceee/ie/common/mock_ie_tab_interfaces.h new file mode 100644 index 0000000..2a7fe22 --- /dev/null +++ b/ceee/ie/common/mock_ie_tab_interfaces.h @@ -0,0 +1,106 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ie_tab_interfaces mocks. + +#ifndef CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ +#define CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ + +#include <atlbase.h> +#include <atlcom.h> + +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "ceee/testing/utils/mock_static.h" + +namespace testing { + +MOCK_STATIC_CLASS_BEGIN(MockIeTabInterfaces) + MOCK_STATIC_INIT_BEGIN(MockIeTabInterfaces) + MOCK_STATIC_INIT2(ie_tab_interfaces::TabWindowManagerFromFrame, + TabWindowManagerFromFrame) + MOCK_STATIC_INIT_END() + + MOCK_STATIC3(HRESULT, , TabWindowManagerFromFrame, HWND, REFIID, void **) +MOCK_STATIC_CLASS_END(MockIeTabInterfaces) + +class MockITabWindowManagerIe7 + : public CComObjectRootEx<CComSingleThreadModel>, + public ITabWindowManagerIe7 { + public: + DECLARE_NOT_AGGREGATABLE(MockITabWindowManagerIe7) + BEGIN_COM_MAP(MockITabWindowManagerIe7) + COM_INTERFACE_ENTRY(ITabWindowManagerIe7) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, AddTab, + HRESULT(LPCITEMIDLIST, UINT, ULONG, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SelectTab, HRESULT(long)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, CloseAllTabs, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetActiveTab, HRESULT(IUnknown**)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetCount, HRESULT(long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetItem, HRESULT(long, IUnknown**)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, IndexFromID, HRESULT(long, long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, IndexFromHWND, HRESULT(HWND, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowserFrame, HRESULT(IUnknown**)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, AddBlankTab, + HRESULT(unsigned long, long*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, AddTabGroup, + HRESULT(LPCITEMIDLIST*, long, unsigned long)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetCurrentTabGroup, + HRESULT(LPCITEMIDLIST**, long*, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OpenHomePages, HRESULT(int)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, RepositionTab, + HRESULT(long, long, int)); +}; + +class MockITabWindowIe7 + : public CComObjectRootEx<CComSingleThreadModel>, + public ITabWindowIe7 { + public: + DECLARE_NOT_AGGREGATABLE(MockITabWindowIe7) + BEGIN_COM_MAP(MockITabWindowIe7) + COM_INTERFACE_ENTRY(ITabWindowIe7) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetID, HRESULT(long*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Close, HRESULT()); + MOCK_METHOD5_WITH_CALLTYPE(__stdcall, AsyncExec, + HRESULT(REFGUID, DWORD, DWORD, VARIANT*, VARIANT*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabWindowManager, + HRESULT(ITabWindowManagerIe7**)); + MOCK_METHOD6_WITH_CALLTYPE(__stdcall, OnBrowserCreated, + HRESULT(int, int, int, int, int, void*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, OnNewWindow, + HRESULT(ULONG, IDispatch**)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, OnBrowserClosed, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnRequestAttention, HRESULT(int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, FrameTranslateAccelerator, + HRESULT(MSG*, ULONG)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetTitle, HRESULT(LPCWSTR, int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetIcon, HRESULT(HICON, int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetStatusBarState, HRESULT(int, long)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetTitle, HRESULT(LPWSTR, ULONG, int)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetIcon, HRESULT(HICON*, int*, int)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetLocationPidl, + HRESULT(LPCITEMIDLIST*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetNavigationState, HRESULT(ULONG*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetProgress, + HRESULT(int*, long*, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetFlags, HRESULT(ULONG*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowser, HRESULT(IDispatch**)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowserToolbarWindow, + HRESULT(HWND*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetSearchState, HRESULT(int*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetAttentionState, HRESULT(int*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, ResampleImageAsync, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnTabImageResampled, HRESULT(HBITMAP)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetStatusBarState, + HRESULT(int*, long*)); +}; + +} // namespace testing + +#endif // CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ diff --git a/ceee/ie/common/precompile.cc b/ceee/ie/common/precompile.cc new file mode 100644 index 0000000..46a23d0 --- /dev/null +++ b/ceee/ie/common/precompile.cc @@ -0,0 +1,6 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile generator file. +#include "ceee/ie/common/precompile.h" diff --git a/ceee/ie/common/precompile.h b/ceee/ie/common/precompile.h new file mode 100644 index 0000000..5a934ee --- /dev/null +++ b/ceee/ie/common/precompile.h @@ -0,0 +1,14 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile header for CEEE. + +#ifndef CEEE_IE_COMMON_PRECOMPILE_H_ +#define CEEE_IE_COMMON_PRECOMPILE_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <atlstr.h> + +#endif // CEEE_IE_COMMON_PRECOMPILE_H_ diff --git a/ceee/ie/ie.gyp b/ceee/ie/ie.gyp new file mode 100644 index 0000000..e2ea88e --- /dev/null +++ b/ceee/ie/ie.gyp @@ -0,0 +1,146 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'targets': [ + { + 'target_name': 'ceee_ie_all', + 'type': 'none', + 'dependencies': [ + 'common/common.gyp:*', + 'broker/broker.gyp:*', + 'plugin/bho/bho.gyp:*', + 'plugin/scripting/scripting.gyp:*', + 'plugin/toolband/toolband.gyp:*', + 'ie_unittests', + 'mediumtest_ie', + ] + }, + { + 'target_name': 'ie_unittests', + 'type': 'executable', + 'sources': [ + 'broker/api_dispatcher_unittest.cc', + 'broker/broker_unittest.cc', + 'broker/cookie_api_module_unittest.cc', + 'broker/executors_manager_unittest.cc', + 'broker/infobar_api_module_unittest.cc', + 'broker/tab_api_module_unittest.cc', + 'broker/window_api_module_unittest.cc', + 'broker/window_events_funnel_unittest.cc', + 'common/chrome_frame_host_unittest.cc', + 'common/crash_reporter_unittest.cc', + 'common/extension_manifest_unittest.cc', + 'common/ceee_module_util_unittest.cc', + 'plugin/bho/browser_helper_object_unittest.cc', + 'plugin/bho/cookie_accountant_unittest.cc', + 'plugin/bho/cookie_events_funnel_unittest.cc', + 'plugin/bho/dom_utils_unittest.cc', + 'plugin/bho/events_funnel_unittest.cc', + 'plugin/bho/executor_unittest.cc', + 'plugin/bho/extension_port_manager.cc', + 'plugin/bho/frame_event_handler_unittest.cc', + 'plugin/bho/infobar_events_funnel_unittest.cc', + 'plugin/bho/tab_events_funnel_unittest.cc', + 'plugin/bho/tool_band_visibility_unittest.cc', + 'plugin/bho/webnavigation_events_funnel_unittest.cc', + 'plugin/bho/webrequest_events_funnel_unittest.cc', + 'plugin/bho/webrequest_notifier_unittest.cc', + 'plugin/bho/web_progress_notifier_unittest.cc', + 'plugin/scripting/content_script_manager.rc', + 'plugin/scripting/content_script_manager_unittest.cc', + 'plugin/scripting/content_script_native_api_unittest.cc', + 'plugin/scripting/renderer_extension_bindings_unittest.cc', + 'plugin/scripting/renderer_extension_bindings_unittest.rc', + 'plugin/scripting/script_host_unittest.cc', + 'plugin/scripting/userscripts_librarian_unittest.cc', + 'plugin/toolband/tool_band_unittest.cc', + 'plugin/toolband/toolband_module_reporting_unittest.cc', + 'testing/ie_unittest_main.cc', + 'testing/mock_broker_and_friends.h', + 'testing/mock_chrome_frame_host.h', + 'testing/mock_browser_and_friends.h', + 'testing/precompile.cc', + 'testing/precompile.h', + ], + 'configurations': { + 'Debug': { + 'msvs_settings': { + 'VCCLCompilerTool': { + # GMock and GTest appear to be really fat, so bump + # precompile header memory setting to 332 megs. + 'AdditionalOptions': ['/Zm332', '/bigobj'], + }, + }, + 'msvs_precompiled_source': 'testing/precompile.cc', + 'msvs_precompiled_header': 'testing/precompile.h', + }, + }, + 'dependencies': [ + 'common/common.gyp:ie_common', + 'common/common.gyp:ie_common_settings', + 'common/common.gyp:ie_guids', + 'broker/broker.gyp:broker', + 'plugin/bho/bho.gyp:bho', + 'plugin/scripting/scripting.gyp:javascript_bindings', + 'plugin/scripting/scripting.gyp:scripting', + 'plugin/toolband/toolband.gyp:ceee_ie_lib', + 'plugin/toolband/toolband.gyp:ie_toolband_common', + 'plugin/toolband/toolband.gyp:toolband_idl', + '../../base/base.gyp:base', + '../../breakpad/breakpad.gyp:breakpad_handler', + '../testing/sidestep/sidestep.gyp:sidestep', + '../testing/utils/test_utils.gyp:test_utils', + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + { + 'target_name': 'mediumtest_ie', + 'type': 'executable', + 'sources': [ + 'plugin/bho/mediumtest_browser_event.cc', + 'plugin/bho/mediumtest_browser_helper_object.cc', + 'testing/mediumtest_ie_common.cc', + 'testing/mediumtest_ie_common.h', + 'testing/mediumtest_ie_main.cc', + 'testing/precompile.cc', + 'testing/precompile.h', + ], + 'configurations': { + 'Debug': { + 'msvs_settings': { + 'VCCLCompilerTool': { + # GMock and GTest appear to be really fat, so bump + # precompile header memory setting to 332 megs. + 'AdditionalOptions': ['/Zm332'], + }, + }, + 'msvs_precompiled_source': 'testing/precompile.cc', + 'msvs_precompiled_header': 'testing/precompile.h', + }, + }, + 'dependencies': [ + 'common/common.gyp:ie_common', + 'common/common.gyp:ie_common_settings', + 'common/common.gyp:ie_guids', + 'plugin/bho/bho.gyp:bho', + 'plugin/scripting/scripting.gyp:scripting', + 'plugin/toolband/toolband.gyp:toolband_idl', + '../../base/base.gyp:base', + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + '../testing/utils/test_utils.gyp:test_utils', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + ] +} diff --git a/ceee/ie/plugin/bho/bho.gyp b/ceee/ie/plugin/bho/bho.gyp new file mode 100644 index 0000000..fe773d39 --- /dev/null +++ b/ceee/ie/plugin/bho/bho.gyp @@ -0,0 +1,87 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'bho', + 'type': 'static_library', + 'dependencies': [ + '../toolband/toolband.gyp:toolband_idl', + '../../broker/broker.gyp:broker', + '../../common/common.gyp:ie_common', + '../../common/common.gyp:ie_common_settings', + '../../../common/common.gyp:ceee_common', + '../../../common/common.gyp:initializing_coclass', + '../../../../base/base.gyp:base', + # For the vtable patching stuff... + '../../../../chrome_frame/chrome_frame.gyp:chrome_frame_ie', + ], + 'sources': [ + 'browser_helper_object.cc', + 'browser_helper_object.h', + 'browser_helper_object.rgs', + 'cookie_accountant.cc', + 'cookie_accountant.h', + 'cookie_events_funnel.cc', + 'cookie_events_funnel.h', + 'dom_utils.cc', + 'dom_utils.h', + 'events_funnel.cc', + 'events_funnel.h', + 'executor.cc', + 'executor.h', + 'extension_port_manager.cc', + 'extension_port_manager.h', + 'frame_event_handler.cc', + 'frame_event_handler.h', + 'http_negotiate.cc', + 'http_negotiate.h', + 'infobar_browser_window.cc', + 'infobar_browser_window.h', + 'infobar_events_funnel.cc', + 'infobar_events_funnel.h', + 'infobar_manager.cc', + 'infobar_manager.h', + 'infobar_window.cc', + 'infobar_window.h', + '../../common/precompile.cc', + '../../common/precompile.h', + 'tab_events_funnel.cc', + 'tab_events_funnel.h', + 'tab_window_manager.cc', + 'tab_window_manager.h', + 'tool_band_visibility.cc', + 'tool_band_visibility.h', + 'web_browser_events_source.h', + 'webnavigation_events_funnel.cc', + 'webnavigation_events_funnel.h', + 'webrequest_events_funnel.cc', + 'webrequest_events_funnel.h', + 'webrequest_notifier.cc', + 'webrequest_notifier.h', + 'web_progress_notifier.cc', + 'web_progress_notifier.h', + 'window_message_source.cc', + 'window_message_source.h', + + '../../../../chrome_frame/renderer_glue.cc', # needed for cf_ie.lib + '../../../../chrome/common/extensions/extension_resource.cc', + '../../../../chrome/common/extensions/extension_resource.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + ] +} diff --git a/ceee/ie/plugin/bho/browser_helper_object.cc b/ceee/ie/plugin/bho/browser_helper_object.cc new file mode 100644 index 0000000..7a9f4d7 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.cc @@ -0,0 +1,1372 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/browser_helper_object.h" + +#include <atlsafe.h> +#include <shlguid.h> + +#include <algorithm> + +#include "base/debug/trace_event.h" +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/tuple.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/cookie_accountant.h" +#include "ceee/ie/plugin/bho/http_negotiate.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/automation/automation_constants.h" +#include "googleurl/src/gurl.h" + +#include "broker_lib.h" // NOLINT + +namespace keys = extension_tabs_module_constants; +namespace ext = extension_automation_constants; + + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatch_5variantptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 7, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO BrowserHelperObject::handler_type_idispatch_variantptr_ = { + CC_STDCALL, VT_EMPTY, 2, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF + } +}; + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatch_3variantptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 5, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO BrowserHelperObject::handler_type_idispatchptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 2, { + VT_DISPATCH | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatchptr_boolptr_dword_2bstr_ = { + CC_STDCALL, VT_EMPTY, 5, { + VT_DISPATCH | VT_BYREF, + VT_BOOL | VT_BYREF, + VT_UI4, + VT_BSTR, + VT_BSTR + } +}; + +// Remove the need to AddRef/Release for Tasks that target this class. +DISABLE_RUNNABLE_METHOD_REFCOUNT(BrowserHelperObject); + +BrowserHelperObject::BrowserHelperObject() + : already_tried_installing_(false), + tab_window_(NULL), + tab_id_(kInvalidChromeSessionId), + fired_on_created_event_(false), + lower_bound_ready_state_(READYSTATE_UNINITIALIZED), + ie7_or_later_(false), + thread_id_(::GetCurrentThreadId()), + full_tab_chrome_frame_(false) { + TRACE_EVENT_BEGIN("ceee.bho", this, ""); + // Only the first call to this function really does anything. + CookieAccountant::GetInstance()->PatchWininetFunctions(); +} + +BrowserHelperObject::~BrowserHelperObject() { + TRACE_EVENT_END("ceee.bho", this, ""); +} + +HRESULT BrowserHelperObject::FinalConstruct() { + return S_OK; +} + +void BrowserHelperObject::FinalRelease() { + web_browser_.Release(); +} + +STDMETHODIMP BrowserHelperObject::SetSite(IUnknown* site) { + typedef IObjectWithSiteImpl<BrowserHelperObject> SuperSite; + + // From experience, we know the site may be set multiple times. + // Let's ignore second and subsequent set or unset. + if (NULL != site && NULL != m_spUnkSite.p || + NULL == site && NULL == m_spUnkSite.p) { + LOG(WARNING) << "Duplicate call to SetSite, previous site " + << m_spUnkSite.p << " new site " << site; + return S_OK; + } + + if (NULL == site) { + // We're being torn down. + TearDown(); + + FireOnRemovedEvent(); + // This call should be the last thing we send to the broker. + FireOnUnmappedEvent(); + } + + HRESULT hr = SuperSite::SetSite(site); + if (FAILED(hr)) + return hr; + + if (NULL != site) { + // We're being initialized. + hr = Initialize(site); + + // Release the site in case of failure. + if (FAILED(hr)) + SuperSite::SetSite(NULL); + } + + return hr; +} + +HRESULT BrowserHelperObject::GetParentBrowser(IWebBrowser2* browser, + IWebBrowser2** parent_browser) { + DCHECK(browser != NULL); + DCHECK(parent_browser != NULL && *parent_browser == NULL); + + // Get the parent object. + CComPtr<IDispatch> parent_disp; + HRESULT hr = browser->get_Parent(&parent_disp); + if (FAILED(hr)) { + // NO DCHECK, or even log here, the caller will handle and log the error. + return hr; + } + + // Then get the associated browser through the appropriate contortions. + CComQIPtr<IServiceProvider> parent_sp(parent_disp); + if (parent_sp == NULL) + return E_NOINTERFACE; + + CComPtr<IWebBrowser2> parent; + hr = parent_sp->QueryService(SID_SWebBrowserApp, + IID_IWebBrowser2, + reinterpret_cast<void**>(&parent)); + if (FAILED(hr)) + return hr; + + DCHECK(parent != NULL); + // IE seems to define the top-level browser as its own parent, + // hence this check and error return. + if (parent == browser) + return E_FAIL; + + LOG(INFO) << "Child: " << std::hex << browser << " -> Parent: " << + std::hex << parent.p; + + *parent_browser = parent.Detach(); + return S_OK; +} + +HRESULT BrowserHelperObject::GetBrokerRegistrar( + ICeeeBrokerRegistrar** broker) { + // This is a singleton and will not create a new one. + return ::CoCreateInstance(CLSID_CeeeBroker, NULL, CLSCTX_ALL, + IID_ICeeeBrokerRegistrar, + reinterpret_cast<void**>(broker)); +} + +HRESULT BrowserHelperObject::CreateExecutor(IUnknown** executor) { + HRESULT hr = ::CoCreateInstance( + CLSID_CeeeExecutor, NULL, CLSCTX_INPROC_SERVER, + IID_IUnknown, reinterpret_cast<void**>(executor)); + if (SUCCEEDED(hr)) { + CComQIPtr<IObjectWithSite> executor_with_site(*executor); + DCHECK(executor_with_site != NULL) + << "Executor must implement IObjectWithSite."; + if (executor_with_site != NULL) { + CComPtr<IUnknown> bho_identity; + hr = QueryInterface(IID_IUnknown, + reinterpret_cast<void**>(&bho_identity)); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (SUCCEEDED(hr)) + executor_with_site->SetSite(bho_identity); + } + } + + return hr; +} + +WebProgressNotifier* BrowserHelperObject::CreateWebProgressNotifier() { + scoped_ptr<WebProgressNotifier> web_progress_notifier( + new WebProgressNotifier()); + HRESULT hr = web_progress_notifier->Initialize(this, tab_window_, + web_browser_); + + return SUCCEEDED(hr) ? web_progress_notifier.release() : NULL; +} + +HRESULT BrowserHelperObject::Initialize(IUnknown* site) { + TRACE_EVENT_INSTANT("ceee.bho.initialize", this, ""); + + ie7_or_later_ = ie_util::GetIeVersion() > ie_util::IEVERSION_IE6; + + HRESULT hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + // Initialize the extension port manager. + extension_port_manager_.Initialize(chrome_frame_host_); + + // Preemptively feed the broker with an executor in our thread. + hr = GetBrokerRegistrar(&broker_registrar_); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create broker, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Broker. " << com::LogHr(hr); + if (SUCCEEDED(hr)) { + DCHECK(executor_ == NULL); + hr = CreateExecutor(&executor_); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + if (SUCCEEDED(hr)) { + // TODO(mad@chromium.org): Implement the proper manual/secure + // registration. + hr = broker_registrar_->RegisterTabExecutor(::GetCurrentThreadId(), + executor_); + DCHECK(SUCCEEDED(hr)) << "Registering Executor. " << com::LogHr(hr); + } + } + + // We need the service provider for both the sink connections and + // to get a handle to the tab window. + CComQIPtr<IServiceProvider> service_provider(site); + DCHECK(service_provider); + if (service_provider == NULL) { + TearDown(); + return E_FAIL; + } + + hr = ConnectSinks(service_provider); + DCHECK(SUCCEEDED(hr)) << "ConnectSinks failed " << com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + hr = GetTabWindow(service_provider); + DCHECK(SUCCEEDED(hr)) << "GetTabWindow failed " << com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + // Patch IHttpNegotiate for user-agent and cookie functionality. + HttpNegotiatePatch::Initialize(); + + CheckToolBandVisibility(web_browser_); + + web_progress_notifier_.reset(CreateWebProgressNotifier()); + DCHECK(web_progress_notifier_ != NULL) + << "Failed to initialize WebProgressNotifier"; + if (web_progress_notifier_ == NULL) { + TearDown(); + return E_FAIL; + } + + return S_OK; +} + +HRESULT BrowserHelperObject::TearDown() { + TRACE_EVENT_INSTANT("ceee.bho.teardown", this, ""); + + if (web_progress_notifier_ != NULL) { + web_progress_notifier_->TearDown(); + web_progress_notifier_.reset(NULL); + } + + ToolBandVisibility::TearDown(); + + if (executor_ != NULL) { + CComQIPtr<IObjectWithSite> executor_with_site(executor_); + DCHECK(executor_with_site != NULL) + << "Executor must implement IObjectWithSite."; + if (executor_with_site != NULL) { + executor_with_site->SetSite(NULL); + } + } + + // Unregister our executor so that the broker don't have to rely + // on the thread dying to release it and confuse COM in trying to release it. + if (broker_registrar_ != NULL) { + DCHECK(executor_ != NULL); + // Manually disconnect executor_, + // so it doesn't get called while we unregister it below. + HRESULT hr = ::CoDisconnectObject(executor_, 0); + DCHECK(SUCCEEDED(hr)) << "Couldn't disconnect Executor. " << com::LogHr(hr); + + // TODO(mad@chromium.org): Implement the proper manual/secure + // unregistration. + hr = broker_registrar_->UnregisterExecutor(::GetCurrentThreadId()); + DCHECK(SUCCEEDED(hr)) << "Unregistering Executor. " << com::LogHr(hr); + } else { + DCHECK(executor_ == NULL); + } + executor_.Release(); + + if (web_browser_) + DispEventUnadvise(web_browser_, &DIID_DWebBrowserEvents2); + web_browser_.Release(); + + HttpNegotiatePatch::Uninitialize(); + + if (chrome_frame_host_) { + chrome_frame_host_->SetEventSink(NULL); + chrome_frame_host_->TearDown(); + } + chrome_frame_host_.Release(); + + return S_OK; +} + +HRESULT BrowserHelperObject::InitializeChromeFrameHost() { + HRESULT hr = CreateChromeFrameHost(); + DCHECK(SUCCEEDED(hr) && chrome_frame_host_ != NULL); + if (FAILED(hr) || chrome_frame_host_ == NULL) { + LOG(ERROR) << "Failed to get chrome frame host " << com::LogHr(hr); + return com::AlwaysError(hr); + } + + chrome_frame_host_->SetChromeProfileName( + ceee_module_util::GetBrokerProfileNameForIe()); + chrome_frame_host_->SetEventSink(this); + hr = chrome_frame_host_->StartChromeFrame(); + DCHECK(SUCCEEDED(hr)) << "Failed to start Chrome Frame." << com::LogHr(hr); + if (FAILED(hr)) { + chrome_frame_host_->SetEventSink(NULL); + + LOG(ERROR) << "Failed to start chrome frame " << com::LogHr(hr); + return hr; + } + + return hr; +} + +HRESULT BrowserHelperObject::OnCfReadyStateChanged(LONG state) { + // If EnsureTabId() returns false, the session_id isn't available. + // This means that the ExternalTab hasn't been created yet, which is certainly + // a bug. Calling this here will also ensure we call all the deferred calls if + // they haven't been called yet. + bool id_available = EnsureTabId(); + if (!id_available) { + NOTREACHED(); + return E_UNEXPECTED; + } + + if (state == READYSTATE_COMPLETE) { + extension_path_ = ceee_module_util::GetExtensionPath(); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_) && + ceee_module_util::NeedToInstallExtension()) { + LOG(INFO) << "Installing extension: \"" << extension_path_ << "\""; + chrome_frame_host_->InstallExtension(CComBSTR(extension_path_.c_str())); + } else { + // In the case where we don't have a CRX (or we don't need to install it), + // we must ask for the currently enabled extension before we can decide + // what we need to do. + chrome_frame_host_->GetEnabledExtensions(); + } + } + + return S_OK; +} + +HRESULT BrowserHelperObject::OnCfExtensionReady(BSTR path, int response) { + TRACE_EVENT_INSTANT("ceee.bho.oncfextensionready", this, ""); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath( + FilePath(extension_path_)); + } + + // Now list enabled extensions so that we can properly start it whether + // it's a CRX file or an exploded folder. + // + // Note that we do this even if Chrome says installation failed, + // as that is the error code it uses when we try to install an + // older version of the extension than it already has, which happens + // on overinstall when Chrome has already auto-updated. + // + // If it turns out no extension is installed, we will handle that + // error in the OnCfGetEnabledExtensionsComplete callback. + chrome_frame_host_->GetEnabledExtensions(); + return S_OK; +} + +void BrowserHelperObject::StartExtension(const wchar_t* base_dir) { + chrome_frame_host_->SetUrl(CComBSTR(chrome::kAboutBlankURL)); + + LoadManifestFile(base_dir); + + // There is a race between launching Chrome to get the directory of + // the extension, and the first page this BHO is attached to being loaded. + // If we hadn't loaded the manifest file when injection of scripts and + // CSS should have been done for that page, then do it now as it is the + // earliest opportunity. + BrowserHandlerMap::const_iterator it = browsers_.begin(); + for (; it != browsers_.end(); ++it) { + DCHECK(it->second.m_T != NULL); + it->second.m_T->RedoDoneInjections(); + } + + // Now we should know the extension id and can pass it to the executor. + if (extension_id_.empty()) { + LOG(ERROR) << "Have no extension id after loading the extension."; + } else if (executor_ != NULL) { + CComPtr<ICeeeInfobarExecutor> infobar_executor; + HRESULT hr = executor_->QueryInterface(IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&infobar_executor)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ICeeeInfobarExecutor interface " << + com::LogHr(hr); + if (SUCCEEDED(hr)) { + infobar_executor->SetExtensionId(CComBSTR(extension_id_.c_str())); + } + } +} + +HRESULT BrowserHelperObject::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* base_dirs) { + CComSafeArray<BSTR> directories; + directories.Attach(base_dirs); // MUST DETACH BEFORE RETURNING + + // TODO(joi@chromium.org) Deal with multiple extensions. + if (directories.GetCount() > 0) { + // If our extension_path is not a CRX, it MUST be the same as the installed + // extension path which would be an exploded extension. + // If you get this DCHECK, you may have changed your registry settings to + // debug with an exploded extension, but you didn't uninstall the previous + // extension, either via the Chrome UI or by simply wiping out your + // profile folder. + DCHECK(ceee_module_util::IsCrxOrEmpty(extension_path_) || + extension_path_ == std::wstring(directories.GetAt(0))); + StartExtension(directories.GetAt(0)); + } else if (!ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // We have an extension path that isn't a CRX and we don't have any + // enabled extension, so we must load the exploded extension from this + // given path. WE MUST DO THIS BEFORE THE NEXT ELSE IF because it assumes + // a CRX file. + chrome_frame_host_->LoadExtension(CComBSTR(extension_path_.c_str())); + } else if (!already_tried_installing_ && !extension_path_.empty()) { + // We attempt to install the .crx file from the CEEE folder; in the + // default case this will happen only once after installation. + already_tried_installing_ = true; + chrome_frame_host_->InstallExtension(CComBSTR(extension_path_.c_str())); + } + + // If no extension is installed, we do nothing. The toolband handles + // this error and will show an explanatory message to the user. + directories.Detach(); + return S_OK; +} + +HRESULT BrowserHelperObject::OnCfGetExtensionApisToAutomate( + BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +HRESULT BrowserHelperObject::OnCfChannelError() { + return S_FALSE; +} + +bool BrowserHelperObject::EnsureTabId() { + if (tab_id_ != kInvalidChromeSessionId) { + return true; + } + + HRESULT hr = chrome_frame_host_->GetSessionId(&tab_id_); + DCHECK(SUCCEEDED(hr)); + if (hr == S_FALSE) { + // The server returned false, the session_id isn't available yet + return false; + } + + // At this point if tab_id_ is still invalid we have a problem. + if (tab_id_ == kInvalidChromeSessionId) { + // TODO(hansl@google.com): uncomment the following code when the CF change + // has landed. + //// Something really bad happened. + //NOTREACHED(); + //return false; + tab_id_ = reinterpret_cast<int>(tab_window_); + } + + CeeeWindowHandle handle = reinterpret_cast<CeeeWindowHandle>(tab_window_); + hr = broker_registrar_->SetTabIdForHandle(tab_id_, handle); + if (FAILED(hr)) { + DCHECK(SUCCEEDED(hr)) << "An error occured when setting the tab_id: " << + com::LogHr(hr); + tab_id_ = kInvalidChromeSessionId; + return false; + } + VLOG(2) << "TabId(" << tab_id_ << ") set for Handle(" << handle << ")"; + + // Call back all the methods we deferred. In order, please. + while (!deferred_tab_id_call_.empty()) { + // We pop here so that if an error happens in the call we don't recall the + // faulty method. This has the side-effect of losing events. + DeferredCallListType::value_type call = deferred_tab_id_call_.front(); + deferred_tab_id_call_.pop_front(); + call->Run(); + delete call; + } + + return true; +} + +// Fetch and remembers the tab window we are attached to. +HRESULT BrowserHelperObject::GetTabWindow(IServiceProvider* service_provider) { + CComQIPtr<IOleWindow> ole_window; + HRESULT hr = service_provider->QueryService( + SID_SShellBrowser, IID_IOleWindow, reinterpret_cast<void**>(&ole_window)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ole window " << com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + + hr = ole_window->GetWindow(&tab_window_); + DCHECK(SUCCEEDED(hr)) << "Failed to get window from ole window " << + com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + DCHECK(tab_window_ != NULL); + + // Initialize our executor to the right HWND + if (executor_ == NULL) + return E_POINTER; + CComPtr<ICeeeTabExecutor> tab_executor; + hr = executor_->QueryInterface(IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&tab_executor)); + if (SUCCEEDED(hr)) { + CeeeWindowHandle handle = reinterpret_cast<CeeeWindowHandle>(tab_window_); + hr = tab_executor->Initialize(handle); + } + return hr; +} + +// Connect for notifications. +HRESULT BrowserHelperObject::ConnectSinks(IServiceProvider* service_provider) { + HRESULT hr = service_provider->QueryService( + SID_SWebBrowserApp, IID_IWebBrowser2, + reinterpret_cast<void**>(&web_browser_)); + DCHECK(SUCCEEDED(hr)) << "Failed to get web browser " << com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + + // Start sinking events from the web browser object. + hr = DispEventAdvise(web_browser_, &DIID_DWebBrowserEvents2); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to set event sink " << com::LogHr(hr); + return hr; + } + return hr; +} + +HRESULT BrowserHelperObject::CreateChromeFrameHost() { + return ChromeFrameHost::CreateInitializedIID(IID_IChromeFrameHost, + &chrome_frame_host_); +} + +HRESULT BrowserHelperObject::FireOnCreatedEvent(BSTR url) { + DCHECK(url != NULL); + DCHECK(tab_window_ != NULL); + DCHECK(!fired_on_created_event_); + HRESULT hr = tab_events_funnel().OnCreated(tab_window_, url, + lower_bound_ready_state_ == READYSTATE_COMPLETE); + fired_on_created_event_ = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the tab.onCreated event " << + com::LogHr(hr); + return hr; +} + +HRESULT BrowserHelperObject::FireOnRemovedEvent() { + DCHECK(tab_window_ != NULL); + HRESULT hr = + tab_events_funnel().OnRemoved(tab_window_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the tab.onRemoved event " << + com::LogHr(hr); + return hr; +} + +HRESULT BrowserHelperObject::FireOnUnmappedEvent() { + DCHECK(tab_window_ != NULL); + DCHECK(tab_id_ != kInvalidChromeSessionId); + HRESULT hr = tab_events_funnel().OnTabUnmapped(tab_window_, tab_id_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the ceee.onTabUnmapped event " << + com::LogHr(hr); + return hr; +} + +void BrowserHelperObject::CloseAll(IContentScriptNativeApi* instance) { + extension_port_manager_.CloseAll(instance); +} + +HRESULT BrowserHelperObject::OpenChannelToExtension( + IContentScriptNativeApi* instance, const std::string& extension, + const std::string& channel_name, int cookie) { + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OpenChannelToExtension, + instance, extension, channel_name, cookie)); + return S_OK; + } + + scoped_ptr<DictionaryValue> tab_info(new DictionaryValue()); + + DCHECK(tab_id_ != kInvalidChromeSessionId); + tab_info->SetInteger(keys::kIdKey, tab_id_); + + return extension_port_manager_.OpenChannelToExtension(instance, + extension, + channel_name, + tab_info.release(), + cookie); +} + +HRESULT BrowserHelperObject::PostMessage(int port_id, + const std::string& message) { + return extension_port_manager_.PostMessage(port_id, message); +} + +HRESULT BrowserHelperObject::OnCfPrivateMessage(BSTR msg, + BSTR origin, + BSTR target) { + // OnPortMessage uses tab_id_, so we need to check here. + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnCfPrivateMessage, + CComBSTR(msg), origin, target)); + return S_OK; + } + + const wchar_t* start = com::ToString(target); + const wchar_t* end = start + ::SysStringLen(target); + if (LowerCaseEqualsASCII(start, end, ext::kAutomationPortRequestTarget) || + LowerCaseEqualsASCII(start, end, ext::kAutomationPortResponseTarget)) { + extension_port_manager_.OnPortMessage(msg); + return S_OK; + } + + // TODO(siggi@chromium.org): What to do here? + LOG(ERROR) << "Unexpected message: '" << msg << "' to invalid target: " + << target; + return E_UNEXPECTED; +} + + +STDMETHODIMP_(void) BrowserHelperObject::OnBeforeNavigate2( + IDispatch* webbrowser_disp, VARIANT* url, VARIANT* /*flags*/, + VARIANT* /*target_frame_name*/, VARIANT* /*post_data*/, + VARIANT* /*headers*/, VARIANT_BOOL* /*cancel*/) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnBeforeNavigate2 got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + VARIANT* null_param = NULL; + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnBeforeNavigate2, + webbrowser_disp, url, null_param, null_param, null_param, null_param, + reinterpret_cast<VARIANT_BOOL*>(NULL))); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnBeforeNavigate2 failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnBeforeNavigate2 url VT=" << V_VT(url); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnBeforeNavigate(webbrowser, url->bstrVal); + } + + // Notify the infobar executor on the event but only for the main browser. + // TODO(vadimb@google.com): Refactor this so that the executor can just + // register self as WebBrowserEventsSource::Sink. Right now this is + // problematic because the executor is COM object. + CComPtr<IWebBrowser2> parent_browser; + if (executor_ != NULL && web_browser_ != NULL && + web_browser_.IsEqualObject(webbrowser_disp)) { + CComPtr<ICeeeInfobarExecutor> infobar_executor; + HRESULT hr = executor_->QueryInterface(IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&infobar_executor)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ICeeeInfobarExecutor interface " << + com::LogHr(hr); + if (SUCCEEDED(hr)) { + infobar_executor->OnTopFrameBeforeNavigate(CComBSTR(url->bstrVal)); + } + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnDocumentComplete( + IDispatch* webbrowser_disp, VARIANT* url) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnDocumentComplete got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnDocumentComplete, webbrowser_disp, url)); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnDocumentComplete failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnDocumentComplete url VT=" << V_VT(url); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnDocumentComplete(webbrowser, url->bstrVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNavigateComplete2( + IDispatch* webbrowser_disp, VARIANT* url) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnNavigateComplete2 got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnNavigateComplete2, webbrowser_disp, url)); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnNavigateComplete2 failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnNavigateComplete2 url VT=" << V_VT(url); + return; + } + + HandleNavigateComplete(webbrowser, url->bstrVal); + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNavigateComplete(webbrowser, url->bstrVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNavigateError( + IDispatch* webbrowser_disp, VARIANT* url, VARIANT* /*target_frame_name*/, + VARIANT* status_code, VARIANT_BOOL* /*cancel*/) { + if (webbrowser_disp == NULL || url == NULL || status_code == NULL) { + LOG(ERROR) << "OnNavigateError got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + VARIANT* null_param = NULL; + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnNavigateError, + webbrowser_disp, url, null_param, status_code, + reinterpret_cast<VARIANT_BOOL*>(NULL))); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnNavigateError failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnNavigateError url VT=" << V_VT(url); + return; + } + + if (V_VT(status_code) != VT_I4) { + LOG(ERROR) << "OnNavigateError status_code VT=" << V_VT(status_code); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNavigateError(webbrowser, url->bstrVal, status_code->lVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNewWindow2( + IDispatch** /*webbrowser_disp*/, VARIANT_BOOL* /*cancel*/) { + // When a new window/tab is created, IE7 or later version of IE will also + // fire NewWindow3 event, which extends NewWindow2 with additional + // information. As a result, we ignore NewWindow2 event in that case. + if (ie7_or_later_) + return; + + CComBSTR url_context(L""); + CComBSTR url(L""); + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNewWindow(url_context, url); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNewWindow3( + IDispatch** /*webbrowser_disp*/, VARIANT_BOOL* /*cancel*/, DWORD /*flags*/, + BSTR url_context, BSTR url) { + // IE6 uses NewWindow2 event. + if (!ie7_or_later_) + return; + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNewWindow(url_context, url); + } +} + +bool BrowserHelperObject::BrowserContainsChromeFrame(IWebBrowser2* browser) { + CComPtr<IDispatch> document_disp; + HRESULT hr = browser->get_Document(&document_disp); + if (FAILED(hr)) { + // This should never happen. + NOTREACHED() << "IWebBrowser2::get_Document failed " << com::LogHr(hr); + return false; + } + + CComQIPtr<IPersist> document_persist(document_disp); + if (document_persist != NULL) { + CLSID clsid = {}; + hr = document_persist->GetClassID(&clsid); + if (SUCCEEDED(hr) && clsid == CLSID_ChromeFrame) { + return true; + } + } + return false; +} + +HRESULT BrowserHelperObject::AttachBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler) { + // We're not attached yet, figure out whether we're attaching + // to the top-level browser or a frame, and looukup the parentage + // in the latter case. + CComPtr<IWebBrowser2> parent_browser; + HRESULT hr = S_OK; + bool is_top_frame = web_browser_.IsEqualObject(webbrowser); + if (!is_top_frame) { + // Not the top-level browser, so find the parent. If this fails, + // we assume webbrowser is orphaned, and will not attach to it. + // This can happen when a FRAME/IFRAME is removed from the DOM tree. + hr = GetParentBrowser(webbrowser, &parent_browser); + if (FAILED(hr)) + LOG(WARNING) << "Failed to get parent browser " << com::LogHr(hr); + } + + // Attempt to attach to the web browser. + if (SUCCEEDED(hr)) { + hr = CreateFrameEventHandler(webbrowser, parent_browser, handler); + bool document_is_mshtml = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr) || hr == E_DOCUMENT_NOT_MSHTML) << + "Unexpected error creating a frame handler " << com::LogHr(hr); + + if (is_top_frame) { + // Check if it is a chrome frame. + bool is_chrome_frame = BrowserContainsChromeFrame(webbrowser); + + if (is_chrome_frame) { + fired_on_created_event_ = true; + full_tab_chrome_frame_ = true; + // Send a tabs.onRemoved event to make the extension believe the tab is + // dead. + hr = FireOnRemovedEvent(); + DCHECK(SUCCEEDED(hr)); + } else if (document_is_mshtml) { + // We know it's MSHTML. We check if the last page was chrome frame. + if (full_tab_chrome_frame_) { + // This will send a tabs.onCreated event later to balance the + // onRemoved event above. + fired_on_created_event_ = false; + full_tab_chrome_frame_ = false; + } + } + } + } + + return hr; +} + +void BrowserHelperObject::HandleNavigateComplete(IWebBrowser2* webbrowser, + BSTR url) { + // If the top-level document or a sub-frame is navigated, we'll already + // be attached to the browser in question, so don't reattach. + HRESULT hr = S_OK; + CComPtr<IFrameEventHandler> handler; + if (FAILED(GetBrowserHandler(webbrowser, &handler))) { + hr = AttachBrowserHandler(webbrowser, &handler); + + DCHECK(SUCCEEDED(hr)) << "Failed to attach ourselves to the web browser " << + com::LogHr(hr); + } + + bool is_hash_change = false; + if (handler) { + // Find out if this is a hash change. + CComBSTR prev_url; + handler->GetUrl(&prev_url); + is_hash_change = IsHashChange(url, prev_url); + + // Notify the handler of the current URL. + hr = handler->SetUrl(url); + DCHECK(SUCCEEDED(hr)) << "Failed setting the handler URL " << + com::LogHr(hr); + } + + // SetUrl returns S_FALSE if the URL didn't change. + if (SUCCEEDED(hr) && hr != S_FALSE) { + // And we should only fire events for URL changes on the top frame. + if (web_browser_.IsEqualObject(webbrowser)) { + // We can assume that we are NOT in a complete state when we get + // a navigation complete. + lower_bound_ready_state_ = READYSTATE_UNINITIALIZED; + + // At this point, we should have all the tab windows created, + // including the proper lower bound ready state set just before, + // so setup the tab info if it has not been set yet. + if (!fired_on_created_event_) { + hr = FireOnCreatedEvent(url); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab created event " << + com::LogHr(hr); + } + + // The onUpdate event usually gets fired after the onCreated, + // which is fired from FireOnCreatedEvent above. + DCHECK(tab_window_ != NULL); + hr = tab_events_funnel().OnUpdated(tab_window_, url, + lower_bound_ready_state_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab updated event " << + com::LogHr(hr); + + // If this is a hash change, we manually fire the OnUpdated for the + // complete ready state as we don't receive ready state notifications + // for hash changes. + if (is_hash_change) { + hr = tab_events_funnel().OnUpdated(tab_window_, url, + READYSTATE_COMPLETE); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab updated event " << + com::LogHr(hr); + } + } + } +} + +HRESULT BrowserHelperObject::CreateFrameEventHandler( + IWebBrowser2* browser, IWebBrowser2* parent_browser, + IFrameEventHandler** handler) { + return FrameEventHandler::CreateInitializedIID( + browser, parent_browser, this, IID_IFrameEventHandler, handler); +} + +HRESULT BrowserHelperObject::AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + DCHECK(browser); + DCHECK(handler); + // Get the identity unknown of the browser. + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + std::pair<BrowserHandlerMap::iterator, bool> inserted = + browsers_.insert(std::make_pair(browser_identity, handler)); + // We shouldn't have a previous entry for any inserted browser. + // map::insert().second is true iff an item was inserted. + DCHECK(inserted.second); + + // Now try and find a parent event handler for this browser. + // If we have a parent browser, locate its handler + // and notify it of the association. + if (parent_browser) { + CComPtr<IFrameEventHandler> parent_handler; + hr = GetBrowserHandler(parent_browser, &parent_handler); + + // Notify the parent of its new underling. + if (parent_handler != NULL) { + hr = parent_handler->AddSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "AddSubHandler()" << com::LogHr(hr); + } else { + LOG(INFO) << "Received an orphan handler: " << std::hex << handler << + com::LogHr(hr); + // Lets see if we can find an ancestor up the chain of parent browsers + // that we could connect to our existing hierarchy of handlers. + CComQIPtr<IWebBrowser2> grand_parent_browser; + hr = GetParentBrowser(parent_browser, &grand_parent_browser); + if (FAILED(hr)) + LOG(WARNING) << "Failed to get parent browser " << com::LogHr(hr); + bool valid_grand_parent = (grand_parent_browser != NULL && + !grand_parent_browser.IsEqualObject(parent_browser)); + DCHECK(valid_grand_parent) << + "Orphan handler without a valid grand parent!"; + LOG_IF(ERROR, !valid_grand_parent) << "Orphan handler: " << std::hex << + handler << ", with parent browser: " << std::hex << parent_browser; + if (grand_parent_browser != NULL && + !grand_parent_browser.IsEqualObject(parent_browser)) { + DCHECK(!web_browser_.IsEqualObject(parent_browser)); + // We have a grand parent IWebBrowser2, so create a handler for the + // parent we were given that doesn't have a handler already. + CComBSTR parent_browser_url; + parent_browser->get_LocationURL(&parent_browser_url); + DLOG(INFO) << "Creating handler for parent browser: " << std::hex << + parent_browser << ", at URL: " << parent_browser_url; + hr = CreateFrameEventHandler(parent_browser, grand_parent_browser, + &parent_handler); + // If we have a handler for the child, we must be able to create one for + // the parent... And CreateFrameEventHandler should have attached it + // to the parent by calling us again via IFrameEventHandler::Init... + DCHECK(SUCCEEDED(hr) && parent_handler != NULL) << com::LogHr(hr); + if (FAILED(hr) || parent_handler == NULL) + return hr; + + // When we create a handler, we must set its URL. + hr = parent_handler->SetUrl(parent_browser_url); + DCHECK(SUCCEEDED(hr)) << "Handler::SetUrl()" << com::LogHr(hr); + if (FAILED(hr)) + return hr; + + // And now that we have a fully created parent handler, we can add + // the handler that looked orphan, to its newly created parent. + hr = parent_handler->AddSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "AddSubHandler()" << com::LogHr(hr); + } else { + // No grand parent for the orphan handler? + return E_UNEXPECTED; + } + } + } + + if (inserted.second) + return S_OK; + else + return E_FAIL; +} + +HRESULT BrowserHelperObject::DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown of the browser. + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + // If we have a parent browser, locate its handler + // and notify it of the disassociation. + if (parent_browser) { + CComPtr<IFrameEventHandler> parent_handler; + + hr = GetBrowserHandler(parent_browser, &parent_handler); + LOG_IF(WARNING, FAILED(hr) || parent_handler == NULL) << "No Parent " << + "Handler" << com::LogHr(hr); + + // Notify the parent of its underling removal. + if (parent_handler != NULL) { + hr = parent_handler->RemoveSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "RemoveSubHandler" << com::LogHr(hr); + } + } + + BrowserHandlerMap::iterator it = browsers_.find(browser_identity); + DCHECK(it != browsers_.end()); + if (it == browsers_.end()) + return E_FAIL; + + browsers_.erase(it); + return S_OK; +} + +HRESULT BrowserHelperObject::GetTopLevelBrowser(IWebBrowser2** browser) { + DCHECK(browser != NULL); + return web_browser_.CopyTo(browser); +} + +HRESULT BrowserHelperObject::OnReadyStateChanged(READYSTATE ready_state) { + // We make sure to always keep the lowest ready state of all the handlers + // and only fire an event when we get from not completed to completed or + // vice versa. + READYSTATE new_lowest_ready_state = READYSTATE_COMPLETE; + BrowserHandlerMap::const_iterator it = browsers_.begin(); + for (; it != browsers_.end(); ++it) { + DCHECK(it->second.m_T != NULL); + READYSTATE this_ready_state = it->second.m_T->GetReadyState(); + if (this_ready_state < new_lowest_ready_state) { + new_lowest_ready_state = this_ready_state; + } + } + + return HandleReadyStateChanged(lower_bound_ready_state_, + new_lowest_ready_state); +} + +HRESULT BrowserHelperObject::GetReadyState(READYSTATE* ready_state) { + DCHECK(ready_state != NULL); + if (ready_state != NULL) { + *ready_state = lower_bound_ready_state_; + return S_OK; + } else { + return E_POINTER; + } +} + +HRESULT BrowserHelperObject::GetExtensionId(std::wstring* extension_id) { + *extension_id = extension_id_; + return S_OK; +} + +HRESULT BrowserHelperObject::GetExtensionPath(std::wstring* extension_path) { + *extension_path = extension_base_dir_; + return S_OK; +} + +HRESULT BrowserHelperObject::GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) { + GetUnknown()->AddRef(); + *messaging_provider = this; + return S_OK; +} + +HRESULT BrowserHelperObject::InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) { + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::InsertCode, + code, file, all_frames, type)); + return S_OK; + } + + // If all_frames is false, we execute only in the top level frame. Otherwise + // we do the top level frame as well as all the inner frames. + if (all_frames) { + // Make a copy of the browser handler map since it could potentially be + // modified in the loop. + BrowserHandlerMap browsers_copy(browsers_.begin(), browsers_.end()); + BrowserHandlerMap::const_iterator it = browsers_copy.begin(); + for (; it != browsers_copy.end(); ++it) { + DCHECK(it->second.m_T != NULL); + if (it->second.m_T != NULL) { + HRESULT hr = it->second.m_T->InsertCode(code, file, type); + DCHECK(SUCCEEDED(hr)) << "IFrameEventHandler::InsertCode()" << + com::LogHr(hr); + } + } + } else if (web_browser_ != NULL) { + CComPtr<IFrameEventHandler> handler; + HRESULT hr = GetBrowserHandler(web_browser_, &handler); + DCHECK(SUCCEEDED(hr) && handler != NULL) << com::LogHr(hr); + + if (handler != NULL) { + hr = handler->InsertCode(code, file, type); + // TODO(joi@chromium.org) We don't DCHECK for now, because Chrome may have + // multiple extensions loaded whereas CEEE only knows about a single + // extension. Clean this up once we support multiple extensions. + } + } + + return S_OK; +} + +HRESULT BrowserHelperObject::HandleReadyStateChanged(READYSTATE old_state, + READYSTATE new_state) { + if (old_state == new_state) + return S_OK; + + // Remember the new lowest ready state as our current one. + lower_bound_ready_state_ = new_state; + + if (old_state == READYSTATE_COMPLETE || new_state == READYSTATE_COMPLETE) { + // The new ready state got us to or away from complete, so fire the event. + DCHECK(tab_window_ != NULL); + return tab_events_funnel().OnUpdated(tab_window_, NULL, new_state); + } + return S_OK; +} + +HRESULT BrowserHelperObject::GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) { + return librarian_.GetMatchingUserScriptsCssContent(url, require_all_frames, + css_content); +} + +HRESULT BrowserHelperObject::GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list) { + return librarian_.GetMatchingUserScriptsJsContent(url, location, + require_all_frames, + js_file_list); +} + +HRESULT BrowserHelperObject::GetBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler) { + DCHECK(webbrowser != NULL); + DCHECK(handler != NULL && *handler == NULL); + CComPtr<IUnknown> browser_identity; + HRESULT hr = webbrowser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << com::LogHr(hr); + + BrowserHandlerMap::iterator found(browsers_.find(browser_identity)); + if (found != browsers_.end()) { + found->second.m_T.CopyTo(handler); + return S_OK; + } + + return E_FAIL; +} + +void BrowserHelperObject::LoadManifestFile(const std::wstring& base_dir) { + // TODO(siggi@chromium.org): Generalize this to the possibility of + // multiple extensions. + FilePath extension_path(base_dir); + if (extension_path.empty()) { + // expected case if no extensions registered/found + return; + } + + extension_base_dir_ = base_dir; + + ExtensionManifest manifest; + if (SUCCEEDED(manifest.ReadManifestFile(extension_path, true))) { + extension_id_ = UTF8ToWide(manifest.extension_id()); + librarian_.AddUserScripts(manifest.content_scripts()); + } +} + +void BrowserHelperObject::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +LRESULT BrowserHelperObject::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + return 0; +} + +bool BrowserHelperObject::IsHashChange(BSTR url1, BSTR url2) { + if (::SysStringLen(url1) == 0 || ::SysStringLen(url2) == 0) { + return false; + } + + GURL gurl1(url1); + GURL gurl2(url2); + + // The entire URL should be the same except for the hash. + // We could compare gurl1.ref() to gurl2.ref() on the last step, but this + // doesn't differentiate between URLs like http://a/ and http://a/#. + return gurl1.scheme() == gurl2.scheme() && + gurl1.username() == gurl2.username() && + gurl1.password() == gurl2.password() && + gurl1.host() == gurl2.host() && + gurl1.port() == gurl2.port() && + gurl1.path() == gurl2.path() && + gurl1.query() == gurl2.query() && + gurl1 != gurl2; +} + +void BrowserHelperObject::RegisterSink(Sink* sink) { + DCHECK(thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + // Although the registration logic guarantees that the same sink won't be + // registered twice, we prefer to use std::vector rather than std::set. + // Using std::vector, we could keep "first-registered-first-notified". + // + // With std::set, however, the notifying order can only be decided at + // run-time. Moreover, in different runs, the notifying order may be + // different, since the value of the pointer to each sink is changing. The may + // cause unstable behavior and hard-to-debug issues. + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter == sinks_.end()) + sinks_.push_back(sink); +} + +void BrowserHelperObject::UnregisterSink(Sink* sink) { + DCHECK(thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter != sinks_.end()) + sinks_.erase(iter); +} diff --git a/ceee/ie/plugin/bho/browser_helper_object.h b/ceee/ie/plugin/bho/browser_helper_object.h new file mode 100644 index 0000000..bf4afb9 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.h @@ -0,0 +1,345 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#ifndef CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ +#define CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <mshtml.h> // Needed for exdisp.h +#include <exdisp.h> +#include <exdispid.h> +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/scoped_ptr.h" +#include "base/task.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "ceee/ie/common/chrome_frame_host.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/bho/extension_port_manager.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/web_progress_notifier.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/toolband/resource.h" +#include "broker_lib.h" // NOLINT +#include "toolband.h" // NOLINT + +// Implementation of an IE browser helper object. +class ATL_NO_VTABLE BrowserHelperObject + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<BrowserHelperObject, &CLSID_BrowserHelperObject>, + public IObjectWithSiteImpl<BrowserHelperObject>, + public IDispEventSimpleImpl<0, + BrowserHelperObject, + &DIID_DWebBrowserEvents2>, + public IFrameEventHandlerHost, + public IExtensionPortMessagingProvider, + public IChromeFrameHostEvents, + public ToolBandVisibility, + public WebBrowserEventsSource { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_BROWSERHELPEROBJECT) + DECLARE_NOT_AGGREGATABLE(BrowserHelperObject) + + BEGIN_COM_MAP(BrowserHelperObject) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandlerHost, IFrameEventHandlerHost) + END_COM_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + BEGIN_SINK_MAP(BrowserHelperObject) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2, + OnBeforeNavigate2, + &handler_type_idispatch_5variantptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, + OnDocumentComplete, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, + OnNavigateComplete2, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATEERROR, + OnNavigateError, + &handler_type_idispatch_3variantptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NEWWINDOW2, + OnNewWindow2, &handler_type_idispatchptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NEWWINDOW3, + OnNewWindow3, + &handler_type_idispatchptr_boolptr_dword_2bstr_) + END_SINK_MAP() + + BrowserHelperObject(); + ~BrowserHelperObject(); + + HRESULT FinalConstruct(); + void FinalRelease(); + + // @name IObjectWithSite override. + STDMETHOD(SetSite)(IUnknown* site); + + // @name IExtensionPortMessagingProvider implementation + // @{ + virtual void CloseAll(IContentScriptNativeApi* instance); + virtual HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie); + virtual HRESULT PostMessage(int port_id, const std::string& message); + // @} + + // @name IChromeFrameHostEvents implementation + virtual HRESULT OnCfReadyStateChanged(LONG state); + virtual HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target); + virtual HRESULT OnCfExtensionReady(BSTR path, int response); + virtual HRESULT OnCfGetEnabledExtensionsComplete( + SAFEARRAY* tab_delimited_paths); + virtual HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled); + virtual HRESULT OnCfChannelError(); + + // @name WebBrowser event handlers + // @{ + STDMETHOD_(void, OnBeforeNavigate2)(IDispatch* webbrowser_disp, VARIANT* url, + VARIANT* flags, + VARIANT* target_frame_name, + VARIANT* post_data, VARIANT* headers, + VARIANT_BOOL* cancel); + STDMETHOD_(void, OnDocumentComplete)(IDispatch* webbrowser_disp, + VARIANT* url); + STDMETHOD_(void, OnNavigateComplete2)(IDispatch* webbrowser_disp, + VARIANT* url); + STDMETHOD_(void, OnNavigateError)(IDispatch* webbrowser_disp, VARIANT* url, + VARIANT* target_frame_name, + VARIANT* status_code, VARIANT_BOOL* cancel); + STDMETHOD_(void, OnNewWindow2)(IDispatch** webbrowser_disp, + VARIANT_BOOL* cancel); + STDMETHOD_(void, OnNewWindow3)(IDispatch** webbrowser_disp, + VARIANT_BOOL* cancel, DWORD flags, + BSTR url_context, BSTR url); + // @} + + // @name IFrameEventHandlerHost + // @{ + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler); + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler); + virtual HRESULT GetTopLevelBrowser(IWebBrowser2** browser); + virtual HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content); + virtual HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list); + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state); + virtual HRESULT GetReadyState(READYSTATE* ready_state); + virtual HRESULT GetExtensionId(std::wstring* extension_id); + virtual HRESULT GetExtensionPath(std::wstring* extension_path); + virtual HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider); + virtual HRESULT InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type); + // @} + + // @name WebBrowserEventsSource + // @{ + // Both RegisterSink and UnregisterSink are supposed to be called from the + // main browser thread of the tab to which this BHO is attached. Sinks will + // receive notifications on the same thread. + virtual void RegisterSink(Sink* sink); + virtual void UnregisterSink(Sink* sink); + // @} + + protected: + // Finds the handler attached to webbrowser. + // @returns S_OK if handler is found. + HRESULT GetBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler); + + virtual void HandleNavigateComplete(IWebBrowser2* webbrowser, BSTR url); + virtual HRESULT HandleReadyStateChanged(READYSTATE old_state, + READYSTATE new_state); + + // Unit testing seems to create the frame event handler. + virtual HRESULT CreateFrameEventHandler(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler); + + // Unit testing seems to get the parent of a browser. + virtual HRESULT GetParentBrowser(IWebBrowser2* browser, + IWebBrowser2** parent_browser); + + // Unit testing seems to create the broker registrar. + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker); + + // Unit testing seems to create an executor. + virtual HRESULT CreateExecutor(IUnknown** executor); + + // Unit testing seems to create a WebProgressNotifier instance. + virtual WebProgressNotifier* CreateWebProgressNotifier(); + + // Initializes the BHO to the given site. + // Called from SetSite. + HRESULT Initialize(IUnknown* site); + + // Tears down an initialized bho. + // Called from SetSite. + HRESULT TearDown(); + + // Creates and initializes the chrome frame host. + HRESULT InitializeChromeFrameHost(); + + // Fetch and remembers the tab window we are attached to. + // Virtual for testing purposes. + virtual HRESULT GetTabWindow(IServiceProvider* service_provider); + + // Connect for notifications. + HRESULT ConnectSinks(IServiceProvider* service_provider); + + // Isolate the creation of the host so we can overload it to mock + // the Chrome Frame Host in our tests. + virtual HRESULT CreateChromeFrameHost(); + + // Accessor so that we can mock it in unit tests. + virtual TabEventsFunnel& tab_events_funnel() { return tab_events_funnel_; } + + // Fires the tab.onCreated event via the tab event funnel. + virtual HRESULT FireOnCreatedEvent(BSTR url); + + // Fires the tab.onRemoved event via the tab event funnel. + virtual HRESULT FireOnRemovedEvent(); + + // Fires the private message to unmap a tab to its BHO. + virtual HRESULT FireOnUnmappedEvent(); + + // Loads our manifest and initialize our librarian. + virtual void LoadManifestFile(const std::wstring& base_dir); + + // Called when we know the base directory of our extension. + void StartExtension(const wchar_t* base_dir); + + // Our ToolBandVisibility window maintains a refcount on us for the duration + // of its lifetime. The self-reference is managed with these two methods. + virtual void OnFinalMessage(HWND window); + virtual LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + // Compares two URLs and returns whether they represent a hash change. + virtual bool IsHashChange(BSTR url1, BSTR url2); + + // Ensure that the tab ID is correct. On the first time it's set, it will + // call all deferred methods added to deferred_tab_id_call_. + // This method should be called by every method that send a message or use + // the tab event funnel, as they need the tab_id to be mapped. + // If this method returns false, the caller should defer itself using the + // deferred_tab_id_call_ list. + virtual bool EnsureTabId(); + + // Returns true if the browser interface passed in contains a full tab + // chrome frame. + virtual bool BrowserContainsChromeFrame(IWebBrowser2* browser); + + // Attach ourselves and the event handler to the browser, and launches the + // right events when going to and from a Full Tab Chrome Frame. + virtual HRESULT AttachBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler); + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_5variantptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + static _ATL_FUNC_INFO handler_type_idispatch_3variantptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatchptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatchptr_boolptr_dword_2bstr_; + + // The top-level web browser (window) we're attached to. NULL before SetSite. + CComPtr<IWebBrowser2> web_browser_; + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<IChromeFrameHost> chrome_frame_host_; + + // The Broker Registrar we use to un/register executors for our thread. + CComPtr<ICeeeBrokerRegistrar> broker_registrar_; + + // We keep a reference to the executor we registered so that we can + // manually disconnect it, so it doesn't get called while we unregister it. + CComPtr<IUnknown> executor_; + + // Maintains a map from browser (top-level and sub-browsers) to the + // attached FrameEventHandlers. + typedef std::map<CAdapt<CComPtr<IUnknown> >, + CAdapt<CComPtr<IFrameEventHandler> > > BrowserHandlerMap; + BrowserHandlerMap browsers_; + + // Initialized by LoadManifestFile() at + // OnCfGetEnabledExtensionsComplete-time. Valid from that point forward. + UserScriptsLibrarian librarian_; + + // Filesystem path to the .crx we will install (or have installed), or the + // empty string, or (if not ending in .crx) the path to an exploded extension + // directory to load (or which we have loaded). + std::wstring extension_path_; + + // The extension we're associated with. Set at + // OnCfGetEnabledExtensionsComplete-time. + // TODO(siggi@chromium.org): Generalize this to multiple extensions. + std::wstring extension_id_; + + // The base directory of the extension we're associated with. + // Set at OnCfGetEnabledExtensionsComplete time. + std::wstring extension_base_dir_; + + // Extension port messaging and management is delegated to this. + ExtensionPortManager extension_port_manager_; + + // Used to dispatch tab events back to Chrome. + TabEventsFunnel tab_events_funnel_; + + // Remember the tab window handle so that we can use it. + HWND tab_window_; + + // Remember the tab id so we can pass it to the underlying Chrome. + int tab_id_; + + // Makes sure we fire the onCreated event only once. + bool fired_on_created_event_; + + // True if we found no enabled extensions and tried to install one. + bool already_tried_installing_; + + // The last known ready state lower bound, so that we decide when to fire a + // tabs.onUpdated event, which only when we go from all frames completed to + // at least one of them not completed, and vice versa (from incomplete to + // fully completely completed :-)... + READYSTATE lower_bound_ready_state_; + + // Consumers of WebBrowser events. + std::vector<Sink*> sinks_; + + // Used to generate and fire Web progress notifications. + scoped_ptr<WebProgressNotifier> web_progress_notifier_; + + // True if the user is running IE7 or later. + bool ie7_or_later_; + + // The thread we are running into. + DWORD thread_id_; + + // Indicates if the current shown page is a full-tab chrome frame. + bool full_tab_chrome_frame_; + + private: + // Used during initialization to get the tab information from Chrome and + // register ourselves with the broker. + HRESULT RegisterTabInfo(); + + typedef std::deque<Task*> DeferredCallListType; + DeferredCallListType deferred_tab_id_call_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ diff --git a/ceee/ie/plugin/bho/browser_helper_object.rgs b/ceee/ie/plugin/bho/browser_helper_object.rgs new file mode 100644 index 0000000..f7331a6 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.rgs @@ -0,0 +1,28 @@ +HKCR { +NoRemove CLSID { + ForceRemove {E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC} = s 'Google Chrome Extensions Execution Environment Helper' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + 'TypeLib' = s '{7C09079D-F9CB-4E9E-9293-D224B071D8BA}' + } + } +} + +HKLM { + NoRemove SOFTWARE { + NoRemove Microsoft { + NoRemove Windows { + NoRemove CurrentVersion { + NoRemove Explorer { + NoRemove 'Browser Helper Objects' { + ForceRemove '{E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC}' = s 'Google Chrome Extensions Execution Environment Helper' { + val 'NoExplorer' = d '1' + } + } + } + } + } + } + } +} diff --git a/ceee/ie/plugin/bho/browser_helper_object_unittest.cc b/ceee/ie/plugin/bho/browser_helper_object_unittest.cc new file mode 100644 index 0000000..285a733 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object_unittest.cc @@ -0,0 +1,743 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/browser_helper_object.h" + +#include <exdisp.h> +#include <shlguid.h> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_browser_and_friends.h" +#include "ceee/ie/testing/mock_chrome_frame_host.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::_; +using testing::CopyBSTRToArgument; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::GetConnectionCount; +using testing::InstanceCountMixin; +using testing::MockChromeFrameHost; +using testing::MockDispatchEx; +using testing::MockIOleWindow; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::TestBrowser; +using testing::TestBrowserSite; + +// Tab Ids passed to the API +const int kGoodTabId = 1; +const CeeeWindowHandle kGoodTabHandle = kGoodTabId + 1; +const HWND kGoodTab = (HWND)kGoodTabHandle; + +class TestFrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<StrictMock<TestFrameEventHandler> >, + public InstanceCountMixin<TestFrameEventHandler>, + public IFrameEventHandler { + public: + BEGIN_COM_MAP(TestFrameEventHandler) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandler, IFrameEventHandler) + END_COM_MAP() + + HRESULT Initialize(TestFrameEventHandler** self) { + *self = this; + return S_OK; + } + + MOCK_METHOD1(GetUrl, void(BSTR* url)); + MOCK_METHOD1(SetUrl, HRESULT(BSTR url)); + // no need to mock it yet and it is called from a DCHECK so... + virtual READYSTATE GetReadyState() { return READYSTATE_COMPLETE; } + MOCK_METHOD1(AddSubHandler, HRESULT(IFrameEventHandler* handler)); + MOCK_METHOD1(RemoveSubHandler, HRESULT(IFrameEventHandler* handler)); + MOCK_METHOD0(TearDown, void()); + MOCK_METHOD3(InsertCode, HRESULT(BSTR code, BSTR file, + CeeeTabCodeType type)); + MOCK_METHOD0(RedoDoneInjections, void()); +}; + +class TestingBrowserHelperObject + : public BrowserHelperObject, + public InstanceCountMixin<TestingBrowserHelperObject>, + public InitializingCoClass<TestingBrowserHelperObject> { + public: + HRESULT Initialize(TestingBrowserHelperObject** self) { + // Make sure this is done early so we can mock it. + EXPECT_HRESULT_SUCCEEDED(MockChromeFrameHost::CreateInitializedIID( + &mock_chrome_frame_host_, IID_IChromeFrameHost, + &mock_chrome_frame_host_keeper_)); + chrome_frame_host_ = mock_chrome_frame_host_; + *self = this; + return S_OK; + } + + virtual TabEventsFunnel& tab_events_funnel() { + return mock_tab_events_funnel_; + } + + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker) { + broker_keeper_.CopyTo(broker); + return S_OK; + } + + virtual HRESULT CreateExecutor(IUnknown** executor) { + executor_keeper_.CopyTo(executor); + return S_OK; + } + + virtual HRESULT CreateChromeFrameHost() { + EXPECT_TRUE(chrome_frame_host_ != NULL); + return S_OK; + } + + virtual HRESULT GetTabWindow(IServiceProvider* service_provider) { + tab_window_ = reinterpret_cast<HWND>(kGoodTab); + return S_OK; + } + + virtual void SetTabId(int tab_id) { + tab_id_ = tab_id; + } + + virtual WebProgressNotifier* CreateWebProgressNotifier() { + // Without calling Initialize(), the class won't do anything. + return new WebProgressNotifier(); + } + + MOCK_METHOD0(SetupNewTabInfo, bool()); + MOCK_METHOD3(CreateFrameEventHandler, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler)); + MOCK_METHOD1(BrowserContainsChromeFrame, bool(IWebBrowser2* browser)); + + MOCK_METHOD2(IsHashChange, bool(BSTR, BSTR)); + bool CallIsHashChange(BSTR url1, BSTR url2) { + return BrowserHelperObject::IsHashChange(url1, url2); + } + + MOCK_METHOD2(GetParentBrowser, HRESULT(IWebBrowser2*, IWebBrowser2**)); + + // Pulicize + using BrowserHelperObject::HandleNavigateComplete; + + StrictMock<testing::MockTabEventsFunnel> mock_tab_events_funnel_; + MockChromeFrameHost* mock_chrome_frame_host_; + CComPtr<IChromeFrameHost> mock_chrome_frame_host_keeper_; + + testing::MockExecutorIUnknown* executor_; + CComPtr<IUnknown> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; +}; + +class BrowserHelperObjectTest: public testing::Test { + public: + BrowserHelperObjectTest() : bho_(NULL), site_(NULL), browser_(NULL) { + } + ~BrowserHelperObjectTest() { + } + + virtual void SetUp() { + // Create the instance to test. + ASSERT_HRESULT_SUCCEEDED( + TestingBrowserHelperObject::CreateInitialized(&bho_, &bho_with_site_)); + bho_with_site_ = bho_; + + // TODO(mad@chromium.org): Test this method. + EXPECT_CALL(*bho_, SetupNewTabInfo()).WillRepeatedly(Return(true)); + + // We always go beyond Chrome Frame start and event funnel init. + // Create the broker registrar related objects + ASSERT_HRESULT_SUCCEEDED(testing::MockExecutorIUnknown::CreateInitialized( + &bho_->executor_, &bho_->executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED(testing::MockBroker::CreateInitialized( + &bho_->broker_, &bho_->broker_keeper_)); + + // We always go beyond Chrome Frame start, broker reg and event funnel init. + // TODO(mad@chromium.org): Also cover failure cases from those. + ExpectBrokerRegistration(); + ExpectChromeFrameStart(); + + // Assert on successful TearDown. + ExpectBrokerUnregistration(); + ExpectChromeFrameTearDown(); + } + + virtual void TearDown() { + bho_->executor_ = NULL; + bho_->executor_keeper_.Release(); + + bho_->broker_ = NULL; + bho_->broker_keeper_.Release(); + + bho_ = NULL; + bho_with_site_.Release(); + + site_ = NULL; + site_keeper_.Release(); + + browser_ = NULL; + browser_keeper_.Release(); + + handler_ = NULL; + handler_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateSite() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, &site_keeper_)); + } + + void CreateBrowser() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser_, &browser_keeper_)); + + // Fail get_Parent calls for the root. + EXPECT_CALL(*bho_, GetParentBrowser(browser_keeper_.p, NotNull())). + WillRepeatedly(Return(E_NOTIMPL)); + + if (site_) + site_->browser_ = browser_keeper_; + } + + void CreateHandler() { + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler_, IID_IFrameEventHandler, &handler_keeper_)); + } + + bool BhoHasSite() { + // Check whether BHO has a site set. + CComPtr<IUnknown> site; + if (SUCCEEDED(bho_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&site)))) + return true; + if (site != NULL) + return true; + + return false; + } + + void ExpectChromeFrameStart() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetEventSink(_)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetChromeProfileName(_)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), StartChromeFrame()). + WillOnce(Return(S_OK)); + } + + CComBSTR CreateTabInfo(int tab_id) { + std::ostringstream iss; + iss << L"{\"id\":" << tab_id << L"}"; + return CComBSTR(iss.str().c_str()); + } + void ExpectChromeFrameGetSessionId() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), GetSessionId(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(kGoodTabId), Return(S_OK))); + EXPECT_CALL(*(bho_->broker_), SetTabIdForHandle(kGoodTabId, + kGoodTabHandle)).WillOnce(Return(S_OK)); + } + + void ExpectChromeFrameTearDown() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetEventSink(NULL)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), TearDown()). + WillOnce(Return(S_OK)); + } + + void ExpectBrokerRegistration() { + EXPECT_CALL(*bho_->broker_, RegisterTabExecutor(::GetCurrentThreadId(), + bho_->executor_keeper_.p)).WillOnce(Return(S_OK)); + } + + void ExpectBrokerUnregistration() { + EXPECT_CALL(*bho_->broker_, UnregisterExecutor(::GetCurrentThreadId())). + WillOnce(Return(S_OK)); + } + + void ExpectHandleNavigation(TestFrameEventHandler* handler, + bool hash_change) { + EXPECT_CALL(*handler, GetUrl(_)).Times(1). + WillOnce(CopyBSTRToArgument<0>(kUrl2)); + EXPECT_CALL(*bho_, IsHashChange(StrEq(kUrl1), StrEq(kUrl2))). + WillOnce(Return(hash_change)); + // We should get the URL poked at the handler. + EXPECT_CALL(*handler, SetUrl(StrEq(kUrl1))).WillOnce(Return(S_OK)); + } + + void ExpectTopBrowserNavigation(bool hash_change, bool first_call) { + // We also get a tab update notification. + if (first_call) { + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnCreated(_, StrEq(kUrl1), false)).Times(1); + } + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_UNINITIALIZED)).Times(1); + if (hash_change) { + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_COMPLETE)).Times(1); + } + } + + void ExpectFireOnRemovedEvent() { + EXPECT_CALL(bho_->mock_tab_events_funnel_, OnRemoved(_)); + } + + void ExpectFireOnUnmappedEvent() { + EXPECT_CALL(bho_->mock_tab_events_funnel_, OnTabUnmapped(_, _)); + } + + static const wchar_t* kUrl1; + static const wchar_t* kUrl2; + + // Logging quenched for all tests. + testing::LogDisabler no_dchecks_; + + TestingBrowserHelperObject* bho_; + CComPtr<IObjectWithSite> bho_with_site_; + + testing::TestBrowserSite* site_; + CComPtr<IUnknown> site_keeper_; + + TestBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + TestFrameEventHandler* handler_; + CComPtr<IFrameEventHandler> handler_keeper_; +}; + +const wchar_t* BrowserHelperObjectTest::kUrl1 = +L"http://www.google.com/search?q=Google+Buys+Iceland"; +const wchar_t* BrowserHelperObjectTest::kUrl2 = L"http://www.google.com"; + + +// Setting the BHO site with a non-service provider fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNoServiceProviderFails) { + // Create an object that doesn't implement IServiceProvider. + MockDispatchEx* site = NULL; + CComPtr<IUnknown> site_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&site, + &site_keeper)); + // Setting a site that doesn't implement IServiceProvider fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with no browser fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNullBrowserFails) { + CreateSite(); + + // Setting a site with no browser fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with a non-browser fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNonBrowserFails) { + CreateSite(); + + // Endow the site with a non-browser service. + MockDispatchEx* mock_non_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_non_browser, + &site_->browser_)); + // Setting a site with a non-browser fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with a browser that doesn't implement the +// DIID_DWebBrowserEvents2 connection point fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNoEventsFails) { + CreateSite(); + CreateBrowser(); + + // Disable the connection point. + browser_->no_events_ = true; + + // No connection point site fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +TEST_F(BrowserHelperObjectTest, SetSiteWithBrowserSucceeds) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + // Check that the we set up a connection. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site == site_keeper_); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); + + // And check that the connection was severed. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); +} + +TEST_F(BrowserHelperObjectTest, OnNavigateCompleteHandled) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + EXPECT_CALL(*bho_, CreateFrameEventHandler(browser_, NULL, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler_keeper_), + Return(S_OK))); + EXPECT_CALL(*bho_, BrowserContainsChromeFrame(browser_)). + WillOnce(Return(false)); + ExpectHandleNavigation(handler_, true); + ExpectTopBrowserNavigation(true, true); + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, RenavigationNotifiesUrl) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Make as if a handler has been attached to the browser. + ASSERT_HRESULT_SUCCEEDED( + bho_->AttachBrowser(browser_, NULL, handler_keeper_)); + + EXPECT_CALL(*handler_, GetUrl(_)).Times(1). + WillOnce(CopyBSTRToArgument<0>(kUrl2)); + EXPECT_CALL(*bho_, IsHashChange(StrEq(kUrl1), StrEq(kUrl2))). + WillOnce(Return(false)); + // We should get the "new" URL poked at the handler. + EXPECT_CALL(*handler_, SetUrl(StrEq(kUrl1))).Times(1); + + // We also get a tab update notification. + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnCreated(_, StrEq(kUrl1), false)).Times(1); + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_UNINITIALIZED)).Times(1); + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +// Test that we filter OnNavigateComplete invocations with +// non-IWebBrowser2 or non BSTR arguments. +TEST_F(BrowserHelperObjectTest, OnNavigateCompleteUnhandled) { + CreateSite(); + CreateBrowser(); + ExpectChromeFrameGetSessionId(); + + // Create an object that doesn't implement IWebBrowser2. + MockDispatchEx* non_browser = NULL; + CComPtr<IDispatch> non_browser_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized( + &non_browser, &non_browser_keeper)); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // HandleNavigateComplete should not be called by the invocations below. + EXPECT_CALL(*bho_, CreateFrameEventHandler(_, _, _)).Times(0); + + // Non-browser target. + browser_->FireOnNavigateComplete(non_browser, &CComVariant(kUrl1)); + + // Non-BSTR url parameter. + browser_->FireOnNavigateComplete(browser_, &CComVariant(non_browser)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, HandleNavigateComplete) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + EXPECT_CALL(*bho_, CreateFrameEventHandler(browser_, NULL, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler_keeper_), + Return(S_OK))); + EXPECT_CALL(*bho_, BrowserContainsChromeFrame(browser_)). + WillOnce(Return(false)); + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, true); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + // Now handle the case without the creation of a handler. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, false); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + // Now navigate a sub-frame. + TestBrowser* browser2; + CComPtr<IWebBrowser2> browser2_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser2, &browser2_keeper)); + EXPECT_CALL(*bho_, GetParentBrowser(browser2, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<1>(browser_keeper_), + Return(S_OK))); + TestFrameEventHandler* handler2; + CComPtr<IFrameEventHandler> handler2_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler2, IID_IFrameEventHandler, &handler2_keeper)); + + EXPECT_CALL(*bho_, + CreateFrameEventHandler(browser2, browser_, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler2_keeper), + Return(S_OK))); + + ExpectHandleNavigation(handler2, false); + bho_->HandleNavigateComplete(browser2, CComBSTR(kUrl1)); + EXPECT_CALL(*handler_, AddSubHandler(handler2)). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser2, browser_, handler2)); + + // Now, navigating the top browser again. + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, false); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, AttachOrphanedBrowser) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Attach the root. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Now attach an apparent orphan which is actually the grand child of an + // existing frame which parent wasn't seen yet. + TestBrowser* browser3; + CComPtr<IWebBrowser2> browser3_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser3, &browser3_keeper)); + + TestFrameEventHandler* handler3; + CComPtr<IFrameEventHandler> handler_keeper_3; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler3, IID_IFrameEventHandler, &handler_keeper_3)); + + TestBrowser* browser3_parent; + CComPtr<IWebBrowser2> browser3_parent_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser3_parent, + &browser3_parent_keeper)); + + TestFrameEventHandler* handler3_parent; + CComPtr<IFrameEventHandler> handler3_parent_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler3_parent, IID_IFrameEventHandler, &handler3_parent_keeper)); + + EXPECT_CALL(*bho_, GetParentBrowser(browser3_parent, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<1>(browser_keeper_.p), + Return(S_OK))); + EXPECT_CALL(*browser3_parent, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_CALL(*bho_, + CreateFrameEventHandler(browser3_parent, browser_keeper_.p, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler3_parent_keeper), + Return(S_OK))); + EXPECT_CALL(*handler3_parent, SetUrl(StrEq(kUrl1))).WillOnce(Return(S_OK)); + EXPECT_CALL(*handler3_parent, AddSubHandler(handler3)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser3, browser3_parent, + handler3)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, IFrameEventHandlerHost) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Detaching a non-attached browser should fail. + EXPECT_HRESULT_FAILED(bho_->DetachBrowser(browser_, NULL, handler_)); + + // First-time attach should succeed. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + // Second attach should fail. + EXPECT_HRESULT_FAILED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Subsequent detach should succeed. + EXPECT_HRESULT_SUCCEEDED(bho_->DetachBrowser(browser_, NULL, handler_)); + // But not twice. + EXPECT_HRESULT_FAILED(bho_->DetachBrowser(browser_, NULL, handler_)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); + + // TODO(siggi@chromium.org): test hierarchial attach/detach/TearDown. +} + +TEST_F(BrowserHelperObjectTest, InsertCode) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(BhoHasSite()); + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + CComBSTR code; + CComBSTR file; + EXPECT_CALL(*handler_, InsertCode(_, _, kCeeeTabCodeTypeCss)) + .WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED(bho_->InsertCode(code, file, FALSE, + kCeeeTabCodeTypeCss)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, InsertCodeAllFrames) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(BhoHasSite()); + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Add a second browser to the BHO and make sure that both get called. + TestBrowser* browser2; + CComPtr<IWebBrowser2> browser_keeper_2; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser2, &browser_keeper_2)); + + TestFrameEventHandler* handler2; + CComPtr<IFrameEventHandler> handler_keeper_2; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler2, IID_IFrameEventHandler, &handler_keeper_2)); + + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_keeper_2, + NULL, + handler_keeper_2)); + + CComBSTR code; + CComBSTR file; + EXPECT_CALL(*handler_, InsertCode(_, _, kCeeeTabCodeTypeJs)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*handler2, InsertCode(_, _, kCeeeTabCodeTypeJs)) + .WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED(bho_->InsertCode(code, file, TRUE, + kCeeeTabCodeTypeJs)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, IsHashChange) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + CComBSTR url1("http://www.google.com/"); + CComBSTR url2("http://www.google.com/#"); + CComBSTR url3("http://www.google.com/#test"); + CComBSTR url4("http://www.google.com/#bingo"); + CComBSTR url5("http://www.bingo.com/"); + CComBSTR url6("http://www.twitter.com/#test"); + CComBSTR empty; + + // Passing cases. + EXPECT_TRUE(bho_->CallIsHashChange(url1, url2)); + EXPECT_TRUE(bho_->CallIsHashChange(url1, url3)); + EXPECT_TRUE(bho_->CallIsHashChange(url2, url3)); + EXPECT_TRUE(bho_->CallIsHashChange(url3, url4)); + + // Failing cases. + EXPECT_FALSE(bho_->CallIsHashChange(url1, empty)); + EXPECT_FALSE(bho_->CallIsHashChange(empty, url1)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url1)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url5)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url6)); + EXPECT_FALSE(bho_->CallIsHashChange(url3, url6)); + EXPECT_FALSE(bho_->CallIsHashChange(url5, url6)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/cookie_accountant.cc b/ceee/ie/plugin/bho/cookie_accountant.cc new file mode 100644 index 0000000..7453bb9 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant.cc @@ -0,0 +1,226 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CookieAccountant implementation. +#include "ceee/ie/plugin/bho/cookie_accountant.h" + +#include <atlbase.h> +#include <wininet.h> + +#include <set> + +#include "base/format_macros.h" // For PRIu64. +#include "base/logging.h" +#include "base/process_util.h" +#include "base/string_tokenizer.h" +#include "base/time.h" +#include "base/utf_string_conversions.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/common/ceee_module_util.h" + +namespace { + +static const char kSetCookieHeaderName[] = "Set-Cookie:"; +static const char kHttpResponseHeaderDelimiter[] = "\n"; +static const wchar_t kMsHtmlModuleName[] = L"mshtml.dll"; +static const char kWinInetModuleName[] = "wininet.dll"; +static const char kInternetSetCookieExAFunctionName[] = "InternetSetCookieExA"; +static const char kInternetSetCookieExWFunctionName[] = "InternetSetCookieExW"; + +} // namespace + +CookieAccountant* CookieAccountant::singleton_instance_ = NULL; + +CookieAccountant::~CookieAccountant() { + if (internet_set_cookie_ex_a_patch_.is_patched()) + internet_set_cookie_ex_a_patch_.Unpatch(); + if (internet_set_cookie_ex_w_patch_.is_patched()) + internet_set_cookie_ex_w_patch_.Unpatch(); +} + +ProductionCookieAccountant::ProductionCookieAccountant() { +} + +CookieAccountant* CookieAccountant::GetInstance() { + // Unit tests can set singleton_instance_ directly. + if (singleton_instance_ == NULL) + singleton_instance_ = ProductionCookieAccountant::get(); + return singleton_instance_; +} + +DWORD WINAPI CookieAccountant::InternetSetCookieExAPatch( + LPCSTR url, LPCSTR cookie_name, LPCSTR cookie_data, + DWORD flags, DWORD_PTR reserved) { + base::Time current_time = base::Time::Now(); + DWORD cookie_state = ::InternetSetCookieExA(url, cookie_name, cookie_data, + flags, reserved); + CookieAccountant::GetInstance()->RecordCookie(url, cookie_data, + current_time); + return cookie_state; +} + +DWORD WINAPI CookieAccountant::InternetSetCookieExWPatch( + LPCWSTR url, LPCWSTR cookie_name, LPCWSTR cookie_data, + DWORD flags, DWORD_PTR reserved) { + base::Time current_time = base::Time::Now(); + DWORD cookie_state = ::InternetSetCookieExW(url, cookie_name, cookie_data, + flags, reserved); + CookieAccountant::GetInstance()->RecordCookie( + std::string(CW2A(url)), std::string(CW2A(cookie_data)), current_time); + return cookie_state; +} + +class CurrentProcessFilter : public base::ProcessFilter { + public: + CurrentProcessFilter() : current_process_id_(base::GetCurrentProcId()) { + } + + virtual bool Includes(const base::ProcessEntry& entry) const { + return entry.pid() == current_process_id_; + } + + private: + base::ProcessId current_process_id_; + + DISALLOW_COPY_AND_ASSIGN(CurrentProcessFilter); +}; + +// TODO(cindylau@chromium.org): Make this function more robust. +void CookieAccountant::RecordCookie( + const std::string& url, const std::string& cookie_data, + const base::Time& current_time) { + cookie_api::CookieInfo cookie; + std::string cookie_data_string = cookie_data; + net::CookieMonster::ParsedCookie parsed_cookie(cookie_data_string); + DCHECK(parsed_cookie.IsValid()); + if (!parsed_cookie.IsValid()) + return; + + // Fill the cookie info from the parsed cookie. + // TODO(cindylau@chromium.org): Add a helper function to convert an + // std::string to a BSTR. + cookie.name = ::SysAllocString(ASCIIToWide(parsed_cookie.Name()).c_str()); + cookie.value = ::SysAllocString(ASCIIToWide(parsed_cookie.Value()).c_str()); + SetScriptCookieDomain(parsed_cookie, &cookie); + SetScriptCookiePath(parsed_cookie, &cookie); + cookie.secure = parsed_cookie.IsSecure() ? TRUE : FALSE; + cookie.http_only = parsed_cookie.IsHttpOnly() ? TRUE : FALSE; + SetScriptCookieExpirationDate(parsed_cookie, current_time, &cookie); + SetScriptCookieStoreId(&cookie); + + // Send the cookie event to the broker. + // TODO(cindylau@chromium.org): Set the removed parameter properly. + cookie_events_funnel().OnChanged(false, cookie); +} + +void CookieAccountant::SetScriptCookieDomain( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie) { + if (parsed_cookie.HasDomain()) { + cookie->domain = ::SysAllocString( + ASCIIToWide(parsed_cookie.Domain()).c_str()); + cookie->host_only = FALSE; + } else { + // TODO(cindylau@chromium.org): If the domain is not provided, get + // it from the URL. + cookie->host_only = TRUE; + } +} + +void CookieAccountant::SetScriptCookiePath( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie) { + // TODO(cindylau@chromium.org): If the path is not provided, get it + // from the URL. + if (parsed_cookie.HasPath()) + cookie->path = ::SysAllocString(ASCIIToWide(parsed_cookie.Path()).c_str()); +} + +void CookieAccountant::SetScriptCookieExpirationDate( + const net::CookieMonster::ParsedCookie& parsed_cookie, + const base::Time& current_time, + cookie_api::CookieInfo* cookie) { + // First, try the Max-Age attribute. + uint64 max_age = 0; + if (parsed_cookie.HasMaxAge() && + sscanf_s(parsed_cookie.MaxAge().c_str(), " %" PRIu64, &max_age) == 1) { + cookie->session = FALSE; + base::Time expiration_time = current_time + + base::TimeDelta::FromSeconds(max_age); + cookie->expiration_date = expiration_time.ToDoubleT(); + } else if (parsed_cookie.HasExpires()) { + cookie->session = FALSE; + base::Time expiration_time = net::CookieMonster::ParseCookieTime( + parsed_cookie.Expires()); + cookie->expiration_date = expiration_time.ToDoubleT(); + } else { + cookie->session = TRUE; + } +} + +void CookieAccountant::SetScriptCookieStoreId(cookie_api::CookieInfo* cookie) { + // The store ID is either the current process ID, or the process ID of the + // parent process, if that parent process is an IE frame process. + // First collect all IE process IDs. + std::set<base::ProcessId> ie_pids; + base::NamedProcessIterator ie_iter( + ceee_module_util::kInternetExplorerModuleName, NULL); + while (const base::ProcessEntry* process_entry = ie_iter.NextProcessEntry()) { + ie_pids.insert(process_entry->pid()); + } + // Now get the store ID process by finding the current process, and seeing if + // its parent process is an IE process. + DWORD process_id = 0; + CurrentProcessFilter filter; + base::ProcessIterator it(&filter); + while (const base::ProcessEntry* process_entry = it.NextProcessEntry()) { + // There should only be one matching process entry. + DCHECK_EQ(process_id, DWORD(0)); + if (ie_pids.find(process_entry->parent_pid()) != ie_pids.end()) { + process_id = process_entry->parent_pid(); + } else { + DCHECK(ie_pids.find(process_entry->pid()) != ie_pids.end()); + process_id = process_entry->pid(); + } + } + DCHECK_NE(process_id, DWORD(0)); + std::ostringstream store_id_stream; + store_id_stream << process_id; + // The broker is responsible for checking that the store ID is registered. + cookie->store_id = + ::SysAllocString(ASCIIToWide(store_id_stream.str()).c_str()); +} + +void CookieAccountant::RecordHttpResponseCookies( + const std::string& response_headers, const base::Time& current_time) { + StringTokenizer t(response_headers, kHttpResponseHeaderDelimiter); + while (t.GetNext()) { + std::string header_line = t.token(); + size_t name_pos = header_line.find(kSetCookieHeaderName); + if (name_pos == std::string::npos) + continue; // Skip non-cookie headers. + std::string cookie_data = header_line.substr( + name_pos + std::string(kSetCookieHeaderName).size()); + // TODO(cindylau@chromium.org): Get the URL for the HTTP request from + // IHttpNegotiate::BeginningTransaction. + RecordCookie(std::string(), cookie_data, current_time); + } +} + +void CookieAccountant::PatchWininetFunctions() { + if (!internet_set_cookie_ex_a_patch_.is_patched()) { + DWORD error = internet_set_cookie_ex_a_patch_.Patch( + kMsHtmlModuleName, kWinInetModuleName, + kInternetSetCookieExAFunctionName, InternetSetCookieExAPatch); + DCHECK(error == NO_ERROR || !internet_set_cookie_ex_a_patch_.is_patched()); + } + if (!internet_set_cookie_ex_w_patch_.is_patched()) { + DWORD error = internet_set_cookie_ex_w_patch_.Patch( + kMsHtmlModuleName, kWinInetModuleName, + kInternetSetCookieExWFunctionName, InternetSetCookieExWPatch); + DCHECK(error == NO_ERROR || !internet_set_cookie_ex_w_patch_.is_patched()); + } + DCHECK(internet_set_cookie_ex_a_patch_.is_patched() || + internet_set_cookie_ex_w_patch_.is_patched()); +} diff --git a/ceee/ie/plugin/bho/cookie_accountant.h b/ceee/ie/plugin/bho/cookie_accountant.h new file mode 100644 index 0000000..f26b102 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant.h @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Defines the CookieAccountant class, which is responsible for observing +// and recording all cookie-related information generated by a particular +// IE browser session. It records and fires cookie change events, it provides +// access to session and persistent cookies. + +#ifndef CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ +#define CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ + +#include <string> + +#include "app/win/iat_patch_function.h" +#include "base/singleton.h" +#include "base/time.h" +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "net/base/cookie_monster.h" + +// The class that accounts for all cookie-related activity for a single IE +// browser session context. There should only need to be one of these allocated +// per process; use ProductionCookieAccountant instead of using this class +// directly. +class CookieAccountant { + public: + // Patch cookie-related functions to observe IE session cookies. + void PatchWininetFunctions(); + + // Record Set-Cookie changes coming from the HTTP response headers. + void RecordHttpResponseCookies( + const std::string& response_headers, const base::Time& current_time); + + // An accessor for the singleton (useful for unit testing). + static CookieAccountant* GetInstance(); + + // InternetSetCookieExA function patch implementation for recording scripted + // cookie changes. + static DWORD WINAPI InternetSetCookieExAPatch( + LPCSTR lpszURL, LPCSTR lpszCookieName, LPCSTR lpszCookieData, + DWORD dwFlags, DWORD_PTR dwReserved); + + // InternetSetCookieExW function patch implementation for recording scripted + // cookie changes. + static DWORD WINAPI InternetSetCookieExWPatch( + LPCWSTR lpszURL, LPCWSTR lpszCookieName, LPCWSTR lpszCookieData, + DWORD dwFlags, DWORD_PTR dwReserved); + + protected: + // Exposed to subclasses mainly for unit testing purposes; production code + // should use the ProductionCookieAccountant class instead. + CookieAccountant() {} + virtual ~CookieAccountant(); + + // Records the modification or creation of a cookie. Fires off a + // cookies.onChanged event to Chrome Frame. + virtual void RecordCookie( + const std::string& url, const std::string& cookie_data, + const base::Time& current_time); + + // Unit test seam. + virtual CookieEventsFunnel& cookie_events_funnel() { + return cookie_events_funnel_; + } + + // Function patches that allow us to intercept scripted cookie changes. + app::win::IATPatchFunction internet_set_cookie_ex_a_patch_; + app::win::IATPatchFunction internet_set_cookie_ex_w_patch_; + + // Cached singleton instance. Useful for unit testing. + static CookieAccountant* singleton_instance_; + + private: + // Helper functions for extracting cookie information from a scripted cookie + // being set, to pass to the cookie onChanged event. + + // Sets the cookie domain for a script cookie event. + void SetScriptCookieDomain( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie); + + // Sets the cookie path for a script cookie event. + void SetScriptCookiePath( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie); + + // Sets the cookie expiration date for a script cookie event. + void SetScriptCookieExpirationDate( + const net::CookieMonster::ParsedCookie& parsed_cookie, + const base::Time& current_time, + cookie_api::CookieInfo* cookie); + + // Sets the cookie store ID for a script cookie event. + void SetScriptCookieStoreId(cookie_api::CookieInfo* cookie); + + // The funnel for sending cookie events to the broker. + CookieEventsFunnel cookie_events_funnel_; + + DISALLOW_COPY_AND_ASSIGN(CookieAccountant); +}; + +// A singleton that initializes and keeps the CookieAccountant used by +// production code. This class is separate so that CookieAccountant can still +// be accessed for unit testing. +class ProductionCookieAccountant : public CookieAccountant, + public Singleton<ProductionCookieAccountant> { + private: + // This ensures no construction is possible outside of the class itself. + friend struct DefaultSingletonTraits<ProductionCookieAccountant>; + DISALLOW_IMPLICIT_CONSTRUCTORS(ProductionCookieAccountant); +}; + +#endif // CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ diff --git a/ceee/ie/plugin/bho/cookie_accountant_unittest.cc b/ceee/ie/plugin/bho/cookie_accountant_unittest.cc new file mode 100644 index 0000000..6bd00ec --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant_unittest.cc @@ -0,0 +1,141 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/cookie_accountant.h" + +#include <wininet.h> + +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::MockCookieEventsFunnel; +using testing::Return; + +bool StringsNullOrEqual(wchar_t* a, wchar_t* b) { + if (a == NULL && b == NULL) + return true; + if (a != NULL && b != NULL && wcscmp(a, b) == 0) + return true; + return false; +} + +MATCHER_P(CookiesEqual, cookie, "") { + return + StringsNullOrEqual(arg.name, cookie->name) && + StringsNullOrEqual(arg.value, cookie->value) && + StringsNullOrEqual(arg.domain, cookie->domain) && + arg.host_only == cookie->host_only && + StringsNullOrEqual(arg.path, cookie->path) && + arg.secure == cookie->secure && + arg.http_only == cookie->http_only && + arg.session == cookie->session && + arg.expiration_date == cookie->expiration_date; +} + +// Mock WinInet functions. +MOCK_STATIC_CLASS_BEGIN(MockWinInet) + MOCK_STATIC_INIT_BEGIN(MockWinInet) + MOCK_STATIC_INIT(InternetSetCookieExA); + MOCK_STATIC_INIT(InternetSetCookieExW); + MOCK_STATIC_INIT_END() + + MOCK_STATIC5(DWORD, CALLBACK, InternetSetCookieExA, LPCSTR, LPCSTR, LPCSTR, + DWORD, DWORD_PTR); + MOCK_STATIC5(DWORD, CALLBACK, InternetSetCookieExW, LPCWSTR, LPCWSTR, + LPCWSTR, DWORD, DWORD_PTR); +MOCK_STATIC_CLASS_END(MockWinInet) + +class MockCookieAccountant : public CookieAccountant { + public: + MOCK_METHOD3(RecordCookie, + void(const std::string&, const std::string&, + const base::Time&)); + + void CallRecordCookie(const std::string& url, + const std::string& cookie_data, + const base::Time& current_time) { + CookieAccountant::RecordCookie(url, cookie_data, current_time); + } + + virtual CookieEventsFunnel& cookie_events_funnel() { + return mock_cookie_events_funnel_; + } + + static void set_singleton_instance(CookieAccountant* instance) { + singleton_instance_ = instance; + } + + MockCookieEventsFunnel mock_cookie_events_funnel_; +}; + +class CookieAccountantTest : public testing::Test { +}; + +TEST_F(CookieAccountantTest, SetCookiePatchFiresCookieEvent) { + MockWinInet mock_wininet; + MockCookieAccountant cookie_accountant; + MockCookieAccountant::set_singleton_instance(&cookie_accountant); + + EXPECT_CALL(mock_wininet, InternetSetCookieExA(_, _, _, _, _)). + WillOnce(Return(5)); + EXPECT_CALL(cookie_accountant, RecordCookie("foo.com", "foo=bar", _)); + EXPECT_EQ(5, CookieAccountant::InternetSetCookieExAPatch( + "foo.com", NULL, "foo=bar", 0, NULL)); + + EXPECT_CALL(mock_wininet, InternetSetCookieExW(_, _, _, _, _)). + WillOnce(Return(6)); + EXPECT_CALL(cookie_accountant, RecordCookie("foo.com", "foo=bar", _)); + EXPECT_EQ(6, CookieAccountant::InternetSetCookieExWPatch( + L"foo.com", NULL, L"foo=bar", 0, NULL)); +} + +TEST_F(CookieAccountantTest, RecordCookie) { + testing::LogDisabler no_dchecks; + MockCookieAccountant cookie_accountant; + cookie_api::CookieInfo expected_cookie; + expected_cookie.name = ::SysAllocString(L"FOO"); + expected_cookie.value = ::SysAllocString(L"bar"); + expected_cookie.host_only = TRUE; + expected_cookie.http_only = TRUE; + expected_cookie.expiration_date = 1278201600; + EXPECT_CALL(cookie_accountant.mock_cookie_events_funnel_, + OnChanged(false, CookiesEqual(&expected_cookie))); + cookie_accountant.CallRecordCookie( + "http://www.google.com", + "FOO=bar; httponly; expires=Sun, 4 Jul 2010 00:00:00 UTC", + base::Time::Now()); + + cookie_api::CookieInfo expected_cookie2; + expected_cookie2.name = ::SysAllocString(L""); + expected_cookie2.value = ::SysAllocString(L"helloworld"); + expected_cookie2.domain = ::SysAllocString(L"omg.com"); + expected_cookie2.path = ::SysAllocString(L"/leaping/lizards"); + expected_cookie2.secure = TRUE; + expected_cookie2.session = TRUE; + EXPECT_CALL(cookie_accountant.mock_cookie_events_funnel_, + OnChanged(false, CookiesEqual(&expected_cookie2))); + cookie_accountant.CallRecordCookie( + "http://www.omg.com", + "helloworld; path=/leaping/lizards; secure; domain=omg.com", + base::Time::Now()); +} + +TEST_F(CookieAccountantTest, RecordHttpResponseCookies) { + testing::LogDisabler no_dchecks; + MockCookieAccountant cookie_accountant; + EXPECT_CALL(cookie_accountant, RecordCookie("", " foo=bar", _)); + EXPECT_CALL(cookie_accountant, RecordCookie("", "HELLO=world235", _)); + cookie_accountant.RecordHttpResponseCookies( + "HTTP/1.1 200 OK\nSet-Cookie: foo=bar\nCookie: not_a=cookie\n" + "Set-Cookie:HELLO=world235", base::Time::Now()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/cookie_events_funnel.cc b/ceee/ie/plugin/bho/cookie_events_funnel.cc new file mode 100644 index 0000000..39ea727 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel.cc @@ -0,0 +1,28 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Cookie Events to the Broker. + +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" + +HRESULT CookieEventsFunnel::OnChanged(bool removed, + const cookie_api::CookieInfo& cookie) { + DictionaryValue change_info; + change_info.SetBoolean(extension_cookies_api_constants::kRemovedKey, + removed); + cookie_api::CookieApiResult api_result( + cookie_api::CookieApiResult::kNoRequestId); + bool success = api_result.CreateCookieValue(cookie); + DCHECK(success); + if (!success) { + return E_FAIL; + } + change_info.Set(extension_cookies_api_constants::kCookieKey, + api_result.value()->DeepCopy()); + return SendEvent(extension_cookies_api_constants::kOnChanged, change_info); +} diff --git a/ceee/ie/plugin/bho/cookie_events_funnel.h b/ceee/ie/plugin/bho/cookie_events_funnel.h new file mode 100644 index 0000000..06ada896 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel.h @@ -0,0 +1,28 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Cookie Events. + +#ifndef CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ + +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send cookie related events to the Broker. +class CookieEventsFunnel : public EventsFunnel { + public: + CookieEventsFunnel() : EventsFunnel(false) {} + + // Sends the cookies.onChanged event to the Broker. + // @param removed True if the cookie was removed vs. set. + // @param cookie Information about the cookie that was set or removed. + virtual HRESULT OnChanged(bool removed, + const cookie_api::CookieInfo& cookie); + + private: + DISALLOW_COPY_AND_ASSIGN(CookieEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc b/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc new file mode 100644 index 0000000..650e31d --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for CookieEventsFunnel. + +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestCookieEventsFunnel : public CookieEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(CookieEventsFunnelTest, OnChanged) { + TestCookieEventsFunnel cookie_events_funnel; + + bool removed = true; + cookie_api::CookieInfo cookie_info; + cookie_info.name = ::SysAllocString(L"FOO"); + cookie_info.value = ::SysAllocString(L"BAR"); + cookie_info.secure = TRUE; + cookie_info.session = TRUE; + cookie_info.store_id = ::SysAllocString(L"a store id!!"); + + DictionaryValue dict; + dict.SetBoolean(extension_cookies_api_constants::kRemovedKey, removed); + DictionaryValue* cookie = new DictionaryValue(); + cookie->SetString(extension_cookies_api_constants::kNameKey, "FOO"); + cookie->SetString(extension_cookies_api_constants::kValueKey, "BAR"); + cookie->SetString(extension_cookies_api_constants::kDomainKey, ""); + cookie->SetBoolean(extension_cookies_api_constants::kHostOnlyKey, false); + cookie->SetString(extension_cookies_api_constants::kPathKey, ""); + cookie->SetBoolean(extension_cookies_api_constants::kSecureKey, true); + cookie->SetBoolean(extension_cookies_api_constants::kHttpOnlyKey, false); + cookie->SetBoolean(extension_cookies_api_constants::kSessionKey, true); + cookie->SetString(extension_cookies_api_constants::kStoreIdKey, + "a store id!!"); + dict.Set(extension_cookies_api_constants::kCookieKey, cookie); + + EXPECT_CALL(cookie_events_funnel, SendEvent( + StrEq(extension_cookies_api_constants::kOnChanged), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED( + cookie_events_funnel.OnChanged(removed, cookie_info)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/dom_utils.cc b/ceee/ie/plugin/bho/dom_utils.cc new file mode 100644 index 0000000..714e108 --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils.cc @@ -0,0 +1,126 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A collection of utility functions that interrogate or mutate the IE DOM. +#include "ceee/ie/plugin/bho/dom_utils.h" + +#include <atlbase.h> + +#include "ceee/common/com_utils.h" +#include "base/logging.h" + +HRESULT DomUtils::InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code) { + DCHECK(document != NULL); + DCHECK(head_node != NULL); + + CComPtr<IHTMLElement> elem; + HRESULT hr = document->createElement(CComBSTR(L"style"), &elem); + if (FAILED(hr)) { + LOG(ERROR) << "Could not create style element " << com::LogHr(hr); + return hr; + } + + CComQIPtr<IHTMLStyleElement> style_elem(elem); + DCHECK(style_elem != NULL) << + "Could not QueryInterface for IHTMLStyleElement"; + + hr = style_elem->put_type(CComBSTR(L"text/css")); + DCHECK(SUCCEEDED(hr)) << "Could not set type of style element" << + com::LogHr(hr); + + CComPtr<IHTMLStyleSheet> style_sheet; + hr = style_elem->get_styleSheet(&style_sheet); + DCHECK(SUCCEEDED(hr)) << "Could not get styleSheet of style element." << + com::LogHr(hr); + + hr = style_sheet->put_cssText(CComBSTR(code)); + if (FAILED(hr)) { + LOG(ERROR) << "Could not set cssText of styleSheet." << com::LogHr(hr); + return hr; + } + + CComQIPtr<IHTMLDOMNode> style_node(style_elem); + DCHECK(style_node != NULL) << "Could not query interface for IHTMLDomNode."; + + CComPtr<IHTMLDOMNode> dummy; + hr = head_node->appendChild(style_node, &dummy); + if (FAILED(hr)) + LOG(ERROR) << "Could not append style node to head node." << com::LogHr(hr); + + return hr; +} + +HRESULT DomUtils::GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** head_node) { + DCHECK(document != NULL); + DCHECK(head_node != NULL && *head_node == NULL); + + // Find the HEAD element through document.getElementsByTagName. + CComQIPtr<IHTMLDocument3> document3(document); + CComPtr<IHTMLElementCollection> head_elements; + DCHECK(document3 != NULL); // Should be there on IE >= 5 + if (document3 == NULL) { + LOG(ERROR) << L"Unable to retrieve IHTMLDocument3 interface"; + return E_NOINTERFACE; + } + HRESULT hr = GetElementsByTagName(document3, CComBSTR(L"head"), + &head_elements, NULL); + if (FAILED(hr)) { + LOG(ERROR) << "Could not retrieve head elements collection " + << com::LogHr(hr); + return hr; + } + + return GetElementFromCollection(head_elements, 0, IID_IHTMLDOMNode, + reinterpret_cast<void**>(head_node)); +} + +HRESULT DomUtils::GetElementsByTagName(IHTMLDocument3* document, + BSTR tag_name, + IHTMLElementCollection** elements, + long* length) { + DCHECK(document != NULL); + DCHECK(tag_name != NULL); + DCHECK(elements != NULL && *elements == NULL); + + HRESULT hr = document->getElementsByTagName(tag_name, elements); + if (FAILED(hr) || *elements == NULL) { + hr = com::AlwaysError(hr); + LOG(ERROR) << "Could not retrieve elements collection " << com::LogHr(hr); + return hr; + } + + if (length != NULL) { + hr = (*elements)->get_length(length); + if (FAILED(hr)) { + (*elements)->Release(); + *elements = NULL; + LOG(ERROR) << "Could not retrieve collection length " << com::LogHr(hr); + } + } + return hr; +} + +HRESULT DomUtils::GetElementFromCollection(IHTMLElementCollection* collection, + long index, + REFIID id, + void** element) { + DCHECK(collection != NULL); + DCHECK(element != NULL && *element == NULL); + + CComPtr<IDispatch> item; + CComVariant index_variant(index, VT_I4); + HRESULT hr = collection->item(index_variant, index_variant, &item); + // As per http://msdn.microsoft.com/en-us/library/aa703930(VS.85).aspx + // item may still be NULL even if S_OK is returned. + if (FAILED(hr) || item == NULL) { + hr = com::AlwaysError(hr); + LOG(ERROR) << "Could not access item " << com::LogHr(hr); + return hr; + } + + return item->QueryInterface(id, element); +} diff --git a/ceee/ie/plugin/bho/dom_utils.h b/ceee/ie/plugin/bho/dom_utils.h new file mode 100644 index 0000000..e988a68 --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils.h @@ -0,0 +1,53 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A collection of utility functions that interrogate or mutate the IE DOM. +#ifndef CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ +#define CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ + +#include <mshtml.h> +#include <string> + +// This class is a namespace for hosting utility functions that interrogate +// or mutate the IE DOM. +// TODO(siggi@chromium.org): should this be a namespace? +class DomUtils { + public: + // Inject a style tag into the head of the document. + // @param document The DOM document object. + // @param head_node The HEAD DOM node from |document|. + // @param code The CSS code to inject. + static HRESULT InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code); + + // Retrieve the "HEAD" DOM node from @p document. + // @param document the DOM document object. + // @param head_node on success returns the "HEAD" DOM node. + static HRESULT GetHeadNode(IHTMLDocument* document, IHTMLDOMNode** head_node); + + // Retrieves all elements with the specified tag name from the document. + // @param document The document object. + // @param tag_name The tag name. + // @param elements On success returns a collection of elements. + // @param length On success returns the number of elements in the collection. + // The caller could pass in NULL to indicate that length + // information is not needed. + static HRESULT GetElementsByTagName(IHTMLDocument3* document, + BSTR tag_name, + IHTMLElementCollection** elements, + long* length); + + // Retrieves an element from the collection. + // @param collection The collection object. + // @param index The zero-based index of the element to retrieve. + // @param id The interface ID of the element to retrieve. + // @param element On success returns the element. + static HRESULT GetElementFromCollection(IHTMLElementCollection* collection, + long index, + REFIID id, + void** element); +}; + +#endif // CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ diff --git a/ceee/ie/plugin/bho/dom_utils_unittest.cc b/ceee/ie/plugin/bho/dom_utils_unittest.cc new file mode 100644 index 0000000..180c1cc --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils_unittest.cc @@ -0,0 +1,197 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unittests for DOM utils. +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/test_utils.h" + +namespace { + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::StrEq; +using testing::VariantEq; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl>, + public StrictMock<IHTMLDocument3MockImpl> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLDocument2) + COM_INTERFACE_ENTRY(IHTMLDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + COM_INTERFACE_ENTRY(IHTMLDocument3) + END_COM_MAP() + + HRESULT Initialize(MockDocument** self) { + *self = this; + return S_OK; + } +}; + +class MockElementNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockElementNode>, + public StrictMock<IHTMLElementMockImpl>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockElementNode) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize(MockElementNode** self) { + *self = this; + return S_OK; + } +}; + +class MockStyleElementNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockStyleElementNode>, + public StrictMock<IHTMLElementMockImpl>, + public StrictMock<IHTMLStyleElementMockImpl>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockStyleElementNode) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLStyleElement) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize(MockStyleElementNode** self) { + *self = this; + return S_OK; + } +}; + +class MockStyleSheet + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockStyleSheet>, + public StrictMock<IHTMLStyleSheetMockImpl> { + BEGIN_COM_MAP(MockStyleSheet) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLStyleSheet) + END_COM_MAP() + + HRESULT Initialize(MockStyleSheet** self) { + *self = this; + return S_OK; + } +}; + +class DomUtilsTest: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockElementNode::CreateInitialized(&head_node_, &head_node_keeper_)); + } + + virtual void TearDown() { + document_ = NULL; + document_keeper_.Release(); + head_node_ = NULL; + head_node_keeper_.Release(); + } + + protected: + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + MockElementNode* head_node_; + CComPtr<IHTMLDOMNode> head_node_keeper_; +}; + +TEST_F(DomUtilsTest, InjectStyleTag) { + MockStyleElementNode* style_node; + CComPtr<IHTMLElement> style_node_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockStyleElementNode::CreateInitialized(&style_node, &style_node_keeper)); + + MockStyleSheet* style_sheet; + CComPtr<IHTMLStyleSheet> style_sheet_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockStyleSheet::CreateInitialized(&style_sheet, &style_sheet_keeper)); + + EXPECT_CALL(*document_, createElement(StrEq(L"style"), _)). + WillOnce(DoAll(CopyInterfaceToArgument<1>(style_node_keeper), + Return(S_OK))); + + EXPECT_CALL(*style_node, put_type(StrEq(L"text/css"))). + WillOnce(Return(S_OK)); + + EXPECT_CALL(*style_node, get_styleSheet(_)). + WillOnce(DoAll(CopyInterfaceToArgument<0>(style_sheet_keeper), + Return(S_OK))); + + EXPECT_CALL(*style_sheet, put_cssText(StrEq(L"foo"))).WillOnce(Return(S_OK)); + + EXPECT_CALL(*head_node_, appendChild(style_node, _)). + WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + DomUtils::InjectStyleTag(document_keeper_, head_node_keeper_, L"foo")); +} + +class MockElementCollection + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockElementCollection>, + public StrictMock<IHTMLElementCollectionMockImpl> { + public: + BEGIN_COM_MAP(MockElementCollection) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLElementCollection) + END_COM_MAP() + + HRESULT Initialize(MockElementCollection** self) { + *self = this; + return S_OK; + } +}; + +TEST_F(DomUtilsTest, GetHeadNode) { + MockElementCollection* collection; + CComPtr<IHTMLElementCollection> collection_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockElementCollection::CreateInitialized(&collection, + &collection_keeper)); + + EXPECT_CALL(*document_, getElementsByTagName(StrEq(L"head"), _)) + .WillRepeatedly(DoAll(CopyInterfaceToArgument<1>(collection_keeper), + Return(S_OK))); + + CComVariant zero(0L); + // First verify that we gracefuly fail when there are no heads. + // bb2333090 + EXPECT_CALL(*collection, item(_, VariantEq(zero), _)) + .WillOnce(Return(S_OK)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_FAILED(DomUtils::GetHeadNode(document_, &head_node)); + ASSERT_EQ(static_cast<IHTMLDOMNode*>(NULL), head_node); + + // And now properly return a valid head node. + EXPECT_CALL(*collection, item(_, VariantEq(zero), _)) + .WillOnce(DoAll(CopyInterfaceToArgument<2>( + static_cast<IDispatch*>(head_node_keeper_)), Return(S_OK))); + + ASSERT_HRESULT_SUCCEEDED( + DomUtils::GetHeadNode(document_, &head_node)); + + ASSERT_TRUE(head_node == head_node_); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/events_funnel.cc b/ceee/ie/plugin/bho/events_funnel.cc new file mode 100644 index 0000000..dfa0cdc --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel.cc @@ -0,0 +1,42 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common base class for funnels of Chrome Extension events that originate +// from the BHO and are sent to the Broker. + +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "ceee/ie/common/ceee_module_util.h" + + +EventsFunnel::EventsFunnel(bool keep_broker_alive) + : keep_broker_alive_(keep_broker_alive) { + if (keep_broker_alive_) + ceee_module_util::AddRefModuleWorkerThread(); +} + +EventsFunnel::~EventsFunnel() { + if (keep_broker_alive_) + ceee_module_util::ReleaseModuleWorkerThread(); +} + +HRESULT EventsFunnel::SendEvent(const char* event_name, + const Value& event_args) { + // Event arguments for FireEventToBroker always need to be stored in a list. + std::string event_args_str; + if (event_args.IsType(Value::TYPE_LIST)) { + base::JSONWriter::Write(&event_args, false, &event_args_str); + } else { + ListValue list; + list.Append(event_args.DeepCopy()); + base::JSONWriter::Write(&list, false, &event_args_str); + } + + EventsFunnel thread_locker(!keep_broker_alive_); + ceee_module_util::FireEventToBroker(event_name, event_args_str); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/events_funnel.h b/ceee/ie/plugin/bho/events_funnel.h new file mode 100644 index 0000000..add53f8 --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel.h @@ -0,0 +1,38 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common base class for funnels of Chrome Extension events that originate +// from the BHO. + +#ifndef CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ + +#include <windows.h> + +#include "base/basictypes.h" + +class Value; + +// Defines a base class for sending events to the Broker. +class EventsFunnel { + protected: + // @param keep_broker_alive If true broker will be alive during + // lifetime of this funnel, otherwise only during SendEvent. + explicit EventsFunnel(bool keep_broker_alive); + virtual ~EventsFunnel(); + + // Send the given event to the Broker. + // @param event_name The name of the event. + // @param event_args The arguments to be sent with the event. + // protected virtual for testability... + virtual HRESULT SendEvent(const char* event_name, const Value& event_args); + + private: + // If true constructor/destructor of class increments/decrements ref counter + // of broker thread. Otherwise SendEvent does it for every event. + const bool keep_broker_alive_; + DISALLOW_COPY_AND_ASSIGN(EventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/events_funnel_unittest.cc b/ceee/ie/plugin/bho/events_funnel_unittest.cc new file mode 100644 index 0000000..64f7061 --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel_unittest.cc @@ -0,0 +1,62 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for EventsFunnel. + +#include "base/json/json_writer.h" +#include "base/values.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/events_funnel.h" +#include "ceee/testing/utils/mock_static.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::StrEq; + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT2(ceee_module_util::FireEventToBroker, + FireEventToBroker); + MOCK_STATIC_INIT_END() + MOCK_STATIC2(void, , FireEventToBroker, const std::string&, + const std::string&); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtils) + +// Test subclass used to provide access to protected functionality in the +// EventsFunnel class. +class TestEventsFunnel : public EventsFunnel { + public: + TestEventsFunnel() : EventsFunnel(true) {} + + HRESULT CallSendEvent(const char* event_name, const Value& event_args) { + return SendEvent(event_name, event_args); + } +}; + +TEST(EventsFunnelTest, SendEvent) { + TestEventsFunnel events_funnel; + MockCeeeModuleUtils mock_ceee_module; + + static const char* kEventName = "MADness"; + DictionaryValue event_args; + event_args.SetInteger("Answer to the Ultimate Question of Life," + "the Universe, and Everything", 42); + event_args.SetString("AYBABTU", "All your base are belong to us"); + event_args.SetReal("www.unrealtournament.com", 3.0); + + ListValue args_list; + args_list.Append(event_args.DeepCopy()); + + std::string event_args_str; + base::JSONWriter::Write(&args_list, false, &event_args_str); + + EXPECT_CALL(mock_ceee_module, FireEventToBroker(StrEq(kEventName), + StrEq(event_args_str))).Times(1); + EXPECT_HRESULT_SUCCEEDED( + events_funnel.CallSendEvent(kEventName, event_args)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/executor.cc b/ceee/ie/plugin/bho/executor.cc new file mode 100644 index 0000000..d7719f1 --- /dev/null +++ b/ceee/ie/plugin/bho/executor.cc @@ -0,0 +1,771 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CeeeExecutor implementation +// +// We use interfaces named ITabWindowManager and ITabWindow +// (documented at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindowmanager.htm +// and +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindow.htm) +// to help implement this API. These are available in IE7, IE8 and +// IE9 (with minor differences between browser versions), so we use a +// wrapper class that takes care of delegating to the available +// interface. +// +// Alternate approach considered: Using the IAccessible interface to find out +// about the order (indexes) of tabs, create new tabs and close tabs in a +// reliable way. The main drawback was that currently, the only way we've +// found to go from the IAccessible interface to the tab window itself (and +// hence the IWebBrowser2 object) is to match the description +// fetched using IAccessible::get_accDescription(), which contains the title +// and URL of the tab, to the title and URL retrieved via the IWebBrowser2 +// object. This limitation would mean that tab indexes could be incorrect +// when two or more tabs are navigated to the same page (and have the same +// title). + +#include "ceee/ie/plugin/bho/executor.h" + +#include <atlcomcli.h> +#include <mshtml.h> +#include <wininet.h> + +#include <vector> + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "base/scoped_ptr.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/bho/infobar_manager.h" +#include "ceee/ie/plugin/bho/tab_window_manager.h" +#include "chrome_frame/utils.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +// Static per-process variable to indicate whether the process has been +// registered as a cookie store yet. +static bool g_cookie_store_is_registered = false; + +// INTERNET_COOKIE_HTTPONLY is only available for IE8 or later, which allows +// Wininet API to read cookies that are marked as HTTPOnly. +#ifndef INTERNET_COOKIE_HTTPONLY +#define INTERNET_COOKIE_HTTPONLY 0x00002000 +#endif + +// Default maximum height of the infobar. From the experience with the design of +// infobars this value is found to provide enough space and not to be too +// restrictive - for example this is approximately the height of Chrome infobar. +const int kMaxInfobarHeight = 39; +} // namespace + +// The message which will be posted to the destination thread. +const UINT CeeeExecutorCreator::kCreateWindowExecutorMessage = + ::RegisterWindowMessage( + L"CeeeExecutor{D91E23A6-1C2E-4984-8528-1F1771004F37}"); + +CeeeExecutorCreator::CeeeExecutorCreator() + : current_thread_id_(0), hook_(NULL) { +} + +void CeeeExecutorCreator::FinalRelease() { + if (hook_ != NULL) { + HRESULT hr = Teardown(current_thread_id_); + DCHECK(SUCCEEDED(hr)) << "Self-Tearing down. " << com::LogHr(hr); + } +} + +HRESULT CeeeExecutorCreator::CreateWindowExecutor(long thread_id, + CeeeWindowHandle window) { + DCHECK_EQ(0, current_thread_id_); + current_thread_id_ = thread_id; + // Verify we're a window, not just a tab. + DCHECK_EQ(window_utils::GetTopLevelParent(reinterpret_cast<HWND>(window)), + reinterpret_cast<HWND>(window)); + + hook_ = ::SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, + static_cast<HINSTANCE>(_AtlBaseModule.GetModuleInstance()), thread_id); + if (hook_ == NULL) { + LOG(ERROR) << "Couldn't hook into thread: " << thread_id << " " << + com::LogWe(); + current_thread_id_ = 0; + return E_FAIL; + } + + // We unfortunately can't Send a synchronous message here. + // If we do, any calls back to the broker fail with the following error: + // "An outgoing call cannot be made since the application is dispatching an + // input synchronous call." + BOOL success = ::PostThreadMessage(thread_id, kCreateWindowExecutorMessage, + 0, static_cast<LPARAM>(window)); + if (success) + return S_OK; + else + return HRESULT_FROM_WIN32(::GetLastError()); +} + +HRESULT CeeeExecutorCreator::Teardown(long thread_id) { + if (hook_ != NULL) { + DCHECK(current_thread_id_ == thread_id); + current_thread_id_ = 0; + // Don't return the failure since it may fail when we get called after + // the destination thread/module we hooked to vanished into thin air. + BOOL success = ::UnhookWindowsHookEx(hook_); + LOG_IF(INFO, !success) << "Failed to unhook. " << com::LogWe(); + hook_ = NULL; + } + return S_OK; +} + +LRESULT CeeeExecutorCreator::GetMsgProc(int code, WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION) { + MSG* message_data = reinterpret_cast<MSG*>(lparam); + if (message_data != NULL && + message_data->message == kCreateWindowExecutorMessage) { + // Remove the message from the queue so that we don't get called again + // while we wait for CoCreateInstance to complete, since COM will run + // a message loop in there. And some loop don't PM_REMOVE us (some do). + if (wparam != PM_REMOVE) { + MSG dummy; + BOOL success = ::PeekMessage(&dummy, NULL, kCreateWindowExecutorMessage, + kCreateWindowExecutorMessage, PM_REMOVE); + DCHECK(success) << "Peeking Hook Message. " << com::LogWe(); + // We must return here since we will get called again from within + // PeekMessage, and with PM_REMOVE this time (so no, we won't + // infinitely recurse :-), so this ensure that we get called just once. + return 0; + } + + CComPtr<ICeeeWindowExecutor> executor; + HRESULT hr = executor.CoCreateInstance(CLSID_CeeeExecutor); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + CeeeWindowHandle window = static_cast<CeeeWindowHandle>( + message_data->lParam); + if (window) { + hr = executor->Initialize(window); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + } + + CComPtr<ICeeeBrokerRegistrar> broker; + hr = broker.CoCreateInstance(CLSID_CeeeBroker); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create broker, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Broker. " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + hr = broker->RegisterWindowExecutor(::GetCurrentThreadId(), executor); + DCHECK(SUCCEEDED(hr)) << "Registering Executor. " << com::LogHr(hr); + } + } + return 0; + } + } + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +HRESULT CeeeExecutor::Initialize(CeeeWindowHandle hwnd) { + DCHECK(hwnd); + hwnd_ = reinterpret_cast<HWND>(hwnd); + + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // If this is tab window then create the infobab manager. + // TODO(mad@chromium.org): We are starting to need to have different classes + // for the different executors. + if (window_utils::GetTopLevelParent(hwnd_) != hwnd_) + infobar_manager_.reset(new infobar_api::InfobarManager(hwnd_)); + + return S_OK; +} + +HRESULT CeeeExecutor::GetWebBrowser(IWebBrowser2** browser) { + DCHECK(browser); + CComPtr<IFrameEventHandlerHost> frame_handler_host; + HRESULT hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " << + com::LogHr(hr); + return hr; + } + return frame_handler_host->GetTopLevelBrowser(browser); +} + +STDMETHODIMP CeeeExecutor::GetWindow(BOOL populate_tabs, + CeeeWindowInfo* window_info) { + DCHECK(window_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Zero the window info. + ZeroMemory(window_info, sizeof(CeeeWindowInfo)); + + // The window we use to represent IE windows is the top-level (parentless) + // frame for the collection of windows that make up a logical "window" in the + // sense of the chrome.window.* API. Therefore, compare the provided window + // with the top-level parent of the current foreground (focused) window to + // see if the logical window has focus. + HWND top_level = window_utils::GetTopLevelParent(::GetForegroundWindow()); + window_info->focused = (top_level == hwnd_); + + if (!::GetWindowRect(hwnd_, &window_info->rect)) { + DWORD we = ::GetLastError(); + DCHECK(false) << "GetWindowRect failed " << com::LogWe(we); + return HRESULT_FROM_WIN32(we); + } + + if (populate_tabs) { + return GetTabs(&window_info->tab_list); + } + + return S_OK; +} + +BOOL CALLBACK CeeeExecutor::GetTabsEnumProc(HWND window, LPARAM param) { + if (window_utils::IsWindowClass(window, windows::kIeTabWindowClass)) { + std::vector<HWND>* tab_windows = + reinterpret_cast<std::vector<HWND>*>(param); + DCHECK(tab_windows); + tab_windows->push_back(window); + } + return TRUE; +} + +STDMETHODIMP CeeeExecutor::GetTabs(BSTR* tab_list) { + DCHECK(tab_list); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + std::vector<HWND> tab_windows; + ::EnumChildWindows(hwnd_, GetTabsEnumProc, + reinterpret_cast<LPARAM>(&tab_windows)); + // We don't DCHECK that we found as many windows as the tab window manager + // GetCount(), because there are cases where it sees more than we do... :-( + // When we navigate to a new page, IE8 actually creates a new temporary page + // (not sure if it always do it, or just in some cases), and the + // TabWindowManager actually count this temporary tab, even though we can't + // see it when we enumerate the kIeTabWindowClass windows. + ListValue tabs_list; + for (size_t index = 0; index < tab_windows.size(); ++index) { + HWND tab_window = tab_windows[index]; + long tab_index = -1; + hr = manager->IndexFromHWND(tab_window, &tab_index); + if (SUCCEEDED(hr)) { + tabs_list.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_window))); + tabs_list.Append(Value::CreateIntegerValue(static_cast<int>(tab_index))); + // The tab window may have died by the time we get here. + // Simply ignore that tab in this case. + } else if (::IsWindow(tab_window)) { + // But if it's still alive, then something wrong happened. + return hr; + } + } + std::string tabs_json; + base::JSONWriter::Write(&tabs_list, false, &tabs_json); + *tab_list = ::SysAllocString(CA2W(tabs_json.c_str())); + if (*tab_list == NULL) { + return E_OUTOFMEMORY; + } + return S_OK; +} + +STDMETHODIMP CeeeExecutor::UpdateWindow( + long left, long top, long width, long height, + CeeeWindowInfo* window_info) { + DCHECK(window_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Any part of the window rect can optionally be set by the caller. + // We need the original rect if only some dimensions are set by caller. + RECT rect = { 0 }; + BOOL success = ::GetWindowRect(hwnd_, &rect); + DCHECK(success); + + if (left == -1) { + left = rect.left; + } + if (top == -1) { + top = rect.top; + } + if (width == -1) { + width = rect.right - left; + } + if (height == -1) { + height = rect.bottom - top; + } + + // In IE8 this would yield ERROR_ACCESS_DENIED when called from another + // thread/process and protected mode is enabled, because the process owning + // the frame window is medium integrity. See UIPI in MSDN. + // So this is why we must do this via an injected executor. + success = ::MoveWindow(hwnd_, left, top, width, height, TRUE); + DCHECK(success) << "Failed to move the window to the update rect. " << + com::LogWe(); + return GetWindow(FALSE, window_info); +} + +STDMETHODIMP CeeeExecutor::RemoveWindow() { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + return manager->CloseAllTabs(); +} + +STDMETHODIMP CeeeExecutor::GetTabInfo(CeeeTabInfo* tab_info) { + DCHECK(tab_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Zero the info. + ZeroMemory(tab_info, sizeof(CeeeTabInfo)); + + CComPtr<IFrameEventHandlerHost> frame_handler_host; + hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " << + com::LogHr(hr); + return hr; + } + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + hr = frame_handler_host->GetReadyState(&ready_state); + if (FAILED(hr)) { + NOTREACHED() << "Can't get ReadyState, Wazzup???. " << com::LogHr(hr); + return hr; + } + + tab_info->status = kCeeeTabStatusComplete; + if (ready_state != READYSTATE_COMPLETE) { + // Chrome only has two states, so all incomplete states are "loading". + tab_info->status = kCeeeTabStatusLoading; + } + + CComPtr<IWebBrowser2> browser; + hr = frame_handler_host->GetTopLevelBrowser(&browser); + if (FAILED(hr)) { + NOTREACHED(); + return hr; + } + + hr = browser->get_LocationURL(&tab_info->url); + DCHECK(SUCCEEDED(hr)) << "get_LocationURL()" << com::LogHr(hr); + + hr = browser->get_LocationName(&tab_info->title); + DCHECK(SUCCEEDED(hr)) << "get_LocationName()" << com::LogHr(hr); + + // TODO(mad@chromium.org): Favicon support (see Chrome + // implementation, kFavIconUrlKey). AFAJoiCT, this is only set if + // there is a <link rel="icon" ...> tag, so we could parse this out + // of the IHTMLDocument2::get_links() collection. + tab_info->fav_icon_url = NULL; + return S_OK; +} + +STDMETHODIMP CeeeExecutor::GetTabIndex(CeeeWindowHandle tab, long* index) { + DCHECK(index); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), index); + DCHECK(SUCCEEDED(hr)) << "Couldn't get index for tab: " << + tab << ", " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::MoveTab(CeeeWindowHandle tab, long index) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long src_index = 0; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &src_index); + if (FAILED(hr)) { + NOTREACHED() << "Failed IndexFromHWND " << com::LogHr(hr); + return hr; + } + + LONG num_tabs = -1; + hr = manager->GetCount(&num_tabs); + if (FAILED(hr)) { + NOTREACHED() << "Failed to GetCount " << com::LogHr(hr); + return hr; + } + + // Clamp new index (as per Chrome implementation) so that extension authors + // can for convenience sakes use index=999 (or some such) to move the tab + // to the far right. + if (index >= num_tabs) { + index = num_tabs - 1; + } + + // Clamp current index so we can move newly-created tabs easily. + if (src_index >= num_tabs) { + src_index = num_tabs - 1; + } + + if (index == src_index) + return S_FALSE; // nothing to be done + + scoped_ptr<TabWindow> dest_tab; + hr = manager->GetItemWrapper(index, &dest_tab); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetItem or QI on dest tab " << com::LogHr(hr); + return hr; + } + + long dest_id = -1; + hr = dest_tab->GetID(&dest_id); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetID on dest tab " << com::LogHr(hr); + return hr; + } + + scoped_ptr<TabWindow> moving_tab; + hr = manager->GetItemWrapper(src_index, &moving_tab); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetItem or QI on moving tab " << com::LogHr(hr); + return hr; + } + + long moving_id = -1; + hr = moving_tab->GetID(&moving_id); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetID on moving tab " << com::LogHr(hr); + return hr; + } + + hr = manager->RepositionTab(moving_id, dest_id, 0); + if (FAILED(hr)) { + NOTREACHED() << "Failed to reposition tab " << com::LogHr(hr); + return hr; + } + + return hr; +} + +STDMETHODIMP CeeeExecutor::Navigate(BSTR url, long flags, BSTR target) { + DCHECK(url); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + CComPtr<IWebBrowser2> tab_browser; + hr = GetWebBrowser(&tab_browser); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get browser " << com::LogHr(hr); + return hr; + } + + CComBSTR current_url; + hr = tab_browser->get_LocationURL(¤t_url); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get URL " << com::LogHr(hr); + return hr; + } + + if (current_url == url && + 0 != lstrcmpW(L"_blank", com::ToString(target))) { + LOG(INFO) << "Got update request, but URL & target is unchanged: " << url; + return S_FALSE; + } + + hr = tab_browser->Navigate(url, &CComVariant(flags), &CComVariant(target), + &CComVariant(), &CComVariant()); + // We don't DCHECK here since there are cases where we get an error + // 0x800700aa "The requested resource is in use." if the main UI + // thread is currently blocked... and sometimes... it is blocked by + // us... if we are too slow to respond (e.g. because too busy + // navigating when the user performs an extension action that causes + // navigation again and again and again)... So we might as well + // abandon ship and let the UI thread be happy... + LOG_IF(ERROR, FAILED(hr)) << "Failed to navigate tab: " << hwnd_ << + " to " << url << ". " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::RemoveTab(CeeeWindowHandle tab) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long index = -1; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &index); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get index of tab " << com::LogHr(hr); + return hr; + } + + scoped_ptr<TabWindow> tab_item; + hr = manager->GetItemWrapper(index, &tab_item); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get tab object " << com::LogHr(hr); + return hr; + } + + hr = tab_item->Close(); + DCHECK(SUCCEEDED(hr)) << "Failed to close tab " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::SelectTab(CeeeWindowHandle tab) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long index = -1; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &index); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get index of tab, wnd=" << + std::hex << hwnd_ << " " << com::LogHr(hr); + return hr; + } + + hr = manager->SelectTab(index); + DCHECK(SUCCEEDED(hr)) << "Failed to select window, wnd=" << + std::hex << hwnd_ << " " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + CComPtr<IFrameEventHandlerHost> frame_handler_host; + hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " + << com::LogHr(hr); + return hr; + } + + hr = frame_handler_host->InsertCode(code, file, all_frames, type); + if (FAILED(hr)) { + NOTREACHED() << "Failed to insert code. " << com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT CeeeExecutor::GetCookieValue(BSTR url, BSTR name, BSTR* value) { + DCHECK(value); + if (!value) + return E_POINTER; + + // INTERNET_COOKIE_HTTPONLY only works for IE8+. + DWORD flags = 0; + if (ie_util::GetIeVersion() > ie_util::IEVERSION_IE7) + flags |= INTERNET_COOKIE_HTTPONLY; + + // First find out the size of the cookie data. + DWORD size = 0; + BOOL cookie_found = ::InternetGetCookieExW( + url, name, NULL, &size, flags, NULL); + if (!cookie_found) { + if (::GetLastError() == ERROR_NO_MORE_ITEMS) + return S_FALSE; + else + return E_FAIL; + } else if (size == 0) { + return E_FAIL; + } + + // Now retrieve the data. + std::vector<wchar_t> cookie_data(size + 1); + cookie_found = ::InternetGetCookieExW( + url, name, &cookie_data[0], &size, flags, NULL); + DCHECK(cookie_found); + if (!cookie_found) + return E_FAIL; + + // Copy the data to the output parameter. + cookie_data[size] = 0; + std::wstring cookie_data_string(&cookie_data[0], size); + std::wstring data_prefix(name); + data_prefix.append(L"="); + if (cookie_data_string.find(data_prefix) != 0) { + DCHECK(false) << "The cookie name or data format does not match the " + << "expected 'name=value'. Name: " << name << ", Data: " + << cookie_data_string; + return E_FAIL; + } + *value = ::SysAllocString( + cookie_data_string.substr(data_prefix.size()).c_str()); + return S_OK; +} + +STDMETHODIMP CeeeExecutor::GetCookie(BSTR url, BSTR name, + CeeeCookieInfo* cookie_info) { + DCHECK(cookie_info); + if (!cookie_info) + return E_POINTER; + HRESULT hr = GetCookieValue(url, name, &cookie_info->value); + if (hr == S_OK) { + cookie_info->name = ::SysAllocString(name); + } + DCHECK(hr == S_OK || cookie_info->value == NULL); + return hr; +} + +void CeeeExecutor::set_cookie_store_is_registered(bool is_registered) { + g_cookie_store_is_registered = is_registered; +} + +STDMETHODIMP CeeeExecutor::RegisterCookieStore() { + set_cookie_store_is_registered(true); + return S_OK; +} + +HRESULT CeeeExecutor::EnsureWindowThread() { + if (!window_utils::IsWindowThread(hwnd_)) { + LOG(ERROR) << "Executor not running in appropriate thread for window: " << + hwnd_; + return E_UNEXPECTED; + } + + return S_OK; +} + +STDMETHODIMP CeeeExecutor::CookieStoreIsRegistered() { + return g_cookie_store_is_registered ? S_OK : S_FALSE; +} + +STDMETHODIMP CeeeExecutor::SetExtensionId(BSTR extension_id) { + DCHECK(extension_id); + if (extension_id == NULL) + return E_FAIL; + + WideToUTF8(extension_id, SysStringLen(extension_id), &extension_id_); + return S_OK; +} + +STDMETHODIMP CeeeExecutor::ShowInfobar(BSTR url, + CeeeWindowHandle* window_handle) { + DCHECK(infobar_manager_ != NULL) << "infobar_manager_ is not initialized"; + if (infobar_manager_ == NULL) + return E_FAIL; + + // Consider infobar navigation to an empty url as the request to hide it. + // Note that this is not a part of the spec so it is up to the implementation + // how to treat this. + size_t url_string_length = SysStringLen(url); + if (0 == url_string_length) { + infobar_manager_->HideAll(); + return S_OK; + } + + // Translate relative path to the absolute path using our extension URL + // as the root. + std::string url_utf8; + WideToUTF8(url, url_string_length, &url_utf8); + if (extension_id_.empty()) { + LOG(ERROR) << "Extension id is not set before the request to show infobar."; + } else { + url_utf8 = ResolveURL( + StringPrintf("chrome-extension://%s", extension_id_.c_str()), url_utf8); + } + std::wstring full_url; + UTF8ToWide(url_utf8.c_str(), url_utf8.size(), &full_url); + + // Show and navigate the infobar window. + HRESULT hr = infobar_manager_->Show(infobar_api::TOP_INFOBAR, + kMaxInfobarHeight, full_url, true); + if (SUCCEEDED(hr) && window_handle != NULL) { + *window_handle = reinterpret_cast<CeeeWindowHandle>( + window_utils::GetTopLevelParent(hwnd_)); + } + + return hr; +} + +STDMETHODIMP CeeeExecutor::OnTopFrameBeforeNavigate(BSTR url) { + DCHECK(infobar_manager_ != NULL) << "infobar_manager_ is not initialized"; + if (infobar_manager_ == NULL) + return E_FAIL; + + // According to the specification, tab navigation closes the infobar. + infobar_manager_->HideAll(); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/executor.h b/ceee/ie/plugin/bho/executor.h new file mode 100644 index 0000000..379ba32 --- /dev/null +++ b/ceee/ie/plugin/bho/executor.h @@ -0,0 +1,166 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// CeeeExecutor & CeeeExecutorCreator implementation, interfaces to +// execute code in other threads which can be running in other another process. + +#ifndef CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ +#define CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <string.h> + +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/infobar_manager.h" +#include "ceee/ie/plugin/toolband/resource.h" + +#include "toolband.h" // NOLINT + +struct IWebBrowser2; +namespace infobar_api { + class InfobarManager; +}; + +// The executor creator hooks itself in the destination thread where +// the executor will then be created and register in the CeeeBroker. + +// The creator of CeeeExecutors. +class ATL_NO_VTABLE CeeeExecutorCreator + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<CeeeExecutorCreator, + &CLSID_CeeeExecutorCreator>, + public ICeeeExecutorCreator { + public: + CeeeExecutorCreator(); + void FinalRelease(); + + DECLARE_REGISTRY_RESOURCEID(IDR_EXECUTOR_CREATOR) + + DECLARE_NOT_AGGREGATABLE(CeeeExecutorCreator) + BEGIN_COM_MAP(CeeeExecutorCreator) + COM_INTERFACE_ENTRY(ICeeeExecutorCreator) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + // @name ICeeeExecutorCreator implementation. + // @{ + STDMETHOD(CreateWindowExecutor)(long thread_id, CeeeWindowHandle window); + STDMETHOD(Teardown)(long thread_id); + // @} + + protected: + // The registered message we use to communicate with the destination thread. + static const UINT kCreateWindowExecutorMessage; + + // The function that will be hooked in the destination thread. + // See http://msdn.microsoft.com/en-us/library/ms644981(VS.85).aspx + // for more details. + static LRESULT CALLBACK GetMsgProc(int code, WPARAM wparam, LPARAM lparam); + + // We must remember the hook so that we can unhook when we are done. + HHOOK hook_; + + // We can only work for one thread at a time. Used to validate that the + // call to ICeeeExecutorCreator::Teardown are balanced to a previous call + // to ICeeeExecutorCreator::CreateExecutor. + long current_thread_id_; +}; + +// The executor object that is instantiated in the destination thread and +// then called to... execute stuff... +class ATL_NO_VTABLE CeeeExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<CeeeExecutor, &CLSID_CeeeExecutor>, + public IObjectWithSiteImpl<CeeeExecutor>, + public ICeeeWindowExecutor, + public ICeeeTabExecutor, + public ICeeeCookieExecutor, + public ICeeeInfobarExecutor { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_EXECUTOR) + + DECLARE_NOT_AGGREGATABLE(CeeeExecutor) + BEGIN_COM_MAP(CeeeExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(ICeeeWindowExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(ICeeeCookieExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + DECLARE_CLASSFACTORY() + + // @name ICeeeWindowExecutor implementation. + // @{ + STDMETHOD(Initialize)(CeeeWindowHandle hwnd); + STDMETHOD(GetWindow)(BOOL populate_tabs, CeeeWindowInfo* window_info); + STDMETHOD(GetTabs)(BSTR* tab_list); + STDMETHOD(UpdateWindow)(long left, long top, long width, long height, + CeeeWindowInfo* window_info); + STDMETHOD(RemoveWindow)(); + STDMETHOD(GetTabIndex)(CeeeWindowHandle tab, long* index); + STDMETHOD(MoveTab)(CeeeWindowHandle tab, long index); + STDMETHOD(RemoveTab)(CeeeWindowHandle tab); + STDMETHOD(SelectTab)(CeeeWindowHandle tab); + // @} + + // @name ICeeeTabExecutor implementation. + // @{ + // Initialize was already declared in ICeeeWindowExecutor, so we don't + // add it here, even if it's part of the interface. + STDMETHOD(GetTabInfo)(CeeeTabInfo* tab_info); + STDMETHOD(Navigate)(BSTR url, long flags, BSTR target); + STDMETHOD(InsertCode)(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type); + // @} + + // @name ICeeeCookieExecutor implementation. + // @{ + STDMETHOD(GetCookie)(BSTR url, BSTR name, CeeeCookieInfo* cookie_info); + STDMETHOD(RegisterCookieStore)(); + STDMETHOD(CookieStoreIsRegistered)(); + // @} + + // @name ICeeeInfobarExecutor implementation. + // @{ + STDMETHOD(SetExtensionId)(BSTR extension_id); + STDMETHOD(ShowInfobar)(BSTR url, CeeeWindowHandle* window_handle); + STDMETHOD(OnTopFrameBeforeNavigate)(BSTR url); + // @} + + CeeeExecutor() : hwnd_(NULL) {} + + protected: + // Get the IWebBrowser2 interface of the + // frame event host that was set as our site. + virtual HRESULT GetWebBrowser(IWebBrowser2** browser); + + // Used via EnumChildWindows to get all tabs. + static BOOL CALLBACK GetTabsEnumProc(HWND window, LPARAM param); + + // Ensure we're running inside the right thread. + HRESULT EnsureWindowThread(); + + // The HWND of the tab/window we are associated to. + HWND hwnd_; + + // Extension id. + std::string extension_id_; + + // Get the value of the cookie with the given name, associated with the given + // URL. Returns S_FALSE if the cookie does not exist, and returns an error + // code if something unexpected occurs. + virtual HRESULT GetCookieValue(BSTR url, BSTR name, BSTR* value); + + // Mainly for unit testing purposes. + void set_cookie_store_is_registered(bool is_registered); + + // Instance of InfobarManager for the tab associated with the thread to which + // the executor is attached. + scoped_ptr<infobar_api::InfobarManager> infobar_manager_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ diff --git a/ceee/ie/plugin/bho/executor.rgs b/ceee/ie/plugin/bho/executor.rgs new file mode 100644 index 0000000..4fed5bc --- /dev/null +++ b/ceee/ie/plugin/bho/executor.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{057FCFE3-F872-483d-86B0-0430E375E41F}' = s 'Google CEEE Executor' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + } + } +}
\ No newline at end of file diff --git a/ceee/ie/plugin/bho/executor_creator.rgs b/ceee/ie/plugin/bho/executor_creator.rgs new file mode 100644 index 0000000..3a81441 --- /dev/null +++ b/ceee/ie/plugin/bho/executor_creator.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{4A562910-2D54-4e98-B87F-D4A7F5F5D0B9}' = s 'Google CEEE Executor Creator' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Free' + } + } + } +} diff --git a/ceee/ie/plugin/bho/executor_unittest.cc b/ceee/ie/plugin/bho/executor_unittest.cc new file mode 100644 index 0000000..e260fd3 --- /dev/null +++ b/ceee/ie/plugin/bho/executor_unittest.cc @@ -0,0 +1,1080 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Executor implementation unit tests. + +// MockWin32 must not be included after atlwin, which is included by some +// headers in here, so we need to put it at the top: +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include <wininet.h> + +#include <vector> + +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/common_api_module.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/mock_ie_tab_interfaces.h" +#include "ceee/ie/plugin/bho/executor.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::_; +using testing::AddRef; +using testing::CopyInterfaceToArgument; +using testing::CopyBSTRToArgument; +using testing::InstanceCountMixin; +using testing::Invoke; +using testing::IsNull; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; + +const int kGoodWindowId = 42; +const HWND kGoodWindow = reinterpret_cast<HWND>(kGoodWindowId); +const int kOtherGoodWindowId = 84; +const HWND kOtherGoodWindow = reinterpret_cast<HWND>(kOtherGoodWindowId); +const int kTabIndex = 26; +const int kOtherTabIndex = 62; + +const wchar_t* kUrl1 = L"http://www.google.com"; +const wchar_t* kUrl2 = L"http://myintranet"; +const wchar_t* kTitle1 = L"MyLord"; +const wchar_t* kTitle2 = L"Your MADness"; + +class TestingMockExecutorCreatorTeardown + : public CeeeExecutorCreator, + public InitializingCoClass< + StrictMock<TestingMockExecutorCreatorTeardown>> { + public: + // TODO(mad@chromium.org): Add reference counting testing/validation. + TestingMockExecutorCreatorTeardown() { + hook_ = reinterpret_cast<HHOOK>(1); + current_thread_id_ = 42L; + } + HRESULT Initialize(StrictMock<TestingMockExecutorCreatorTeardown>** self) { + // Yes, this seems fishy, but it is called from InitializingCoClass + // which does it on the class we pass it as a template, so we are OK. + *self = static_cast<StrictMock<TestingMockExecutorCreatorTeardown>*>(this); + return S_OK; + } + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Teardown, HRESULT(long)); +}; + +TEST(ExecutorCreator, ProperTearDownOnDestruction) { + StrictMock<TestingMockExecutorCreatorTeardown>* executor_creator = NULL; + CComPtr<ICeeeExecutorCreator> executor_creator_keeper; + ASSERT_HRESULT_SUCCEEDED(StrictMock<TestingMockExecutorCreatorTeardown>:: + CreateInitialized(&executor_creator, &executor_creator_keeper)); + EXPECT_CALL(*executor_creator, Teardown(42L)).WillOnce(Return(S_OK)); + // The Release of the last reference should call FinalRelease. +} + +// Mock object for some functions used for hooking. +MOCK_STATIC_CLASS_BEGIN(MockHooking) + MOCK_STATIC_INIT_BEGIN(MockHooking) + MOCK_STATIC_INIT(SetWindowsHookEx); + MOCK_STATIC_INIT(UnhookWindowsHookEx); + MOCK_STATIC_INIT(PostThreadMessage); + MOCK_STATIC_INIT(PeekMessage); + MOCK_STATIC_INIT(CallNextHookEx); + MOCK_STATIC_INIT(CoCreateInstance); + MOCK_STATIC_INIT_END() + + MOCK_STATIC4(HHOOK, CALLBACK, SetWindowsHookEx, int, HOOKPROC, HINSTANCE, + DWORD); + MOCK_STATIC1(BOOL, CALLBACK, UnhookWindowsHookEx, HHOOK); + MOCK_STATIC4(BOOL, CALLBACK, PostThreadMessage, DWORD, UINT, WPARAM, LPARAM); + MOCK_STATIC5(BOOL, CALLBACK, PeekMessage, LPMSG, HWND, UINT, UINT, UINT); + MOCK_STATIC4(LRESULT, CALLBACK, CallNextHookEx, HHOOK, int, WPARAM, LPARAM); + MOCK_STATIC5(HRESULT, CALLBACK, CoCreateInstance, REFCLSID, LPUNKNOWN, + DWORD, REFIID, LPVOID*); +MOCK_STATIC_CLASS_END(MockHooking) + +class TestingExecutorCreator : public CeeeExecutorCreator { + public: + // TODO(mad@chromium.org): Add reference counting testing/validation. + STDMETHOD_(ULONG, AddRef)() { return 1; } + STDMETHOD_(ULONG, Release)() { return 1; } + STDMETHOD (QueryInterface)(REFIID, LPVOID*) { return S_OK; } + + // Accessorize... :-) + long current_thread_id() const { return current_thread_id_; } + void set_current_thread_id(long thread_id) { current_thread_id_ = thread_id; } + HHOOK hook() const { return hook_; } + void set_hook(HHOOK hook) { hook_ = hook; } + + // Publicize... + using CeeeExecutorCreator::kCreateWindowExecutorMessage; + using CeeeExecutorCreator::GetMsgProc; +}; + +TEST(ExecutorCreator, CreateExecutor) { + testing::LogDisabler no_dchecks; + + // Start with hooking failure. + StrictMock<MockHooking> mock_hooking; + EXPECT_CALL(mock_hooking, SetWindowsHookEx(WH_GETMESSAGE, _, _, 42L)). + WillOnce(Return(static_cast<HHOOK>(NULL))); + TestingExecutorCreator executor_creator; + EXPECT_HRESULT_FAILED(executor_creator.CreateWindowExecutor(42L, 42L)); + EXPECT_EQ(0L, executor_creator.current_thread_id()); + EXPECT_EQ(NULL, executor_creator.hook()); + + // Then succeed hooking but fail message posting. + EXPECT_CALL(mock_hooking, SetWindowsHookEx(WH_GETMESSAGE, _, _, 42L)). + WillRepeatedly(Return(reinterpret_cast<HHOOK>(1))); + EXPECT_CALL(mock_hooking, PostThreadMessage(42L, _, 0, 0)). + WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_ACCESS); + EXPECT_HRESULT_FAILED(executor_creator.CreateWindowExecutor(42L, 0L)); + ::SetLastError(ERROR_SUCCESS); + + // Success!!! + EXPECT_CALL(mock_hooking, PostThreadMessage(42L, _, 0, 0)). + WillRepeatedly(Return(TRUE)); + EXPECT_HRESULT_SUCCEEDED(executor_creator.CreateWindowExecutor(42L, 0L)); +} + +TEST(ExecutorCreator, Teardown) { + testing::LogDisabler no_dchecks; + + // Start with nothing to do. + TestingExecutorCreator executor_creator; + EXPECT_HRESULT_SUCCEEDED(executor_creator.Teardown(0)); + + // OK check that we properly unhook now... + StrictMock<MockHooking> mock_hooking; + HHOOK fake_hook = reinterpret_cast<HHOOK>(1); + EXPECT_CALL(mock_hooking, UnhookWindowsHookEx(fake_hook)). + WillOnce(Return(TRUE)); + executor_creator.set_current_thread_id(42L); + executor_creator.set_hook(fake_hook); + EXPECT_HRESULT_SUCCEEDED(executor_creator.Teardown(42L)); + EXPECT_EQ(0L, executor_creator.current_thread_id()); + EXPECT_EQ(NULL, executor_creator.hook()); +} + +class ExecutorCreatorTest : public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &executor_, &executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED(testing::MockBroker::CreateInitialized( + &broker_, &broker_keeper_)); + } + + virtual void TearDown() { + executor_ = NULL; + executor_keeper_.Release(); + + broker_ = NULL; + broker_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + testing::MockWindowExecutor* executor_; + CComPtr<ICeeeWindowExecutor> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; +}; + +TEST_F(ExecutorCreatorTest, GetMsgProc) { + testing::LogDisabler no_dchecks; + + // Start with nothing to do. + StrictMock<MockHooking> mock_hooking; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + + TestingExecutorCreator executor_creator; + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_SKIP, 0, 0)); + + // NULL message. + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, 0)); + + // Not our message. + MSG message; + message.message = WM_TIMER; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, + reinterpret_cast<LPARAM>(&message))); + + // OK check our own code paths now. + message.message = executor_creator.kCreateWindowExecutorMessage; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).Times(0); + + // Not a PM_REMOVE message, delegates to PeekMessage. + EXPECT_CALL(mock_hooking, PeekMessage(_, _, _, _, _)).WillOnce(Return(TRUE)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, + reinterpret_cast<LPARAM>(&message))); + + // With a PM_REMOVE, we get the job done. + // But lets see if we can silently handle a CoCreateInstance Failure + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Now fail getting the broker registrar. + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Now fail the registration itself. + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(DoAll(SetArgumentPointee<4>(broker_keeper_.p), + AddRef(broker_keeper_.p), Return(S_OK))); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + DWORD current_thread_id = ::GetCurrentThreadId(); + EXPECT_CALL(*broker_, RegisterWindowExecutor(current_thread_id, + executor_keeper_.p)).WillOnce(Return(E_FAIL)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Success!!! + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(DoAll(SetArgumentPointee<4>(broker_keeper_.p), + AddRef(broker_keeper_.p), Return(S_OK))); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(*broker_, RegisterWindowExecutor(current_thread_id, + executor_keeper_.p)).WillOnce(Return(S_OK)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); +} + +MOCK_STATIC_CLASS_BEGIN(MockWinInet) + MOCK_STATIC_INIT_BEGIN(MockWinInet) + MOCK_STATIC_INIT(InternetGetCookieExW); + MOCK_STATIC_INIT_END() + + MOCK_STATIC6(BOOL, CALLBACK, InternetGetCookieExW, LPCWSTR, LPCWSTR, LPWSTR, + LPDWORD, DWORD, LPVOID); +MOCK_STATIC_CLASS_END(MockWinInet) + + +// Mock object for some functions used for hooking. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIeVersion, GetIeVersion); + MOCK_STATIC_INIT_END() + + MOCK_STATIC0(ie_util::IeVersion, , GetIeVersion); +MOCK_STATIC_CLASS_END(MockIeUtil) + +class TestingExecutor + : public CeeeExecutor, + public InitializingCoClass<TestingExecutor> { + public: + HRESULT Initialize(TestingExecutor** self) { + *self = this; + return S_OK; + } + static void set_tab_windows(std::vector<HWND> tab_windows) { + tab_windows_ = tab_windows; + } + void set_id(HWND hwnd) { + hwnd_ = hwnd; + } + static BOOL MockEnumChildWindows(HWND, WNDENUMPROC, LPARAM p) { + std::vector<HWND>* tab_windows = reinterpret_cast<std::vector<HWND>*>(p); + *tab_windows = tab_windows_; + return TRUE; + } + static void set_cookie_data(const std::wstring& cookie_data) { + cookie_data_ = cookie_data; + } + static BOOL MockInternetGetCookieExW(LPCWSTR, LPCWSTR, LPWSTR data, + LPDWORD size, DWORD, LPVOID) { + EXPECT_TRUE(data != NULL); + EXPECT_TRUE(*size > cookie_data_.size()); + wcscpy_s(data, *size, cookie_data_.data()); + *size = cookie_data_.size() + 1; + return TRUE; + } + + MOCK_METHOD1(GetWebBrowser, HRESULT(IWebBrowser2** browser)); + HRESULT CallGetWebBrowser(IWebBrowser2** browser) { + return CeeeExecutor::GetWebBrowser(browser); + } + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabs, HRESULT(BSTR*)); + HRESULT CallGetTabs(BSTR* tab_list) { + return CeeeExecutor::GetTabs(tab_list); + } + + MOCK_METHOD3(GetCookieValue, HRESULT(BSTR, BSTR, BSTR*)); + HRESULT CallGetCookieValue(BSTR url, BSTR name, BSTR* value) { + return CeeeExecutor::GetCookieValue(url, name, value); + } + + // Publicize... + using CeeeExecutor::GetTabsEnumProc; + using CeeeExecutor::MoveTab; + using CeeeExecutor::set_cookie_store_is_registered; + private: + static std::vector<HWND> tab_windows_; + static std::wstring cookie_data_; +}; +std::vector<HWND> TestingExecutor::tab_windows_; +std::wstring TestingExecutor::cookie_data_; + +// Override to handle DISPID_READYSTATE. +class ExecutorTests: public testing::Test { + public: + void SetUp() { + ASSERT_HRESULT_SUCCEEDED(TestingExecutor::CreateInitialized( + &executor_, &executor_keeper_)); + + browser_ = NULL; + manager_ = NULL; + } + + void MockBrowser() { + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance( + &browser_); + DCHECK(browser_ != NULL); + browser_keeper_ = browser_; + EXPECT_CALL(*executor_, GetWebBrowser(NotNull())). + WillRepeatedly(DoAll(CopyInterfaceToArgument<0>(browser_keeper_), + Return(S_OK))); + } + + void MockSite() { + ASSERT_HRESULT_SUCCEEDED( + testing::MockFrameEventHandlerHost::CreateInitializedIID( + &mock_site_, IID_IUnknown, &mock_site_keeper_)); + executor_->SetSite(mock_site_keeper_); + } + + void MockTabManager() { + CComObject<StrictMock<testing::MockITabWindowManagerIe7>>::CreateInstance( + &manager_); + DCHECK(manager_ != NULL); + manager_keeper_ = manager_; + EXPECT_CALL(ie_tab_interfaces_, TabWindowManagerFromFrame(_, _, _)). + WillRepeatedly(DoAll(AddRef(manager_keeper_.p), SetArgumentPointee<2>( + reinterpret_cast<void*>(manager_keeper_.p)), Return(S_OK))); + + EXPECT_CALL(*manager_, IndexFromHWND(_, _)).WillRepeatedly(DoAll( + SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + } + + void NotRunningInThisWindowThread(HWND window) { + EXPECT_CALL(mock_window_utils_, IsWindowThread(window)). + WillOnce(Return(false)); + } + + void RepeatedlyRunningInThisWindowThread(HWND window) { + EXPECT_CALL(mock_window_utils_, IsWindowThread(window)). + WillRepeatedly(Return(true)); + } + + void RepeatedlyRunningInThisParentWindowThread(HWND child_window, + HWND parent_window) { + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(child_window)). + WillRepeatedly(Return(parent_window)); + EXPECT_CALL(mock_window_utils_, IsWindowThread(parent_window)). + WillRepeatedly(Return(true)); + } + protected: + TestingExecutor* executor_; + StrictMock<testing::MockWindowUtils> mock_window_utils_; + + + // TODO(mad@chromium.org): We should standardize on the Mock COM + // objects creation. + // Using InitializingCoClass would probably be better. + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + CComObject<StrictMock<testing::MockITabWindowManagerIe7>>* manager_; + + testing::MockFrameEventHandlerHost* mock_site_; + + StrictMock<testing::MockUser32> user32_; + StrictMock<testing::MockIeTabInterfaces> ie_tab_interfaces_; + + private: + CComPtr<IUnknown> executor_keeper_; + CComPtr<IUnknown> mock_site_keeper_; + CComPtr<ITabWindowManagerIe7> manager_keeper_; +}; + +TEST_F(ExecutorTests, GetWebBrowser) { + MockSite(); + CComPtr<IWebBrowser2> browser; + { + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->CallGetWebBrowser(&browser)); + } + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(browser_), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetWebBrowser(&browser)); + EXPECT_EQ(browser, browser.p); +} + +TEST_F(ExecutorTests, GetWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->GetWindow(FALSE, NULL)); + + // Fail getting the window RECT. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(user32_, GetForegroundWindow()). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_ACCESS); + common_api::WindowInfo window_info; + EXPECT_HRESULT_FAILED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + ::SetLastError(ERROR_SUCCESS); + + // Success without tab population. + RECT window_rect = {1, 2, 3, 5}; + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + EXPECT_HRESULT_SUCCEEDED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(TRUE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Try the not focused case and a bigger rect. + window_rect.left = 8; + window_rect.top = 13; + window_rect.right = 21; + window_rect.bottom = 34; + EXPECT_CALL(user32_, GetWindowRect(kOtherGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + // Different parent, means we are not focused. + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kOtherGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + executor_->set_id(kOtherGoodWindow); + EXPECT_HRESULT_SUCCEEDED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(FALSE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Fail with tab population. We'll test tab population with GetTabs later. + // GetTabs will fail but at least we confirm that it gets called :-)... + EXPECT_CALL(*executor_, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->GetWindow(TRUE, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); +} + +TEST_F(ExecutorTests, GetTabsEnumProc) { + std::vector<HWND> tab_windows; + HWND bad_window1 = reinterpret_cast<HWND>(666); + HWND bad_window2 = reinterpret_cast<HWND>(999); + EXPECT_CALL(mock_window_utils_, IsWindowClass(kGoodWindow, _)). + WillOnce(Return(true)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(kOtherGoodWindow, _)). + WillOnce(Return(true)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(bad_window1, _)). + WillOnce(Return(false)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(bad_window2, _)). + WillOnce(Return(false)); + LPARAM param = reinterpret_cast<LPARAM>(&tab_windows); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(kGoodWindow, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(bad_window1, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(kOtherGoodWindow, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(bad_window2, param)); + EXPECT_EQ(2, tab_windows.size()); + EXPECT_EQ(kGoodWindow, tab_windows[0]); + EXPECT_EQ(kOtherGoodWindow, tab_windows[1]); +} + +TEST_F(ExecutorTests, GetTabs) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->CallGetTabs(NULL)); + + // We already tested the case where we don't have a tab manager. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + MockTabManager(); + EXPECT_CALL(user32_, EnumChildWindows(kGoodWindow, _, _)). + WillRepeatedly(Invoke(TestingExecutor::MockEnumChildWindows)); + + // No tabs case. + CComBSTR result; + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + EXPECT_STREQ(L"[]", result.m_str); + result.Empty(); + + static const int kBadWindowId = 21; + static const HWND kBadWindow = reinterpret_cast<HWND>(kBadWindowId); + + // Fail to get a tab index. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + std::vector<HWND> tab_windows; + tab_windows.push_back(kGoodWindow); + EXPECT_CALL(user32_, IsWindow(kGoodWindow)).WillOnce(Return(TRUE)); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_FAILED(executor_->CallGetTabs(&result)); + + // Successfully return 1 tab. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wchar_t expected_result[16]; + wnsprintf(expected_result, 16, L"[%d,%d]", kGoodWindowId, kTabIndex); + EXPECT_STREQ(expected_result, result.m_str); + result.Empty(); + + // Successfully return 2 tabs. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kOtherGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kOtherTabIndex), Return(S_OK))); + tab_windows.push_back(kOtherGoodWindow); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wnsprintf(expected_result, 16, L"[%d,%d,%d,%d]", kGoodWindowId, kTabIndex, + kOtherGoodWindowId, kOtherTabIndex); + EXPECT_STREQ(expected_result, result.m_str); + + // Successfully return 2 out of 3 tabs. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kOtherGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kOtherTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kBadWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(user32_, IsWindow(kBadWindow)).WillOnce(Return(FALSE)); + tab_windows.push_back(kBadWindow); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wnsprintf(expected_result, 16, L"[%d,%d,%d,%d]", kGoodWindowId, kTabIndex, + kOtherGoodWindowId, kOtherTabIndex); + EXPECT_STREQ(expected_result, result.m_str); +} + +TEST_F(ExecutorTests, UpdateWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->UpdateWindow(0, 0, 0, 0, NULL)); + // No other failure path, go straight to success... + RepeatedlyRunningInThisWindowThread(kGoodWindow); + + RECT window_rect = {1, 2, 3, 5}; + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + // These will be called from GetWindow which is called at the end of Update. + EXPECT_CALL(user32_, GetForegroundWindow()). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + // Try with no change at first. + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 2, 3, TRUE)).Times(1); + common_api::WindowInfo window_info; + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, -1, -1, &window_info)); + EXPECT_EQ(TRUE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Now try with some changes incrementally. + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 2, 30, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, -1, 30, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 20, 3, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, 20, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 0, 2, 5, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, 0, -1, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 0, 2, 3, 3, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + 0, -1, -1, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 8, 13, 21, 34, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + 8, 13, 21, 34, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); +} + +TEST_F(ExecutorTests, RemoveWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->RemoveWindow()); + + // Now let the manager succeed. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + MockTabManager(); + EXPECT_CALL(*manager_, CloseAllTabs()).WillOnce(Return(S_OK)); + EXPECT_CALL(user32_, PostMessage(kGoodWindow, WM_CLOSE, 0, 0)). + Times(0); + EXPECT_HRESULT_SUCCEEDED(executor_->RemoveWindow()); +} + +TEST_F(ExecutorTests, GetTabInfo) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(NULL)); + + // Now can't get ready state. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(Return(E_FAIL)); + tab_api::TabInfo tab_info; + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(&tab_info)); + DCHECK_EQ((BSTR)NULL, tab_info.url); + DCHECK_EQ((BSTR)NULL, tab_info.title); + DCHECK_EQ((BSTR)NULL, tab_info.fav_icon_url); + + // And can't get the top level browser. + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_COMPLETE), Return(S_OK))); + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(Return(E_FAIL)); + tab_info.Clear(); + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(&tab_info)); + DCHECK_EQ((BSTR)NULL, tab_info.url); + DCHECK_EQ((BSTR)NULL, tab_info.title); + DCHECK_EQ((BSTR)NULL, tab_info.fav_icon_url); + + // Success time! + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_COMPLETE), Return(S_OK))); + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser_); + DCHECK(browser_ != NULL); + browser_keeper_ = browser_; + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillRepeatedly(DoAll(CopyInterfaceToArgument<0>(browser_keeper_), + Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationName(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kTitle1), Return(S_OK))); + + tab_info.Clear(); + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabInfo(&tab_info)); + EXPECT_STREQ(kUrl1, tab_info.url); + EXPECT_STREQ(kTitle1, tab_info.title); + EXPECT_EQ(kCeeeTabStatusComplete, tab_info.status); + + // With other values + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + // Reset the HWND + executor_->set_id(kOtherGoodWindow); + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_LOADING), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl2), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationName(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kTitle2), Return(S_OK))); + + tab_info.Clear(); + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabInfo(&tab_info)); + EXPECT_STREQ(kUrl2, tab_info.url); + EXPECT_STREQ(kTitle2, tab_info.title); + EXPECT_EQ(kCeeeTabStatusLoading, tab_info.status); +} + +TEST_F(ExecutorTests, GetTabIndex) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + EXPECT_CALL(mock_window_utils_, IsWindowThread(kGoodWindow)). + WillOnce(Return(false)); + EXPECT_HRESULT_FAILED(executor_->GetTabIndex(kGoodWindowId, NULL)); + + // Success. + EXPECT_CALL(mock_window_utils_, IsWindowThread(kGoodWindow)). + WillOnce(Return(true)); + MockTabManager(); + long index = 0; + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabIndex(kOtherGoodWindowId, &index)); + EXPECT_EQ(kTabIndex, index); +} + +TEST_F(ExecutorTests, Navigate) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now make GetWebBrowser fail. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(*executor_, GetWebBrowser(NotNull())). + WillRepeatedly(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now get URL fails. + MockBrowser(); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now succeed by not needing to navigate since same URL. + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillRepeatedly(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->Navigate(CComBSTR(kUrl1), 0, NULL)); + + // And finally succeed completely. + EXPECT_CALL(*browser_, Navigate(_, _, _, _, _)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->Navigate(CComBSTR(kUrl2), 0, NULL)); + // And fail if navigate fails. + EXPECT_CALL(*browser_, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(CComBSTR(kUrl2), 0, NULL)); +} + +TEST_F(ExecutorTests, RemoveTab) { + testing::LogDisabler no_dchecks; + MockSite(); + + // Since we're in a WindowExecutor and not a TabExecutor, we don't get our + // HWND and there's no call to parent. + executor_->set_id(kOtherGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Fail to get the tab index. + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_CALL(user32_, GetParent(_)).WillRepeatedly(Return(kGoodWindow)); + MockTabManager(); + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Fail to get the tab interface. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, GetItem(kTabIndex, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Success. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance(&mock_tab); + CComPtr<ITabWindowIe7> mock_tab_holder(mock_tab); + EXPECT_CALL(*manager_, GetItem(kTabIndex, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab), AddRef(mock_tab), Return(S_OK))); + EXPECT_CALL(*mock_tab, Close()).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->RemoveTab(kGoodWindowId)); + + // Failure to close. + EXPECT_CALL(*mock_tab, Close()).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); +} + +TEST_F(ExecutorTests, SelectTab) { + testing::LogDisabler no_dchecks; + MockSite(); + + executor_->set_id(kOtherGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); + + // Fail to get the tab index. + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_CALL(user32_, GetParent(_)).WillRepeatedly(Return(kGoodWindow)); + MockTabManager(); + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); + + // Success! + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, SelectTab(kTabIndex)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->SelectTab(kGoodWindowId)); + + // Failure to Select. + EXPECT_CALL(*manager_, SelectTab(kTabIndex)).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); +} + +TEST_F(ExecutorTests, MoveTab) { + testing::LogDisabler no_dchecks; + MockTabManager(); + MockSite(); + RepeatedlyRunningInThisWindowThread(kGoodWindow); + executor_->set_id(kGoodWindow); + + // Fail to get tab count. + EXPECT_CALL(*manager_, GetCount(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Nothing to be done. + LONG nb_tabs = 3; + EXPECT_CALL(*manager_, GetCount(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(nb_tabs), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 2)); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 99)); + + // Fail to get the first tab interface. + EXPECT_CALL(*manager_, GetItem(0, NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the tab id. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab0; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance( + &mock_tab0); + CComPtr<ITabWindowIe7> mock_tab_holder0(mock_tab0); + EXPECT_CALL(*manager_, GetItem(0, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab0), AddRef(mock_tab0), Return(S_OK))); + EXPECT_CALL(*mock_tab0, GetID(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the second tab interface. + EXPECT_CALL(*mock_tab0, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, GetItem(nb_tabs - 1, NotNull())) + .WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the second tab id. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab1; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance( + &mock_tab1); + CComPtr<ITabWindowIe7> mock_tab_holder1(mock_tab1); + EXPECT_CALL(*manager_, GetItem(nb_tabs - 1, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab1), AddRef(mock_tab1), Return(S_OK))); + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to reposition. + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kOtherGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, RepositionTab(kOtherGoodWindowId, kGoodWindowId, 0)) + .WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Success!! + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kOtherGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, RepositionTab(kOtherGoodWindowId, kGoodWindowId, 0)) + .WillRepeatedly(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 0)); +} + +TEST_F(ExecutorTests, GetCookieValue) { + testing::LogDisabler no_dchecks; + MockWinInet mock_wininet; + + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + // Bad parameters. + EXPECT_HRESULT_FAILED(executor_->CallGetCookieValue(url, name, NULL)); + + // Failure to get cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_PARAMETER); + CComBSTR value; + EXPECT_HRESULT_FAILED(executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Nonexistent cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillOnce(Return(FALSE)); + ::SetLastError(ERROR_NO_MORE_ITEMS); + EXPECT_EQ(S_FALSE, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Malformed cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, _, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + executor_->set_cookie_data(L"malformed_cookie_data"); + EXPECT_EQ(E_FAIL, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + executor_->set_cookie_data(L"AnotherCookie=FOOBAR"); + EXPECT_EQ(E_FAIL, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Well-behaved cookie. + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_STREQ(L"1234567890", value); + executor_->set_cookie_data(L"=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, CComBSTR(L""), &value)); + EXPECT_STREQ(L"1234567890", value); + executor_->set_cookie_data(L"HELLOWORLD="); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_STREQ(L"", value); + executor_->set_cookie_data(L"="); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, CComBSTR(L""), &value)); + EXPECT_STREQ(L"", value); +} + +TEST_F(ExecutorTests, GetCookieValueFlagsByIeVersion) { + testing::LogDisabler no_dchecks; + MockWinInet mock_wininet; + MockIeUtil mock_ie_util; + + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + + // Test IE7 and below. + DWORD expected_flags = 0; + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, + expected_flags, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, + expected_flags, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE6)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + CComBSTR value; + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE7)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + // Test IE8 and above. + expected_flags = INTERNET_COOKIE_HTTPONLY; + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, + expected_flags, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, + expected_flags, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE8)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE9)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + +} + +TEST_F(ExecutorTests, GetCookie) { + testing::LogDisabler no_dchecks; + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + + EXPECT_HRESULT_FAILED(executor_->GetCookie(url, name, NULL)); + + // Failure to get cookie. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillOnce(Return(E_FAIL)); + cookie_api::CookieInfo cookie_info; + cookie_info.name = NULL; + cookie_info.value = NULL; + EXPECT_HRESULT_FAILED(executor_->GetCookie(url, name, &cookie_info)); + EXPECT_EQ((BSTR)NULL, cookie_info.name); + EXPECT_EQ((BSTR)NULL, cookie_info.value); + + // Nonexistent cookie. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillOnce(Return(S_FALSE)); + EXPECT_EQ(S_FALSE, executor_->GetCookie(url, name, &cookie_info)); + EXPECT_EQ((BSTR)NULL, cookie_info.name); + EXPECT_EQ((BSTR)NULL, cookie_info.value); + + // Success. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillRepeatedly(DoAll( + SetArgumentPointee<2>(::SysAllocString(L"abcde")), + Return(S_OK))); + EXPECT_EQ(S_OK, executor_->GetCookie(url, name, &cookie_info)); + EXPECT_STREQ(L"HELLOWORLD", cookie_info.name); + EXPECT_STREQ(L"abcde", cookie_info.value); + EXPECT_EQ(S_OK, executor_->GetCookie(url, CComBSTR("ABC"), &cookie_info)); + EXPECT_STREQ(L"ABC", cookie_info.name); + EXPECT_STREQ(L"abcde", cookie_info.value); +} + +TEST_F(ExecutorTests, RegisterCookieStore) { + testing::LogDisabler no_dchecks; + + executor_->set_cookie_store_is_registered(false); + EXPECT_EQ(S_FALSE, executor_->CookieStoreIsRegistered()); + EXPECT_HRESULT_SUCCEEDED(executor_->RegisterCookieStore()); + EXPECT_EQ(S_OK, executor_->CookieStoreIsRegistered()); + EXPECT_HRESULT_SUCCEEDED(executor_->RegisterCookieStore()); + EXPECT_EQ(S_OK, executor_->CookieStoreIsRegistered()); +} + +// TODO(vadimb@google.com): Add unit tests for infobar APIs. + +} // namespace diff --git a/ceee/ie/plugin/bho/extension_port_manager.cc b/ceee/ie/plugin/bho/extension_port_manager.cc new file mode 100644 index 0000000..9bc28f5 --- /dev/null +++ b/ceee/ie/plugin/bho/extension_port_manager.cc @@ -0,0 +1,193 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Extension port manager takes care of managing the state of +// connecting and connected ports. +#include "ceee/ie/plugin/bho/extension_port_manager.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "ceee/ie/common/chrome_frame_host.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "chrome/browser/automation/extension_automation_constants.h" + +namespace ext = extension_automation_constants; + +ExtensionPortManager::ExtensionPortManager() { +} + +ExtensionPortManager::~ExtensionPortManager() { +} + +void ExtensionPortManager::Initialize(IChromeFrameHost* chrome_frame_host) { + chrome_frame_host_ = chrome_frame_host; +} + +void ExtensionPortManager::CloseAll(IContentScriptNativeApi* instance) { + DCHECK(instance != NULL); + + // TODO(siggi@chromium.org): Deal better with these cases. Connected + // ports probably ought to be closed, and connecting ports may + // need to hang around until we get the connected message, to be + // terminated at that point. + ConnectedPortMap::iterator it(connected_ports_.begin()); + while (it != connected_ports_.end()) { + if (it->second.instance = instance) { + connected_ports_.erase(it++); + } else { + ++it; + } + } + + ConnectingPortMap::iterator jt(connecting_ports_.begin()); + while (jt != connecting_ports_.end()) { + if (jt->second.instance = instance) { + connecting_ports_.erase(jt++); + } else { + ++jt; + } + } +} + +HRESULT ExtensionPortManager::OpenChannelToExtension( + IContentScriptNativeApi* instance, const std::string& extension, + const std::string& channel_name, Value* tab, int cookie) { + int connection_id = next_connection_id_++; + + // Prepare the connection request. + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + if (dict.get() == NULL) + return E_OUTOFMEMORY; + + dict->SetInteger(ext::kAutomationRequestIdKey, ext::OPEN_CHANNEL); + dict->SetInteger(ext::kAutomationConnectionIdKey, connection_id); + dict->SetString(ext::kAutomationExtensionIdKey, extension); + dict->SetString(ext::kAutomationChannelNameKey, channel_name); + dict->Set(ext::kAutomationTabJsonKey, tab); + + // JSON encode it. + std::string request_json; + base::JSONWriter::Write(dict.get(), false, &request_json); + // And fire it off. + HRESULT hr = PostMessageToHost(request_json, + ext::kAutomationPortRequestTarget); + if (FAILED(hr)) + return hr; + + ConnectingPort connecting_port = { instance, cookie }; + connecting_ports_[connection_id] = connecting_port; + + return S_OK; +} + +HRESULT ExtensionPortManager::PostMessage(int port_id, + const std::string& message) { + // Wrap the message for sending as a port request. + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + if (dict.get() == NULL) + return E_OUTOFMEMORY; + + dict->SetInteger(ext::kAutomationRequestIdKey, ext::POST_MESSAGE); + dict->SetInteger(ext::kAutomationPortIdKey, port_id); + dict->SetString(ext::kAutomationMessageDataKey, message); + + // JSON encode it. + std::string message_json; + base::JSONWriter::Write(dict.get(), false, &message_json); + + // And fire it off. + return PostMessageToHost(message_json, + std::string(ext::kAutomationPortRequestTarget)); +} + +void ExtensionPortManager::OnPortMessage(BSTR message) { + std::string message_json = CW2A(message); + scoped_ptr<Value> value(base::JSONReader::Read(message_json, true)); + if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) { + NOTREACHED(); + LOG(ERROR) << "Invalid message"; + return; + } + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + int request = -1; + if (!dict->GetInteger(ext::kAutomationRequestIdKey, &request)) { + NOTREACHED(); + LOG(ERROR) << "Request ID missing"; + return; + } + + if (request == ext::CHANNEL_OPENED) { + int connection_id = -1; + if (!dict->GetInteger(ext::kAutomationConnectionIdKey, &connection_id)) { + NOTREACHED(); + LOG(ERROR) << "Connection ID missing"; + return; + } + + int port_id = -1; + if (!dict->GetInteger(ext::kAutomationPortIdKey, &port_id)) { + NOTREACHED(); + LOG(ERROR) << "Port ID missing"; + return; + } + + ConnectingPortMap::iterator it(connecting_ports_.find(connection_id)); + if (it == connecting_ports_.end()) { + // TODO(siggi@chromium.org): This can happen legitimately on a + // race between connect and document unload. We should + // probably respond with a close port message here. + NOTREACHED(); + LOG(ERROR) << "No such connection id " << connection_id; + return; + } + ConnectingPort port = it->second; + connecting_ports_.erase(it); + // Did it connect successfully? + if (port_id != -1) + connected_ports_[port_id].instance = port.instance; + + port.instance->OnChannelOpened(port.cookie, port_id); + return; + } else if (request == ext::POST_MESSAGE) { + int port_id = -1; + if (!dict->GetInteger(ext::kAutomationPortIdKey, &port_id)) { + NOTREACHED(); + LOG(ERROR) << "No port id"; + return; + } + + std::string data; + if (!dict->GetString(ext::kAutomationMessageDataKey, &data)) { + NOTREACHED(); + LOG(ERROR) << "No message data"; + return; + } + + ConnectedPortMap::iterator it(connected_ports_.find(port_id)); + if (it == connected_ports_.end()) { + NOTREACHED(); + LOG(ERROR) << "No such port " << port_id; + return; + } + + it->second.instance->OnPostMessage(port_id, data); + return; + } else if (request == ext::CHANNEL_CLOSED) { + // TODO(siggi@chromium.org): handle correctly. + return; + } + + NOTREACHED(); +} + +HRESULT ExtensionPortManager::PostMessageToHost(const std::string& message, + const std::string& target) { + // Post our message through the ChromeFrameHost. We allow queueing, + // because we don't synchronize to the destination extension loading. + return chrome_frame_host_->PostMessage(CComBSTR(message.c_str()), + CComBSTR(target.c_str())); +} diff --git a/ceee/ie/plugin/bho/extension_port_manager.h b/ceee/ie/plugin/bho/extension_port_manager.h new file mode 100644 index 0000000..dc156da --- /dev/null +++ b/ceee/ie/plugin/bho/extension_port_manager.h @@ -0,0 +1,86 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Extension port manager takes care of managing the state of +// connecting and connected ports. +#ifndef CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <map> +#include <string> +#include "base/values.h" + +// Fwd. +class IContentScriptNativeApi; +class IChromeFrameHost; + +// The extension port manager takes care of: +// - Managing the state of connecting and connected ports +// - Transforming connection and post requests to outgoing message traffic. +// - Processing incoming message traffic and routing it to the appropriate +// Native API instances for handling. +class ExtensionPortManager { + public: + ExtensionPortManager(); + virtual ~ExtensionPortManager(); + + void Initialize(IChromeFrameHost* host); + + // Cleanup ports on native API uninitialization. + // @param instance the instance that's going away. + void CloseAll(IContentScriptNativeApi* instance); + + // Request opening a chnnel to an extension. + // @param instance the API instance that will be handling this connection. + // @param extension the extension to connect to. + // @param tab info on the tab we're initiating a connection from. + // @param cookie an opaque cookie that will be handed back to the + // instance once the connection completes or fails. + HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + Value* tab, + int cookie); + + // Post a message on an open port. + // @param port_id the port's ID. + // @param message the message to send. + HRESULT PostMessage(int port_id, const std::string& message); + + // Process an incoming automation port message from the host. + // @param message the automation message to process. + void OnPortMessage(BSTR message); + + private: + virtual HRESULT PostMessageToHost(const std::string& message, + const std::string& target); + + // Represents a connected port. + struct ConnectedPort { + CComPtr<IContentScriptNativeApi> instance; + }; + typedef std::map<int, ConnectedPort> ConnectedPortMap; + + // Represents a connecting port. + struct ConnectingPort { + CComPtr<IContentScriptNativeApi> instance; + int cookie; + }; + typedef std::map<int, ConnectingPort> ConnectingPortMap; + + // Map from port_id to page api instance. + ConnectedPortMap connected_ports_; + + // Map from connection_id to page api and callback instances. + ConnectingPortMap connecting_ports_; + + // The next connection id we'll assign. + int next_connection_id_; + + CComPtr<IChromeFrameHost> chrome_frame_host_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/frame_event_handler.cc b/ceee/ie/plugin/bho/frame_event_handler.cc new file mode 100644 index 0000000..a32cc6f --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler.cc @@ -0,0 +1,518 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler implementation. +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include <mshtml.h> +#include <shlguid.h> +#include "base/file_util.h" +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/com_utils.h" +#include "chrome/common/extensions/extension_resource.h" +#include "third_party/activscp/activdbg.h" +#include "toolband.h" // NOLINT + +// {242CD33B-B386-44a7-B685-3A9999B95420} +const GUID IID_IFrameEventHandler = + { 0x242cd33b, 0xb386, 0x44a7, + { 0xb6, 0x85, 0x3a, 0x99, 0x99, 0xb9, 0x54, 0x20 } }; + +// {E68D8538-0E0F-4d42-A4A2-1A635675F376} +const GUID IID_IFrameEventHandlerHost = + { 0xe68d8538, 0xe0f, 0x4d42, + { 0xa4, 0xa2, 0x1a, 0x63, 0x56, 0x75, 0xf3, 0x76 } }; + + +FrameEventHandler::FrameEventHandler() + : property_notify_sink_cookie_(kInvalidCookie), + advise_sink_cookie_(kInvalidCookie), + document_ready_state_(READYSTATE_UNINITIALIZED), + initialized_debugging_(false), + loaded_css_(false), + loaded_start_scripts_(false), + loaded_end_scripts_(false) { +} + +FrameEventHandler::~FrameEventHandler() { + DCHECK_EQ(property_notify_sink_cookie_, kInvalidCookie); + DCHECK_EQ(advise_sink_cookie_, kInvalidCookie); +} + +HRESULT FrameEventHandler::Initialize(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + DCHECK(browser != NULL); + DCHECK(host != NULL); + + InitializeContentScriptManager(); + + CComPtr<IDispatch> document_disp; + HRESULT hr = browser->get_Document(&document_disp); + if (FAILED(hr)) { + // This should never happen. + NOTREACHED() << "IWebBrowser2::get_Document failed " << com::LogHr(hr); + return hr; + } + + // We used to check for whether the document implements IHTMLDocument2 + // to see whether we have an HTML document instance, but that check + // proved not to be specific enough, as e.g. Google Chrome Frame implements + // IHTMLDocument2. So instead we check specifically for MSHTMLs CLSID. + CComQIPtr<IPersist> document_persist(document_disp); + bool document_is_mshtml = false; + if (document_persist != NULL) { + CLSID clsid = {}; + hr = document_persist->GetClassID(&clsid); + if (SUCCEEDED(hr) && clsid == CLSID_HTMLDocument) + document_is_mshtml = true; + } + + // Check that the document is an MSHTML instance, as opposed to e.g. a + // PDF document or a ChromeFrame server. + if (document_is_mshtml) { + document_ = document_disp; + DCHECK(document_ != NULL); + + // Attach to the document, any error here is abnormal + // and should be returned to our caller. + hr = AttachToHtmlDocument(browser, parent_browser, host); + + if (SUCCEEDED(hr)) + // If we have a parent browser, then this frame is an iframe and we only + // want to match content scripts where all_frames is true. + hr = content_script_manager_->Initialize(host, parent_browser != NULL); + + if (FAILED(hr)) + TearDown(); + + return hr; + } + + // It wasn't an HTML document, we kindly decline to attach to this one. + return E_DOCUMENT_NOT_MSHTML; +} + +HRESULT FrameEventHandler::AttachToHtmlDocument(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + DCHECK(browser); + DCHECK(host); + + // Check that we can retrieve the document's ready state. + READYSTATE new_ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = GetDocumentReadyState(&new_ready_state); + DCHECK(SUCCEEDED(hr)) << "Failed to retrieve document ready state " + << com::LogHr(hr); + + // Set up the advise sink. + if (SUCCEEDED(hr)) { + CComQIPtr<IOleObject> ole_object(document_); + DCHECK(ole_object != NULL) << "Document is not an OLE Object"; + + hr = ole_object->Advise(this, &advise_sink_cookie_); + } + + if (SUCCEEDED(hr)) { + DCHECK(advise_sink_cookie_ != kInvalidCookie); + } else { + DCHECK(advise_sink_cookie_ == kInvalidCookie); + LOG(ERROR) << "IOleObject::Advise failed " << com::LogHr(hr); + } + + // Set up the property notify sink. + if (SUCCEEDED(hr)) { + hr = AtlAdvise(document_, + GetUnknown(), + IID_IPropertyNotifySink, + &property_notify_sink_cookie_); + if (SUCCEEDED(hr)) { + DCHECK(property_notify_sink_cookie_ != kInvalidCookie); + } else { + DCHECK(property_notify_sink_cookie_ == kInvalidCookie); + LOG(ERROR) << "Subscribing IPropertyNotifySink failed " + << com::LogHr(hr); + } + } + + if (SUCCEEDED(hr)) { + // We're all good. + browser_ = browser; + parent_browser_ = parent_browser; + host_ = host; + + if (!initialized_debugging_) { + initialized_debugging_ = true; + ScriptHost::default_debug_application()->Initialize(document_); + } + + // Notify the host that we've attached this browser instance. + hr = host_->AttachBrowser(browser_, parent_browser_, this); + } else { + // We had some sort of failure, tear down any initialization we've done. + TearDown(); + } + + return hr; +} + +HRESULT FrameEventHandler::AddSubHandler(IFrameEventHandler* handler) { + std::pair<SubHandlerSet::iterator, bool> ret = + sub_handlers_.insert(CAdapt<CComPtr<IFrameEventHandler> >(handler)); + DCHECK(ret.second == true) << "Double notification for a sub handler"; + + return S_OK; +} + +HRESULT FrameEventHandler::RemoveSubHandler(IFrameEventHandler* handler) { + size_t removed = + sub_handlers_.erase(CAdapt<CComPtr<IFrameEventHandler> >(handler)); + DCHECK_NE(0U, removed) << "Errant removal notification for a sub handler"; + DCHECK_EQ(1U, removed); // There can be only one in a set. + + return S_OK; +} + +void FrameEventHandler::InitializeContentScriptManager() { + content_script_manager_.reset(new ContentScriptManager); +} + +HRESULT FrameEventHandler::GetExtensionResourceContents(const FilePath& file, + std::string* contents) { + DCHECK(contents); + + std::wstring extension_path; + host_->GetExtensionPath(&extension_path); + DCHECK(extension_path.size()); + + FilePath file_path(ExtensionResource::GetFilePath( + FilePath(extension_path), file)); + + if (!file_util::ReadFileToString(file_path, contents)) { + return STG_E_FILENOTFOUND; + } + + return S_OK; +} + +HRESULT FrameEventHandler::GetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents) { + DCHECK(contents); + + // Must have one of code or file, but not both. + bool has_code = ::SysStringLen(code) > 0; + bool has_file = ::SysStringLen(file) > 0; + if (!(has_code ^ has_file)) + return E_INVALIDARG; + + if (has_code) { + *contents = code; + } else { + std::string code_string_a; + HRESULT hr = GetExtensionResourceContents(FilePath(file), &code_string_a); + if (FAILED(hr)) + return hr; + + *contents = CA2W(code_string_a.c_str()); + } + + return S_OK; +} + +void FrameEventHandler::TearDownSubHandlers() { + // Copy the set to avoid problems with reentrant + // modification of the set during teardown. + SubHandlerSet sub_handlers(sub_handlers_); + SubHandlerSet::iterator it(sub_handlers.begin()); + SubHandlerSet::iterator end(sub_handlers.end()); + for (; it != end; ++it) { + DCHECK(it->m_T); + it->m_T->TearDown(); + } + + // In the case where we tear down subhandlers on a readystate + // drop, it appears that by this time, the child->parent relationship + // between the sub-browsers and our attached browser has been severed. + // We therefore can't expect our host to have issued RemoveSubHandler + // for our subhandlers, and should do manual cleanup. + sub_handlers_.clear(); +} + +void FrameEventHandler::TearDown() { + // Start by tearing down subframes. + TearDownSubHandlers(); + + // Flush content script state. + content_script_manager_->TearDown(); + + // Then detach all event sinks. + if (host_ != NULL) { + DCHECK(browser_ != NULL); + + // Notify our host that we're detaching the browser. + HRESULT hr = host_->DetachBrowser(browser_, parent_browser_, this); + DCHECK(SUCCEEDED(hr)); + + parent_browser_.Release(); + browser_.Release(); + host_.Release(); + } + + // Unadvise any document events we're sinking. + if (property_notify_sink_cookie_ != kInvalidCookie) { + HRESULT hr = AtlUnadvise(document_, + IID_IPropertyNotifySink, + property_notify_sink_cookie_); + DCHECK(SUCCEEDED(hr)) << "Failed to unsubscribe IPropertyNotifySink " + << com::LogHr(hr); + property_notify_sink_cookie_ = kInvalidCookie; + } + + if (advise_sink_cookie_ != kInvalidCookie) { + CComQIPtr<IOleObject> ole_object(document_); + DCHECK(ole_object != NULL); + HRESULT hr = ole_object->Unadvise(advise_sink_cookie_); + DCHECK(SUCCEEDED(hr)) << "Failed to unadvise IOleObject " << com::LogHr(hr); + advise_sink_cookie_ = kInvalidCookie; + } + + document_.Release(); +} + +HRESULT FrameEventHandler::InsertCode(BSTR code, BSTR file, + CeeeTabCodeType type) { + std::wstring extension_id; + host_->GetExtensionId(&extension_id); + if (!extension_id.size()) { + // We haven't loaded an extension yet; defer until we do. + DeferredInjection injection = {code ? code : L"", file ? file : L"", type}; + deferred_injections_.push_back(injection); + LOG(INFO) << "Deferring InsertCode"; + return S_OK; + } else { + LOG(INFO) << "Executing InsertCode"; + } + + std::wstring code_string; + HRESULT hr = GetCodeOrFileContents(code, file, &code_string); + if (FAILED(hr)) + return hr; + + if (type == kCeeeTabCodeTypeCss) { + return content_script_manager_->InsertCss(code_string.c_str(), document_); + } else if (type == kCeeeTabCodeTypeJs) { + const wchar_t* file_string; + if (::SysStringLen(file) > 0) { + file_string = OLE2W(file); + } else { + // TODO(rogerta@chromium.org): should not use a hardcoded name, + // but one either extracted from the script itself or hashed + // from the code. bb2146033. + file_string = L"ExecuteScript.code"; + } + + return content_script_manager_->ExecuteScript(code_string.c_str(), + file_string, + document_); + } + + return E_INVALIDARG; +} + +void FrameEventHandler::RedoDoneInjections() { + // Any type of injection we attempted to do before extensions were + // loaded would have been a no-op. This function is called once + // extensions have been loaded to redo the ones that have + // already been attempted. Those that have not yet been attempted will + // happen later, when appropriate (e.g. on readystate complete). + GURL match_url(com::ToString(browser_url_)); + + if (loaded_css_) { + LoadCss(match_url); + } + + if (loaded_start_scripts_) { + LoadStartScripts(match_url); + } + + if (loaded_end_scripts_) { + LoadEndScripts(match_url); + } + + // Take a copy to avoid an endless loop if we should for whatever + // reason still not know the extension dir (this is just belt and + // suspenders). + std::list<DeferredInjection> injections = deferred_injections_; + std::list<DeferredInjection>::iterator it = injections.begin(); + for (; it != injections.end(); ++it) { + InsertCode(CComBSTR(it->code.c_str()), + CComBSTR(it->file.c_str()), + it->type); + } +} + +void FrameEventHandler::FinalRelease() { + if (initialized_debugging_) + ScriptHost::default_debug_application()->Terminate(); +} + +HRESULT FrameEventHandler::GetDocumentReadyState(READYSTATE* ready_state) { + DCHECK(document_ != NULL); + + CComVariant ready_state_var; + CComDispatchDriver document(document_); + HRESULT hr = document.GetProperty(DISPID_READYSTATE, &ready_state_var); + if (FAILED(hr)) + return hr; + + if (ready_state_var.vt != VT_I4) + return E_UNEXPECTED; + + READYSTATE tmp = static_cast<READYSTATE>(ready_state_var.lVal); + DCHECK(tmp >= READYSTATE_UNINITIALIZED && tmp <= READYSTATE_COMPLETE); + *ready_state = tmp; + return S_OK; +} + +void FrameEventHandler::HandleReadyStateChange(READYSTATE old_ready_state, + READYSTATE new_ready_state) { + // We should always have been notified of our corresponding document's URL + DCHECK(browser_url_ != NULL); + DCHECK(document_ != NULL); + + if (new_ready_state <= READYSTATE_LOADING && + old_ready_state > READYSTATE_LOADING) { + ReInitialize(); + } + + GURL match_url(com::ToString(browser_url_)); + + if (new_ready_state >= READYSTATE_LOADED && !loaded_css_) { + loaded_css_ = true; + LoadCss(match_url); + } + + if (new_ready_state >= READYSTATE_LOADED && !loaded_start_scripts_) { + loaded_start_scripts_ = true; + LoadStartScripts(match_url); + } + + if (new_ready_state == READYSTATE_COMPLETE && !loaded_end_scripts_) { + loaded_end_scripts_ = true; + LoadEndScripts(match_url); + } + + // Let our host know of this change. + DCHECK(host_ != NULL); + if (host_ != NULL) { + HRESULT hr = host_->OnReadyStateChanged(new_ready_state); + DCHECK(SUCCEEDED(hr)) << com::LogHr(hr); + } +} + +void FrameEventHandler::SetNewReadyState(READYSTATE new_ready_state) { + READYSTATE old_ready_state = document_ready_state_; + if (old_ready_state != new_ready_state) { + document_ready_state_ = new_ready_state; + HandleReadyStateChange(old_ready_state, new_ready_state); + } +} + +void FrameEventHandler::ReInitialize() { + // This function should only be called when the readystate + // drops from above LOADING to LOADING (or below). + DCHECK(document_ready_state_ <= READYSTATE_LOADING); + + // A readystate drop means our associated document is being + // re-navigated or refreshed. We need to tear down all sub + // frame handlers, because they'll otherwise receive no + // notification of this event. + TearDownSubHandlers(); + + // Reset our indicators, and manager. + // We'll need to re-inject on subsquent up-transitions. + loaded_css_ = false; + loaded_start_scripts_ = false; + loaded_end_scripts_ = false; + + content_script_manager_->TearDown(); +} + +STDMETHODIMP FrameEventHandler::OnChanged(DISPID property_disp_id) { + if (property_disp_id == DISPID_READYSTATE) { + READYSTATE new_ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = GetDocumentReadyState(&new_ready_state); + DCHECK(SUCCEEDED(hr)); + SetNewReadyState(new_ready_state); + } + return S_OK; +} + +STDMETHODIMP FrameEventHandler::OnRequestEdit(DISPID property_disp_id) { + return S_OK; +} + +STDMETHODIMP_(void) FrameEventHandler::OnDataChange(FORMATETC* format, + STGMEDIUM* storage) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnViewChange(DWORD aspect, LONG index) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnRename(IMoniker* moniker) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnSave() { +} + +STDMETHODIMP_(void) FrameEventHandler::OnClose() { + // TearDown may release all references to ourselves, so we have to + // maintain a self-reference over this call. + CComPtr<IUnknown> staying_alive_oooh_oooh_oooh_staying_alive(GetUnknown()); + + TearDown(); +} + +void FrameEventHandler::GetUrl(BSTR* url) { + *url = CComBSTR(browser_url_).Detach(); +} + +HRESULT FrameEventHandler::SetUrl(BSTR url) { + // This method is called by our host on NavigateComplete, at which time + // our corresponding browser is either doing first-time navigation, or + // it's being re-navigated to a different URL, or possibly to the same URL. + // Note that as there's no NavigateComplete event fired for refresh, + // we won't hit here in that case, but will rather notice that case on + // our readystate dropping on an IPropertyNotifySink change notification. + // The readystate for first-time navigation on the top-level browser + // will be interactive, whereas for subsequent navigations and for + // sub-browsers, the readystate will be loading. + // This would be a fine time to probe and act on the readystate, except + // for the fact that in some weird cases, GetDocumentReadyState incorrectly + // returns READYSTATE_COMPLETE, which means we act too soon. So instead + // we patiently wait for a property change notification and act on the + // document's ready state then. + if (browser_url_ == url) + return S_FALSE; + + browser_url_ = url; + return S_OK; +} + +void FrameEventHandler::LoadCss(const GURL& match_url) { + content_script_manager_->LoadCss(match_url, document_); +} + +void FrameEventHandler::LoadStartScripts(const GURL& match_url) { + // Run the document start scripts. + content_script_manager_->LoadStartScripts(match_url, document_); +} + +void FrameEventHandler::LoadEndScripts(const GURL& match_url) { + // Run the document end scripts. + content_script_manager_->LoadEndScripts(match_url, document_); +} diff --git a/ceee/ie/plugin/bho/frame_event_handler.h b/ceee/ie/plugin/bho/frame_event_handler.h new file mode 100644 index 0000000..16bba7d --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler.h @@ -0,0 +1,309 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler declaration. + +#ifndef CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ +#define CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <mshtml.h> // Must be before <exdisp.h> +#include <exdisp.h> +#include <ocidl.h> +#include <objidl.h> + +#include <list> +#include <set> +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/common/initializing_coclass.h" + +#include "toolband.h" // NOLINT + +// Error code to signal that a browser has a non-MSHTML document attached. +const HRESULT E_DOCUMENT_NOT_MSHTML = + MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200); + +// This is the interface the frame event handler presents to its host. +// The BHO maintains a mapping of browsers to frame event handlers, +// and takes care of notifying frame event handlers when they acquire +// a subframe. +extern const GUID IID_IFrameEventHandler; +class IFrameEventHandler: public IUnknown { + public: + // Get the frame's current URL. + virtual void GetUrl(BSTR* url) = 0; + + // Notify the frame event handler of the associated browser's current URL. + virtual HRESULT SetUrl(BSTR url) = 0; + + // Returns the current document ready state. + virtual READYSTATE GetReadyState() = 0; + + // Notify the frame event handler that |handler| has been attached + // to an immediate sub-browser of the browser it's attached to. + virtual HRESULT AddSubHandler(IFrameEventHandler* handler) = 0; + // Notify the frame event handler that |handler| has detached from + // an immediate sub-browser of the browser it's attached to. + virtual HRESULT RemoveSubHandler(IFrameEventHandler* handler) = 0; + + // A parent frame handler has seen a readystate drop, indicating + // that our associated browser instance has gone out of scope. + // @pre this frame event handler is attached to a browser. + virtual void TearDown() = 0; + + // Insert code inside a tab whether by execution or injection. + // @param code The code to insert. + // @param file A file containing the code to insert. + // @param type The type of the code to insert. + virtual HRESULT InsertCode(BSTR code, BSTR file, + CeeeTabCodeType type) = 0; + + // Re-does any injections of code or CSS that should have been already done. + // Called by the host when extensions have been loaded, as before then we + // don't have details on which scripts to load. + virtual void RedoDoneInjections() = 0; +}; + +// Fwd. +class IExtensionPortMessagingProvider; + +// The interface presented to a frame event handler by its host. +extern const GUID IID_IFrameEventHandlerHost; +class IFrameEventHandlerHost: public IUnknown { + public: + // Notify the host that |handler| has attached to |browser|, + // whose parent browser is |parent_browser|. + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) = 0; + // Notify the host that |handler| has detached from |browser|. + // whose parent browser is |parent_browser|. + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) = 0; + // Returns the top level browser associated to this frame event handler. + virtual HRESULT GetTopLevelBrowser(IWebBrowser2** browser) = 0; + + // Notify the host that our ready state has changed. + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state) = 0; + + // Get the current ready state of the host. + virtual HRESULT GetReadyState(READYSTATE* ready_state) = 0; + + // Retrieve the CSS content from user scripts that match @p url. + // @param url The URL to match. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param css_content The single stream of CSS content. + virtual HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) = 0; + + // Retrieve the JS content from user scripts that match @p url. + // @param url The URL to match. + // @param location The location where the scripts will be run at. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param js_file_list A vector of file path/content pairs. + virtual HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list) = 0; + + // Retrieve our extension ID. + // @param extension_id on success returns the extension id. + virtual HRESULT GetExtensionId(std::wstring* extension_id) = 0; + + // Retrieve our extension base dir. + // @param extension_path on success returns the extension base dir. + virtual HRESULT GetExtensionPath(std::wstring* extension_path) = 0; + + // Retrieve the native API host. + // @param host on success returns the native API host. + virtual HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) = 0; + + // Execute the given code or file in the top level frame or all frames. + // Note that only one of code or file can be non-empty. + // @param code The script to execute. + // @param file A file containing the script to execute. + // @param all_frames If true, applies to the top level frame as well as + // contained iframes. Otherwise, applies only to the + // top level frame. + // @param type The type of the code to insert. + virtual HRESULT InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) = 0; +}; + +// The frame event handler is attached to an IWebBrowser2 instance, either +// a top-level instance or sub instances associated with frames. +// It is responsible for listening for events from the associated frame in +// order to e.g. instantiate content scripts that interact with the frame. +class FrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<FrameEventHandler>, + public IPropertyNotifySink, + public IAdviseSink, + public IFrameEventHandler { + public: + BEGIN_COM_MAP(FrameEventHandler) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + COM_INTERFACE_ENTRY(IAdviseSink) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandler, IFrameEventHandler) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + FrameEventHandler(); + virtual ~FrameEventHandler(); + + // Initialize the event handler. + // @returns S_OK on success, E_DOCUMENT_NOT_MSHTML if the browser + // is not attached to an MSTHML document instance. + HRESULT Initialize(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host); + void FinalRelease(); + + // @name IPropertyNotifySink implementation + // @{ + STDMETHOD(OnChanged)(DISPID property_disp_id); + STDMETHOD(OnRequestEdit)(DISPID property_disp_id); + // @} + + // @name IAdviseSink implementation + // @{ + STDMETHOD_(void, OnDataChange)(FORMATETC* format, STGMEDIUM* storage); + STDMETHOD_(void, OnViewChange)(DWORD aspect, LONG index); + STDMETHOD_(void, OnRename)(IMoniker* moniker); + STDMETHOD_(void, OnSave)(); + // We use this event to tear down + STDMETHOD_(void, OnClose)(); + // @} + + // @name IFrameEventHandler implementation. + // @{ + virtual void GetUrl(BSTR* url); + virtual HRESULT SetUrl(BSTR url); + virtual READYSTATE GetReadyState() { return document_ready_state_; } + virtual HRESULT AddSubHandler(IFrameEventHandler* handler); + virtual HRESULT RemoveSubHandler(IFrameEventHandler* handler); + virtual void TearDown(); + virtual HRESULT InsertCode(BSTR code, BSTR file, CeeeTabCodeType type); + virtual void RedoDoneInjections(); + // @} + + BSTR browser_url() const { return browser_url_; } + + protected: + // Reinitialize state on a readystate drop to LOADING, which + // signifies that either our associated browser is being refreshed + // or is being re-navigated. + void ReInitialize(); + + // Issues a teardown call to all sub frame handlers. + void TearDownSubHandlers(); + + // Creates and initializes the content script manager for this handler. + // This method is virtual to allow overriding by tests. + virtual void InitializeContentScriptManager(); + + // Reads the contents of an extension resource. The file path is assumed + // to be relative to the root of the extension. + virtual HRESULT GetExtensionResourceContents(const FilePath& file, + std::string* contents); + + // Validates and returns the code content of either code or file. + // Used by ExecuteScript and InsertCss. + virtual HRESULT GetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents); + + // Handle a ready state change from document_ready_state_ to new_ready_state. + virtual void HandleReadyStateChange(READYSTATE old_ready_state, + READYSTATE new_ready_state); + + // Change the current document ready state to new_ready_state + // and invoke HandleReadyStateChange if the ready state changed. + void SetNewReadyState(READYSTATE new_ready_state); + + // Retrieves our document's ready state. + HRESULT GetDocumentReadyState(READYSTATE* ready_state); + + // Inject CSS for @p match_url. + virtual void LoadCss(const GURL& match_url); + + // Inject start scripts for @p match_url. + virtual void LoadStartScripts(const GURL& match_url); + + // Inject end scripts for @p match_url. + virtual void LoadEndScripts(const GURL& match_url); + + // Subscribes for events etc. + // @pre document_ is non-NULL and implements IHTMLDocument2. + HRESULT AttachToHtmlDocument(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host); + + // Sentinel value for non-subscribed cookies. + static const DWORD kInvalidCookie = -1; + + // Connection cookie for IPropertyNotifySink connection point. + DWORD property_notify_sink_cookie_; + + // Connection cookie for IAdviseSink subscription. + DWORD advise_sink_cookie_; + + // The browser we're attached to. + CComPtr<IWebBrowser2> browser_; + + // Our parent browser, if any. + CComPtr<IWebBrowser2> parent_browser_; + + // The current URL browser_ is navigated or navigating to. + CComBSTR browser_url_; + + // Our host object. + CComPtr<IFrameEventHandlerHost> host_; + + // The document object of browser_, but only if it implements + // IHTMLDocument2 - e.g. is an HTML document object. + CComPtr<IHTMLDocument2> document_; + + // The last recorded document_ ready state. + READYSTATE document_ready_state_; + + // True iff we've initialized debugging. + bool initialized_debugging_; + + // Each of these is true iff we've attempted content script + // CSS/start/end script injection. + bool loaded_css_; + bool loaded_start_scripts_; + bool loaded_end_scripts_; + + // Our content script manager. + scoped_ptr<ContentScriptManager> content_script_manager_; + + typedef std::set<CAdapt<CComPtr<IFrameEventHandler> > > SubHandlerSet; + // The sub frames handlers we've been advised of by our host. + SubHandlerSet sub_handlers_; + + struct DeferredInjection { + std::wstring code; + std::wstring file; + CeeeTabCodeType type; + }; + + // Injections we deferred until extension information is available. + std::list<DeferredInjection> deferred_injections_; + + DISALLOW_COPY_AND_ASSIGN(FrameEventHandler); +}; + +#endif // CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ diff --git a/ceee/ie/plugin/bho/frame_event_handler_unittest.cc b/ceee/ie/plugin/bho/frame_event_handler_unittest.cc new file mode 100644 index 0000000..2094591 --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler_unittest.cc @@ -0,0 +1,740 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler unittests. +#include "ceee/ie/plugin/bho/frame_event_handler.h" + +#include <atlctl.h> +#include <map> + +#include "base/file_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::IOleObjecMockImpl; +using testing::IWebBrowser2MockImpl; +using testing::InstanceCountMixinBase; +using testing::InstanceCountMixin; + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Return; +using testing::StrEq; +using testing::StrictMock; +using testing::SetArgumentPointee; + +ScriptHost::DebugApplication debug_app(L"FrameEventHandlerUnittest"); + +// We need to implement this interface separately, because +// there are name conflicts with methods in IConnectionPointImpl, +// and we don't want to override those methods. +class TestIOleObjectImpl: public StrictMock<IOleObjecMockImpl> { + public: + // Implement the advise functions. + STDMETHOD(Advise)(IAdviseSink* sink, DWORD* advise_cookie) { + return advise_holder_->Advise(sink, advise_cookie); + } + STDMETHOD(Unadvise)(DWORD advise_cookie) { + return advise_holder_->Unadvise(advise_cookie); + } + STDMETHOD(EnumAdvise)(IEnumSTATDATA **enum_advise) { + return advise_holder_->EnumAdvise(enum_advise); + } + + HRESULT Initialize() { + return ::CreateOleAdviseHolder(&advise_holder_); + } + + public: + CComPtr<IOleAdviseHolder> advise_holder_; +}; + +class IPersistMockImpl: public IPersist { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetClassID, HRESULT(CLSID *clsid)); +}; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public InstanceCountMixin<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl>, + public StrictMock<IPersistMockImpl>, + public TestIOleObjectImpl, + public IConnectionPointContainerImpl<MockDocument>, + public IConnectionPointImpl<MockDocument, &IID_IPropertyNotifySink> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + COM_INTERFACE_ENTRY(IOleObject) + COM_INTERFACE_ENTRY(IPersist) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(MockDocument) + CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink) + END_CONNECTION_POINT_MAP() + + MockDocument() : ready_state_(READYSTATE_UNINITIALIZED) { + } + + void FireOnClose() { + EXPECT_HRESULT_SUCCEEDED(advise_holder_->SendOnClose()); + } + + // Override to handle DISPID_READYSTATE. + STDMETHOD(Invoke)(DISPID member, REFIID iid, LCID locale, WORD flags, + DISPPARAMS* params, VARIANT *result, EXCEPINFO* ex_info, + unsigned int* arg_error) { + if (member == DISPID_READYSTATE && flags == DISPATCH_PROPERTYGET) { + result->vt = VT_I4; + result->lVal = ready_state_; + return S_OK; + } + + return StrictMock<IHTMLDocument2MockImpl>::Invoke(member, iid, locale, + flags, params, result, ex_info, arg_error); + } + + STDMETHOD(get_URL)(BSTR* url) { + return url_.CopyTo(url); + } + + HRESULT Initialize(MockDocument** self) { + *self = this; + return TestIOleObjectImpl::Initialize(); + } + + + READYSTATE ready_state() const { return ready_state_; } + void set_ready_state(READYSTATE ready_state) { ready_state_ = ready_state; } + + void FireReadyStateChange() { + CFirePropNotifyEvent::FireOnChanged(GetUnknown(), DISPID_READYSTATE); + } + + // Sets our ready state and fires the change event. + void SetReadyState(READYSTATE new_ready_state) { + if (ready_state_ == new_ready_state) + return; + ready_state_ = new_ready_state; + FireReadyStateChange(); + } + + const wchar_t *url() const { return com::ToString(url_); } + void set_url(const wchar_t* url) { url_ = url; } + + protected: + CComBSTR url_; + READYSTATE ready_state_; +}; + +class MockBrowser + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockBrowser>, + public InstanceCountMixin<MockBrowser>, + public StrictMock<IWebBrowser2MockImpl> { + public: + BEGIN_COM_MAP(MockBrowser) + COM_INTERFACE_ENTRY(IWebBrowser2) + COM_INTERFACE_ENTRY(IWebBrowserApp) + COM_INTERFACE_ENTRY(IWebBrowser) + END_COM_MAP() + + HRESULT Initialize(MockBrowser** self) { + *self = this; + return S_OK; + } + + STDMETHOD(get_Parent)(IDispatch** parent) { + this->GetUnknown()->AddRef(); + *parent = this; + return S_OK; + } +}; + +class IFrameEventHandlerHostMockImpl : public IFrameEventHandlerHost { + public: + MOCK_METHOD1(GetReadyState, HRESULT(READYSTATE* readystate)); + MOCK_METHOD3(GetMatchingUserScriptsCssContent, + HRESULT(const GURL& url, bool require_all_frames, + std::string* css_content)); + MOCK_METHOD4(GetMatchingUserScriptsJsContent, + HRESULT(const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list)); + MOCK_METHOD1(GetExtensionId, HRESULT(std::wstring* extension_id)); + MOCK_METHOD1(GetExtensionPath, HRESULT(std::wstring* extension_path)); + MOCK_METHOD1(GetExtensionPortMessagingProvider, + HRESULT(IExtensionPortMessagingProvider** messaging_provider)); + MOCK_METHOD4(InsertCode, HRESULT(BSTR, BSTR, BOOL, CeeeTabCodeType)); +}; + +class TestFrameEventHandlerHost + : public testing::MockFrameEventHandlerHostBase<TestFrameEventHandlerHost> { + public: + HRESULT Initialize(TestFrameEventHandlerHost** self) { + *self = this; + return S_OK; + } + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown. + CComPtr<IUnknown> browser_identity_unknown; + EXPECT_HRESULT_SUCCEEDED( + browser->QueryInterface(&browser_identity_unknown)); + + std::pair<HandlerMap::iterator, bool> result = + handlers_.insert(std::make_pair(browser_identity_unknown, handler)); + EXPECT_TRUE(result.second); + return S_OK; + } + + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown. + CComPtr<IUnknown> browser_identity_unknown; + EXPECT_HRESULT_SUCCEEDED( + + browser->QueryInterface(&browser_identity_unknown)); + EXPECT_EQ(1, handlers_.erase(browser_identity_unknown)); + return S_OK; + } + + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state) { + return S_OK; + } + + bool has_browser(IUnknown* browser) { + CComPtr<IUnknown> browser_identity(browser); + + return handlers_.find(browser_identity) != handlers_.end(); + } + + FrameEventHandler* GetHandler(IUnknown* browser) { + CComPtr<IUnknown> browser_identity(browser); + + HandlerMap::iterator it(handlers_.find(browser_identity)); + if (it != handlers_.end()) + return NULL; + + return static_cast<FrameEventHandler*>(it->second); + } + + private: + typedef std::map<IUnknown*, IFrameEventHandler*> HandlerMap; + HandlerMap handlers_; +}; + +class MockContentScriptManager : public ContentScriptManager { + public: + MOCK_METHOD3(ExecuteScript, HRESULT(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document)); + MOCK_METHOD2(InsertCss, HRESULT(const wchar_t* code, + IHTMLDocument2* document)); +}; + +// This testing class is used to test the higher-level event handling +// behavior of FrameEventHandler by mocking out the implementation +// functions invoked on readystate transitions. +class TestingFrameEventHandler + : public FrameEventHandler, + public InitializingCoClass<TestingFrameEventHandler>, + public InstanceCountMixin<TestingFrameEventHandler> { + public: + TestingFrameEventHandler() {} + ~TestingFrameEventHandler() {} + + HRESULT Initialize(TestingFrameEventHandler **self, + IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + *self = this; + return FrameEventHandler::Initialize(browser, parent_browser, host); + } + + virtual void InitializeContentScriptManager() { + content_script_manager_.reset(new MockContentScriptManager); + } + MockContentScriptManager* GetContentScriptManager() { + return reinterpret_cast<MockContentScriptManager*>( + content_script_manager_.get()); + } + + // Mock out or publicize our internal helper methods. + MOCK_METHOD2(GetExtensionResourceContents, + HRESULT(const FilePath& file, std::string* contents)); + MOCK_METHOD3(GetCodeOrFileContents, + HRESULT(BSTR code, BSTR file, std::wstring* contents)); + HRESULT CallGetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents) { + return FrameEventHandler::GetCodeOrFileContents(code, file, contents); + } + + const std::list<DeferredInjection>& deferred_injections() { + return deferred_injections_; + } + + void SetupForRedoDoneInjectionsTest(BSTR url) { + browser_url_ = url; + loaded_css_ = true; + loaded_start_scripts_ = true; + loaded_end_scripts_ = true; + } + + // Disambiguate. + using InitializingCoClass<TestingFrameEventHandler>:: + CreateInitialized; + + // Mock out the state transition implementation functions. + MOCK_METHOD1(LoadCss, void(const GURL& match_url)); + MOCK_METHOD1(LoadStartScripts, void(const GURL& match_url)); + MOCK_METHOD1(LoadEndScripts, void(const GURL& match_url)); +}; + +class FrameEventHandlerTestBase: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockBrowser::CreateInitialized(&browser_, &browser_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandlerHost::CreateInitializedIID( + &host_, IID_IUnknown, &host_keeper_)); + + ExpectGetDocument(); + } + + virtual void TearDown() { + // Fire a close event just in case. + if (document_) + document_->FireOnClose(); + + browser_ = NULL; + browser_keeper_.Release(); + document_ = NULL; + document_keeper_.Release(); + host_ = NULL; + host_keeper_.Release(); + + handler_keeper_.Release(); + + ASSERT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + void ExpectGetDocument() { + EXPECT_CALL(*browser_, get_Document(_)) + .WillRepeatedly(DoAll( + CopyInterfaceToArgument<0>(static_cast<IDispatch*>(document_)), + Return(S_OK))); + } + + protected: + MockBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + TestFrameEventHandlerHost* host_; + CComPtr<IFrameEventHandlerHost> host_keeper_; + + CComPtr<IUnknown> handler_keeper_; +}; + +class FrameEventHandlerTest: public FrameEventHandlerTestBase { + public: + typedef FrameEventHandlerTestBase Base; + + static void SetUpTestCase() { + // Never torn down as other threads in the test may need it after + // teardown. + ScriptHost::set_default_debug_application(&debug_app); + } + + void TearDown() { + handler_ = NULL; + + Base::TearDown(); + } + + void CreateHandler() { + EXPECT_CALL(*document_, GetClassID(_)).WillOnce( + DoAll(SetArgumentPointee<0>(CLSID_HTMLDocument), Return(S_OK))); + IWebBrowser2* parent_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + TestingFrameEventHandler::CreateInitialized( + &handler_, browser_, parent_browser, host_, &handler_keeper_)); + } + + protected: + TestingFrameEventHandler* handler_; +}; + +TEST_F(FrameEventHandlerTest, WillNotAttachToNonHTMLDocument) { + EXPECT_CALL(*document_, GetClassID(_)).WillOnce( + DoAll(SetArgumentPointee<0>(GUID_NULL), Return(S_OK))); + + // If the document is not MSHTML, we should not attach, and + // we should return E_DOCUMENT_NOT_MSHTML to our caller to signal this. + IWebBrowser2* parent_browser = NULL; + HRESULT hr = TestingFrameEventHandler::CreateInitialized( + &handler_, browser_, parent_browser, host_, &handler_keeper_); + + EXPECT_EQ(E_DOCUMENT_NOT_MSHTML, hr); + EXPECT_FALSE(host_->has_browser(browser_)); +} + +TEST_F(FrameEventHandlerTest, CreateAndDetachDoesNotCrash) { + ASSERT_EQ(0, TestingFrameEventHandler::instance_count()); + + CreateHandler(); + ASSERT_EQ(1, TestingFrameEventHandler::instance_count()); + + // Assert that it registered. + ASSERT_TRUE(host_->has_browser(browser_)); + + // Release the handler early to ensure its last reference will + // be released while handling FireOnClose. + handler_keeper_.Release(); + handler_ = NULL; + EXPECT_EQ(1, TestingFrameEventHandler::instance_count()); + + // Should tear down and destroy itself on this event. + document_->FireOnClose(); + ASSERT_EQ(0, TestingFrameEventHandler::instance_count()); +} + +const wchar_t kGoogleUrl[] = + L"http://www.google.com/search?q=Google+Buys+Iceland"; +const wchar_t kSlashdotUrl[] = + L"http://hardware.slashdot.org/"; + +TEST_F(FrameEventHandlerTest, InjectsCSSAndStartScriptsOnLoadedReadystate) { + CreateHandler(); + + document_->set_url(kGoogleUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + // Notify the handler of the URL. + handler_->SetUrl(CComBSTR(kGoogleUrl)); + document_->FireReadyStateChange(); + + const GURL google_url(kGoogleUrl); + // Transitioning to LOADED should load Css and start scripts. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Now make like a re-navigation. + document_->SetReadyState(READYSTATE_LOADING); + + // Transitioning back to LOADED should load Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Now navigate to a different URL. + document_->set_url(kSlashdotUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + handler_->SetUrl(CComBSTR(kSlashdotUrl)); + document_->FireReadyStateChange(); + + const GURL slashdot_url(kSlashdotUrl); + + // Transitioning back to LOADED on the new URL should load + // Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(slashdot_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(slashdot_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); +} + +TEST_F(FrameEventHandlerTest, InjectsEndScriptsOnCompleteReadystate) { + CreateHandler(); + + document_->set_url(kGoogleUrl); + document_->set_ready_state(READYSTATE_LOADING); + + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + // Notify the handler of the URL. + handler_->SetUrl(CComBSTR(kGoogleUrl)); + document_->FireReadyStateChange(); + + const GURL google_url(kGoogleUrl); + // Transitioning to LOADED should load Css and start scripts. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Transitioning to INTERACTIVE should be a no-op. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // Transitioning to COMPLETE should load end scripts. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(google_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); + + // Now make like a re-navigation. + document_->SetReadyState(READYSTATE_LOADING); + + // Transitioning back to LOADED should load Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Transitioning back to INTERACTIVE should be a no-op. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // Transitioning back to COMPLETE should load end scripts. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(google_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); + + // Now navigate to a different URL. + document_->set_url(kSlashdotUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + handler_->SetUrl(CComBSTR(kSlashdotUrl)); + document_->FireReadyStateChange(); + + const GURL slashdot_url(kSlashdotUrl); + + // Transitioning back to LOADED on the new URL should load + // Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(slashdot_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(slashdot_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Back to INTERACTIVE is still a noop. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // And COMPLETE loads end scripts again. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(slashdot_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); +} + +TEST_F(FrameEventHandlerTest, InsertCodeCss) { + CreateHandler(); + MockContentScriptManager* content_script_manager = + handler_->GetContentScriptManager(); + + // Css code type + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + // Must respond with non-empty extension path or call will get deferred. + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, NULL, kCeeeTabCodeTypeCss)); + + // Js code type with no file + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + + wchar_t* default_file = L"ExecuteScript.code"; + EXPECT_CALL(*content_script_manager, ExecuteScript(_, StrEq(default_file), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, NULL, kCeeeTabCodeTypeJs)); + + // Js code type with a file + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + + wchar_t* test_file = L"test_file.js"; + EXPECT_CALL(*content_script_manager, ExecuteScript(_, StrEq(test_file), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + + CComBSTR test_file_bstr(test_file); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, test_file_bstr, kCeeeTabCodeTypeJs)); +} + +TEST_F(FrameEventHandlerTest, DeferInsertCodeCss) { + CreateHandler(); + + // Does not set extension path, so it stays empty. + EXPECT_CALL(*host_, GetExtensionId(_)).WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(L"boo", NULL, kCeeeTabCodeTypeCss)); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, L"moo", kCeeeTabCodeTypeJs)); + ASSERT_EQ(2, handler_->deferred_injections().size()); + + ASSERT_EQ(L"boo", handler_->deferred_injections().begin()->code); + ASSERT_EQ(L"", handler_->deferred_injections().begin()->file); + ASSERT_EQ(kCeeeTabCodeTypeCss, + handler_->deferred_injections().begin()->type); + + // The ++ syntax is ugly but it's either this or make DeferredInjection + // a public struct. + ASSERT_EQ(L"", (++handler_->deferred_injections().begin())->code); + ASSERT_EQ(L"moo", (++handler_->deferred_injections().begin())->file); + ASSERT_EQ(kCeeeTabCodeTypeJs, + (++handler_->deferred_injections().begin())->type); +} + +TEST_F(FrameEventHandlerTest, RedoDoneInjections) { + CreateHandler(); + MockContentScriptManager* content_script_manager = + handler_->GetContentScriptManager(); + + // Expects no calls since nothing to redo. + handler_->RedoDoneInjections(); + + CComBSTR url(L"http://www.google.com/"); + handler_->SetupForRedoDoneInjectionsTest(url); + GURL match_url(com::ToString(url)); + + // Does not set extension path, so it stays empty. + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce(Return(S_OK)); + // Will get deferred. + ASSERT_HRESULT_SUCCEEDED(handler_->InsertCode(L"boo", NULL, + kCeeeTabCodeTypeCss)); + + EXPECT_CALL(*handler_, LoadCss(match_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(match_url)).Times(1); + EXPECT_CALL(*handler_, LoadEndScripts(match_url)).Times(1); + + // Expect to get this once, as we deferred it before. + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(L"boo", NULL, kCeeeTabCodeTypeCss)); + + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + handler_->RedoDoneInjections(); +} + +TEST_F(FrameEventHandlerTest, GetCodeOrFileContents) { + CreateHandler(); + + CComBSTR code(L"test"); + CComBSTR file(L"test.js"); + CComBSTR empty; + std::wstring contents; + + // Failure cases. + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(0); + + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, NULL, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(code, file, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(empty, NULL, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, empty, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(empty, empty, + &contents)); + + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)) + .WillOnce(Return(E_FAIL)); + + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, file, + &contents)); + + // Success cases. + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(0); + + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(code, NULL, + &contents)); + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(code, empty, + &contents)); + + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(2) + .WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(NULL, file, + &contents)); + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(empty, file, + &contents)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/http_negotiate.cc b/ceee/ie/plugin/bho/http_negotiate.cc new file mode 100644 index 0000000..ac39ba3 --- /dev/null +++ b/ceee/ie/plugin/bho/http_negotiate.cc @@ -0,0 +1,197 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/http_negotiate.h" + +#include <atlbase.h> +#include <atlctl.h> +#include <htiframe.h> +#include <urlmon.h> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "base/time.h" + +#include "ceee/ie/plugin/bho/cookie_accountant.h" +#include "chrome_frame/vtable_patch_manager.h" +#include "chrome_frame/utils.h" +#include "base/scoped_comptr_win.h" + +static const int kHttpNegotiateBeginningTransactionIndex = 3; +static const int kHttpNegotiateOnResponseIndex = 4; + +CComAutoCriticalSection HttpNegotiatePatch::bho_instance_count_crit_; +int HttpNegotiatePatch::bho_instance_count_ = 0; + + +BEGIN_VTABLE_PATCHES(IHttpNegotiate) + VTABLE_PATCH_ENTRY(kHttpNegotiateBeginningTransactionIndex, + HttpNegotiatePatch::BeginningTransaction) + VTABLE_PATCH_ENTRY(kHttpNegotiateOnResponseIndex, + HttpNegotiatePatch::OnResponse) +END_VTABLE_PATCHES() + +namespace { + +class SimpleBindStatusCallback : public CComObjectRootEx<CComSingleThreadModel>, + public IBindStatusCallback { + public: + BEGIN_COM_MAP(SimpleBindStatusCallback) + COM_INTERFACE_ENTRY(IBindStatusCallback) + END_COM_MAP() + + // IBindStatusCallback implementation + STDMETHOD(OnStartBinding)(DWORD reserved, IBinding* binding) { + return E_NOTIMPL; + } + + STDMETHOD(GetPriority)(LONG* priority) { + return E_NOTIMPL; + } + STDMETHOD(OnLowResource)(DWORD reserved) { + return E_NOTIMPL; + } + + STDMETHOD(OnProgress)(ULONG progress, ULONG max_progress, + ULONG status_code, LPCWSTR status_text) { + return E_NOTIMPL; + } + STDMETHOD(OnStopBinding)(HRESULT result, LPCWSTR error) { + return E_NOTIMPL; + } + + STDMETHOD(GetBindInfo)(DWORD* bind_flags, BINDINFO* bind_info) { + return E_NOTIMPL; + } + + STDMETHOD(OnDataAvailable)(DWORD flags, DWORD size, FORMATETC* formatetc, + STGMEDIUM* storage) { + return E_NOTIMPL; + } + STDMETHOD(OnObjectAvailable)(REFIID iid, IUnknown* object) { + return E_NOTIMPL; + } +}; + +} // end namespace + +HttpNegotiatePatch::HttpNegotiatePatch() { +} + +HttpNegotiatePatch::~HttpNegotiatePatch() { +} + +// static +bool HttpNegotiatePatch::Initialize() { + // Patch IHttpNegotiate for user-agent and cookie functionality. + { + CComCritSecLock<CComAutoCriticalSection> lock(bho_instance_count_crit_); + bho_instance_count_++; + if (bho_instance_count_ != 1) { + return true; + } + } + + if (IS_PATCHED(IHttpNegotiate)) { + LOG(WARNING) << __FUNCTION__ << ": already patched."; + return true; + } + + // Use our SimpleBindStatusCallback class as we need a temporary object that + // implements IBindStatusCallback. + CComObjectStackEx<SimpleBindStatusCallback> request; + ScopedComPtr<IBindCtx> bind_ctx; + HRESULT hr = ::CreateAsyncBindCtx(0, &request, NULL, bind_ctx.Receive()); + + DCHECK(SUCCEEDED(hr)) << "CreateAsyncBindCtx"; + if (bind_ctx) { + ScopedComPtr<IUnknown> bscb_holder; + bind_ctx->GetObjectParam(L"_BSCB_Holder_", bscb_holder.Receive()); + if (bscb_holder) { + hr = PatchHttpNegotiate(bscb_holder); + } else { + NOTREACHED() << "Failed to get _BSCB_Holder_"; + hr = E_UNEXPECTED; + } + bind_ctx.Release(); + } + + return SUCCEEDED(hr); +} + +// static +void HttpNegotiatePatch::Uninitialize() { + CComCritSecLock<CComAutoCriticalSection> lock(bho_instance_count_crit_); + bho_instance_count_--; + DCHECK_GE(bho_instance_count_, 0); + if (bho_instance_count_ == 0) { + vtable_patch::UnpatchInterfaceMethods(IHttpNegotiate_PatchInfo); + } +} + +// static +HRESULT HttpNegotiatePatch::PatchHttpNegotiate(IUnknown* to_patch) { + DCHECK(to_patch); + DCHECK_IS_NOT_PATCHED(IHttpNegotiate); + + ScopedComPtr<IHttpNegotiate> http; + HRESULT hr = http.QueryFrom(to_patch); + if (FAILED(hr)) { + hr = DoQueryService(IID_IHttpNegotiate, to_patch, http.Receive()); + } + + if (http) { + hr = vtable_patch::PatchInterfaceMethods(http, IHttpNegotiate_PatchInfo); + DLOG_IF(ERROR, FAILED(hr)) + << StringPrintf("HttpNegotiate patch failed 0x%08X", hr); + } else { + DLOG(WARNING) + << StringPrintf("IHttpNegotiate not supported 0x%08X", hr); + } + + return hr; +} + + +// static +HRESULT HttpNegotiatePatch::BeginningTransaction( + IHttpNegotiate_BeginningTransaction_Fn original, IHttpNegotiate* me, + LPCWSTR url, LPCWSTR headers, DWORD reserved, LPWSTR* additional_headers) { + DLOG(INFO) << __FUNCTION__ << " " << url << " headers:\n" << headers; + + HRESULT hr = original(me, url, headers, reserved, additional_headers); + + if (FAILED(hr)) { + DLOG(WARNING) << __FUNCTION__ << " Delegate returned an error"; + return hr; + } + + // TODO(skare@google.com): Modify User-Agent here. + + return hr; +} + +// static +HRESULT HttpNegotiatePatch::OnResponse( + IHttpNegotiate_OnResponse_Fn original, IHttpNegotiate* me, + DWORD response_code, LPCWSTR response_headers, LPCWSTR request_headers, + LPWSTR* additional_request_headers) { + DLOG(INFO) << __FUNCTION__ << " response headers:\n" << response_headers; + + base::Time current_time = base::Time::Now(); + + HRESULT hr = original(me, response_code, response_headers, request_headers, + additional_request_headers); + + if (FAILED(hr)) { + DLOG(WARNING) << __FUNCTION__ << " Delegate returned an error"; + return hr; + } + + CookieAccountant::GetInstance()->RecordHttpResponseCookies( + std::string(CW2A(response_headers)), current_time); + + return hr; +} diff --git a/ceee/ie/plugin/bho/http_negotiate.h b/ceee/ie/plugin/bho/http_negotiate.h new file mode 100644 index 0000000..d2975d3 --- /dev/null +++ b/ceee/ie/plugin/bho/http_negotiate.h @@ -0,0 +1,69 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ +#define CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ + +#include <atlbase.h> +#include <shdeprecated.h> + +#include "base/basictypes.h" + +// Typedefs for IHttpNegotiate methods. +typedef HRESULT (STDMETHODCALLTYPE* IHttpNegotiate_BeginningTransaction_Fn)( + IHttpNegotiate* me, LPCWSTR url, LPCWSTR headers, DWORD reserved, + LPWSTR* additional_headers); +typedef HRESULT (STDMETHODCALLTYPE* IHttpNegotiate_OnResponse_Fn)( + IHttpNegotiate* me, DWORD response_code, LPCWSTR response_header, + LPCWSTR request_header, LPWSTR* additional_request_headers); + +// Typedefs for IBindStatusCallback methods. +typedef HRESULT (STDMETHODCALLTYPE* IBindStatusCallback_StartBinding_Fn)( + IBindStatusCallback* me, DWORD reserved, IBinding *binding); +typedef HRESULT (STDMETHODCALLTYPE* IBindStatusCallback_OnProgress_Fn)( + IBindStatusCallback* me, ULONG progress, ULONG progress_max, + ULONG status_code, LPCWSTR status_text); + +// Typedefs for IInternetProtocolSink methods. +typedef HRESULT (STDMETHODCALLTYPE* IInternetProtocolSink_ReportProgress_Fn)( + IInternetProtocolSink* me, ULONG status_code, LPCWSTR status_text); + +// Patches methods of urlmon's IHttpNegotiate implementation for the purposes +// of adding to the http user agent header. + +class HttpNegotiatePatch { + private: + // Class is not to be instantiated at the moment. + HttpNegotiatePatch(); + ~HttpNegotiatePatch(); + + public: + static bool Initialize(); + static void Uninitialize(); + + // IHttpNegotiate patch methods + static STDMETHODIMP BeginningTransaction( + IHttpNegotiate_BeginningTransaction_Fn original, IHttpNegotiate* me, + LPCWSTR url, LPCWSTR headers, DWORD reserved, LPWSTR* additional_headers); + static STDMETHODIMP OnResponse(IHttpNegotiate_OnResponse_Fn original, + IHttpNegotiate* me, DWORD response_code, LPCWSTR response_headers, + LPCWSTR request_headers, LPWSTR* additional_request_headers); + + protected: + static HRESULT PatchHttpNegotiate(IUnknown* to_patch); + + private: + // Count number of BHOs depending on this patch. + // Unhook when the last one goes away. + static CComAutoCriticalSection bho_instance_count_crit_; + static int bho_instance_count_; + + DISALLOW_COPY_AND_ASSIGN(HttpNegotiatePatch); +}; + +// Attempts to get to the associated browser service for an active request. +HRESULT GetBrowserServiceFromProtocolSink(IInternetProtocolSink* sink, + IBrowserService** browser_service); + +#endif // CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ diff --git a/ceee/ie/plugin/bho/infobar_browser_window.cc b/ceee/ie/plugin/bho/infobar_browser_window.cc new file mode 100644 index 0000000..fecf5e6 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_browser_window.cc @@ -0,0 +1,205 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the infobar browser window. + +#include "ceee/ie/plugin/bho/infobar_browser_window.h" + +#include <atlapp.h> +#include <atlcrack.h> +#include <atlmisc.h> +#include <atlsafe.h> +#include <atlwin.h> + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/common/chrome_switches.h" +#include "chrome_frame/com_message_event.h" + +namespace infobar_api { + +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_void_= + { CC_STDCALL, VT_EMPTY, 0, { } }; + +InfobarBrowserWindow::InfobarBrowserWindow() : delegate_(NULL) { +} + +InfobarBrowserWindow::~InfobarBrowserWindow() { +} + +STDMETHODIMP InfobarBrowserWindow::GetWantsPrivileged( + boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Must enable experimental extensions because we want to load html pages + // from our extension. + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetChromeProfileName(BSTR* profile_name) { + *profile_name = ::SysAllocString( + ceee_module_util::GetBrokerProfileNameForIe()); + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetExtensionApisToAutomate( + BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfReadyStateChanged(LONG state) { + if (state == READYSTATE_COMPLETE) { + // We already loaded the extension, enable them in this CF. + chrome_frame_->getEnabledExtensions(); + // Also we should already have URL, navigate to it. + Navigate(); + infobar_events_funnel().OnDocumentComplete(); + } +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfExtensionReady(BSTR path, + int response) { + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath(FilePath(extension_path_)); + } + + chrome_frame_->getEnabledExtensions(); +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfClose() { + if (delegate_ != NULL) + delegate_->OnWindowClose(); +} + + HRESULT InfobarBrowserWindow::Initialize(HWND parent) { + HRESULT hr = InitializeAndShowWindow(parent); + if (FAILED(hr)) { + LOG(ERROR) << "Infobar browser failed to initialize its site window: " << + com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT InfobarBrowserWindow::InitializeAndShowWindow(HWND parent) { + if (NULL == Create(parent)) + return E_FAIL; + + BOOL shown = ShowWindow(SW_SHOW); + DCHECK(shown); + + return shown ? S_OK : E_FAIL; +} + +HRESULT InfobarBrowserWindow::Teardown() { + if (IsWindow()) { + // Teardown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + } + + return S_OK; +} + +void InfobarBrowserWindow::SetUrl(const std::wstring& url) { + // Navigate to the URL if the browser exists, otherwise just store the URL. + url_ = url; + Navigate(); +} + + +LRESULT InfobarBrowserWindow::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CAxHostWindow::CreateInstance(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Infobar failed to create ActiveX host window. " << + com::LogHr(hr); + return 1; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = chrome_frame_.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create the Chrome Frame instance. " << + com::LogHr(hr); + return 1; + } + + // And attach it to our window. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host. " << + com::LogHr(hr); + return 1; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink. " << com::LogHr(hr); + } + + return S_OK; +} + +void InfobarBrowserWindow::OnDestroy() { + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + chrome_frame_.Release(); + } +} + +void InfobarBrowserWindow::Navigate() { + // If the browser has not been created then just return. + if (!chrome_frame_) + return; + + if (url_.empty()) { + LOG(WARNING) << "Navigating infobar to not specified URL"; + } else { + HRESULT hr = chrome_frame_->put_src(CComBSTR(url_.c_str())); + LOG_IF(WARNING, FAILED(hr)) << + "Infobar: ChromeFrame::put_src returned: " << com::LogHr(hr); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_browser_window.h b/ceee/ie/plugin/bho/infobar_browser_window.h new file mode 100644 index 0000000..fd6c975 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_browser_window.h @@ -0,0 +1,156 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Infobar browser window. This is the window that hosts CF and navigates it +// to the infobar URL. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlmisc.h> + +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" + +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +namespace infobar_api { + +class InfobarBrowserWindow; + +typedef IDispEventSimpleImpl<0, InfobarBrowserWindow, &DIID_DIChromeFrameEvents> + ChromeFrameEvents; + +// The window that hosts CF where infobar URL is loaded. It implements a limited +// site functionality needed for CF as well as handles sink events from CF. +// TODO(vadimb@google.com): Refactor this class, ChromeFrameHost and ToolBand +// to have a common functionality supported in a common base class. +class ATL_NO_VTABLE InfobarBrowserWindow + : public CComObjectRootEx<CComSingleThreadModel>, + public IObjectWithSiteImpl<InfobarBrowserWindow>, + public IServiceProviderImpl<InfobarBrowserWindow>, + public IChromeFramePrivileged, + public ChromeFrameEvents, + public CWindowImpl<InfobarBrowserWindow> { + public: + // Class to connect this an instance of InfobarBrowserWindow with a hosting + // object who should inherit from InfobarBrowserWindow::Delegate and set it + // with set_delegate() functions. + class Delegate { + public: + virtual ~Delegate() {} + // Informs about window.close() event. + virtual void OnWindowClose() = 0; + }; + + InfobarBrowserWindow(); + ~InfobarBrowserWindow(); + + BEGIN_COM_MAP(InfobarBrowserWindow) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(InfobarBrowserWindow) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + BEGIN_SINK_MAP(InfobarBrowserWindow) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONCLOSE, + OnCfClose, &handler_type_void_) + END_SINK_MAP() + + BEGIN_MSG_MAP(InfobarBrowserWindow) + MSG_WM_CREATE(OnCreate) + MSG_WM_DESTROY(OnDestroy) + END_MSG_MAP() + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean *wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR *args); + STDMETHOD(GetChromeProfileName)(BSTR *args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR *args); + // @} + + // @name ChromeFrame event handlers. + // @{ + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfClose)(); + // @} + + // Initializes the browser window to the given site. + HRESULT Initialize(HWND parent); + // Tears down an initialized browser window. + HRESULT Teardown(); + + // Navigates the browser to the given URL if the browser has already been + // created, otherwise stores the URL to navigate later on. + void SetUrl(const std::wstring& url); + + // Set the delegate to be informed about window.close() events. + void set_delegate(Delegate* delegate) { delegate_ = delegate; } + + // Unit test seam. + virtual InfobarEventsFunnel& infobar_events_funnel() { + return infobar_events_funnel_; + } + + protected: + // @name Message handlers. + // @{ + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + void OnDestroy(); + // @} + + private: + // The funnel for sending infobar events to the broker. + InfobarEventsFunnel infobar_events_funnel_; + + // Our Chrome frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // Url to navigate infobar to. + std::wstring url_; + // Filesystem path to the .crx we will install, or the empty string, or + // (if not ending in .crx) the path to an exploded extension directory to + // load. + std::wstring extension_path_; + + // Delegate. Not owned by the instance of this object. + Delegate* delegate_; + + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_void_; + + // Subroutine of general initialization. Extracted to make testable. + virtual HRESULT InitializeAndShowWindow(HWND parent); + + // Navigate the browser to url_ if the browser has been created. + void Navigate(); + + DISALLOW_COPY_AND_ASSIGN(InfobarBrowserWindow); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ diff --git a/ceee/ie/plugin/bho/infobar_events_funnel.cc b/ceee/ie/plugin/bho/infobar_events_funnel.cc new file mode 100644 index 0000000..d1a0b67 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Infobar Events to the Broker. + +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "base/json/json_writer.h" + +namespace { +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; +} + +HRESULT InfobarEventsFunnel::OnDocumentComplete() { + DictionaryValue info; + return SendEvent(kOnDocumentCompleteEventName, info); +} diff --git a/ceee/ie/plugin/bho/infobar_events_funnel.h b/ceee/ie/plugin/bho/infobar_events_funnel.h new file mode 100644 index 0000000..fcfa274d7 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel.h @@ -0,0 +1,24 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Infobar Events. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ + +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send infobar related events to the Broker. +class InfobarEventsFunnel : public EventsFunnel { + public: + InfobarEventsFunnel() : EventsFunnel(false) {} + + // Sends the infobar.onDocumentComplete event to the Broker. + virtual HRESULT OnDocumentComplete(); + + private: + DISALLOW_COPY_AND_ASSIGN(InfobarEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc b/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc new file mode 100644 index 0000000..9fe15ea --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc @@ -0,0 +1,38 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for InfobarEventsFunnel. + +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" +#include "base/values.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; + +class TestInfobarEventsFunnel : public InfobarEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(InfobarEventsFunnelTest, OnDocumentComplete) { + TestInfobarEventsFunnel infobar_events_funnel; + DictionaryValue dict; + + EXPECT_CALL(infobar_events_funnel, SendEvent( + StrEq(kOnDocumentCompleteEventName), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(infobar_events_funnel.OnDocumentComplete()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/infobar_manager.cc b/ceee/ie/plugin/bho/infobar_manager.cc new file mode 100644 index 0000000..6b969f6 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_manager.cc @@ -0,0 +1,270 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the manager for infobar windows. + +#include "ceee/ie/plugin/bho/infobar_manager.h" + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlmisc.h> +#include <atlwin.h> + +#include "base/logging.h" +#include "ceee/common/windows_constants.h" +#include "chrome_frame/utils.h" + +namespace { + +enum ContainerWindowUserMessages { + TM_NOTIFY_UPDATE_POSITION = WM_USER + 600, + TM_DELAYED_CLOSE_INFOBAR = (TM_NOTIFY_UPDATE_POSITION + 1), +}; + +} // namespace + +namespace infobar_api { + +// ContainerWindow subclasses IE content window's container window, handles +// WM_NCCALCSIZE to resize its client area. It also handles WM_SIZE and WM_MOVE +// messages to make infobars consistent with IE content window's size and +// position. +class InfobarManager::ContainerWindow : public CWindowImpl<ContainerWindow> { + public: + ContainerWindow(HWND container, InfobarManager* manager) + : infobar_manager_(manager) { + PinModule(); + destroyed_ = !::IsWindow(container) || !SubclassWindow(container); + } + + virtual ~ContainerWindow() { + if (!destroyed_) + UnsubclassWindow(); + } + + bool destroyed() const { + return destroyed_; + } + + BEGIN_MSG_MAP_EX(ContainerWindow) + MSG_WM_NCCALCSIZE(OnNcCalcSize) + MSG_WM_SIZE(OnSize) + MSG_WM_MOVE(OnMove) + MSG_WM_DESTROY(OnDestroy) + MESSAGE_HANDLER(TM_NOTIFY_UPDATE_POSITION, OnNotifyUpdatePosition) + MESSAGE_HANDLER(TM_DELAYED_CLOSE_INFOBAR, OnDelayedCloseInfobar) + END_MSG_MAP() + + private: + // Handles WM_NCCALCSIZE message. + LRESULT OnNcCalcSize(BOOL calc_valid_rects, LPARAM lparam) { + // Adjust client area for infobar. + LRESULT ret = DefWindowProc(WM_NCCALCSIZE, + static_cast<WPARAM>(calc_valid_rects), lparam); + // Whether calc_valid_rects is true or false, we could treat beginning of + // lparam as a RECT object. + RECT* rect = reinterpret_cast<RECT*>(lparam); + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowNcCalcSize(rect); + + // If infobars reserve all the space and rect becomes empty, the container + // window won't receive subsequent WM_SIZE and WM_MOVE messages. + // In this case, we have to explicitly notify infobars to update their + // position. + if (rect->right - rect->left <= 0 || rect->bottom - rect->top <= 0) + PostMessage(TM_NOTIFY_UPDATE_POSITION, 0, 0); + return ret; + } + + // Handles WM_SIZE message. + void OnSize(UINT type, CSize size) { + DefWindowProc(WM_SIZE, static_cast<WPARAM>(type), + MAKELPARAM(size.cx, size.cy)); + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + } + + // Handles WM_MOVE message. + void OnMove(CPoint point) { + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + } + + // Handles WM_DESTROY message. + void OnDestroy() { + // When refreshing IE window, this window may be destroyed. + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowDestroy(); + if (m_hWnd && IsWindow()) + UnsubclassWindow(); + destroyed_ = true; + } + + // Handles TM_NOTIFY_UPDATE_POSITION message - delayed window resize message. + LRESULT OnNotifyUpdatePosition(UINT message, WPARAM wparam, LPARAM lparam, + BOOL& handled) { + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + + handled = TRUE; + return 0; + } + + // Handles TM_DELAYED_CLOSE_INFOBAR - delayed infobar window close request. + LRESULT OnDelayedCloseInfobar(UINT message, WPARAM wparam, LPARAM lparam, + BOOL& handled) { + if (infobar_manager_ != NULL) { + InfobarType type = static_cast<InfobarType>(wparam); + infobar_manager_->OnContainerWindowDelayedCloseInfobar(type); + } + + handled = TRUE; + return 0; + } + + // Pointer to infobar manager. This object is not owned by the class instance. + InfobarManager* infobar_manager_; + + // True is this window was destroyed or not subclassed. + bool destroyed_; + + DISALLOW_COPY_AND_ASSIGN(ContainerWindow); +}; + +InfobarManager::InfobarManager(HWND tab_window) + : tab_window_(tab_window) { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + // Note that when InfobarManager is being initialized the IE has not created + // the tab. Therefore we cannot find the container window here and have to + // pass interface for a function that finds windows to be called later. + infobars_[index].reset( + InfobarWindow::CreateInfobar(static_cast<InfobarType>(index), this)); + } +} + +HRESULT InfobarManager::Show(InfobarType type, int max_height, + const std::wstring& url, bool slide) { + if (type < FIRST_INFOBAR_TYPE || type >= END_OF_INFOBAR_TYPE || + infobars_[type] == NULL) { + return E_INVALIDARG; + } + // Set the URL. If the window is not created it will navigate there as soon as + // it is created. + infobars_[type]->Navigate(url); + // Create the window if not created. + if (!infobars_[type]->IsWindow()) { + infobars_[type]->Create(tab_window_, NULL, NULL, + WS_CHILD | WS_CLIPCHILDREN); + } + if (!infobars_[type]->IsWindow()) + return E_UNEXPECTED; + + HRESULT hr = infobars_[type]->Show(max_height, slide); + return hr; +} + +HRESULT InfobarManager::Hide(InfobarType type) { + if (type < FIRST_INFOBAR_TYPE || type >= END_OF_INFOBAR_TYPE || + infobars_[type] == NULL) { + return E_INVALIDARG; + } + // There is a choice either to hide or to destroy the infobar window. + // This implementation destroys the infobar to save resources and stop all + // scripts that possibly still run in the window. If we want to just hide the + // infobar window instead then we should change Reset to Hide here, possibly + // navigate the infobar window to "about:blank" and make sure that the code + // in Show() does not try to create the chrome frame window again. + infobars_[type]->Reset(); + return S_OK; +} + +void InfobarManager::HideAll() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) + Hide(static_cast<InfobarType>(index)); +} + +// Callback function for EnumChildWindows. lParam should be the pointer to +// HWND variable where the handle of the window which class is +// kIeTabContentParentWindowClass will be written. +static BOOL CALLBACK FindContentParentWindowsProc(HWND hwnd, LPARAM lparam) { + HWND* window_handle = reinterpret_cast<HWND*>(lparam); + if (NULL == window_handle) { + // It makes no sense to continue enumeration. + return FALSE; + } + + // Variable to hold the class name. The size does not matter as long as it + // is at least can hold kIeTabContentParentWindowClass. + wchar_t class_name[100]; + if (::GetClassName(hwnd, class_name, arraysize(class_name)) && + lstrcmpi(windows::kIeTabContentParentWindowClass, class_name) == 0) { + // We found the window. Return its handle and stop enumeration. + *window_handle = hwnd; + return FALSE; + } + return TRUE; +} + +HWND InfobarManager::GetContainerWindow() { + if (container_window_ != NULL && container_window_->destroyed()) + container_window_.reset(NULL); + + if (container_window_ == NULL) { + if (tab_window_ != NULL && ::IsWindow(tab_window_)) { + // Find the window which is the container for the HTML view (parent of + // the content). + HWND content_parent_window = NULL; + ::EnumChildWindows(tab_window_, FindContentParentWindowsProc, + reinterpret_cast<LPARAM>(&content_parent_window)); + DCHECK(content_parent_window); + if (content_parent_window != NULL) { + container_window_.reset( + new ContainerWindow(content_parent_window, this)); + } + } + } + DCHECK(container_window_ != NULL && container_window_->IsWindow()); + return container_window_->m_hWnd; +} + +void InfobarManager::OnWindowClose(InfobarType type) { + // This callback is called from CF callback so we should not destroy the + // infobar window right away as it may result on deleting the object that + // started this callback. So instead we post ourtselves the message. + if (container_window_ != NULL) + container_window_->PostMessage(TM_DELAYED_CLOSE_INFOBAR, + static_cast<WPARAM>(type), 0); +} + +void InfobarManager::OnContainerWindowNcCalcSize(RECT* rect) { + if (rect == NULL) + return; + + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->ReserveSpace(rect); + } +} + +void InfobarManager::OnContainerWindowUpdatePosition() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->UpdatePosition(); + } +} + +void InfobarManager::OnContainerWindowDelayedCloseInfobar(InfobarType type) { + // Hide the infobar window. Parameter validation is handled in Hide(). + Hide(type); +} + +void InfobarManager::OnContainerWindowDestroy() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->Reset(); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_manager.h b/ceee/ie/plugin/bho/infobar_manager.h new file mode 100644 index 0000000..dead924 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_manager.h @@ -0,0 +1,67 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Manager for infobar windows. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ + +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "ceee/ie/plugin/bho/infobar_window.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" + +namespace infobar_api { + +// InfobarManager creates and manages infobars, which are displayed at the top +// or bottom of IE content window. +class InfobarManager : public InfobarWindow::Delegate, + public WebBrowserEventsSource::Sink { + public: + explicit InfobarManager(HWND tab_window); + + // Shows the infobar of the specified type and navigates it to the specified + // URL. + HRESULT Show(InfobarType type, int max_height, const std::wstring& url, + bool slide); + // Hides the infobar of the specified type. + HRESULT Hide(InfobarType type); + // Hides all infobars. + void HideAll(); + + // Implementation of InfobarWindow::Delegate. + // Finds the handle of the container window. + virtual HWND GetContainerWindow(); + // Informs about window.close() event. + virtual void OnWindowClose(InfobarType type); + + private: + class ContainerWindow; + + // The HWND of the tab window the infobars are associated with. + HWND tab_window_; + + // Parent window for IE content window. + scoped_ptr<ContainerWindow> container_window_; + + // Infobar windows. + scoped_ptr<InfobarWindow> infobars_[END_OF_INFOBAR_TYPE]; + + // ContainerWindow callbacks. + // Callback for WM_NCCALCSIZE. + void OnContainerWindowNcCalcSize(RECT* rect); + // Callback for messages on size or position change. + void OnContainerWindowUpdatePosition(); + // Callback for message requesting closing the infobar. + void OnContainerWindowDelayedCloseInfobar(InfobarType type); + // Callback for WM_DESTROY. + void OnContainerWindowDestroy(); + + DISALLOW_COPY_AND_ASSIGN(InfobarManager); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/infobar_window.cc b/ceee/ie/plugin/bho/infobar_window.cc new file mode 100644 index 0000000..62e9301 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_window.cc @@ -0,0 +1,314 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the manager for infobar windows. + +#include "ceee/ie/plugin/bho/infobar_window.h" + +#include <atlapp.h> +#include <atlcrack.h> +#include <atlmisc.h> + +#include "base/logging.h" +#include "base/scoped_ptr.h" + +namespace { + +const UINT_PTR kInfobarSlidingTimerId = 1U; +// Interval for sliding the infobar in milliseconds. +const UINT kInfobarSlidingTimerIntervalMs = 50U; +// The step when the infobar is sliding, in pixels. +const int kInfobarSlidingStep = 10; +// The default height of the infobar. See also similar constant in +// ceee/ie/plugin/bho/executor.cc which overrides this one. +const int kInfobarDefaultHeight = 39; + +} // namespace + + +namespace infobar_api { + +InfobarWindow* InfobarWindow::CreateInfobar(InfobarType type, + Delegate* delegate) { + DCHECK(delegate); + return NULL == delegate ? NULL : new InfobarWindow(type, delegate); +} + +InfobarWindow::InfobarWindow(InfobarType type, Delegate* delegate) + : type_(type), + delegate_(delegate), + show_(false), + target_height_(1), + current_height_(1), + sliding_infobar_(false) { + DCHECK(delegate); +} + +InfobarWindow::~InfobarWindow() { + Reset(); + + if (IsWindow()) { + DestroyWindow(); + } else { + NOTREACHED() << "Infobar window was not successfully created."; + } +} + +void InfobarWindow::OnWindowClose() { + // Propagate the event to the manager. + if (delegate_ != NULL) + delegate_->OnWindowClose(type_); +} + +HRESULT InfobarWindow::Show(int max_height, bool slide) { + if (url_.empty()) + return E_UNEXPECTED; + + StartUpdatingLayout(true, max_height, slide); + return S_OK; +} + +HRESULT InfobarWindow::Hide() { + StartUpdatingLayout(false, 0, false); + + return S_OK; +} + +HRESULT InfobarWindow::Navigate(const std::wstring& url) { + // If the CF exists (which means the infobar has already been created) then + // navigate it. Otherwise just store the URL, it will be passed to the CF when + // it will be created. + url_ = url; + if (chrome_frame_host_) + chrome_frame_host_->SetUrl(url_); + return S_OK; +} + +void InfobarWindow::ReserveSpace(RECT* rect) { + DCHECK(rect); + if (rect == NULL || !show_) + return; + + switch (type_) { + case TOP_INFOBAR: + rect->top += current_height_; + if (rect->top > rect->bottom) + rect->top = rect->bottom; + break; + case BOTTOM_INFOBAR: + rect->bottom -= current_height_; + if (rect->bottom < rect->top) + rect->bottom = rect->top; + break; + default: + NOTREACHED() << "Unknown InfobarType value."; + break; + } +} + +void InfobarWindow::UpdatePosition() { + // Make infobar be consistent with IE window's size. + // NOTE: Even if currently it is not visible, we still need to update its + // position, since the contents may need to decide its layout based on the + // width of the infobar. + + CRect rect = CalculatePosition(); + if (IsWindow()) + MoveWindow(&rect, TRUE); +} + +void InfobarWindow::Reset() { + Hide(); + + if (chrome_frame_host_) + chrome_frame_host_->set_delegate(NULL); + + DCHECK(!show_ && !sliding_infobar_); + if (m_hWnd != NULL) { + DestroyWindow(); + m_hWnd = NULL; + } + url_.clear(); + chrome_frame_host_.Release(); +} + +void InfobarWindow::StartUpdatingLayout(bool show, int max_height, bool slide) { + if (!IsWindow()) { + LOG(ERROR) << "Updating infobar layout when window has not been created"; + return; + } + + show_ = show; + if (show) { + int html_content_height = kInfobarDefaultHeight; + CSize html_content_size(0, 0); + if (SUCCEEDED(GetContentSize(&html_content_size)) && + html_content_size.cy > 0) { + html_content_height = html_content_size.cy; + } + target_height_ = (max_height == 0 || html_content_height < max_height) ? + html_content_height : max_height; + if (target_height_ <= 0) { + target_height_ = 1; + } + } else { + target_height_ = 1; + } + + if (!slide || !show) { + current_height_ = target_height_; + + if (sliding_infobar_) { + KillTimer(kInfobarSlidingTimerId); + sliding_infobar_ = false; + } + } else { + // If the infobar is visible and sliding effect is requested, we need to + // start expanding/shrinking the infobar according to its current height. + current_height_ = CalculateNextHeight(); + + if (!sliding_infobar_) { + SetTimer(kInfobarSlidingTimerId, kInfobarSlidingTimerIntervalMs, NULL); + sliding_infobar_ = true; + } + } + + UpdateLayout(); +} + +int InfobarWindow::CalculateNextHeight() { + if (current_height_ < target_height_) { + return std::min(current_height_ + kInfobarSlidingStep, target_height_); + } else if (current_height_ > target_height_) { + return std::max(current_height_ - kInfobarSlidingStep, target_height_); + } else { + return current_height_; + } +} + +RECT InfobarWindow::CalculatePosition() { + CRect rect(0, 0, 0, 0); + + if (NULL == delegate_) + return rect; + HWND container_window = delegate_->GetContainerWindow(); + if (container_window == NULL || !::IsWindow(container_window)) + return rect; + HWND container_parent_window = ::GetParent(container_window); + if (!::IsWindow(container_parent_window)) + return rect; + + ::GetWindowRect(container_window, &rect); + ::MapWindowPoints(NULL, container_parent_window, + reinterpret_cast<POINT*>(&rect), 2); + + switch (type_) { + case TOP_INFOBAR: + if (rect.top + current_height_ < rect.bottom) + rect.bottom = rect.top + current_height_; + break; + case BOTTOM_INFOBAR: + if (rect.bottom - current_height_ > rect.top) + rect.top = rect.bottom - current_height_; + break; + default: + NOTREACHED() << "Unknown InfobarType value."; + break; + } + return rect; +} + +void InfobarWindow::UpdateLayout() { + CRect rect = CalculatePosition(); + if (IsWindow()) { + // Set infobar's z-order, place it at the top, so that it won't be hidden by + // IE window. + SetWindowPos(HWND_TOP, &rect, show_ ? SWP_SHOWWINDOW : SWP_HIDEWINDOW); + } + + HWND container_window = NULL; + if (delegate_ != NULL) + container_window = delegate_->GetContainerWindow(); + if (container_window != NULL && ::IsWindow(container_window)) { + // Call SetWindowPos with SWP_FRAMECHANGED for IE window, then IE + // window would receive WM_NCCALCSIZE to recalculate its client size. + ::SetWindowPos(container_window, + NULL, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_FRAMECHANGED); + } +} + +HRESULT InfobarWindow::GetContentSize(SIZE* size) { + DCHECK(size); + if (NULL == size) + return E_POINTER; + + // Set the size to 0 that means we do not know it. + // TODO(vadimb@google.com): Find how to get the content size from the CF. + size->cx = 0; + size->cy = 0; + return S_OK; +} + +LRESULT InfobarWindow::OnTimer(UINT_PTR nIDEvent) { + DCHECK(nIDEvent == kInfobarSlidingTimerId); + if (show_ && sliding_infobar_ && current_height_ != target_height_) { + current_height_ = CalculateNextHeight(); + UpdateLayout(); + } else if (sliding_infobar_) { + KillTimer(kInfobarSlidingTimerId); + sliding_infobar_ = false; + } + + return S_OK; +} + +LRESULT InfobarWindow::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // TODO(vadimb@google.com): Better way to do this is to derive + // InfobarBrowserWindow from InitializingCoClass and give it an + // InitializeMethod, then create it with + // InfobarBrowserWindow::CreateInitialized(...). + CComObject<InfobarBrowserWindow>* chrome_frame_host = NULL; + CComObject<InfobarBrowserWindow>::CreateInstance(&chrome_frame_host); + if (chrome_frame_host) { + chrome_frame_host_.Attach(chrome_frame_host); + chrome_frame_host_->SetUrl(url_); + chrome_frame_host_->Initialize(m_hWnd); + chrome_frame_host_->set_delegate(this); + AdjustSize(); + } + return S_OK; +} + +void InfobarWindow::OnPaint(CDCHandle dc) { + RECT rc; + if (GetUpdateRect(&rc, FALSE)) { + PAINTSTRUCT ps = {}; + BeginPaint(&ps); + + BOOL ret = GetClientRect(&rc); + DCHECK(ret); + FillRect(ps.hdc, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH)); + ::DrawText(ps.hdc, L"Google CEEE. No Chrome Frame found!", -1, + &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); + + EndPaint(&ps); + } +} + +void InfobarWindow::OnSize(UINT type, CSize size) { + AdjustSize(); +} + +void InfobarWindow::AdjustSize() { + if (NULL != chrome_frame_host_) { + CRect rect; + GetClientRect(&rect); + chrome_frame_host_->SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(), + SWP_NOACTIVATE | SWP_NOZORDER); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_window.h b/ceee/ie/plugin/bho/infobar_window.h new file mode 100644 index 0000000..e745cef --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_window.h @@ -0,0 +1,143 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Infobar window. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlmisc.h> +#include <atlwin.h> + +#include "base/singleton.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/infobar_browser_window.h" + +namespace infobar_api { + +enum InfobarType { + FIRST_INFOBAR_TYPE = 0, + TOP_INFOBAR = 0, // Infobar at the top. + BOTTOM_INFOBAR = 1, // Infobar at the bottom. + END_OF_INFOBAR_TYPE = 2 +}; + +// InfobarWindow is the window created on the top or bottom of the browser tab +// window that contains the web browser window. +class InfobarWindow : public InfobarBrowserWindow::Delegate, + public CWindowImpl<InfobarWindow, CWindow> { + public: + class Delegate { + public: + virtual ~Delegate() {} + // Returns the window handle for the HTML container window. + virtual HWND GetContainerWindow() = 0; + // Informs about window.close() event. + virtual void OnWindowClose(InfobarType type) = 0; + }; + + static InfobarWindow* CreateInfobar(InfobarType type, Delegate* delegate); + ~InfobarWindow(); + + // Implementation of InfobarBrowserWindow::Delegate. + // Informs about window.close() event. + virtual void OnWindowClose(); + + // Shows the infobar. + // NOTE: Navigate should be called before Show. + // The height of the infobar is calculated to fit the content (limited to + // max_height if the content is too high; no limit if max_height is set to + // 0). + // slide indicates whether to show sliding effect. + HRESULT Show(int max_height, bool slide); + + // Hides the infobar. + HRESULT Hide(); + + // Navigates the HTML view of the infobar. + HRESULT Navigate(const std::wstring& url); + + // Reserves space for the infobar when IE window recalculates its size. + void ReserveSpace(RECT* rect); + + // Updates the infobar size and position when IE content window size or + // position is changed. + void UpdatePosition(); + + // Destroys the browser window. + void Reset(); + + private: + BEGIN_MSG_MAP(InfobarWindow) + MSG_WM_TIMER(OnTimer); + MSG_WM_CREATE(OnCreate) + MSG_WM_PAINT(OnPaint) + MSG_WM_SIZE(OnSize) + END_MSG_MAP() + + // Type of the infobar - whether it is displayed at the top or at the bottom + // of the IE content window. + InfobarType type_; + + // Delegate, connection to the infobar manager. + Delegate* delegate_; + + // URL to navigate to. + std::wstring url_; + + // Whether the infobar is shown or not. + bool show_; + + // The target height of the infobar. + int target_height_; + + // The current height of the infobar. + int current_height_; + + // Indicates whether the infobar is sliding. + bool sliding_infobar_; + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<InfobarBrowserWindow> chrome_frame_host_; + + // Constructor. + InfobarWindow(InfobarType type, Delegate* delegate); + + // If show is true, shrinks IE content window and shows the infobar + // either at the top or at the bottom. Otherwise, hides the infobar and + // restores IE content window. + void StartUpdatingLayout(bool show, int max_height, bool slide); + + // Based on the current height and the target height, decides the height of + // the next step. This is used when showing sliding effect. + int CalculateNextHeight(); + + // Calculates the position of the infobar based on its current height. + RECT CalculatePosition(); + + // Updates the layout (sizes and positions of infobar and IE content window) + // based on the current height. + void UpdateLayout(); + + HRESULT GetContentSize(SIZE* size); + + // Event handlers. + LRESULT OnTimer(UINT_PTR nIDEvent); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + void OnPaint(CDCHandle dc); + void OnSize(UINT type, CSize size); + + void AdjustSize(); + + DISALLOW_COPY_AND_ASSIGN(InfobarWindow); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ diff --git a/ceee/ie/plugin/bho/mediumtest_browser_event.cc b/ceee/ie/plugin/bho/mediumtest_browser_event.cc new file mode 100644 index 0000000..3288e9d --- /dev/null +++ b/ceee/ie/plugin/bho/mediumtest_browser_event.cc @@ -0,0 +1,495 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A test that hosts and excercises the webbrowser control to test +// its event firing behavior. +#include <atlcrack.h> +#include <atlsync.h> +#include <atlwin.h> +#include <set> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "ceee/ie/testing/mediumtest_ie_common.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + + +namespace { + +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; + +using testing::BrowserEventSinkBase; +using testing::GetTestUrl; +using testing::GetTempPath; +using testing::ShellBrowserTestImpl; + +// {AFF1D082-6B03-4b29-9521-E52240F6333B} +const GUID IID_Dummy = + { 0xaff1d082, 0x6b03, 0x4b29, + { 0x95, 0x21, 0xe5, 0x22, 0x40, 0xf6, 0x33, 0x3b } }; + +class TestBrowserEventSink; + +class TestFrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<TestFrameEventHandler>, + public InstanceCountMixin<TestFrameEventHandler>, + public IPropertyNotifySink, + public IAdviseSink { + public: + BEGIN_COM_MAP(TestFrameEventHandler) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + COM_INTERFACE_ENTRY(IAdviseSink) + END_COM_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + TestFrameEventHandler() : event_sink_(NULL), + document_property_notify_sink_cookie_(-1), + advise_sink_cookie_(-1) { + } + + virtual ~TestFrameEventHandler() { + ATLTRACE("~TestFrameEventHandler[%ws]\n", url_ ? url_ : L""); + } + + void DetachFromSink(); + + STDMETHOD(OnChanged)(DISPID changed_property); + STDMETHOD(OnRequestEdit)(DISPID changed_property) { + ATLTRACE("OnRequestEdit(%d)\n", changed_property); + return S_OK; + } + + // IAdviseSink + STDMETHOD_(void, OnDataChange)(FORMATETC *pFormatetc, STGMEDIUM *pStgmed) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnViewChange)(DWORD dwAspect, LONG lindex) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnRename)(IMoniker *pmk) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnSave)() { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnClose)(); + + virtual void GetDescription(std::string* description) const { + description->clear(); + description->append("TestFrameEventHandler"); + // TODO(siggi@chromium.org): append URL + } + + HRESULT Initialize(TestBrowserEventSink* event_sink, + IWebBrowser2* browser, + BSTR url); + void FinalRelease(); + + void set_url(const wchar_t* url) { url_ = url; } + const wchar_t* url() const { return url_ ? url_ : L""; } + + template <class Interface> + HRESULT GetBrowser(Interface** browser) { + return browser_.QueryInterface(browser); + } + + private: + CComBSTR url_; + CComDispatchDriver document_; + CComPtr<IWebBrowser2> browser_; + TestBrowserEventSink* event_sink_; + + DWORD advise_sink_cookie_; + DWORD document_property_notify_sink_cookie_; +}; + +class TestBrowserEventSink + : public BrowserEventSinkBase, + public InitializingCoClass<TestBrowserEventSink> { + public: + // Disambiguate. + using InitializingCoClass<TestBrowserEventSink>::CreateInitialized; + + HRESULT Initialize(TestBrowserEventSink** self, IWebBrowser2* browser) { + *self = this; + return BrowserEventSinkBase::Initialize(browser); + } + + // We have seen cases where we get destroyed while the ATL::CAxHostWindow + // may still hold references to frame handlers. + void FinalRelease() { + FrameHandlerMap::iterator it(frame_handlers_.begin()); + FrameHandlerMap::iterator end(frame_handlers_.end()); + + // Since they will detach from us as we clear them, we must not do it + // while we loop. + std::vector<TestFrameEventHandler*> to_be_detached; + for (; it != end; ++it) { + to_be_detached.push_back(it->second); + } + for (size_t index = 0; index < to_be_detached.size(); ++index) { + to_be_detached[index]->DetachFromSink(); + } + ASSERT_EQ(0, frame_handlers_.size()); + } + + virtual void GetDescription(std::string* description) const { + description->clear(); + description->append("TestBrowserEventSink"); + } + + void AttachHandler(IWebBrowser2* browser, TestFrameEventHandler* handler) { + ASSERT_TRUE(browser != NULL && handler != NULL); + CComPtr<IUnknown> browser_unk; + ASSERT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + + // We shouldn't already have one. + ASSERT_TRUE(NULL == FindHandlerForBrowser(browser)); + + frame_handlers_.insert(std::make_pair(browser_unk, handler)); + } + + void DetachHandler(IWebBrowser2* browser, TestFrameEventHandler* handler) { + ASSERT_TRUE(browser != NULL && handler != NULL); + + // It should already be registered. + ASSERT_TRUE(NULL != FindHandlerForBrowser(browser)); + + CComPtr<IUnknown> browser_unk; + ASSERT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + ASSERT_EQ(1, frame_handlers_.erase(browser_unk)); + } + + TestFrameEventHandler* FindHandlerForBrowser(IDispatch* browser) { + CComPtr<IUnknown> browser_unk; + EXPECT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + + FrameHandlerMap::iterator it(frame_handlers_.find(browser_unk)); + if (it == frame_handlers_.end()) + return NULL; + + return it->second; + } + + TestFrameEventHandler* FindHandlerForUrl(const std::wstring& url) { + FrameHandlerMap::iterator it(frame_handlers_.begin()); + FrameHandlerMap::iterator end(frame_handlers_.end()); + + for (; it != end; ++it) { + CComQIPtr<IWebBrowser2> browser(it->first); + EXPECT_TRUE(browser != NULL); + CComBSTR location_url; + EXPECT_HRESULT_SUCCEEDED(browser->get_LocationURL(&location_url)); + + if (0 == ::UrlCompare(url.c_str(), location_url, TRUE)) + return it->second; + } + + return NULL; + } + + // Override. + STDMETHOD_(void, OnNavigateComplete)(IDispatch* browser_disp, + VARIANT* url_var) { + CComBSTR url; + if (V_VT(url_var) == VT_BSTR) + url = V_BSTR(url_var); + + TestFrameEventHandler* frame_handler = FindHandlerForBrowser(browser_disp); + if (!frame_handler) { + CComQIPtr<IWebBrowser2> browser(browser_disp); + ASSERT_TRUE(browser != NULL); + + CComPtr<IUnknown> frame_handler_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitialized(this, + browser, + url, + &frame_handler_keeper)); + } else { + ATLTRACE("FrameHandler[%ws] -> %ws\n", frame_handler->url(), url); + frame_handler->set_url(url); + } + } + + private: + typedef std::map<IUnknown*, TestFrameEventHandler*> FrameHandlerMap; + // Keeps a map from a frame or top-level browser's identifying + // IUnknown to the frame event handler instance attached. + FrameHandlerMap frame_handlers_; +}; + + +STDMETHODIMP TestFrameEventHandler::OnChanged(DISPID changed_property) { + ATLTRACE("OnChanged(%d)\n", changed_property); + + if (changed_property == DISPID_READYSTATE) { + CComVariant ready_state; + CComDispatchDriver document(document_); + EXPECT_TRUE(document != NULL); + EXPECT_HRESULT_SUCCEEDED(document.GetProperty(DISPID_READYSTATE, + &ready_state)); + EXPECT_EQ(V_VT(&ready_state), VT_I4); + ATLTRACE("READYSTATE Frame[%ws]: %d\n", url_, ready_state.lVal); + + TestBrowserEventSink::add_state(static_cast<READYSTATE>(ready_state.lVal)); + } + + return S_OK; +} + +HRESULT TestFrameEventHandler::Initialize(TestBrowserEventSink* event_sink, + IWebBrowser2* browser, + BSTR url) { + EXPECT_HRESULT_SUCCEEDED(browser->get_Document(&document_)); + + CComQIPtr<IHTMLDocument2> html_document2(document_); + if (html_document2 != NULL) { + event_sink_ = event_sink; + browser_ = browser; + url_ = url; + EXPECT_HRESULT_SUCCEEDED(AtlAdvise(document_, + GetUnknown(), + IID_IPropertyNotifySink, + &document_property_notify_sink_cookie_)); + + ATLTRACE("TestFrameEventHandler::Initialize[%ws]\n", url_ ? url_ : L""); + + CComQIPtr<IOleObject> document_ole_object(document_); + EXPECT_TRUE(document_ole_object != NULL); + EXPECT_HRESULT_SUCCEEDED( + document_ole_object->Advise(this, &advise_sink_cookie_)); + + event_sink_->AttachHandler(browser_, this); + } else { + // This happens when we're navigated to e.g. a PDF doc or a folder. + } + + return S_OK; +} + + +void TestFrameEventHandler::DetachFromSink() { + ASSERT_TRUE(event_sink_ != NULL); + event_sink_->DetachHandler(browser_, this); + event_sink_ = NULL; +} + +void TestFrameEventHandler::FinalRelease() { + if (event_sink_ && browser_) + event_sink_->DetachHandler(browser_, this); + browser_.Release(); + document_.Release(); +} + +STDMETHODIMP_(void) TestFrameEventHandler::OnClose() { + EXPECT_HRESULT_SUCCEEDED(AtlUnadvise(document_, + IID_IPropertyNotifySink, + document_property_notify_sink_cookie_)); + + CComQIPtr<IOleObject> document_ole_object(document_); + EXPECT_TRUE(document_ole_object != NULL); + EXPECT_HRESULT_SUCCEEDED( + document_ole_object->Unadvise(advise_sink_cookie_)); +} + +class BrowserEventTest: public ShellBrowserTestImpl<TestBrowserEventSink> { +}; + +const wchar_t* kSimplePage = L"simple_page.html"; + +TEST_F(BrowserEventTest, RefreshTopLevelBrowserRetainsFrameHandler) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kSimplePage))); + + // We should have only one frame at this point. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Refreshing the top-level browser retains it. + EXPECT_HRESULT_SUCCEEDED(browser_->Refresh()); + EXPECT_TRUE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // Still there after refresh. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); +} + +const wchar_t* kTwoFramesPage = L"two_frames.html"; +const wchar_t* kFrameOne = L"frame_one.html"; +const wchar_t* kFrameTwo = L"frame_two.html"; + +TEST_F(BrowserEventTest, NavigateToFrames) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kTwoFramesPage))); + + // We should have three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // We should have a handler for each of these. + TestFrameEventHandler* two_frames = + event_sink()->FindHandlerForUrl(GetTestUrl(kTwoFramesPage)); + TestFrameEventHandler* frame_one = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameOne)); + TestFrameEventHandler* frame_two = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + ASSERT_TRUE(two_frames != NULL); + ASSERT_TRUE(frame_one != NULL); + ASSERT_TRUE(frame_two != NULL); + + // Noteworthy fact: the top level browser implements an + // IPropertyNotifySink connection point, but the sub-browsers + // for the frames do not. + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(two_frames->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_SUCCEEDED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(frame_one->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_FAILED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_FAILED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + // Test sub-frame document IPropertyNotifySink. + { + CComPtr<IWebBrowser2> browser; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&browser)); + + CComPtr<IDispatch> document_disp; + ASSERT_HRESULT_SUCCEEDED(browser->get_Document(&document_disp)); + + CComPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(document_disp->QueryInterface(&cpc)); + CComPtr<IConnectionPoint> cp; + ASSERT_HRESULT_SUCCEEDED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } +} + +TEST_F(BrowserEventTest, ReNavigateToSamePageRetainsEventHandler) { + const std::wstring url(GetTestUrl(kSimplePage)); + EXPECT_TRUE(NavigateBrowser(url)); + + // We should have a frame handler attached now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve it and make sure it doesn't die. + TestFrameEventHandler* handler_before = + event_sink()->FindHandlerForUrl(url); + + ASSERT_TRUE(handler_before != NULL); + CComPtr<IUnknown> handler_before_keeper(handler_before->GetUnknown()); + + // Re-navigate the browser to the same page. + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kSimplePage))); + // Note: on re-navigation we don't see the top-level + // browser's readystate drop, I guess that only happens + // on transitions between content types. E.g. when a + // navigation requires the shell browser to instantiate + // a new type of document, such as going from a + // HTML doc to a PDF doc or the like. + EXPECT_FALSE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should only have the one frame handler in existence now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve the new one. + TestFrameEventHandler* handler_after = + event_sink()->FindHandlerForUrl(GetTestUrl(kSimplePage)); + + ASSERT_EQ(handler_before, handler_after); + + // Release the old one, it should still stay around + handler_before_keeper.Release(); + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); +} + +TEST_F(BrowserEventTest, NavigateToDifferentPageRetainsEventHandler) { + const std::wstring first_url(GetTestUrl(kSimplePage)); + EXPECT_TRUE(NavigateBrowser(first_url)); + + // We should have a frame handler attached now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve it and make sure it doesn't die. + TestFrameEventHandler* handler_before = + event_sink()->FindHandlerForUrl(first_url); + + ASSERT_TRUE(handler_before != NULL); + CComPtr<IUnknown> handler_before_keeper(handler_before->GetUnknown()); + + // Navigate the browser to another page. + const std::wstring second_url(GetTestUrl(kTwoFramesPage)); + EXPECT_TRUE(NavigateBrowser(second_url)); + EXPECT_FALSE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should have the three frame handlers in existence now. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // Retrieve the new one for the top-level browser. + TestFrameEventHandler* handler_after = + event_sink()->FindHandlerForUrl(second_url); + + ASSERT_EQ(handler_before, handler_after); + + // Release the old one, it should still stay around + handler_before_keeper.Release(); + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); +} + +TEST_F(BrowserEventTest, RefreshFrameBrowserRetainsHandler) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kTwoFramesPage))); + + // We should have three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // Get one of the frames. + TestFrameEventHandler* frame_two = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + ASSERT_TRUE(frame_two != NULL); + + // Now refresh a sub-browser instance, let it settle, + // observe its frame event handler is still around and + // has signalled a readystate transition. + CComPtr<IWebBrowser2> browser; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&browser)); + ASSERT_HRESULT_SUCCEEDED(browser->Refresh()); + + EXPECT_TRUE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should have all three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + frame_two = event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + EXPECT_TRUE(frame_two != NULL); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc b/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc new file mode 100644 index 0000000..fd433508 --- /dev/null +++ b/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc @@ -0,0 +1,531 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A test that hosts and excercises the webbrowser control to test +// its event firing behavior. +#include <guiddef.h> +#include <mshtml.h> +#include <shlguid.h> +#include "base/utf_string_conversions.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/browser_helper_object.h" +#include "ceee/ie/testing/mediumtest_ie_common.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_chrome_frame_host.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "net/base/net_util.h" + +namespace { + +using testing::_; +using testing::AnyNumber; +using testing::BrowserEventSinkBase; +using testing::GetTestUrl; +using testing::DoAll; +using testing::InstanceCountMixin; +using testing::kAnotherFrameOne; +using testing::kAnotherFrameTwo; +using testing::kAnotherTwoFramesPage; +using testing::kDeepFramesPage; +using testing::kFrameOne; +using testing::kFrameTwo; +using testing::kLevelOneFrame; +using testing::kLevelTwoFrame; +using testing::kOrphansPage; +using testing::kSimplePage; +using testing::kTwoFramesPage; +using testing::MockChromeFrameHost; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::ShellBrowserTestImpl; +using testing::StrictMock; + +ScriptHost::DebugApplication debug_app(L"FrameEventHandlerUnittest"); + +class TestingFrameEventHandler + : public FrameEventHandler, + public InstanceCountMixin<TestingFrameEventHandler>, + public InitializingCoClass<TestingFrameEventHandler> { + public: + // Disambiguate. + using InitializingCoClass<TestingFrameEventHandler>::CreateInitializedIID; + + IWebBrowser2* browser() const { return browser_; } + IHTMLDocument2* document() const { return document_; } +}; + +class TestingBrowserHelperObject + : public BrowserHelperObject, + public InitializingCoClass<TestingBrowserHelperObject>, + public InstanceCountMixin<TestingBrowserHelperObject> { + public: + TestingBrowserHelperObject() : mock_chrome_frame_host_(NULL) { + } + + HRESULT Initialize(TestingBrowserHelperObject** self) { + *self = this; + return S_OK; + } + + HRESULT CreateFrameEventHandler(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler) { + return TestingFrameEventHandler::CreateInitializedIID( + browser, parent_browser, this, IID_IFrameEventHandler, handler); + } + + virtual TabEventsFunnel& tab_events_funnel() { + return mock_tab_events_funnel_; + } + + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker) { + broker_keeper_.CopyTo(broker); + return S_OK; + } + + virtual HRESULT CreateExecutor(IUnknown** executor) { + CComPtr<IUnknown> unknown(executor_keeper_); + unknown.CopyTo(executor); + return S_OK; + } + + HRESULT CreateChromeFrameHost() { + HRESULT hr = MockChromeFrameHost::CreateInitializedIID( + &mock_chrome_frame_host_, IID_IChromeFrameHost, &chrome_frame_host_); + + // Neuter the functions we know are going to be called. + if (SUCCEEDED(hr)) { + EXPECT_CALL(*mock_chrome_frame_host_, SetChromeProfileName(_)) + .Times(1); + EXPECT_CALL(*mock_chrome_frame_host_, StartChromeFrame()) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, SetEventSink(NotNull())) + .Times(1); + EXPECT_CALL(*mock_chrome_frame_host_, SetEventSink(NULL)) + .Times(1); + + EXPECT_CALL(*mock_chrome_frame_host_, PostMessage(_, _)) + .WillRepeatedly(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, TearDown()) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, GetSessionId(NotNull())) + .WillOnce(DoAll(SetArgumentPointee<0>(44), Return(S_OK))); + EXPECT_CALL(*broker_, SetTabIdForHandle(44, _)) + .WillOnce(Return(S_OK)); + } + + return hr; + } + + // Stub content script manifest loading. + void LoadManifestFile() {} + + // Make type public in this class. + typedef BrowserHelperObject::BrowserHandlerMap BrowserHandlerMap; + + BrowserHandlerMap::const_iterator browsers_begin() const { + return browsers_.begin(); + }; + BrowserHandlerMap::const_iterator browsers_end() const { + return browsers_.end(); + }; + + TestingFrameEventHandler* FindHandlerForUrl(const std::wstring& url) { + BrowserHandlerMap::iterator it(browsers_.begin()); + BrowserHandlerMap::iterator end(browsers_.end()); + + for (; it != end; ++it) { + CComQIPtr<IWebBrowser2> browser(it->first.m_T); + EXPECT_TRUE(browser != NULL); + CComBSTR location_url; + EXPECT_HRESULT_SUCCEEDED(browser->get_LocationURL(&location_url)); + + if (0 == ::UrlCompare(url.c_str(), location_url, TRUE)) + return static_cast<TestingFrameEventHandler*>(it->second.m_T.p); + } + + return NULL; + } + + // Returns true iff we have exactly |num_frames| registering + // the urls |resources[0..num_frames)|. + bool ExpectHasFrames(size_t num_frames, const std::wstring* resources) { + typedef BrowserHandlerMap::const_iterator iterator; + iterator it(browsers_.begin()); + + size_t count = 0; + for (; it != browsers_.end(); ++it) { + ++count; + + FrameEventHandler* handler = + static_cast<FrameEventHandler*>(it->second.m_T.p); + std::wstring url(handler->browser_url()); + const std::wstring* resources_end = resources + num_frames; + const std::wstring* found = std::find(resources, resources_end, url); + + if (resources_end == found) { + // A browser navigated to a file: URL reports the + // raw file path as its URL, convert the file path + // to a URL and search again. + FilePath path(handler->browser_url()); + + url = UTF8ToWide(net::FilePathToFileURL(path).spec()); + found = std::find(resources, resources_end, url); + } + + EXPECT_TRUE(resources_end != found) + << " unexpected frame URL " << url; + } + + EXPECT_EQ(num_frames, count); + return num_frames == count; + } + + template <size_t N> + bool ExpectHasFrames(const std::wstring (&resources)[N]) { + return ExpectHasFrames(N, resources); + } + + MockChromeFrameHost* mock_chrome_frame_host() const { + return mock_chrome_frame_host_; + } + + testing::MockTabEventsFunnel* mock_tab_events_funnel() { + return &mock_tab_events_funnel_; + } + + // We should use the executor mock that supports infobar in this test because + // OnBeforeNavigate2 queries the executor for infobar interface. + testing::MockTabInfobarExecutor* executor_; + CComPtr<ICeeeTabExecutor> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; + + private: + MockChromeFrameHost* mock_chrome_frame_host_; + StrictMock<testing::MockTabEventsFunnel> mock_tab_events_funnel_; +}; + +class TestBrowserSite + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowserSite>, + public InitializingCoClass<TestBrowserSite>, + public IServiceProviderImpl<TestBrowserSite> { + public: + BEGIN_COM_MAP(TestBrowserSite) + COM_INTERFACE_ENTRY(IServiceProvider) + END_COM_MAP() + + BEGIN_SERVICE_MAP(TestBrowserSite) + SERVICE_ENTRY_CHAIN(browser_) + END_SERVICE_MAP() + + HRESULT Initialize(TestBrowserSite **self, IWebBrowser2* browser) { + *self = this; + browser_ = browser; + return S_OK; + } + + public: + CComPtr<IWebBrowser> browser_; +}; + +class BrowserEventSink + : public BrowserEventSinkBase, + public InitializingCoClass<BrowserEventSink> { + public: + // Disambiguate. + using InitializingCoClass<BrowserEventSink>::CreateInitialized; + + HRESULT Initialize(BrowserEventSink** self, IWebBrowser2* browser) { + *self = this; + return BrowserEventSinkBase::Initialize(browser); + } +}; + +class BrowerHelperObjectTest: public ShellBrowserTestImpl<BrowserEventSink> { + public: + typedef ShellBrowserTestImpl<BrowserEventSink> Super; + + BrowerHelperObjectTest() : bho_(NULL), site_(NULL) { + } + + virtual void SetUp() { + Super::SetUp(); + + // Never torn down as other threads in the test may need it after + // teardown. + ScriptHost::set_default_debug_application(&debug_app); + + ASSERT_HRESULT_SUCCEEDED( + TestingBrowserHelperObject::CreateInitialized(&bho_, &bho_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, browser_, &site_keeper_)); + + // Create and set expectations for the broker registrar related objects. + ASSERT_HRESULT_SUCCEEDED(testing::MockTabInfobarExecutor::CreateInitialized( + &bho_->executor_, &bho_->executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + testing::MockBroker::CreateInitialized(&bho_->broker_, + &bho_->broker_keeper_)); + EXPECT_CALL(*bho_->broker_, RegisterTabExecutor(_, + bho_->executor_keeper_.p)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*bho_->broker_, UnregisterExecutor(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnCreated(_, _, _)). + Times(AnyNumber()); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnUpdated(_, _, _)). + Times(AnyNumber()); + EXPECT_CALL(*bho_->executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(*bho_->executor_, OnTopFrameBeforeNavigate(_)). + WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED(bho_keeper_->SetSite(site_->GetUnknown())); + } + + virtual void TearDown() { + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnRemoved(_)); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnTabUnmapped(_, _)); + ASSERT_HRESULT_SUCCEEDED(bho_keeper_->SetSite(NULL)); + + site_ = NULL; + site_keeper_.Release(); + + bho_->executor_ = NULL; + bho_->executor_keeper_.Release(); + + bho_->broker_ = NULL; + bho_->broker_keeper_.Release(); + + bho_ = NULL; + bho_keeper_.Release(); + + Super::TearDown(); + } + + protected: + TestingBrowserHelperObject* bho_; + CComPtr<IObjectWithSite> bho_keeper_; + + TestBrowserSite* site_; + CComPtr<IServiceProvider> site_keeper_; +}; + +// This test navigates a webbrowser control instance back and forth +// between a set of resources with our BHO attached. On every navigation +// we're asserting on the urls and number of frame event handlers the BHO +// has recorded, as well as the instance count of the testing frame +// event handler class. +// This is to ensure that: +// 1. Our frame event handlers get attached to all created frames. +// 2. That any "recycled" frame event handlers track their associated +// document's URL changes. +// 3. That we don't leak discarded frame event handlers. +// 4. That we don't crash during any of this. +TEST_F(BrowerHelperObjectTest, FrameHandlerCreationAndDestructionOnNavigation) { + const std::wstring two_frames_resources[] = { + GetTestUrl(kTwoFramesPage), + GetTestUrl(kFrameOne), + GetTestUrl(kFrameTwo)}; + + const std::wstring another_two_frames_resources[] = { + GetTestUrl(kAnotherTwoFramesPage), + GetTestUrl(kAnotherFrameOne), + GetTestUrl(kAnotherFrameTwo)}; + + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + + EXPECT_TRUE(NavigateBrowser(two_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(two_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(two_frames_resources), + TestingFrameEventHandler::instance_count()); + + EXPECT_TRUE(NavigateBrowser(another_two_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(another_two_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(another_two_frames_resources), + TestingFrameEventHandler::instance_count()); + + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); +} + +// What motivated this test is the fact that at teardown time, +// sometimes the child->parent relationship between the browser +// instances we've encountered has been disrupted. +// So if you start with a frame hierarchy like +// A <- B <- C +// if you query the parent for C at the timepoint when B is +// reporting a COMPLETE->LOADING readystate change you'll find this: +// A <- B +// A <- C +// e.g. the C frame has been re-parented to the topmost webbrowser. +// +// Strangely I can't get this to repro under programmatic control +// against the webbrowser control. I suspect conditions are simply +// different in IE proper, or else there's something special to +// navigating by user event. I'm still leaving this code in as +// I hope to find a viable repro for this later, and because this +// code exercises some modes of navigation that the above test does +// not. +TEST_F(BrowerHelperObjectTest, DeepFramesAreCorrectlyHandled) { + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + const std::wstring deep_frames_resources[] = { + GetTestUrl(kDeepFramesPage), + GetTestUrl(kLevelOneFrame), + GetTestUrl(kLevelTwoFrame), + GetTestUrl(kFrameOne) + }; + + // Navigate to a deep frame hierarchy. + EXPECT_TRUE(NavigateBrowser(deep_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate to a simple page with only a top-level frame. + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); + + // And back to a deep frame hierarchy. + EXPECT_TRUE(NavigateBrowser(deep_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + + // Refresh a mid-way frame. + TestingFrameEventHandler* handler = + bho_->FindHandlerForUrl(GetTestUrl(kLevelOneFrame)); + ASSERT_TRUE(handler); + EXPECT_HRESULT_SUCCEEDED(handler->browser()->Refresh()); + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // We should still have the same set of frames. + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate a mid-way frame to a new resource. + CComVariant empty; + EXPECT_HRESULT_SUCCEEDED( + browser_->Navigate2(&CComVariant(GetTestUrl(kTwoFramesPage).c_str()), + &empty, + &CComVariant(L"level_one"), + &empty, &empty)); + + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // This should now be our resource set. + const std::wstring mixed_frames_resources[] = { + GetTestUrl(kDeepFramesPage), + GetTestUrl(kLevelOneFrame), + GetTestUrl(kTwoFramesPage), + GetTestUrl(kFrameOne), + GetTestUrl(kFrameTwo), + }; + EXPECT_TRUE(bho_->ExpectHasFrames(mixed_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(mixed_frames_resources), + TestingFrameEventHandler::instance_count()); +} + +// What motivated this test is the fact that some webpage can dynamically +// create frames that don't get navigated. We first saw this on the +// http://www.zooborns.com site and found that it has some javascript that +// creates and iframe and manually fill its innerHTML which contains an +// iframe with a src that is navigated to. Our code used to assume that +// when a frame is navigated, its parent was previously navigated so we could +// attach the new frame to its parent that we had seen before. So we needed to +// add code to create a handler for the ancestors of such orphans. +// +TEST_F(BrowerHelperObjectTest, OrphanFrame) { + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + const std::wstring orphan_page_resources[] = { + GetTestUrl(kOrphansPage), + GetTestUrl(kOrphansPage), + GetTestUrl(kFrameOne) + }; + + // Navigate to an orphanage. + EXPECT_TRUE(NavigateBrowser(orphan_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate to a simple page with only a top-level frame. + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); + + // And back to the orphanage. + EXPECT_TRUE(NavigateBrowser(orphan_page_resources[0])); + // On fast machines, we don't wait long enough for everything to be completed. + // So we may already be OK, or we may need to wait for an extra COMPLETE. + // When we re-navigate to the ophans page like this, for some reason, we first + // get one COMPLETE ready state, and then a LOADING and then another COMPLETE. + if (arraysize(orphan_page_resources) != + TestingFrameEventHandler::instance_count()) { + ASSERT_TRUE(WaitForReadystateComplete()); + } + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); + + // Refresh a deep frame. + TestingFrameEventHandler* handler = + bho_->FindHandlerForUrl(GetTestUrl(kFrameOne)); + ASSERT_TRUE(handler); + EXPECT_HRESULT_SUCCEEDED(handler->browser()->Refresh()); + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // We should still have the same set of frames. + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/tab_events_funnel.cc b/ceee/ie/plugin/bho/tab_events_funnel.cc new file mode 100644 index 0000000..275da40 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel.cc @@ -0,0 +1,85 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from whereever through the Broker. + +#include "ceee/ie/plugin/bho/tab_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/common/constants.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +HRESULT TabEventsFunnel::OnCreated(HWND tab_handle, BSTR url, bool complete) { + DictionaryValue tab_values; + tab_values.SetInteger(keys::kIdKey, reinterpret_cast<int>(tab_handle)); + tab_values.SetString(keys::kUrlKey, url); + tab_values.SetString(keys::kStatusKey, complete ? keys::kStatusValueComplete : + keys::kStatusValueLoading); + return SendEvent(ext_event_names::kOnTabCreated, tab_values); +} + +HRESULT TabEventsFunnel::OnMoved(HWND tab_handle, int window_id, int from_index, + int to_index) { + // For tab moves, the args are an array of two values, the tab id as an int + // and then a dictionary with window id, from and to indexes. + ListValue tab_moved_args; + tab_moved_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + dict->SetInteger(keys::kFromIndexKey, from_index); + dict->SetInteger(keys::kFromIndexKey, to_index); + tab_moved_args.Append(dict); + return SendEvent(ext_event_names::kOnTabMoved, tab_moved_args); +} + +HRESULT TabEventsFunnel::OnRemoved(HWND tab_handle) { + scoped_ptr<Value> args(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + return SendEvent(ext_event_names::kOnTabRemoved, *args.get()); +} + +HRESULT TabEventsFunnel::OnSelectionChanged(HWND tab_handle, int window_id) { + // For tab selection changes, the args are an array of two values, the tab id + // as an int and then a dictionary with only the window id in it. + ListValue tab_selection_changed_args; + tab_selection_changed_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + tab_selection_changed_args.Append(dict); + return SendEvent(ext_event_names::kOnTabSelectionChanged, + tab_selection_changed_args); +} + +HRESULT TabEventsFunnel::OnUpdated(HWND tab_handle, BSTR url, + READYSTATE ready_state) { + // For tab updates, the args are an array of two values, the tab id as an int + // and then a dictionary with an optional url field as well as a mandatory + // status string value. + ListValue tab_update_args; + tab_update_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + if (url != NULL) + dict->SetString(keys::kUrlKey, url); + dict->SetString(keys::kStatusKey, (ready_state == READYSTATE_COMPLETE) ? + keys::kStatusValueComplete : keys::kStatusValueLoading); + tab_update_args.Append(dict); + return SendEvent(ext_event_names::kOnTabUpdated, tab_update_args); +} + +HRESULT TabEventsFunnel::OnTabUnmapped(HWND tab_handle, int tab_id) { + ListValue tab_unmapped_args; + tab_unmapped_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + tab_unmapped_args.Append(Value::CreateIntegerValue(tab_id)); + return SendEvent(ceee_event_names::kCeeeOnTabUnmapped, tab_unmapped_args); +} diff --git a/ceee/ie/plugin/bho/tab_events_funnel.h b/ceee/ie/plugin/bho/tab_events_funnel.h new file mode 100644 index 0000000..7a9a2c4 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel.h @@ -0,0 +1,61 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Tab Events. + +#ifndef CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ + +#include <ocidl.h> // for READYSTATE + +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send tab related events to the Broker. +class TabEventsFunnel : public EventsFunnel { + public: + TabEventsFunnel() : EventsFunnel(true) {} + + // Sends the tabs.onMoved event to the Broker. + // @param tab_handle The HWND of the tab that moved. + // @param window_id The identifier of the window containing the moving tab. + // @param from_index The index from which the tab moved away. + // @param to_index The index where the tab moved to. + virtual HRESULT OnMoved(HWND tab_handle, int window_id, + int from_index, int to_index); + + // Sends the tabs.onRemoved event to the Broker. + // @param tab_handle The identifier of the tab that was removed. + virtual HRESULT OnRemoved(HWND tab_handle); + + // Sends the tabs.onSelectionChanged event to the Broker. + // @param tab_handle The HWND of the tab was selected. + // @param window_id The identifier of the window containing the selected tab. + virtual HRESULT OnSelectionChanged(HWND tab_handle, int window_id); + + // Sends the tabs.onCreated :b+event to the Broker. + // @param tab_handle The HWND of the tab that was created. + // @param url The current URL of the page. + // @param completed If true, the status of the page is completed, otherwise, + // it is any othe other status values. + virtual HRESULT OnCreated(HWND tab_handle, BSTR url, bool completed); + + // Sends the tabs.onUpdated event to the Broker. + // @param tab_handle The HWND of the tab that was updated. + // @param url The [optional] url where the tab was navigated to. + // @param ready_state The ready state of the tab. + virtual HRESULT OnUpdated(HWND tab_handle, BSTR url, + READYSTATE ready_state); + + // Sends the private message to unmap a tab to its BHO. This is the last + // message a BHO should send, as its tab_id will no longer be mapped afterward + // and will assert if used. + // @param tab_handle The HWND of the tab to unmap. + // @param tab_id The id of the tab to unmap. + virtual HRESULT OnTabUnmapped(HWND tab_handle, int tab_id); + + private: + DISALLOW_COPY_AND_ASSIGN(TabEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc b/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc new file mode 100644 index 0000000..dc2726d --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc @@ -0,0 +1,141 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for TabEventsFunnel. + +#include <atlcomcli.h> + +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestTabEventsFunnel : public TabEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(TabEventsFunnelTest, OnTabCreated) { + TestTabEventsFunnel tab_events_funnel; + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + std::string url("http://www.google.com"); + std::string status(keys::kStatusValueComplete); + + DictionaryValue dict; + dict.SetInteger(keys::kIdKey, tab_id); + dict.SetString(keys::kUrlKey, url); + dict.SetString(keys::kStatusKey, status); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabCreated), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnCreated(tab_handle, + CComBSTR(url.c_str()), + true)); +} + +TEST(TabEventsFunnelTest, OnTabMoved) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + int window_id = 24; + int from_index = 12; + int to_index = 21; + + ListValue tab_moved_args; + tab_moved_args.Append(Value::CreateIntegerValue(tab_id)); + + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + dict->SetInteger(keys::kFromIndexKey, from_index); + dict->SetInteger(keys::kFromIndexKey, to_index); + tab_moved_args.Append(dict); + + EXPECT_CALL(tab_events_funnel, SendEvent(StrEq(ext_event_names::kOnTabMoved), + ValuesEqual(&tab_moved_args))).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnMoved(tab_handle, window_id, + from_index, to_index)); +} + +TEST(TabEventsFunnelTest, OnTabRemoved) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + scoped_ptr<Value> args(Value::CreateIntegerValue(tab_id)); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabRemoved), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnRemoved(tab_handle)); +} + +TEST(TabEventsFunnelTest, OnTabSelectionChanged) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + int window_id = 24; + + ListValue args; + args.Append(Value::CreateIntegerValue(tab_id)); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + args.Append(dict); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabSelectionChanged), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnSelectionChanged(tab_handle, + window_id)); +} + +TEST(TabEventsFunnelTest, OnTabUpdated) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 24; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + READYSTATE ready_state = READYSTATE_INTERACTIVE; + + ListValue args; + args.Append(Value::CreateIntegerValue(tab_id)); + DictionaryValue* dict = new DictionaryValue; + dict->SetString(keys::kStatusKey, keys::kStatusValueLoading); + args.Append(dict); + + // Without a URL. + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabUpdated), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnUpdated(tab_handle, NULL, + ready_state)); + // With a URL. + CComBSTR url(L"http://imbored.com"); + dict->SetString(keys::kUrlKey, url.m_str); + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabUpdated), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnUpdated(tab_handle, url, + ready_state)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/tab_window_manager.cc b/ceee/ie/plugin/bho/tab_window_manager.cc new file mode 100644 index 0000000..c557436 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_window_manager.cc @@ -0,0 +1,244 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/tab_window_manager.h" + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/common/ie_tab_interfaces.h" + +namespace { + +// Adapter for tab-window interfaces on IE7/IE8. +template <class IeTabWindow> +class TabWindowAdapter : public TabWindow { + public: + explicit TabWindowAdapter(IeTabWindow* tab_window) + : tab_window_(tab_window) { + } + + STDMETHOD(GetBrowser)(IDispatch** browser) { + DCHECK(tab_window_ != NULL); + DCHECK(browser != NULL); + DCHECK(*browser == NULL); + return tab_window_->GetBrowser(browser); + } + + STDMETHOD(GetID)(long* id) { + DCHECK(tab_window_ != NULL); + DCHECK(id != NULL); + return tab_window_->GetID(id); + } + + STDMETHOD(Close)() { + DCHECK(tab_window_ != NULL); + return tab_window_->Close(); + } + + private: + CComPtr<IeTabWindow> tab_window_; + DISALLOW_COPY_AND_ASSIGN(TabWindowAdapter); +}; + +// Adapter for tab-window-manager interfaces on IE7/IE8. +template <class IeTabWindowManager, class IeTabWindow> +class TabWindowManagerAdapter : public TabWindowManager { + public: + explicit TabWindowManagerAdapter(IeTabWindowManager* manager) + : manager_(manager) { + } + + STDMETHOD(IndexFromHWND)(HWND window, long* index) { + DCHECK(manager_ != NULL); + DCHECK(index != NULL); + return manager_->IndexFromHWND(window, index); + } + + STDMETHOD(SelectTab)(long index) { + DCHECK(manager_ != NULL); + return manager_->SelectTab(index); + } + + STDMETHOD(GetCount)(long* count) { + DCHECK(manager_ != NULL); + DCHECK(count != NULL); + return manager_->GetCount(count); + } + + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) { + DCHECK(manager_ != NULL); + DCHECK(tab_window != NULL); + + CComPtr<IUnknown> tab_unk; + HRESULT hr = E_FAIL; + hr = manager_->GetItem(index, &tab_unk); + DCHECK(SUCCEEDED(hr)) << "GetItem failed for index: " << index << ". " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + CComPtr<IeTabWindow> ie_tab_window; + hr = tab_unk.QueryInterface(&ie_tab_window); + DCHECK(SUCCEEDED(hr)) << "QI failed IeTabWindow. " << com::LogHr(hr); + if (FAILED(hr)) + return hr; + + tab_window->reset(new TabWindowAdapter<IeTabWindow>(ie_tab_window)); + return S_OK; + } + + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) { + DCHECK(manager_ != NULL); + return manager_->RepositionTab(moving_id, dest_id, unused); + } + + STDMETHOD(CloseAllTabs)() { + DCHECK(manager_ != NULL); + return manager_->CloseAllTabs(); + } + + private: + CComPtr<IeTabWindowManager> manager_; + DISALLOW_COPY_AND_ASSIGN(TabWindowManagerAdapter); +}; + +typedef TabWindowManagerAdapter<ITabWindowManagerIe9, ITabWindowIe9> + TabWindowManagerAdapter9; +typedef TabWindowManagerAdapter<ITabWindowManagerIe8, ITabWindowIe8_1> + TabWindowManagerAdapter8_1; +typedef TabWindowManagerAdapter<ITabWindowManagerIe8, ITabWindowIe8> + TabWindowManagerAdapter8; +typedef TabWindowManagerAdapter<ITabWindowManagerIe7, ITabWindowIe7> + TabWindowManagerAdapter7; + +// Faked tab-window class for IE6. +class TabWindow6 : public TabWindow { + public: + explicit TabWindow6(TabWindowManager *manager) : manager_(manager) {} + + STDMETHOD(GetBrowser)(IDispatch** browser) { + // Currently nobody calls this method. + NOTREACHED(); + return E_NOTIMPL; + } + + STDMETHOD(GetID)(long* id) { + DCHECK(id != NULL); + *id = 0; + return S_OK; + } + + STDMETHOD(Close)() { + DCHECK(manager_ != NULL); + // IE6 has only one tab for each frame window. + // So closing one tab means closing all the tabs. + return manager_->CloseAllTabs(); + } + + private: + TabWindowManager* manager_; + DISALLOW_COPY_AND_ASSIGN(TabWindow6); +}; + +// Faked tab-window-manager class for IE6. +class TabWindowManager6 : public TabWindowManager { + public: + explicit TabWindowManager6(HWND frame_window) : frame_window_(frame_window) {} + + STDMETHOD(IndexFromHWND)(HWND window, long* index) { + DCHECK(window != NULL); + DCHECK(index != NULL); + *index = 0; + return S_OK; + } + + STDMETHOD(SelectTab)(long index) { + DCHECK_EQ(0, index); + return S_OK; + } + + STDMETHOD(GetCount)(long* count) { + DCHECK(count != NULL); + *count = 1; + return S_OK; + } + + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) { + DCHECK(tab_window != NULL); + DCHECK_EQ(0, index); + tab_window->reset(new TabWindow6(this)); + return S_OK; + } + + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) { + DCHECK_EQ(0, moving_id); + DCHECK_EQ(0, dest_id); + return S_OK; + } + + STDMETHOD(CloseAllTabs)() { + DCHECK(IsWindow(frame_window_)); + ::PostMessage(frame_window_, WM_CLOSE, 0, 0); + return S_OK; + } + private: + HWND frame_window_; +}; + +} // anonymous namespace + +HRESULT CreateTabWindowManager(HWND frame_window, + scoped_ptr<TabWindowManager>* manager) { + CComPtr<IUnknown> manager_unknown; + HRESULT hr = ie_tab_interfaces::TabWindowManagerFromFrame( + frame_window, + __uuidof(IUnknown), + reinterpret_cast<void**>(&manager_unknown)); + if (SUCCEEDED(hr)) { + DCHECK(manager_unknown != NULL); + CComQIPtr<ITabWindowManagerIe9> manager_ie9(manager_unknown); + if (manager_ie9 != NULL) { + manager->reset(new TabWindowManagerAdapter9(manager_ie9)); + return S_OK; + } + + CComQIPtr<ITabWindowManagerIe8> manager_ie8(manager_unknown); + if (manager_ie8 != NULL) { + // On IE8, there was a version change that introduced a new version + // of the ITabWindow interface even though the ITabWindowManager didn't + // change. So we must find which one before we make up our mind. + CComPtr<IUnknown> tab_window_punk; + hr = manager_ie8->GetItem(0, &tab_window_punk); + DCHECK(SUCCEEDED(hr) && tab_window_punk != NULL) << com::LogHr(hr); + CComQIPtr<ITabWindowIe8> tab_window8(tab_window_punk); + if (tab_window8 != NULL) { + manager->reset(new TabWindowManagerAdapter8(manager_ie8)); + return S_OK; + } + CComQIPtr<ITabWindowIe8_1> tab_window8_1(tab_window_punk); + if (tab_window8_1 != NULL) { + manager->reset(new TabWindowManagerAdapter8_1(manager_ie8)); + return S_OK; + } + NOTREACHED() << "Found an ITabWindow Punk that is not known by us!!!"; + return E_UNEXPECTED; + } + + CComQIPtr<ITabWindowManagerIe7> manager_ie7(manager_unknown); + if (manager_ie7 != NULL) { + manager->reset(new TabWindowManagerAdapter7(manager_ie7)); + return S_OK; + } + + // Maybe future IE would have another interface. Consider it as IE6 anyway. + NOTREACHED(); + } + + LOG(WARNING) << + "Could not create a sensible brower interface, defaulting to IE6"; + manager->reset(new TabWindowManager6(frame_window)); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/tab_window_manager.h b/ceee/ie/plugin/bho/tab_window_manager.h new file mode 100644 index 0000000..4f1ef2c --- /dev/null +++ b/ceee/ie/plugin/bho/tab_window_manager.h @@ -0,0 +1,39 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ + +#include <atlbase.h> + +#include "base/scoped_ptr.h" + +// A unified tab-window interface for IE6/IE7/IE8. +class TabWindow { + public: + STDMETHOD(GetBrowser)(IDispatch** browser) = 0; + STDMETHOD(GetID)(long* id) = 0; + STDMETHOD(Close)() = 0; + virtual ~TabWindow() {} +}; + +// A unified tab-window-manager interface for IE6/IE7/IE8. +class TabWindowManager { + public: + STDMETHOD(IndexFromHWND)(HWND window, long* index) = 0; + STDMETHOD(SelectTab)(long index) = 0; + STDMETHOD(GetCount)(long* count) = 0; + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) = 0; + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) = 0; + STDMETHOD(CloseAllTabs)() = 0; + virtual ~TabWindowManager() {} +}; + +// Creates a TabWindowManager object for the specified IEFrame window. +// @param frame_window The top-level frame window you wish to manage. +// @param manager The created TabWindowManager object. +HRESULT CreateTabWindowManager(HWND frame_window, + scoped_ptr<TabWindowManager>* manager); + +#endif // CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/tool_band_visibility.cc b/ceee/ie/plugin/bho/tool_band_visibility.cc new file mode 100644 index 0000000..8952457 --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/tool_band_visibility.h" + +#include "base/logging.h" +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/toolband/tool_band.h" + +// The ToolBandVisibility class allows us to recover from a number of +// features of IE that can cause our toolband to become invisible +// unexpectedly. + +// A brief discussion of toolband visibility in IE. +// +// See MS knowledge base article Q219427 for more detail. +// +// IE does some interesting tricks to cache toolband layout information. One +// side effect of this is that sometimes it can get confused about whether a +// toolband should be shown or not. +// +// In theory all that is needed to recover from this is a call to +// IWebBrowser2::ShowBrowserBar. Unfortunately, there are some +// gotchas. +// It's not easy to tell when IE has refused to display your +// toolband. The only way to be sure is to either call +// ShowBrowserBar on every startup or wait for some reasonable +// time to see if IE showed the toolband and then kick it if it +// didn't. +// In IE6, a single call to ShowBrowserBar is often not enough to +// unhide a hidden toolband. It's sometimes necessary to call +// ShowBrowserBar THREE times (show, then hide, then show) to get +// things into a sane state. +// TODO(cindylau@chromium.org): In IE6, just calling ShowBrowserBar +// will usually cause IE to scrunch our toolband up at the end of +// a line instead of giving it its own line. We can combat this +// by requesting to be shown on our own line, but if we do that +// in all cases we'll anger users who WANT our toolband to share +// a line with others. +// Some other toolbands (notably SnagIt versions 6 and 7) will +// cause toolbands to get hidden when opening a new tab in IE7. +// Visibility must be checked on every new tab and window to be +// sure. +// Calls to ShowBrowserBar are slow and we should avoid them +// whenever possible. +// Calls to ShowBrowserBar should be made from the same UI thread +// responsible for the browser object we use to unhide the +// toolband. Failure to do this can cause the toolband to +// believe it belongs to a different thread than it does. +// TODO(cindylau@chromium.org): IE tracks layout information in the +// registry. When toolbands are added or removed the installing +// toolband MUST clear the registry (badness, including possible +// crashes in IE will result if not). When this layout +// information is cleared all third party toolbands are hidden. + +// This code attempts to address all of these issues. +// The core is the VisibilityUtil class. +// When VisibilityUtil::CheckVisibility is called it checks for common +// indicators that a toolband is hidden. If it sees a smoking gun +// it unhides the toolband. Otherwise it creates a notification window and +// a timer. If a toolband doesn't report itself as active for a particular +// browser window before the timer fires it unhides the toolband. + +namespace { +const int kVisibilityTimerId = 1; +const DWORD kVisibilityCheckDelay = 2000; // 2 seconds. +} // anonymous namespace + +std::set<IUnknown*> ToolBandVisibility::visibility_set_; +CComAutoCriticalSection ToolBandVisibility::visibility_set_crit_; + +ToolBandVisibility::ToolBandVisibility() + : web_browser_(NULL) { +} + +ToolBandVisibility::~ToolBandVisibility() { + DCHECK(m_hWnd == NULL); +} + +void ToolBandVisibility::ReportToolBandVisible(IWebBrowser2* web_browser) { + DCHECK(web_browser); + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return; + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + visibility_set_.insert(browser_identity); +} + +bool ToolBandVisibility::IsToolBandVisible(IWebBrowser2* web_browser) { + DCHECK(web_browser); + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return false; + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + return visibility_set_.count(browser_identity) != 0; +} + +void ToolBandVisibility::ClearCachedVisibility(IWebBrowser2* web_browser) { + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + if (web_browser) { + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return; + visibility_set_.erase(browser_identity); + } else { + visibility_set_.clear(); + } +} + +void ToolBandVisibility::CheckToolBandVisibility(IWebBrowser2* web_browser) { + DCHECK(web_browser); + web_browser_ = web_browser; + + if (!ceee_module_util::GetOptionToolbandIsHidden() && + CreateNotificationWindow()) { + SetWindowTimer(kVisibilityTimerId, kVisibilityCheckDelay); + } +} + +void ToolBandVisibility::TearDown() { + if (web_browser_ != NULL) { + ClearCachedVisibility(web_browser_); + } + if (m_hWnd != NULL) { + CloseNotificationWindow(); + } +} + +bool ToolBandVisibility::CreateNotificationWindow() { + return Create(HWND_MESSAGE) != NULL; +} + +void ToolBandVisibility::CloseNotificationWindow() { + DestroyWindow(); +} + +void ToolBandVisibility::SetWindowTimer(UINT timer_id, UINT delay) { + SetTimer(timer_id, delay, NULL); +} + +void ToolBandVisibility::KillWindowTimer(UINT timer_id) { + KillTimer(timer_id); +} + +void ToolBandVisibility::OnTimer(UINT_PTR nIDEvent) { + DCHECK(nIDEvent == kVisibilityTimerId); + KillWindowTimer(nIDEvent); + + if (!IsToolBandVisible(web_browser_)) { + UnhideToolBand(); + } + ClearCachedVisibility(web_browser_); + CloseNotificationWindow(); +} + +void ToolBandVisibility::UnhideToolBand() { + // Ignore ShowDW calls that are triggered by our calls here to + // ShowBrowserBar. + ceee_module_util::SetIgnoreShowDWChanges(true); + CComVariant toolband_class(CLSID_ToolBand); + CComVariant show(false); + CComVariant empty; + show = true; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + + // Force IE to ignore bad caching. This is a problem generally before IE7, + // and at least sometimes even in IE7 (bb1291042). + show = false; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + show = true; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + + ceee_module_util::SetIgnoreShowDWChanges(false); +} diff --git a/ceee/ie/plugin/bho/tool_band_visibility.h b/ceee/ie/plugin/bho/tool_band_visibility.h new file mode 100644 index 0000000..f6e5767 --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ +#define CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ + +#include <atlbase.h> +#include <atlcrack.h> +#include <atlwin.h> +#include <mshtml.h> // Needed for exdisp.h +#include <exdisp.h> +#include <set> + +#include "base/basictypes.h" + +// The ToolBandVisibility class allows us to recover from a number of +// features of IE that can cause our toolband to become invisible +// unexpectedly. See the .cc file for more details. +class ATL_NO_VTABLE ToolBandVisibility + : public CWindowImpl<ToolBandVisibility> { + public: + // Inform ToolBandVisibility that the toolband has been created for this + // browser instance. + static void ReportToolBandVisible(IWebBrowser2* web_browser); + + BEGIN_MSG_MAP(ToolBandVisibility) + MSG_WM_CREATE(OnCreate) + MSG_WM_TIMER(OnTimer) + END_MSG_MAP() + + protected: + // Returns true iff ReportToolBandVisible has been called for the given web + // browser. + static bool IsToolBandVisible(IWebBrowser2* web_browser); + + // Cleans up the visibility set entry for the given browser that was stored + // when the browser called ReportToolBandVisible. If the pointer passed in is + // NULL, all items in the entire visibility set are deleted (this is useful + // for testing). + static void ClearCachedVisibility(IWebBrowser2* web_browser); + + ToolBandVisibility(); + virtual ~ToolBandVisibility(); + + // Checks toolband visibility, and forces the toolband to be shown if it + // isn't, and the user hasn't explicitly hidden the toolband. + void CheckToolBandVisibility(IWebBrowser2* web_browser); + + // Cleans up toolband visibility data when the BHO is being torn down. + void TearDown(); + + // Set up the notification window used for processing ToolBandVisibilityWindow + // messages. + // Unfortunately, we need to create a notification window to handle a delayed + // check for the toolband. Other methods (like creating a new thread and + // sleeping) will not work. + // Returns true on success. + // Also serves as a unit testing seam. + virtual bool CreateNotificationWindow(); + + // Unit testing seam for destroying the window. + virtual void CloseNotificationWindow(); + + // Unit testing seam for setting the timer for the visibility window. + virtual void SetWindowTimer(UINT timer_id, UINT delay); + + // Unit testing seam for killing the timer for the visibility window. + virtual void KillWindowTimer(UINT timer_id); + + // @name Message handlers. + // @{ + // The OnCreate handler is empty; subclasses can override it for more + // functionality. + virtual LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct) { + return 0; + } + void OnTimer(UINT_PTR nIDEvent); + // @} + + // Forces the toolband to be shown. + void UnhideToolBand(); + + // The web browser instance for which we are tracking toolband visibility. + CComPtr<IWebBrowser2> web_browser_; + + private: + // The set of browser windows that have visible toolbands. + // The BHO for each browser window is responsible for cleaning up its + // own entry in this set when it's torn down; see ClearToolBandVisibility. + // This collection does not hold references to the objects it stores. + static std::set<IUnknown*> visibility_set_; + static CComAutoCriticalSection visibility_set_crit_; + + DISALLOW_COPY_AND_ASSIGN(ToolBandVisibility); +}; + +#endif // CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ diff --git a/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc b/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc new file mode 100644 index 0000000..0876c9d --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tests for ToolBandVisibility. + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "ceee/testing/utils/mock_com.h" +#include "gtest/gtest.h" + +namespace { +using testing::_; +using testing::Return; +using testing::StrictMock; + +class TestingToolBandVisibility : public ToolBandVisibility { + public: + TestingToolBandVisibility() {} + virtual ~TestingToolBandVisibility() {} + + IWebBrowser2* GetWindowBrowser() const { + return web_browser_; + } + + using ToolBandVisibility::IsToolBandVisible; + using ToolBandVisibility::ClearCachedVisibility; + using ToolBandVisibility::CheckToolBandVisibility; + using ToolBandVisibility::OnTimer; + + MOCK_METHOD0(CreateNotificationWindow, bool()); + MOCK_METHOD0(CloseNotificationWindow, void()); + MOCK_METHOD2(SetWindowTimer, void(UINT, UINT)); + MOCK_METHOD1(KillWindowTimer, void(UINT)); +}; + +class MockBrowser + : public testing::MockIWebBrowser2, + public InitializingCoClass<MockBrowser> { + public: + HRESULT Initialize(MockBrowser** browser) { + *browser = this; + return S_OK; + } +}; + +class ToolBandVisibilityTest : public testing::Test { + public: + virtual void TearDown() { + TestingToolBandVisibility::ClearCachedVisibility(NULL); + } + + void CreateMockBrowser(MockBrowser** browser, IWebBrowser2** browser_keeper) { + ASSERT_TRUE(browser && browser_keeper); + ASSERT_HRESULT_SUCCEEDED( + MockBrowser::CreateInitialized(browser, browser_keeper)); + } + + void ExpectCheckToolBandVisibilitySucceeded( + TestingToolBandVisibility* visibility) { + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(false)); + EXPECT_CALL(*visibility, CreateNotificationWindow()) + .WillOnce(Return(true)); + EXPECT_CALL(*visibility, SetWindowTimer(1, 2000)).Times(1); + } + + StrictMock<testing::MockCeeeModuleUtils> ceee_module_utils_; + TestingToolBandVisibility visibility; +}; + +TEST_F(ToolBandVisibilityTest, ReportToolBandVisibleSucceeds) { + MockBrowser* browser1; + MockBrowser* browser2; + MockBrowser* browser3; + CComPtr<IWebBrowser2> browser1_keeper, browser2_keeper, browser3_keeper; + CreateMockBrowser(&browser1, &browser1_keeper); + CreateMockBrowser(&browser2, &browser2_keeper); + CreateMockBrowser(&browser3, &browser3_keeper); + + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + TestingToolBandVisibility::ReportToolBandVisible(browser2_keeper); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + TestingToolBandVisibility::ReportToolBandVisible(browser3_keeper); + TestingToolBandVisibility::ClearCachedVisibility(browser2_keeper); + + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + // Clearing visibility for a browser that isn't visible is essentially a + // no-op. + TestingToolBandVisibility::ClearCachedVisibility(browser1_keeper); + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilityHiddenToolband) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(true)); + EXPECT_CALL(visibility, CreateNotificationWindow()).Times(0); + visibility.CheckToolBandVisibility(browser_keeper); + EXPECT_EQ(browser_keeper, visibility.GetWindowBrowser()); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilityCreateFailed) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(false)); + EXPECT_CALL(visibility, CreateNotificationWindow()) + .WillOnce(Return(false)); + EXPECT_CALL(visibility, SetWindowTimer(_, _)).Times(0); + visibility.CheckToolBandVisibility(browser_keeper); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilitySucceeded) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); +} + +TEST_F(ToolBandVisibilityTest, OnTimerVisibleToolBand) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + TestingToolBandVisibility::ReportToolBandVisible(browser_keeper); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser_keeper)); + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); + + EXPECT_CALL(visibility, KillWindowTimer(1)).Times(1); + EXPECT_CALL(visibility, CloseNotificationWindow()).Times(1); + visibility.OnTimer(1); + + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser_keeper)); +} + +TEST_F(ToolBandVisibilityTest, OnTimerInvisibleToolBand) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); + + EXPECT_CALL(visibility, KillWindowTimer(1)).Times(1); + EXPECT_CALL(ceee_module_utils_, SetIgnoreShowDWChanges(true)).Times(1); + EXPECT_CALL(*browser, ShowBrowserBar(_, _, _)).Times(3); + EXPECT_CALL(ceee_module_utils_, SetIgnoreShowDWChanges(false)).Times(1); + EXPECT_CALL(visibility, CloseNotificationWindow()).Times(1); + visibility.OnTimer(1); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/web_browser_events_source.h b/ceee/ie/plugin/bho/web_browser_events_source.h new file mode 100644 index 0000000..8cdec9c --- /dev/null +++ b/ceee/ie/plugin/bho/web_browser_events_source.h @@ -0,0 +1,34 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Interface of WebBrowser events source. +#ifndef CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ +#define CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ + +#include <exdisp.h> + +// WebBrowserEventsSource defines the interface of a WebBrowser event publisher, +// which is used to register/unregister event consumers and fire WebBrowser +// events to them. +class WebBrowserEventsSource { + public: + // The interface of WebBrowser event consumers. + class Sink { + public: + virtual ~Sink() {} + virtual void OnBeforeNavigate(IWebBrowser2* browser, BSTR url) {} + virtual void OnDocumentComplete(IWebBrowser2* browser, BSTR url) {} + virtual void OnNavigateComplete(IWebBrowser2* browser, BSTR url) {} + virtual void OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code) {} + virtual void OnNewWindow(BSTR url_context, BSTR url) {} + }; + + virtual ~WebBrowserEventsSource() {} + + virtual void RegisterSink(Sink* sink) = 0; + virtual void UnregisterSink(Sink* sink) = 0; +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ diff --git a/ceee/ie/plugin/bho/web_progress_notifier.cc b/ceee/ie/plugin/bho/web_progress_notifier.cc new file mode 100644 index 0000000..c32ac70 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier.cc @@ -0,0 +1,653 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web progress notifier implementation. +#include "ceee/ie/plugin/bho/web_progress_notifier.h" + +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/plugin/bho/dom_utils.h" + +namespace { + +// In milliseconds. It defines the "effective period" of user action. A user +// action is considered as a possible cause of the next navigation if the +// navigation happens in this period. +// This is a number we feel confident of based on past experience. +const int kUserActionTimeThresholdMs = 500; + +// String constants for the values of TransitionQualifier. +const char kClientRedirect[] = "client_redirect"; +const char kServerRedirect[] = "server_redirect"; +const char kForwardBack[] = "forward_back"; +const char kRedirectMetaRefresh[] = "redirect_meta_refresh"; +const char kRedirectOnLoad[] = "redirect_onload"; +const char kRedirectJavaScript[] = "redirect_javascript"; + +} // namespace + +WebProgressNotifier::WebProgressNotifier() + : web_browser_events_source_(NULL), + main_frame_info_(PageTransition::LINK, 0, kMainFrameId), + tab_handle_(NULL), + next_subframe_id_(1), + main_frame_document_complete_(true), + tracking_content_window_action_(false), + tracking_browser_ui_action_(false), + has_potential_javascript_redirect_(false), + cached_webrequest_notifier_(NULL), + webrequest_notifier_initialized_(false), + create_thread_id_(::GetCurrentThreadId()) { +} + +WebProgressNotifier::~WebProgressNotifier() { + DCHECK(!webrequest_notifier_initialized_); + DCHECK(web_browser_events_source_ == NULL); + DCHECK(window_message_source_ == NULL); + DCHECK(main_browser_ == NULL); + DCHECK(travel_log_ == NULL); + DCHECK(tab_handle_ == NULL); +} + +HRESULT WebProgressNotifier::Initialize( + WebBrowserEventsSource* web_browser_events_source, + HWND tab_window, + IWebBrowser2* main_browser) { + if (web_browser_events_source == NULL || tab_window == NULL || + main_browser == NULL) { + return E_INVALIDARG; + } + + tab_handle_ = reinterpret_cast<CeeeWindowHandle>(tab_window); + main_browser_ = main_browser; + + CComQIPtr<IServiceProvider> service_provider(main_browser); + if (service_provider == NULL || + FAILED(service_provider->QueryService( + SID_STravelLogCursor, IID_ITravelLogStg, + reinterpret_cast<void**>(&travel_log_))) || + travel_log_ == NULL) { + TearDown(); + return E_FAIL; + } + + web_browser_events_source_ = web_browser_events_source; + web_browser_events_source_->RegisterSink(this); + + window_message_source_.reset(CreateWindowMessageSource()); + if (window_message_source_ == NULL) { + TearDown(); + return E_FAIL; + } + window_message_source_->RegisterSink(this); + + if (!webrequest_notifier()->RequestToStart()) + NOTREACHED() << "Failed to start the WebRequestNotifier service."; + webrequest_notifier_initialized_ = true; + return S_OK; +} + +void WebProgressNotifier::TearDown() { + if (webrequest_notifier_initialized_) { + webrequest_notifier()->RequestToStop(); + webrequest_notifier_initialized_ = false; + } + if (web_browser_events_source_ != NULL) { + web_browser_events_source_->UnregisterSink(this); + web_browser_events_source_ = NULL; + } + if (window_message_source_ != NULL) { + window_message_source_->UnregisterSink(this); + window_message_source_->TearDown(); + window_message_source_.reset(NULL); + } + + main_browser_.Release(); + travel_log_.Release(); + tab_handle_ = NULL; +} + +void WebProgressNotifier::OnBeforeNavigate(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::BEFORE_NAVIGATE)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + // TODO(yzshen@google.com): add support for requestId. + HRESULT hr = webnavigation_events_funnel().OnBeforeNavigate( + tab_handle_, url, frame_info->frame_id, -1, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onBeforeNavigate event " + << com::LogHr(hr); + + if (frame_info->IsMainFrame()) { + frame_info->ClearTransition(); + + // The order in which we set these transitions is **very important.** + // If there was no DocumentComplete, then there are two likely options: + // the transition was a JavaScript redirect, or the user navigated to a + // second page before the first was done loading. We initialize the + // transition to JavaScript redirect first. If there are other signals such + // as the user clicked/typed, we'll overwrite this value with the + // appropriate value. + if (!main_frame_document_complete_ || + wcsncmp(url, L"javascript:", wcslen(L"javascript:")) == 0 || + has_potential_javascript_redirect_) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_JAVASCRIPT); + } + + // Override the transition if there is user action in the tab content window + // or browser UI. + if (IsPossibleUserActionInContentWindow()) { + frame_info->SetTransition(PageTransition::LINK, 0); + } else if (IsPossibleUserActionInBrowserUI()) { + frame_info->SetTransition(PageTransition::TYPED, 0); + } + + // Override the transition if we find some signals that we are more + // confident about. + if (InOnLoadEvent(browser)) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_ONLOAD); + } else if (IsMetaRefresh(browser, url)) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_META_REFRESH); + } + + // Assume that user actions don't have long-lasting effect: user actions + // before the current navigation may be the cause of this navigation; but + // they can not affect any subsequent navigation. + // + // Under this assumption, we don't need to remember previous user actions. + tracking_content_window_action_ = false; + tracking_browser_ui_action_ = false; + } +} + +void WebProgressNotifier::OnDocumentComplete(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::DOCUMENT_COMPLETE)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + if (frame_info->IsMainFrame()) { + main_frame_document_complete_ = true; + + has_potential_javascript_redirect_ = + HasPotentialJavaScriptRedirect(browser); + } + + HRESULT hr = webnavigation_events_funnel().OnCompleted( + tab_handle_, url, frame_info->frame_id, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the webNavigation.onCompleted event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnNavigateComplete(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::NAVIGATE_COMPLETE)) { + filtering_info_.pending_navigate_complete_browser = browser; + filtering_info_.pending_navigate_complete_url = url; + filtering_info_.pending_navigate_complete_timestamp = base::Time::Now(); + } else { + HandleNavigateComplete(browser, url, base::Time::Now()); + } +} + +void WebProgressNotifier::HandleNavigateComplete( + IWebBrowser2* browser, + BSTR url, + const base::Time& timestamp) { + // NOTE: For the first OnNavigateComplete event in a tab/window, this method + // may not be called at the moment when IE fires the event. + // As a result, be careful if you need to query the browser state in this + // method, because the state may have changed after IE fired the event. + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + if (frame_info->IsMainFrame()) { + main_frame_document_complete_ = false; + + if (IsForwardBack(url)) { + frame_info->SetTransition(PageTransition::AUTO_BOOKMARK, FORWARD_BACK); + } + } + + HRESULT hr = webnavigation_events_funnel().OnCommitted( + tab_handle_, url, frame_info->frame_id, + PageTransition::CoreTransitionString(frame_info->transition_type), + TransitionQualifiersString(frame_info->transition_qualifiers).c_str(), + timestamp); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the webNavigation.onCommitted event " + << com::LogHr(hr); + + if (frame_info->IsMainFrame()) + subframe_map_.clear(); +} + +void WebProgressNotifier::OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::NAVIGATE_ERROR)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + HRESULT hr = webnavigation_events_funnel().OnErrorOccurred( + tab_handle_, url, frame_info->frame_id, CComBSTR(L""), base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onErrorOccurred event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnNewWindow(BSTR url_context, BSTR url) { + if (url_context == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(NULL, FilteringInfo::NEW_WINDOW)) + return; + + HRESULT hr = webnavigation_events_funnel().OnBeforeRetarget( + tab_handle_, url_context, url, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onBeforeRetarget event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnHandleMessage( + WindowMessageSource::MessageType type, + const MSG* message_info) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + // This is called when a user input message is about to be handled by any + // window procedure on the current thread, we should not do anything expensive + // here that would degrade user experience. + switch (type) { + case WindowMessageSource::TAB_CONTENT_WINDOW: { + if (IsUserActionMessage(message_info->message)) { + tracking_content_window_action_ = true; + last_content_window_action_time_ = base::Time::Now(); + } + break; + } + case WindowMessageSource::BROWSER_UI_SAME_THREAD: { + if (IsUserActionMessage(message_info->message)) { + tracking_browser_ui_action_ = true; + last_browser_ui_action_time_ = base::Time::Now(); + } + break; + } + default: { + NOTREACHED(); + break; + } + } +} + +WindowMessageSource* WebProgressNotifier::CreateWindowMessageSource() { + scoped_ptr<WindowMessageSource> source(new WindowMessageSource()); + + return source->Initialize() ? source.release() : NULL; +} + +std::string WebProgressNotifier::TransitionQualifiersString( + TransitionQualifiers qualifiers) { + std::string result; + for (unsigned int current_qualifier = FIRST_TRANSITION_QUALIFIER; + current_qualifier <= LAST_TRANSITION_QUALIFIER; + current_qualifier = current_qualifier << 1) { + if ((qualifiers & current_qualifier) != 0) { + if (!result.empty()) + result.append("|"); + switch (current_qualifier) { + case CLIENT_REDIRECT: + result.append(kClientRedirect); + break; + case SERVER_REDIRECT: + result.append(kServerRedirect); + break; + case FORWARD_BACK: + result.append(kForwardBack); + break; + case REDIRECT_META_REFRESH: + result.append(kRedirectMetaRefresh); + break; + case REDIRECT_ONLOAD: + result.append(kRedirectOnLoad); + break; + case REDIRECT_JAVASCRIPT: + result.append(kRedirectJavaScript); + break; + default: + NOTREACHED(); + break; + } + } + } + return result; +} + +bool WebProgressNotifier::GetFrameInfo(IWebBrowser2* browser, + FrameInfo** frame_info) { + DCHECK(browser != NULL && frame_info != NULL); + + if (IsMainFrame(browser)) { + *frame_info = &main_frame_info_; + return true; + } + + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) + return false; + + SubframeMap::iterator iter = subframe_map_.find(browser_identity); + if (iter != subframe_map_.end()) { + *frame_info = &iter->second; + } else { + // PageTransition::MANUAL_SUBFRAME, as well as transition qualifiers for + // subframes, is not supported. + subframe_map_.insert(std::make_pair(browser_identity, + FrameInfo(PageTransition::AUTO_SUBFRAME, + 0, next_subframe_id_++))); + *frame_info = &subframe_map_[browser_identity]; + } + return true; +} + +bool WebProgressNotifier::GetDocument(IWebBrowser2* browser, + REFIID id, + void** document) { + DCHECK(browser != NULL && document != NULL); + + CComPtr<IDispatch> document_disp; + if (FAILED(browser->get_Document(&document_disp)) || document_disp == NULL) + return false; + return SUCCEEDED(document_disp->QueryInterface(id, document)) && + *document != NULL; +} + +bool WebProgressNotifier::IsForwardBack(BSTR url) { + DWORD length = 0; + DWORD position = 0; + + if (FAILED(travel_log_->GetCount(TLEF_RELATIVE_BACK | TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE, + &length))) { + length = -1; + } else { + length++; // Add 1 for the current entry. + } + + if (FAILED(travel_log_->GetCount(TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE, + &position))) { + position = -1; + } + + // Consider this is a forward/back navigation, if: + // (1) state of the forward/back list has been successfully retrieved, and + // (2) the length of the forward/back list is not changed, and + // (3) (a) the current position is not the newest entry of the + // forward/back list, or + // (b) we are not at the newest entry of the list before the current + // navigation and the URL of the newest entry is not changed by the + // current navigation. + bool is_forward_back = + length != -1 && previous_travel_log_info_.length != -1 && + position != -1 && previous_travel_log_info_.position != -1 && + length == previous_travel_log_info_.length && + (position != 0 || + (previous_travel_log_info_.position != 0 && + previous_travel_log_info_.newest_url == url)); + + previous_travel_log_info_.length = length; + previous_travel_log_info_.position = position; + if (position == 0 && !is_forward_back) + previous_travel_log_info_.newest_url = url; + + return is_forward_back; +} + +bool WebProgressNotifier::InOnLoadEvent(IWebBrowser2* browser) { + DCHECK(browser != NULL); + + CComPtr<IHTMLDocument2> document; + if (!GetDocument(browser, IID_IHTMLDocument2, + reinterpret_cast<void**>(&document))) { + return false; + } + + CComPtr<IHTMLWindow2> window; + if (FAILED(document->get_parentWindow(&window)) || window == NULL) + return false; + + CComPtr<IHTMLEventObj> event_obj; + if (FAILED(window->get_event(&event_obj)) || event_obj == NULL) + return false; + + CComBSTR type; + if (FAILED(event_obj->get_type(&type)) || wcscmp(type, L"load") != 0) + return false; + else + return true; +} + +bool WebProgressNotifier::IsMetaRefresh(IWebBrowser2* browser, BSTR url) { + DCHECK(browser != NULL && url != NULL); + + CComPtr<IHTMLDocument3> document; + if (!GetDocument(browser, IID_IHTMLDocument3, + reinterpret_cast<void**>(&document))) { + return false; + } + + std::wstring dest_url(url); + StringToLowerASCII(&dest_url); + + static const wchar_t slash[] = { L'/' }; + // IE can add/remove a slash to/from the URL specified in the meta refresh + // tag. No redirect occurs as a result of this URL change, so we just compare + // without slashes here. + TrimString(dest_url, slash, &dest_url); + + CComPtr<IHTMLElementCollection> meta_elements; + long length = 0; + if (FAILED(DomUtils::GetElementsByTagName(document, CComBSTR(L"meta"), + &meta_elements, &length))) { + return false; + } + + for (long index = 0; index < length; ++index) { + CComPtr<IHTMLMetaElement> meta_element; + if (FAILED(DomUtils::GetElementFromCollection( + meta_elements, index, IID_IHTMLMetaElement, + reinterpret_cast<void**>(&meta_element)))) { + continue; + } + + CComBSTR http_equiv; + if (FAILED(meta_element->get_httpEquiv(&http_equiv)) || + http_equiv == NULL || _wcsicmp(http_equiv, L"refresh") != 0) { + continue; + } + + CComBSTR content_bstr; + if (FAILED(meta_element->get_content(&content_bstr)) || + content_bstr == NULL) + continue; + std::wstring content(content_bstr); + StringToLowerASCII(&content); + size_t pos = content.find(L"url"); + if (pos == std::wstring::npos) + continue; + pos = content.find(L"=", pos + 3); + if (pos == std::wstring::npos) + continue; + + std::wstring content_url(content.begin() + pos + 1, content.end()); + TrimWhitespace(content_url, TRIM_ALL, &content_url); + TrimString(content_url, slash, &content_url); + + // It is possible that the meta tag specifies a relative URL. + if (!content_url.empty() && EndsWith(dest_url, content_url, true)) + return true; + } + return false; +} + +bool WebProgressNotifier::HasPotentialJavaScriptRedirect( + IWebBrowser2* browser) { + DCHECK(browser != NULL); + + CComPtr<IHTMLDocument3> document; + if (!GetDocument(browser, IID_IHTMLDocument3, + reinterpret_cast<void**>(&document))) { + return false; + } + + CComPtr<IHTMLElementCollection> script_elements; + long length = 0; + if (FAILED(DomUtils::GetElementsByTagName(document, CComBSTR(L"script"), + &script_elements, &length))) { + return false; + } + + for (long index = 0; index < length; ++index) { + CComPtr<IHTMLScriptElement> script_element; + if (FAILED(DomUtils::GetElementFromCollection( + script_elements, index, IID_IHTMLScriptElement, + reinterpret_cast<void**>(&script_element)))) { + continue; + } + + CComBSTR text; + if (FAILED(script_element->get_text(&text)) || text == NULL) + continue; + + if (wcsstr(text, L"location.href") != NULL || + wcsstr(text, L"location.replace") != NULL || + wcsstr(text, L"location.assign") != NULL || + wcsstr(text, L"location.reload") != NULL) { + return true; + } + } + + return false; +} + +bool WebProgressNotifier::IsPossibleUserActionInContentWindow() { + if (tracking_content_window_action_) { + base::TimeDelta delta = base::Time::Now() - + last_content_window_action_time_; + if (delta.InMilliseconds() < kUserActionTimeThresholdMs) + return true; + } + + return false; +} + +bool WebProgressNotifier::IsPossibleUserActionInBrowserUI() { + if (tracking_browser_ui_action_) { + base::TimeDelta delta = base::Time::Now() - last_browser_ui_action_time_; + if (delta.InMilliseconds() < kUserActionTimeThresholdMs) + return true; + } + + // TODO(yzshen@google.com): The windows of the browser UI live in + // different threads or even processes, for example: + // 1) The menu bar, add-on toolbands, as well as the status bar at the bottom, + // live in the same thread as the tab content window. + // 2) In IE7, the browser frame lives in a different thread other than the one + // hosting the tab content window; in IE8, it lives in a different process. + // 3) Our extension UI (rendered by Chrome) lives in another process. + // Currently WebProgressNotifier only handles case (1). I need to find out a + // solution that can effectively handle case (2) and (3). + return false; +} + +bool WebProgressNotifier::FilterOutWebBrowserEvent(IWebBrowser2* browser, + FilteringInfo::Event event) { + if (!IsMainFrame(browser)) { + if (filtering_info_.state == FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + HandleNavigateComplete( + filtering_info_.pending_navigate_complete_browser, + filtering_info_.pending_navigate_complete_url, + filtering_info_.pending_navigate_complete_timestamp); + } + } else { + switch (filtering_info_.state) { + case FilteringInfo::END: { + break; + } + case FilteringInfo::START: { + if (event == FilteringInfo::BEFORE_NAVIGATE) + filtering_info_.state = FilteringInfo::FIRST_BEFORE_NAVIGATE; + + break; + } + case FilteringInfo::FIRST_BEFORE_NAVIGATE: { + if (event == FilteringInfo::BEFORE_NAVIGATE || + event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + } else if (event == FilteringInfo::DOCUMENT_COMPLETE) { + filtering_info_.state = FilteringInfo::SUSPICIOUS_DOCUMENT_COMPLETE; + return true; + } + + break; + } + case FilteringInfo::SUSPICIOUS_DOCUMENT_COMPLETE: { + if (event == FilteringInfo::BEFORE_NAVIGATE) { + filtering_info_.state = FilteringInfo::END; + } else if (event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE; + return true; + } + + break; + } + case FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE: { + if (event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + // Ignore the pending OnNavigateComplete event. + } else { + filtering_info_.state = FilteringInfo::END; + HandleNavigateComplete( + filtering_info_.pending_navigate_complete_browser, + filtering_info_.pending_navigate_complete_url, + filtering_info_.pending_navigate_complete_timestamp); + } + break; + } + default: { + NOTREACHED() << "Unknown state type."; + break; + } + } + } + return false; +} diff --git a/ceee/ie/plugin/bho/web_progress_notifier.h b/ceee/ie/plugin/bho/web_progress_notifier.h new file mode 100644 index 0000000..3c364c8 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier.h @@ -0,0 +1,366 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web progress notifier implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ +#define CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ + +#include <atlbase.h> +#include <tlogstg.h> + +#include <map> +#include <string> + +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "ceee/ie/plugin/bho/webrequest_notifier.h" +#include "ceee/ie/plugin/bho/window_message_source.h" +#include "chrome/common/page_transition_types.h" + +// WebProgressNotifier sends to the Broker various Web progress events, +// including Web page navigation events and HTTP request/response events. +class WebProgressNotifier : public WebBrowserEventsSource::Sink, + public WindowMessageSource::Sink { + public: + WebProgressNotifier(); + virtual ~WebProgressNotifier(); + + HRESULT Initialize( + WebBrowserEventsSource* web_browser_events_source, + HWND tab_window, + IWebBrowser2* main_browser); + void TearDown(); + + // @name WebBrowserEventsSource::Sink implementation + // @{ + virtual void OnBeforeNavigate(IWebBrowser2* browser, BSTR url); + virtual void OnDocumentComplete(IWebBrowser2* browser, BSTR url); + virtual void OnNavigateComplete(IWebBrowser2* browser, BSTR url); + virtual void OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code); + virtual void OnNewWindow(BSTR url_context, BSTR url); + // @} + + // @name WindowMessageSource::Sink implementation + // @{ + virtual void OnHandleMessage(WindowMessageSource::MessageType type, + const MSG* message_info); + // @} + + protected: + // The main frame ID. + static const int kMainFrameId = 0; + + // Sometimes IE fires unexpected events, which could possibly corrupt the + // internal state of WebProgressNotifier and lead to incorrect webNavigation + // event sequence. Here is a common issue: + // + // When a URL is opened in a new tab/window (CTRL + mouse click, or using + // "_blank" target in <a> element/IHTMLDocument2::open), + // (1) if the URL results in server-initiated redirect, the event sequence is: + // (1-1) OnNewWindow + // (1-2) OnBeforeNavigate http://www.originalurl.com/ + // (1-3) OnDocumentComplete http://www.originalurl.com/ + // (1-4) OnNavigateComplete http://www.originalurl.com/ + // (1-5) OnNavigateComplete http://www.redirecturl.com/ + // (1-6) OnDocumentComplete http://www.redirecturl.com/ + // (2) otherwise, the event sequence is: + // (2-1) OnNewWindow + // (2-2) OnBeforeNavigate http://www.url.com/ + // (2-3) OnDocumentComplete http://www.url.com/ + // (2-4) OnNavigateComplete http://www.url.com/ + // (2-5) OnDocumentComplete http://www.url.com/ + // (NOTE: HTTP responses with status code 304 fall into the 2nd category.) + // + // Event 1-3, 1-4 and 2-3 are undesired, comparing with the (normal) event + // sequence of opening a link in the current tab/window. + // FilteringInfo is used to filter out these events. + // + // It is easy to get rid of event 1-3 and 2-3. If we observe an + // OnDocumentComplete event immediately after OnBeforeNavigate, we know that + // it should be ignored. + // However, it is hard to get rid of event 1-4, since we have no way to tell + // the difference between 1-4 and 2-4, until we get the next event. + // (At the first glance, we could tell 1-4 from 2-4 by observing + // INTERNET_STATUS_REDIRECT to see whether the navigation involves + // server-initiated redirect. However, INTERNET_STATUS_REDIRECT actually + // happens *after* event 1-4.) As a result, when we receive an + // OnNavigateComplete event after an undesired OnDocumentComplete event, we + // have to postpone making the decision of processing it or not until we + // receive the next event. We process it only if the next event is not another + // OnNavigateComplete. + struct FilteringInfo { + enum State { + // The tab/window is newly created. + START, + // The first OnBeforeNavigate in the main frame has been observed. + FIRST_BEFORE_NAVIGATE, + // A suspicious OnDocumentComplete in the main frame has been observed. + SUSPICIOUS_DOCUMENT_COMPLETE, + // A suspicious OnNavigateComplete in the main frame has been observed. + SUSPICIOUS_NAVIGATE_COMPLETE, + // Filtering has finished. + END + }; + + enum Event { + // An OnBeforeNavigate has been fired. + BEFORE_NAVIGATE, + // An OnDocumentComplete has been fired. + DOCUMENT_COMPLETE, + // An OnNavigateComplete has been fired. + NAVIGATE_COMPLETE, + // An OnNavigateError has been fired. + NAVIGATE_ERROR, + // An OnNewWindow has been fired. + NEW_WINDOW + }; + + FilteringInfo() : state(START) {} + + State state; + + // Arguments of a pending OnNavigateComplete event. + CComBSTR pending_navigate_complete_url; + CComPtr<IWebBrowser2> pending_navigate_complete_browser; + base::Time pending_navigate_complete_timestamp; + }; + + // Any transition type can be augmented by qualifiers, which further define + // the navigation. + // Transition types could be found in chrome/common/page_transition_types.h. + // We are not using the qualifiers defined in that file, since we need to + // define a few IE/FF specific qualifiers for now. + enum TransitionQualifier { + // Redirects caused by JavaScript or a meta refresh tag on the page. + CLIENT_REDIRECT = 0x1, + // Redirects sent from the server by HTTP headers. + SERVER_REDIRECT = 0x2, + // Users use Forward or Back button to navigate among browsing history. + FORWARD_BACK = 0x4, + // Client redirects caused by <meta http-equiv="refresh">. (IE/FF specific) + REDIRECT_META_REFRESH = 0x8, + // Client redirects happening in JavaScript onload event handler. + // (IE/FF specific) + REDIRECT_ONLOAD = 0x10, + // Non-onload JavaScript redirects. (IE/FF specific) + REDIRECT_JAVASCRIPT = 0x20, + FIRST_TRANSITION_QUALIFIER = CLIENT_REDIRECT, + LAST_TRANSITION_QUALIFIER = REDIRECT_JAVASCRIPT + }; + // Represents zero or more transition qualifiers. + typedef unsigned int TransitionQualifiers; + + // Information related to a frame. + struct FrameInfo { + FrameInfo() : transition_type(PageTransition::LINK), + transition_qualifiers(0), + frame_id(-1) { + } + + FrameInfo(PageTransition::Type in_transition_type, + TransitionQualifiers in_transition_qualifiers, + int in_frame_id) + : transition_type(in_transition_type), + transition_qualifiers(in_transition_qualifiers), + frame_id(in_frame_id) { + } + + // Clears transition type as well as qualifiers. + void ClearTransition() { + SetTransition(PageTransition::LINK, 0); + } + + // Sets transition type and qualifiers. + void SetTransition(PageTransition::Type type, + TransitionQualifiers qualifiers) { + transition_type = type; + transition_qualifiers = qualifiers; + } + + // Appends more transition qualifiers. + void AppendTransitionQualifiers(TransitionQualifiers qualifiers) { + transition_qualifiers |= qualifiers; + } + + // Whether this FrameInfo instance is associated with the main frame. + bool IsMainFrame() const { + return frame_id == kMainFrameId; + } + + // The transition type for the current navigation in this frame. + PageTransition::Type transition_type; + // The transition qualifiers for the current navigation in this frame. + TransitionQualifiers transition_qualifiers; + // The frame ID. + const int frame_id; + }; + + // Accessor so that we can mock it in unit tests. + virtual WebNavigationEventsFunnel& webnavigation_events_funnel() { + return webnavigation_events_funnel_; + } + + // Accessor so that we can mock WebRequestNotifier in unit tests. + virtual WebRequestNotifier* webrequest_notifier() { + if (cached_webrequest_notifier_ == NULL) { + cached_webrequest_notifier_ = ProductionWebRequestNotifier::get(); + } + return cached_webrequest_notifier_; + } + + // Unit testing seems to create a WindowMessageSource instance. + virtual WindowMessageSource* CreateWindowMessageSource(); + + // Whether the current navigation is a navigation among the browsing history + // (forward/back list). + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsForwardBack(BSTR url); + + // Whether we are currently inside the onload event handler of the page. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool InOnLoadEvent(IWebBrowser2* browser); + + // Whether there is meta refresh tag on the current page. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsMetaRefresh(IWebBrowser2* browser, BSTR url); + + // Whether there is JavaScript code on the current page that could possibly + // cause a navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool HasPotentialJavaScriptRedirect(IWebBrowser2* browser); + + // Whether there is user action in the content window that could possibly + // cause a navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsPossibleUserActionInContentWindow(); + + // Whether there is user action in the browser UI that could possibly cause a + // navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsPossibleUserActionInBrowserUI(); + + // Converts a set of transition qualifier values into a string. + std::string TransitionQualifiersString(TransitionQualifiers qualifiers); + + // Whether the IWebBrowser2 interface belongs to the main frame. + bool IsMainFrame(IWebBrowser2* browser) { + return browser != NULL && main_browser_.IsEqualObject(browser); + } + + // Gets the information related to a frame. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param frame_info An output parameter to return the information. The caller + // doesn't take ownership of the returned object. The FrameInfo + // instance for the main frame will live as long as the + // WebProgressNotifier instance; all FrameInfo instances for subframes + // will be deleted when the main frame navigates. + // @return Whether the operation is successful or not. + bool GetFrameInfo(IWebBrowser2* browser, FrameInfo** frame_info); + + // Gets the document of the frame. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param id The IID of the document interface to return. + // @param document An output parameter to return the interface pointer. + // @return Whether the operation is successful or not. + bool GetDocument(IWebBrowser2* browser, REFIID id, void** document); + + // Whether the specified Windows message represents a user action. + bool IsUserActionMessage(UINT message) { + return message == WM_LBUTTONUP || message == WM_KEYUP || + message == WM_KEYDOWN; + } + + // Handles OnNavigateComplete events. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param url The URL that the frame navigated to. + // @param timestamp The time when the OnNavigateComplete event was fired. + void HandleNavigateComplete(IWebBrowser2* browser, + BSTR url, + const base::Time& timestamp); + + // Decides whether to filter out a navigation event. + // The method may call HandleNavigateComplete to handle delayed + // OnNavigateComplete events. + // @param browser The frame in which the navigation event happens. + // @param event The navigation event. + // @return Returns true if the event should be ignored. + bool FilterOutWebBrowserEvent(IWebBrowser2* browser, + FilteringInfo::Event event); + + // This class doesn't have ownership of the object that + // web_browser_events_source_ points to. + WebBrowserEventsSource* web_browser_events_source_; + // Publisher of events about Windows message handling. + scoped_ptr<WindowMessageSource> window_message_source_; + + // The funnel for sending webNavigation events to the broker. + WebNavigationEventsFunnel webnavigation_events_funnel_; + + // Information related to the main frame. + FrameInfo main_frame_info_; + + // IWebBrowser2 interface pointer of the main frame. + CComPtr<IWebBrowser2> main_browser_; + // ITravelLogStg interface pointer to manage the forward/back list. + CComPtr<ITravelLogStg> travel_log_; + // Window handle of the tab. + CeeeWindowHandle tab_handle_; + + // The ID to assign to the next subframe. + int next_subframe_id_; + // Maintains a map from subframes and their corresponding FrameInfo instances. + typedef std::map<CAdapt<CComPtr<IUnknown> >, FrameInfo> SubframeMap; + SubframeMap subframe_map_; + + // Information related to the forward/back list. + struct TravelLogInfo { + TravelLogInfo() : length(-1), position(-1) { + } + // The length of the forward/back list, including the current entry. + DWORD length; + // The current position within the forward/back list, defined as the + // distance between the current entry and the newest entry in the + // forward/back list. That is, if the current entry is the newest one + // in the forward/back list then the position is 0. + DWORD position; + // The URL of the newest entry in the forward/back list. + CComBSTR newest_url; + }; + // The state of the forward/back list before the current navigation. + TravelLogInfo previous_travel_log_info_; + + // Whether the previous navigation of the main frame reaches DocumentComplete. + bool main_frame_document_complete_; + + // If tracking_content_window_action_ is true, consider user action in the tab + // content window as a possible cause for the next navigation. + bool tracking_content_window_action_; + // The last time when the user took action in the content window. + base::Time last_content_window_action_time_; + + // If tracking_browser_ui_action_ is true, consider user action in the browser + // UI (except the tab content window) as a possible cause for the next + // navigation. + bool tracking_browser_ui_action_; + // The last time when the user took action in the browser UI. + base::Time last_browser_ui_action_time_; + + // Whether there is JavaScript code on the current page that can possibly + // cause a navigation. + bool has_potential_javascript_redirect_; + + // A cached pointer to the singleton object. + WebRequestNotifier* cached_webrequest_notifier_; + bool webrequest_notifier_initialized_; + + DWORD create_thread_id_; + + FilteringInfo filtering_info_; + private: + DISALLOW_COPY_AND_ASSIGN(WebProgressNotifier); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ diff --git a/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc b/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc new file mode 100644 index 0000000..46a1e57 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc @@ -0,0 +1,593 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit test for WebProgressNotifier class. +#include "ceee/ie/plugin/bho/web_progress_notifier.h" + +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/window_message_source.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::AddRef; +using testing::AnyNumber; +using testing::InSequence; +using testing::MockFunction; +using testing::NiceMock; +using testing::NotNull; +using testing::Return; +using testing::SaveArg; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +class FakeWebBrowserEventsSource : public WebBrowserEventsSource { + public: + FakeWebBrowserEventsSource() : sink_(NULL) {} + virtual ~FakeWebBrowserEventsSource() { + EXPECT_EQ(NULL, sink_); + } + + virtual void RegisterSink(Sink* sink) { + ASSERT_TRUE(sink != NULL && sink_ == NULL); + sink_ = sink; + } + + virtual void UnregisterSink(Sink* sink) { + ASSERT_TRUE(sink == sink_); + sink_ = NULL; + } + + void FireOnBeforeNavigate(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnBeforeNavigate(browser, url); + } + void FireOnDocumentComplete(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnDocumentComplete(browser, url); + } + void FireOnNavigateComplete(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnNavigateComplete(browser, url); + } + void FireOnNavigateError(IWebBrowser2* browser, BSTR url, long status_code) { + if (sink_ != NULL) + sink_->OnNavigateError(browser, url, status_code); + } + void FireOnNewWindow(BSTR url_context, BSTR url) { + if (sink_ != NULL) + sink_->OnNewWindow(url_context, url); + } + + private: + Sink* sink_; +}; + +class FakeWindowMessageSource : public WindowMessageSource { + public: + FakeWindowMessageSource() : sink_(NULL) {} + virtual ~FakeWindowMessageSource() { + EXPECT_EQ(NULL, sink_); + } + + virtual void RegisterSink(Sink* sink) { + ASSERT_TRUE(sink != NULL && sink_ == NULL); + sink_ = sink; + } + + virtual void UnregisterSink(Sink* sink) { + ASSERT_TRUE(sink == sink_); + sink_ = NULL; + } + + void FireOnHandleMessage(MessageType type, + const MSG* message_info) { + if (sink_ != NULL) + sink_->OnHandleMessage(type, message_info); + } + + private: + Sink* sink_; +}; + +class TestWebProgressNotifier : public WebProgressNotifier { + public: + TestWebProgressNotifier() + : mock_is_forward_back_(false), + mock_in_onload_event_(false), + mock_is_meta_refresh_(false), + mock_has_potential_javascript_redirect_(false), + mock_is_possible_user_action_in_content_window_(false), + mock_is_possible_user_action_in_browser_ui_(false) {} + + virtual WebNavigationEventsFunnel& webnavigation_events_funnel() { + return mock_webnavigation_events_funnel_; + } + + virtual WindowMessageSource* CreateWindowMessageSource() { + return new FakeWindowMessageSource(); + } + + virtual bool IsForwardBack(BSTR /*url*/) { return mock_is_forward_back_; } + virtual bool InOnLoadEvent(IWebBrowser2* /*browser*/) { + return mock_in_onload_event_; + } + virtual bool IsMetaRefresh(IWebBrowser2* /*browser*/, BSTR /*url*/) { + return mock_is_meta_refresh_; + } + virtual bool HasPotentialJavaScriptRedirect(IWebBrowser2* /*browser*/) { + return mock_has_potential_javascript_redirect_; + } + virtual bool IsPossibleUserActionInContentWindow() { + return mock_is_possible_user_action_in_content_window_; + } + virtual bool IsPossibleUserActionInBrowserUI() { + return mock_is_possible_user_action_in_browser_ui_; + } + + bool CallRealIsForwardBack(BSTR url) { + return WebProgressNotifier::IsForwardBack(url); + } + + TravelLogInfo& previous_travel_log_info() { + return previous_travel_log_info_; + } + + StrictMock<testing::MockWebNavigationEventsFunnel> + mock_webnavigation_events_funnel_; + bool mock_is_forward_back_; + bool mock_in_onload_event_; + bool mock_is_meta_refresh_; + bool mock_has_potential_javascript_redirect_; + bool mock_is_possible_user_action_in_content_window_; + bool mock_is_possible_user_action_in_browser_ui_; +}; + +class WebProgressNotifierTestFixture : public testing::Test { + protected: + WebProgressNotifierTestFixture() : mock_web_browser_(NULL), + mock_travel_log_stg_(NULL) { + } + + virtual void SetUp() { + CComObject<testing::MockIWebBrowser2>::CreateInstance( + &mock_web_browser_); + ASSERT_TRUE(mock_web_browser_ != NULL); + web_browser_ = mock_web_browser_; + + CComObject<testing::MockITravelLogStg>::CreateInstance( + &mock_travel_log_stg_); + ASSERT_TRUE(mock_travel_log_stg_ != NULL); + travel_log_stg_ = mock_travel_log_stg_; + + // We cannot use CopyInterfaceToArgument here because QueryService takes + // void** as argument. + EXPECT_CALL(*mock_web_browser_, + QueryService(SID_STravelLogCursor, IID_ITravelLogStg, + NotNull())) + .WillRepeatedly(DoAll(SetArgumentPointee<2>(travel_log_stg_.p), + AddRef(travel_log_stg_.p), + Return(S_OK))); + + web_browser_events_source_.reset(new FakeWebBrowserEventsSource()); + + web_progress_notifier_.reset(new TestWebProgressNotifier()); + ASSERT_HRESULT_SUCCEEDED(web_progress_notifier_->Initialize( + web_browser_events_source_.get(), reinterpret_cast<HWND>(1024), + web_browser_)); + } + + virtual void TearDown() { + web_progress_notifier_->TearDown(); + web_progress_notifier_.reset(NULL); + web_browser_events_source_.reset(NULL); + mock_web_browser_ = NULL; + web_browser_.Release(); + mock_travel_log_stg_ = NULL; + travel_log_stg_.Release(); + } + + void IgnoreCallsToEventsFunnelExceptOnCommitted() { + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeRetarget(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnDOMContentLoaded(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnErrorOccurred(_, _, _, _, _)) + .Times(AnyNumber()); + } + + void FireNavigationEvents(IWebBrowser2* browser, BSTR url) { + web_browser_events_source_->FireOnBeforeNavigate(browser, url); + web_browser_events_source_->FireOnNavigateComplete(browser, url); + web_browser_events_source_->FireOnDocumentComplete(browser, url); + } + + HRESULT CreateMockWebBrowser(IWebBrowser2** web_browser) { + EXPECT_TRUE(web_browser != NULL); + CComObject<testing::MockIWebBrowser2>* mock_web_browser = NULL; + CComObject<testing::MockIWebBrowser2>::CreateInstance(&mock_web_browser); + EXPECT_TRUE(mock_web_browser != NULL); + + *web_browser = mock_web_browser; + (*web_browser)->AddRef(); + return S_OK; + } + + CComObject<testing::MockIWebBrowser2>* mock_web_browser_; + CComPtr<IWebBrowser2> web_browser_; + CComObject<testing::MockITravelLogStg>* mock_travel_log_stg_; + CComPtr<ITravelLogStg> travel_log_stg_; + scoped_ptr<FakeWebBrowserEventsSource> web_browser_events_source_; + scoped_ptr<TestWebProgressNotifier> web_progress_notifier_; +}; + +TEST_F(WebProgressNotifierTestFixture, FireEvents) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)); + EXPECT_CALL(check, Call(1)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeRetarget(_, _, _, _)); + EXPECT_CALL(check, Call(2)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(check, Call(3)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(4)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnErrorOccurred(_, _, _, _, _)); + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(1); + web_browser_events_source_->FireOnNewWindow( + CComBSTR(L"http://www.google.com/"), + CComBSTR(L"http://mail.google.com/")); + check.Call(2); + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(3); + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(4); + web_browser_events_source_->FireOnNavigateError( + web_browser_, CComBSTR(L"http://www.google.com/"), 400); + } +} + +TEST_F(WebProgressNotifierTestFixture, FilterAbnormalWebBrowserEvents) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)); + } + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + // Undesired event. + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + // Undesired event. + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://mail.google.com/")); + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://mail.google.com/")); +} + +TEST_F(WebProgressNotifierTestFixture, TestFrameId) { + int subframe_id_1 = -1; + int current_frame_id = -1; + + CComPtr<IWebBrowser2> subframe_1; + ASSERT_HRESULT_SUCCEEDED(CreateMockWebBrowser(&subframe_1)); + + CComPtr<IWebBrowser2> subframe_2; + ASSERT_HRESULT_SUCCEEDED(CreateMockWebBrowser(&subframe_2)); + + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(1)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(2)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(3)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(4)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + // The main frame has 0 as frame ID. + EXPECT_EQ(0, current_frame_id); + check.Call(1); + + current_frame_id = -1; + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + // The main frame has 0 as frame ID. + EXPECT_EQ(0, current_frame_id); + check.Call(2); + + current_frame_id = -1; + web_browser_events_source_->FireOnBeforeNavigate( + subframe_1, CComBSTR(L"http://www.google.com/")); + subframe_id_1 = current_frame_id; + // A subframe should not have 0 as frame ID. + EXPECT_NE(0, subframe_id_1); + check.Call(3); + + current_frame_id = -1; + web_browser_events_source_->FireOnNavigateComplete( + subframe_1, CComBSTR(L"http://www.google.com/")); + // The frame ID of a subframe remains the same. + EXPECT_EQ(subframe_id_1, current_frame_id); + check.Call(4); + + current_frame_id = -1; + web_browser_events_source_->FireOnBeforeNavigate( + subframe_2, CComBSTR(L"http://www.google.com/")); + // Different subframes have different frame IDs. + EXPECT_NE(subframe_id_1, current_frame_id); + } +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionClientRedirect) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + CComBSTR javascript_url(L"javascript:someScript();"); + const char* link = "link"; + const char* redirect_javascript = "client_redirect|redirect_javascript"; + const char* redirect_onload = "client_redirect|redirect_onload"; + const char* redirect_meta_refresh = "client_redirect|redirect_meta_refresh"; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_onload), _)); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_meta_refresh), + _)); + } + + // If target URL starts with "javascript:" and no other signals are found, + // consider the transition as JavaScript redirect. + web_browser_events_source_->FireOnBeforeNavigate(web_browser_, + javascript_url); + web_browser_events_source_->FireOnNavigateComplete(web_browser_, + javascript_url); + check.Call(1); + + // If DocumentComplete hasn't been received for the previous navigation, and + // no other signals are found, consider the transition as JavaScript redirect. + FireNavigationEvents(web_browser_, url); + check.Call(2); + + web_progress_notifier_->mock_has_potential_javascript_redirect_ = true; + FireNavigationEvents(web_browser_, url); + // If JavaScript code to navigate the page is found on the previous page, and + // no other signals are found, consider the transition as JavaScript redirect. + FireNavigationEvents(web_browser_, url); + check.Call(3); + + // If currently we are in the onload event handler, consider the transition as + // onload redirect. + web_progress_notifier_->mock_has_potential_javascript_redirect_ = false; + web_progress_notifier_->mock_in_onload_event_ = true; + FireNavigationEvents(web_browser_, url); + check.Call(4); + + // If the previous page has <meta http-equiv="refresh"> tag, consider the + // transition as meta-refresh redirect. + web_progress_notifier_->mock_in_onload_event_ = false; + web_progress_notifier_->mock_is_meta_refresh_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionUserAction) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + const char* link = "link"; + const char* typed = "typed"; + const char* no_qualifier = ""; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(no_qualifier), _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(typed), StrEq(no_qualifier), _)); + } + + // User actions override JavaScript redirect signals, so setting the following + // flag should have no effect. + web_progress_notifier_->mock_has_potential_javascript_redirect_ = true; + + // If there is user action in the content window, consider the transition as + // link. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + true; + FireNavigationEvents(web_browser_, url); + check.Call(1); + + // If there is user action in the browser UI, consider the transition as + // typed. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + false; + web_progress_notifier_->mock_is_possible_user_action_in_browser_ui_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionForwardBack) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + const char* auto_bookmark = "auto_bookmark"; + const char* forward_back = "forward_back"; + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(auto_bookmark), StrEq(forward_back), + _)); + + // Forward/back overrides other signals, so setting the following flags + // should have no effect. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + true; + web_progress_notifier_->mock_is_possible_user_action_in_browser_ui_ = + true; + web_progress_notifier_->mock_in_onload_event_ = true; + web_progress_notifier_->mock_is_meta_refresh_ = true; + + // If the current navigation doesn't cause browsing history to change, + // consider the transition as forward/back. + web_progress_notifier_->mock_is_forward_back_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestDetectingForwardBack) { + TLENUMF back_fore = TLEF_RELATIVE_BACK | TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE; + TLENUMF fore = TLEF_RELATIVE_FORE | TLEF_INCLUDE_UNINVOKEABLE; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(5), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(3), + Return(S_OK))); + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + } + + CComBSTR google_url(L"http://www.google.com/"); + CComBSTR gmail_url(L"http://mail.google.com/"); + + // Test recording of the previous travel log info. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + EXPECT_EQ(6, web_progress_notifier_->previous_travel_log_info().length); + EXPECT_EQ(0, web_progress_notifier_->previous_travel_log_info().position); + EXPECT_EQ(google_url, + web_progress_notifier_->previous_travel_log_info().newest_url); + check.Call(1); + + // If the length of the forward/back list has changed, the navigation is not + // forward/back. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + check.Call(2); + + // If the length of the forward/back list remains the same, and there are + // entries in the forward list, the navigation is forward/back. + EXPECT_TRUE(web_progress_notifier_->CallRealIsForwardBack(gmail_url)); + EXPECT_NE(gmail_url, + web_progress_notifier_->previous_travel_log_info().newest_url); + check.Call(3); + + // If the length of the forward/back list remains the same, and the URL of the + // last entry in the list remains the same, the navigation is forward/back. + EXPECT_TRUE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + check.Call(4); + + // If the length of the forward/back list remains the same, but the URL of the + // last entry in the list has changed, the navigation is not forward/back. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(gmail_url)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel.cc b/ceee/ie/plugin/bho/webnavigation_events_funnel.cc new file mode 100644 index 0000000..2717131 --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from wherever through the Broker. + +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" + +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" + +namespace keys = extension_webnavigation_api_constants; + +namespace { + +double MilliSecondsFromTime(const base::Time& time) { + return base::Time::kMillisecondsPerSecond * time.ToDoubleT(); +} + +} // namespace + +HRESULT WebNavigationEventsFunnel::OnBeforeNavigate( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeNavigate, args); +} + +HRESULT WebNavigationEventsFunnel::OnBeforeRetarget( + CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kSourceTabIdKey, static_cast<int>(source_tab_handle)); + args.SetString(keys::kSourceUrlKey, source_url); + args.SetString(keys::kTargetUrlKey, target_url); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRetarget, args); +} + +HRESULT WebNavigationEventsFunnel::OnCommitted( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kTransitionTypeKey, transition_type); + args.SetString(keys::kTransitionQualifiersKey, transition_qualifiers); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCommitted, args); +} + +HRESULT WebNavigationEventsFunnel::OnCompleted( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCompleted, args); +} + +HRESULT WebNavigationEventsFunnel::OnDOMContentLoaded( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnDOMContentLoaded, args); +} + +HRESULT WebNavigationEventsFunnel::OnErrorOccurred( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnErrorOccurred, args); +} diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel.h b/ceee/ie/plugin/bho/webnavigation_events_funnel.h new file mode 100644 index 0000000..68243cc --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel.h @@ -0,0 +1,108 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Web Navigation Events. + +#ifndef CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ +#include <atlcomcli.h> + +#include "base/time.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "toolband.h" // NOLINT + +// Implements a set of methods to send web navigation related events to the +// Broker. +class WebNavigationEventsFunnel : public EventsFunnel { + public: + WebNavigationEventsFunnel() : EventsFunnel(false) {} + + // Sends the webNavigation.onBeforeNavigate event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation is + // about to occur. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param request_id The ID of the request to retrieve the document of this + // navigation. + // @param time_stamp The time when the browser was about to start the + // navigation. + virtual HRESULT OnBeforeNavigate(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onBeforeRetarget event to the Broker. + // @param source_tab_handle The window handle of the tab in which the + // navigation is triggered. + // @param source_url The URL of the document that is opening the new window. + // @param target_url The URL to be opened in the new window. + // @param time_stamp The time when the browser was about to create a new view. + virtual HRESULT OnBeforeRetarget(CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp); + + // Sends the webNavigation.onCommitted event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param transition_type Cause of the navigation. + // @param transition_qualifiers Zero or more transition qualifiers delimited + // by "|". + // @param time_stamp The time when the navigation was committed. + virtual HRESULT OnCommitted(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp); + + // Sends the webNavigation.onCompleted event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param time_stamp The time when the document finished loading. + virtual HRESULT OnCompleted(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onDOMContentLoaded event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param time_stamp The time when the page's DOM was fully constructed. + virtual HRESULT OnDOMContentLoaded(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onErrorOccurred event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param error The error description. + // @param time_stamp The time when the error occurred. + virtual HRESULT OnErrorOccurred(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp); + + private: + DISALLOW_COPY_AND_ASSIGN(WebNavigationEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc b/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc new file mode 100644 index 0000000..bc7d7f9b --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc @@ -0,0 +1,180 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WebNavigationEventsFunnel. + +#include <atlcomcli.h> + +#include "base/time.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace keys = extension_webnavigation_api_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWebNavigationEventsFunnel : public WebNavigationEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(WebNavigationEventsFunnelTest, OnBeforeNavigate) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://www.google.com/"); + int frame_id = 512; + int request_id = 1024; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnBeforeNavigate), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnBeforeNavigate( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, request_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnBeforeRetarget) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int source_tab_handle = 256; + std::string source_url("http://docs.google.com/"); + std::string target_url("http://calendar.google.com/"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kSourceTabIdKey, source_tab_handle); + args.SetString(keys::kSourceUrlKey, source_url); + args.SetString(keys::kTargetUrlKey, target_url); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRetarget), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnBeforeRetarget( + static_cast<CeeeWindowHandle>(source_tab_handle), + CComBSTR(source_url.c_str()), CComBSTR(target_url.c_str()), time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnCommitted) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + std::string transition_type("link"); + std::string transition_qualifiers("client_redirect"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kTransitionTypeKey, transition_type); + args.SetString(keys::kTransitionQualifiersKey, transition_qualifiers); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnCommitted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnCommitted( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, transition_type.c_str(), transition_qualifiers.c_str(), + time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnCompleted) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://groups.google.com/"); + int frame_id = 512; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnCompleted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnCompleted( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnDOMContentLoaded) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnDOMContentLoaded), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnDOMContentLoaded( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnErrorOccurred) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + std::string error("not a valid URL"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnErrorOccurred), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnErrorOccurred( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, CComBSTR(error.c_str()), time_stamp)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel.cc b/ceee/ie/plugin/bho/webrequest_events_funnel.cc new file mode 100644 index 0000000..c0d5b1e --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel.cc @@ -0,0 +1,106 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from whereever through the Broker. + +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" + +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" + +namespace keys = extension_webrequest_api_constants; + +namespace { + +double MilliSecondsFromTime(const base::Time& time) { + return base::Time::kMillisecondsPerSecond * time.ToDoubleT(); +} + +} // namespace + +HRESULT WebRequestEventsFunnel::OnBeforeRedirect(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetString(keys::kRedirectUrlKey, redirect_url); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRedirect, args); +} + +HRESULT WebRequestEventsFunnel::OnBeforeRequest(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kMethodKey, method); + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kTypeKey, type); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRequest, args); +} + +HRESULT WebRequestEventsFunnel::OnCompleted(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCompleted, args); +} + +HRESULT WebRequestEventsFunnel::OnErrorOccurred(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnErrorOccurred, args); +} + +HRESULT WebRequestEventsFunnel::OnHeadersReceived( + int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnHeadersReceived, args); +} + +HRESULT WebRequestEventsFunnel::OnRequestSent(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kIpKey, ip); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnRequestSent, args); +} diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel.h b/ceee/ie/plugin/bho/webrequest_events_funnel.h new file mode 100644 index 0000000..55879cc --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Web Request Events. + +#ifndef CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ + +#include <atlcomcli.h> + +#include "base/time.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "toolband.h" // NOLINT + +// Implements a set of methods to send web request related events to the +// Broker. +class WebRequestEventsFunnel : public EventsFunnel { + public: + WebRequestEventsFunnel() : EventsFunnel(false) {} + + // Sends the webRequest.onBeforeRedirect event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the current request. + // @param status_code Standard HTTP status code returned by the server. + // @param redirect_url The new URL. + // @param time_stamp The time when the browser was about to make the redirect. + virtual HRESULT OnBeforeRedirect(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp); + + // Sends the webRequest.onBeforeRequest event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param method Standard HTTP method, such as "GET" or "POST". + // @param tab_handle The window handle of the tab in which the request takes + // place. Set to INVALID_HANDLE_VALUE if the request isn't related to a + // tab. + // @param type How the requested resource will be used, such as "main_frame" + // or "sub_frame". Please find the complete list and explanation on + // http://www.chromium.org/developers/design-documents/extensions/notifications-of-web-request-and-navigation + // @param time_stamp The time when the browser was about to make the request. + virtual HRESULT OnBeforeRequest(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp); + + // Sends the webRequest.onCompleted event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param status_code Standard HTTP status code returned by the server. + // @param time_stamp The time when the response was received completely. + virtual HRESULT OnCompleted(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp); + + // Sends the webRequest.onErrorOccurred event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param error The error description. + // @param time_stamp The time when the error occurred. + virtual HRESULT OnErrorOccurred(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp); + + // Sends the webRequest.onHeadersReceived event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param status_code Standard HTTP status code returned by the server. + // @param time_stamp The time when the status line and response headers were + // received. + virtual HRESULT OnHeadersReceived(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp); + + // Sends the webRequest.onRequestSent event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param ip The server IP address that is actually connected to. + // @param time_stamp The time when the browser finished sending the request. + virtual HRESULT OnRequestSent(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp); + + private: + DISALLOW_COPY_AND_ASSIGN(WebRequestEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc b/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc new file mode 100644 index 0000000..b56a35c --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc @@ -0,0 +1,171 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WebRequestEventsFunnel. + +#include <atlcomcli.h> + +#include "base/time.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace keys = extension_webrequest_api_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWebRequestEventsFunnel : public WebRequestEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(WebRequestEventsFunnelTest, OnBeforeRedirect) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://www.google.com/"); + DWORD status_code = 200; + std::wstring redirect_url(L"http://mail.google.com/"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetString(keys::kRedirectUrlKey, redirect_url); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRedirect), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnBeforeRedirect( + request_id, url.c_str(), status_code, redirect_url.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnBeforeRequest) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://calendar.google.com/"); + std::string method("GET"); + int tab_handle = 512; + std::string type("main_frame"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kMethodKey, method); + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kTypeKey, type); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRequest), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnBeforeRequest( + request_id, url.c_str(), method.c_str(), + static_cast<CeeeWindowHandle>(tab_handle), type.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnCompleted) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://image.google.com/"); + DWORD status_code = 404; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnCompleted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnCompleted( + request_id, url.c_str(), status_code, time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnErrorOccurred) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://docs.google.com/"); + std::wstring error(L"cannot resolve the host"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnErrorOccurred), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnErrorOccurred( + request_id, url.c_str(), error.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnHeadersReceived) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://news.google.com/"); + DWORD status_code = 200; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnHeadersReceived), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnHeadersReceived( + request_id, url.c_str(), status_code, time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnRequestSent) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://finance.google.com/"); + std::string ip("127.0.0.1"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kIpKey, ip); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnRequestSent), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnRequestSent( + request_id, url.c_str(), ip.c_str(), time_stamp)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webrequest_notifier.cc b/ceee/ie/plugin/bho/webrequest_notifier.cc new file mode 100644 index 0000000..e0449e4 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier.cc @@ -0,0 +1,811 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web request notifier implementation. +#include "ceee/ie/plugin/bho/webrequest_notifier.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "chrome_frame/function_stub.h" +#include "chrome_frame/utils.h" + +namespace { + +const wchar_t kUrlMonModuleName[] = L"urlmon.dll"; +const char kWinINetModuleName[] = "wininet.dll"; +const char kInternetSetStatusCallbackAFunctionName[] = + "InternetSetStatusCallbackA"; +const char kInternetSetStatusCallbackWFunctionName[] = + "InternetSetStatusCallbackW"; +const char kInternetConnectAFunctionName[] = "InternetConnectA"; +const char kInternetConnectWFunctionName[] = "InternetConnectW"; +const char kHttpOpenRequestAFunctionName[] = "HttpOpenRequestA"; +const char kHttpOpenRequestWFunctionName[] = "HttpOpenRequestW"; +const char kHttpSendRequestAFunctionName[] = "HttpSendRequestA"; +const char kHttpSendRequestWFunctionName[] = "HttpSendRequestW"; +const char kInternetReadFileFunctionName[] = "InternetReadFile"; + +} // namespace + +WebRequestNotifier::WebRequestNotifier() + : internet_status_callback_stub_(NULL), + start_count_(0), + initialize_state_(NOT_INITIALIZED) { +} + +WebRequestNotifier::~WebRequestNotifier() { + DCHECK_EQ(start_count_, 0); +} + +bool WebRequestNotifier::RequestToStart() { + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + start_count_++; + + if (initialize_state_ != NOT_INITIALIZED) + return initialize_state_ != FAILED_TO_INITIALIZE; + initialize_state_ = INITIALIZING; + } + + bool success = false; + do { + // We are not going to unpatch any of the patched WinINet functions or the + // status callback function. Instead, we pin our DLL in memory so that all + // the patched functions can be accessed until the process goes away. + PinModule(); + + PatchWinINetFunction(kInternetSetStatusCallbackAFunctionName, + &internet_set_status_callback_a_patch_, + InternetSetStatusCallbackAPatch); + PatchWinINetFunction(kInternetSetStatusCallbackWFunctionName, + &internet_set_status_callback_w_patch_, + InternetSetStatusCallbackWPatch); + if (!HasPatchedOneVersion(internet_set_status_callback_a_patch_, + internet_set_status_callback_w_patch_)) { + break; + } + + PatchWinINetFunction(kInternetConnectAFunctionName, + &internet_connect_a_patch_, + InternetConnectAPatch); + PatchWinINetFunction(kInternetConnectWFunctionName, + &internet_connect_w_patch_, + InternetConnectWPatch); + if (!HasPatchedOneVersion(internet_connect_a_patch_, + internet_connect_w_patch_)) { + break; + } + + PatchWinINetFunction(kHttpOpenRequestAFunctionName, + &http_open_request_a_patch_, + HttpOpenRequestAPatch); + PatchWinINetFunction(kHttpOpenRequestWFunctionName, + &http_open_request_w_patch_, + HttpOpenRequestWPatch); + if (!HasPatchedOneVersion(http_open_request_a_patch_, + http_open_request_w_patch_)) { + break; + } + + PatchWinINetFunction(kHttpSendRequestAFunctionName, + &http_send_request_a_patch_, + HttpSendRequestAPatch); + PatchWinINetFunction(kHttpSendRequestWFunctionName, + &http_send_request_w_patch_, + HttpSendRequestWPatch); + if (!HasPatchedOneVersion(http_send_request_a_patch_, + http_send_request_w_patch_)) { + break; + } + + PatchWinINetFunction(kInternetReadFileFunctionName, + &internet_read_file_patch_, + InternetReadFilePatch); + if (!internet_read_file_patch_.is_patched()) + break; + + success = true; + } while (false); + + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + initialize_state_ = success ? SUCCEEDED_TO_INITIALIZE : + FAILED_TO_INITIALIZE; + } + return success; +} + +void WebRequestNotifier::RequestToStop() { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (start_count_ <= 0) { + NOTREACHED(); + return; + } + + start_count_--; + if (start_count_ == 0) { + // It is supposed that every handle must be closed using + // InternetCloseHandle(). However, IE seems to leak handles in some (rare) + // cases. For example, when the current page is a JPG or GIF image and the + // user refreshes the page. + // If that happens, the server_map_ and request_map_ won't be empty. + LOG_IF(WARNING, !server_map_.empty() || !request_map_.empty()) + << "There are Internet handles that haven't been closed when " + << "WebRequestNotifier stops."; + + for (RequestMap::iterator iter = request_map_.begin(); + iter != request_map_.end(); ++iter) { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &iter->second); + } + + server_map_.clear(); + request_map_.clear(); + } +} + +void WebRequestNotifier::PatchWinINetFunction( + const char* name, + app::win::IATPatchFunction* patch_function, + void* handler) { + DWORD error = patch_function->Patch(kUrlMonModuleName, kWinINetModuleName, + name, handler); + // The patching operation is either successful, or failed cleanly. + DCHECK(error == NO_ERROR || !patch_function->is_patched()); +} + +INTERNET_STATUS_CALLBACK STDAPICALLTYPE + WebRequestNotifier::InternetSetStatusCallbackAPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + INTERNET_STATUS_CALLBACK new_callback = + instance->HandleBeforeInternetSetStatusCallback(internet, callback); + return ::InternetSetStatusCallbackA(internet, new_callback); +} + +INTERNET_STATUS_CALLBACK STDAPICALLTYPE + WebRequestNotifier::InternetSetStatusCallbackWPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + INTERNET_STATUS_CALLBACK new_callback = + instance->HandleBeforeInternetSetStatusCallback(internet, callback); + return ::InternetSetStatusCallbackW(internet, new_callback); +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::InternetConnectAPatch( + HINTERNET internet, + LPCSTR server_name, + INTERNET_PORT server_port, + LPCSTR user_name, + LPCSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetConnect(internet); + + HINTERNET server = ::InternetConnectA(internet, server_name, server_port, + user_name, password, service, flags, + context); + + instance->HandleAfterInternetConnect(server, CA2W(server_name), server_port, + service); + return server; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::InternetConnectWPatch( + HINTERNET internet, + LPCWSTR server_name, + INTERNET_PORT server_port, + LPCWSTR user_name, + LPCWSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetConnect(internet); + + HINTERNET server = ::InternetConnectW(internet, server_name, server_port, + user_name, password, service, flags, + context); + + instance->HandleAfterInternetConnect(server, server_name, server_port, + service); + return server; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::HttpOpenRequestAPatch( + HINTERNET connect, + LPCSTR verb, + LPCSTR object_name, + LPCSTR version, + LPCSTR referrer, + LPCSTR* accept_types, + DWORD flags, + DWORD_PTR context) { + HINTERNET request = ::HttpOpenRequestA(connect, verb, object_name, version, + referrer, accept_types, flags, + context); + + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterHttpOpenRequest(connect, request, verb, + CA2W(object_name), flags); + return request; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::HttpOpenRequestWPatch( + HINTERNET connect, + LPCWSTR verb, + LPCWSTR object_name, + LPCWSTR version, + LPCWSTR referrer, + LPCWSTR* accept_types, + DWORD flags, + DWORD_PTR context) { + HINTERNET request = ::HttpOpenRequestW(connect, verb, object_name, version, + referrer, accept_types, flags, + context); + + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterHttpOpenRequest(connect, request, CW2A(verb), + object_name, flags); + return request; +} + +BOOL STDAPICALLTYPE WebRequestNotifier::HttpSendRequestAPatch( + HINTERNET request, + LPCSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeHttpSendRequest(request); + return ::HttpSendRequestA(request, headers, headers_length, optional, + optional_length); +} + +BOOL STDAPICALLTYPE WebRequestNotifier::HttpSendRequestWPatch( + HINTERNET request, + LPCWSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeHttpSendRequest(request); + return ::HttpSendRequestW(request, headers, headers_length, optional, + optional_length); +} + +void CALLBACK WebRequestNotifier::InternetStatusCallbackPatch( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetStatusCallback(original, internet, context, + internet_status, + status_information, + status_information_length); + original(internet, context, internet_status, status_information, + status_information_length); +} + +BOOL STDAPICALLTYPE WebRequestNotifier::InternetReadFilePatch( + HINTERNET file, + LPVOID buffer, + DWORD number_of_bytes_to_read, + LPDWORD number_of_bytes_read) { + BOOL result = ::InternetReadFile(file, buffer, number_of_bytes_to_read, + number_of_bytes_read); + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterInternetReadFile(file, result, number_of_bytes_read); + + return result; +} + +INTERNET_STATUS_CALLBACK + WebRequestNotifier::HandleBeforeInternetSetStatusCallback( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return internet_callback; + + if (internet_callback == NULL) + return NULL; + + if (internet_status_callback_stub_ == NULL) { + return CreateInternetStatusCallbackStub(internet_callback); + } else { + if (internet_status_callback_stub_->argument() != + reinterpret_cast<uintptr_t>(internet_callback)) { + NOTREACHED(); + return CreateInternetStatusCallbackStub(internet_callback); + } else { + return reinterpret_cast<INTERNET_STATUS_CALLBACK>( + internet_status_callback_stub_->code()); + } + } +} + +void WebRequestNotifier::HandleBeforeInternetConnect(HINTERNET internet) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + if (internet_status_callback_stub_ == NULL) { + INTERNET_STATUS_CALLBACK original_callback = + ::InternetSetStatusCallbackA(internet, NULL); + if (original_callback != NULL) { + INTERNET_STATUS_CALLBACK new_callback = CreateInternetStatusCallbackStub( + original_callback); + ::InternetSetStatusCallbackA(internet, new_callback); + } + } +} + +// NOTE: this method must be called within a lock. +INTERNET_STATUS_CALLBACK WebRequestNotifier::CreateInternetStatusCallbackStub( + INTERNET_STATUS_CALLBACK original_callback) { + DCHECK(original_callback != NULL); + + internet_status_callback_stub_ = FunctionStub::Create( + reinterpret_cast<uintptr_t>(original_callback), + InternetStatusCallbackPatch); + // internet_status_callback_stub_ is not NULL if the function stub is + // successfully created. + if (internet_status_callback_stub_ != NULL) { + return reinterpret_cast<INTERNET_STATUS_CALLBACK>( + internet_status_callback_stub_->code()); + } else { + NOTREACHED(); + return original_callback; + } +} + +void WebRequestNotifier::HandleAfterInternetConnect(HINTERNET server, + const wchar_t* server_name, + INTERNET_PORT server_port, + DWORD service) { + if (service != INTERNET_SERVICE_HTTP || server == NULL || + IS_INTRESOURCE(server_name) || wcslen(server_name) == 0) { + return; + } + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // It is not possible that the same connection handle is opened more than + // once. + DCHECK(server_map_.find(server) == server_map_.end()); + + ServerInfo server_info; + server_info.server_name = server_name; + server_info.server_port = server_port; + + server_map_.insert( + std::make_pair<HINTERNET, ServerInfo>(server, server_info)); +} + +void WebRequestNotifier::HandleAfterHttpOpenRequest(HINTERNET server, + HINTERNET request, + const char* method, + const wchar_t* path, + DWORD flags) { + if (server == NULL || request == NULL) + return; + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // It is not possible that the same request handle is opened more than once. + DCHECK(request_map_.find(request) == request_map_.end()); + + ServerMap::iterator server_iter = server_map_.find(server); + // It is possible to find that we haven't recorded the connection handle to + // the server, if we patch WinINet functions after the InternetConnect call. + // In that case, we will ignore all events related to requests happening on + // that connection. + if (server_iter == server_map_.end()) + return; + + RequestInfo request_info; + // TODO(yzshen@google.com): create the request ID. + request_info.server_handle = server; + request_info.method = method == NULL ? "GET" : method; + if (!ConstructUrl((flags & INTERNET_FLAG_SECURE) != 0, + server_iter->second.server_name.c_str(), + server_iter->second.server_port, + path, + &request_info.url)) { + NOTREACHED(); + return; + } + + request_map_.insert( + std::make_pair<HINTERNET, RequestInfo>(request, request_info)); +} + +void WebRequestNotifier::HandleBeforeHttpSendRequest(HINTERNET request) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(request); + if (request_iter != request_map_.end()) { + request_iter->second.before_request_time = base::Time::Now(); + TransitRequestToNextState(RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, + &request_iter->second); + } +} + +void WebRequestNotifier::HandleBeforeInternetStatusCallback( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length) { + switch (internet_status) { + case INTERNET_STATUS_HANDLE_CLOSING: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // We don't know whether we are closing a server or a request handle. As a + // result, we have to test both server_map_ and request_map_. + ServerMap::iterator server_iter = server_map_.find(internet); + if (server_iter != server_map_.end()) { + server_map_.erase(server_iter); + } else { + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + // TODO(yzshen@google.com): For now, we don't bother + // checking whether the content of the response has + // completed downloading in this case. Have to make + // improvement if the requirement for more accurate + // webRequest.onCompleted notifications emerges. + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, + &request_iter->second); + request_map_.erase(request_iter); + } + } + break; + } + case INTERNET_STATUS_REQUEST_SENT: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + TransitRequestToNextState(RequestInfo::NOTIFIED_REQUEST_SENT, + &request_iter->second); + } + break; + } + case INTERNET_STATUS_REDIRECT: { + DWORD status_code = 0; + bool result = QueryHttpInfoNumber(internet, HTTP_QUERY_STATUS_CODE, + &status_code); + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + RequestInfo& info = request_iter->second; + if (result) { + info.status_code = status_code; + TransitRequestToNextState(RequestInfo::NOTIFIED_HEADERS_RECEIVED, + &info); + + info.original_url = info.url; + info.url = CA2W(reinterpret_cast<PCSTR>(status_information)); + TransitRequestToNextState(RequestInfo::NOTIFIED_BEFORE_REDIRECT, + &info); + } else { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &info); + } + } + } + break; + } + case INTERNET_STATUS_REQUEST_COMPLETE: { + DWORD status_code = 0; + DWORD content_length = 0; + RequestInfo::MessageLengthType length_type = + RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + + bool result = QueryHttpInfoNumber(internet, HTTP_QUERY_STATUS_CODE, + &status_code) && + DetermineMessageLength(internet, status_code, + &content_length, &length_type); + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end() && + request_iter->second.state == RequestInfo::NOTIFIED_REQUEST_SENT) { + RequestInfo& info = request_iter->second; + if (result) { + info.status_code = status_code; + info.content_length = content_length; + info.length_type = length_type; + TransitRequestToNextState(RequestInfo::NOTIFIED_HEADERS_RECEIVED, + &info); + if (info.length_type == RequestInfo::NO_MESSAGE_BODY) + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, &info); + } else { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &info); + } + } + } + break; + } + case INTERNET_STATUS_CONNECTED_TO_SERVER: { + // TODO(yzshen@google.com): get IP information. + break; + } + case INTERNET_STATUS_SENDING_REQUEST: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + // Some HttpSendRequest() calls don't actually make HTTP requests, and the + // corresponding request handles are closed right after the calls. + // In that case, we ignore them totally, and don't send + // webRequest.onBeforeRequest notifications. + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end() && + request_iter->second.state == + RequestInfo::WILL_NOTIFY_BEFORE_REQUEST) { + TransitRequestToNextState(RequestInfo::NOTIFIED_BEFORE_REQUEST, + &request_iter->second); + } + break; + } + default: { + break; + } + } +} + +void WebRequestNotifier::HandleAfterInternetReadFile( + HINTERNET request, + BOOL result, + LPDWORD number_of_bytes_read) { + if (!result || number_of_bytes_read == NULL) + return; + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator iter = request_map_.find(request); + if (iter != request_map_.end()) { + RequestInfo& info = iter->second; + // We don't update the length_type field until we reach the last request + // of the redirection chain. As a result, the check below also prevents us + // from firing webRequest.onCompleted before the last request in the + // chain. + if (info.length_type == RequestInfo::CONTENT_LENGTH_HEADER) { + info.read_progress += *number_of_bytes_read; + if (info.read_progress >= info.content_length && + info.state == RequestInfo::NOTIFIED_HEADERS_RECEIVED) { + DCHECK(info.read_progress == info.content_length); + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, &info); + } + } + } +} + +// Currently this method is always called within a lock. +bool WebRequestNotifier::ConstructUrl(bool https, + const wchar_t* server_name, + INTERNET_PORT server_port, + const wchar_t* path, + std::wstring* url) { + if (url == NULL || server_name == NULL || wcslen(server_name) == 0) + return false; + + url->clear(); + url->append(https ? L"https://" : L"http://"); + url->append(server_name); + + bool need_port = server_port != INTERNET_INVALID_PORT_NUMBER && + (https ? server_port != INTERNET_DEFAULT_HTTPS_PORT : + server_port != INTERNET_DEFAULT_HTTP_PORT); + if (need_port) { + static const int kMaxPortLength = 10; + wchar_t buffer[kMaxPortLength]; + if (swprintf(buffer, kMaxPortLength, L":%d", server_port) == -1) + return false; + url->append(buffer); + } + + url->append(path); + return true; +} + +bool WebRequestNotifier::QueryHttpInfoNumber(HINTERNET request, + DWORD info_flag, + DWORD* value) { + DCHECK(value != NULL); + *value = 0; + + DWORD size = sizeof(info_flag); + return ::HttpQueryInfo(request, info_flag | HTTP_QUERY_FLAG_NUMBER, value, + &size, NULL) ? true : false; +} + +bool WebRequestNotifier::DetermineMessageLength( + HINTERNET request, + DWORD status_code, + DWORD* length, + RequestInfo::MessageLengthType* type) { + // Please see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + // for how the length of a message is determined. + + DCHECK(length != NULL && type != NULL); + *length = 0; + *type = RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + + std::wstring method; + // Request methods are case-sensitive. + if ((status_code >= 100 && status_code < 200) || + status_code == 204 || + status_code == 304 || + (QueryHttpInfoString(request, HTTP_QUERY_REQUEST_METHOD, &method) && + method == L"HEAD")) { + *type = RequestInfo::NO_MESSAGE_BODY; + return true; + } + + std::wstring transfer_encoding; + // All transfer-coding values are case-insensitive. + if (QueryHttpInfoString(request, HTTP_QUERY_TRANSFER_ENCODING, + &transfer_encoding) && + _wcsicmp(transfer_encoding.c_str(), L"entity") != 0) { + *type = RequestInfo::VARIABLE_MESSAGE_LENGTH; + return true; + } + + DWORD content_length = 0; + if (QueryHttpInfoNumber(request, HTTP_QUERY_CONTENT_LENGTH, + &content_length)) { + *type = RequestInfo::CONTENT_LENGTH_HEADER; + *length = content_length; + return true; + } + + *type = RequestInfo::VARIABLE_MESSAGE_LENGTH; + return true; +} + +bool WebRequestNotifier::QueryHttpInfoString(HINTERNET request, + DWORD info_flag, + std::wstring* value) { + DCHECK(value != NULL); + value->clear(); + + DWORD size = 20; + scoped_array<wchar_t> buffer(new wchar_t[size]); + + BOOL result = ::HttpQueryInfo(request, info_flag, + reinterpret_cast<LPVOID>(buffer.get()), &size, + NULL); + if (!result && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + buffer.reset(new wchar_t[size]); + result = ::HttpQueryInfo(request, info_flag, + reinterpret_cast<LPVOID>(buffer.get()), &size, + NULL); + } + if (!result) + return false; + + *value = buffer.get(); + return true; +} + +// NOTE: this method must be called within a lock. +void WebRequestNotifier::TransitRequestToNextState( + RequestInfo::State next_state, + RequestInfo* info) { + // TODO(yzshen@google.com): generate and fill in missing parameters + // for notifications. + DCHECK(info != NULL); + + bool fire_on_error_occurred = false; + switch (info->state) { + case RequestInfo::BEGIN: + if (next_state != RequestInfo::WILL_NOTIFY_BEFORE_REQUEST && + next_state != RequestInfo::ERROR_OCCURRED) { + next_state = RequestInfo::ERROR_OCCURRED; + // We don't fire webRequest.onErrorOccurred in this case, since the + // first event for any request has to be webRequest.onBeforeRequest. + } + break; + case RequestInfo::WILL_NOTIFY_BEFORE_REQUEST: + if (next_state == RequestInfo::NOTIFIED_BEFORE_REQUEST) { + webrequest_events_funnel().OnBeforeRequest( + info->id, info->url.c_str(), info->method.c_str(), info->tab_handle, + "other", info->before_request_time); + } else if (next_state != RequestInfo::ERROR_OCCURRED) { + next_state = RequestInfo::ERROR_OCCURRED; + // We don't fire webRequest.onErrorOccurred in this case, since the + // first event for any request has to be webRequest.onBeforeRequest. + } + break; + case RequestInfo::NOTIFIED_BEFORE_REQUEST: + case RequestInfo::NOTIFIED_BEFORE_REDIRECT: + if (next_state == RequestInfo::NOTIFIED_REQUEST_SENT) { + webrequest_events_funnel().OnRequestSent( + info->id, info->url.c_str(), info->ip.c_str(), base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_REQUEST_SENT: + if (next_state == RequestInfo::NOTIFIED_HEADERS_RECEIVED) { + webrequest_events_funnel().OnHeadersReceived( + info->id, info->url.c_str(), info->status_code, base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_HEADERS_RECEIVED: + if (next_state == RequestInfo::NOTIFIED_BEFORE_REDIRECT) { + webrequest_events_funnel().OnBeforeRedirect( + info->id, info->original_url.c_str(), info->status_code, + info->url.c_str(), base::Time::Now()); + } else if (next_state == RequestInfo::NOTIFIED_COMPLETED) { + webrequest_events_funnel().OnCompleted( + info->id, info->url.c_str(), info->status_code, base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_COMPLETED: + // The webRequest.onCompleted notification is supposed to be the last + // event sent for a given request. As a result, if the request is already + // in the NOTIFIED_COMPLETED state, we just keep it in that state without + // sending any further notification. + // + // When a request handle is closed, we consider transiting the state to + // NOTIFIED_COMPLETED. If there is no response body or we have completed + // reading the response body, the request has already been in this state. + // In that case, we will hit this code path. + next_state = RequestInfo::NOTIFIED_COMPLETED; + break; + case RequestInfo::ERROR_OCCURRED: + next_state = RequestInfo::ERROR_OCCURRED; + break; + default: + NOTREACHED(); + break; + } + + if (fire_on_error_occurred) { + webrequest_events_funnel().OnErrorOccurred( + info->id, info->url.c_str(), L"", base::Time::Now()); + } + info->state = next_state; +} diff --git a/ceee/ie/plugin/bho/webrequest_notifier.h b/ceee/ie/plugin/bho/webrequest_notifier.h new file mode 100644 index 0000000..2529c42 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier.h @@ -0,0 +1,453 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web request notifier implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ +#define CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ + +#include <atlbase.h> +#include <wininet.h> + +#include <map> +#include <string> + +#include "app/win/iat_patch_function.h" +#include "base/singleton.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "toolband.h" + +struct FunctionStub; + +// WebRequestNotifier monitors HTTP request/response events via WinINet hooks, +// and sends the events to the broker. +class WebRequestNotifier { + public: + // Starts the service if it hasn't been started. + // @return Returns true if the service is being initialized or has been + // successfully initialized. + bool RequestToStart(); + // Stops the service if it is currently running and nobody is interested in + // the service any more. Every call to RequestToStart (even if it failed) + // should be paired with a call to RequestToStop. + void RequestToStop(); + + protected: + // Information related to an Internet connection. + struct ServerInfo { + ServerInfo() : server_port(INTERNET_INVALID_PORT_NUMBER) {} + + // The host name of the server. + std::wstring server_name; + // The port number on the server. + INTERNET_PORT server_port; + }; + + // Information related to a HTTP request. + struct RequestInfo { + RequestInfo() + : server_handle(NULL), + id(-1), + tab_handle(reinterpret_cast<CeeeWindowHandle>(INVALID_HANDLE_VALUE)), + status_code(0), + state(BEGIN), + content_length(0), + read_progress(0), + length_type(UNKNOWN_MESSAGE_LENGTH_TYPE) { + } + + enum State { + // The start state. + // Possible next state: WILL_NOTIFY_BEFORE_REQUEST; + // ERROR_OCCURRED. + BEGIN = 0, + // We are about to fire webRequest.onBeforeRequest. + // Possible next state: NOTIFIED_BEFORE_REQUEST; + // ERROR_OCCURRED. + WILL_NOTIFY_BEFORE_REQUEST = 1, + // The last event fired is webRequest.onBeforeRequest. + // Possible next state: NOTIFIED_REQUEST_SENT; + // ERROR_OCCURRED. + NOTIFIED_BEFORE_REQUEST = 2, + // The last event fired is webRequest.onRequestSent. + // Possible next state: NOTIFIED_HEADERS_RECEIVED; + // ERROR_OCCURRED. + NOTIFIED_REQUEST_SENT = 3, + // The last event fired is webRequest.onHeadersReceived. + // Possible next state: NOTIFIED_BEFORE_REDIRECT; + // NOTIFIED_COMPLETED; + // ERROR_OCCURRED. + NOTIFIED_HEADERS_RECEIVED = 4, + // The last event fired is webRequest.onBeforeRedirect. + // Possible next state: NOTIFIED_REQUEST_SENT; + // ERROR_OCCURRED. + NOTIFIED_BEFORE_REDIRECT = 5, + // One of the two stop states. + // The last event fired is webRequest.onCompleted. + // Possible next state: NOTIFIED_COMPLETED. + NOTIFIED_COMPLETED = 6, + // One of the two stop states. + // Some error has occurred. + // Possible next state: ERROR_OCCURRED. + ERROR_OCCURRED = 7 + }; + + // How the length of a message body is decided. + enum MessageLengthType { + UNKNOWN_MESSAGE_LENGTH_TYPE = 0, + // There is no message body. + NO_MESSAGE_BODY = 1, + // The length is decided by Content-Length header. + CONTENT_LENGTH_HEADER = 2, + // Including: + // (1) a Transfer-Encoding header field is present and has any value + // other than "identity"; + // (2) the message uses the media type "multipart/byteranges"; + // (3) the length should be decided by the server closing the connection. + VARIABLE_MESSAGE_LENGTH = 3, + }; + + // The handle of the connection to the server. + HINTERNET server_handle; + // The request ID, which is unique within a browser session. + int id; + // The window handle of the tab which sent the request. + CeeeWindowHandle tab_handle; + // The standard HTTP method, such as "GET" or "POST". + std::string method; + // The URL to retrieve. + std::wstring url; + // The URL before redirection. + std::wstring original_url; + // The server IP. + std::string ip; + // The standard HTTP status code returned by the server. + DWORD status_code; + // The request state to help decide what event should be sent next. + State state; + // When the browser was about to make the request. + base::Time before_request_time; + // The length of the response body. It is meaningful only when length_type + // is set to CONTENT_LENGTH_HEADER. + DWORD content_length; + // The progress of reading the response body. + DWORD read_progress; + // How the length of the response body is decided. + MessageLengthType length_type; + }; + + WebRequestNotifier(); + virtual ~WebRequestNotifier(); + + // Accessor so that we can mock it in unit tests. + // Currently this method is always called within a lock. + virtual WebRequestEventsFunnel& webrequest_events_funnel() { + return webrequest_events_funnel_; + } + + // Gets called before calling InternetSetStatusCallback. + // @param internet The handle for which the callback is set. + // @param callback The real callback function. + // @return A patch for the callback function. + INTERNET_STATUS_CALLBACK HandleBeforeInternetSetStatusCallback( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback); + + // Gets called before calling InternetConnect. + // @param internet Handle returned by InternetOpen. + void HandleBeforeInternetConnect(HINTERNET internet); + + // Gets called after calling InternetConnect. + // @param server The sever handle returned by InternetConnect. + // @param server_name The host name of the server. + // @param server_port The port number on the server. + // @param service Type of service to access. + void HandleAfterInternetConnect(HINTERNET server, + const wchar_t* server_name, + INTERNET_PORT server_port, + DWORD service); + + // Gets called after calling HttpOpenRequest. + // @param server The server handle. + // @param request The request handle. + // @param method Standard HTTP method, such as "GET" or "POST". + // @param path The path to the target object. + // @param flags Internet options that are passed into HttpOpenRequest. + void HandleAfterHttpOpenRequest(HINTERNET server, + HINTERNET request, + const char* method, + const wchar_t* path, + DWORD flags); + + // Gets called before calling HttpSendRequest. + // @param request The request handle. + void HandleBeforeHttpSendRequest(HINTERNET request); + + // Gets called before calling InternetStatusCallback. + // @param original The original status callback function. + // @param internet The handle for which the callback function is called. + // @param context The application-defined context value associated with + // the internet parameter. + // @param internet_status A status code that indicates why the callback + // function is called. + // @param status_information A pointer to additional status information. + // @param status_information_length The size, in bytes, of the additional + // status information. + void HandleBeforeInternetStatusCallback(INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length); + + // Gets called after calling InternetReadFile. + // @param request The request handle. + // @param result Whether the read operation is successful or not. + // @param number_of_bytes_read How many bytes have been read. + void HandleAfterInternetReadFile(HINTERNET request, + BOOL result, + LPDWORD number_of_bytes_read); + + // InternetSetStatusCallback function patches. + // InternetSetStatusCallback documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385120(VS.85).aspx + static INTERNET_STATUS_CALLBACK STDAPICALLTYPE + InternetSetStatusCallbackAPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback); + static INTERNET_STATUS_CALLBACK STDAPICALLTYPE + InternetSetStatusCallbackWPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback); + + // InternetConnect function patches. + // InternetConnect documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384363(VS.85).aspx + static HINTERNET STDAPICALLTYPE InternetConnectAPatch( + HINTERNET internet, + LPCSTR server_name, + INTERNET_PORT server_port, + LPCSTR user_name, + LPCSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context); + static HINTERNET STDAPICALLTYPE InternetConnectWPatch( + HINTERNET internet, + LPCWSTR server_name, + INTERNET_PORT server_port, + LPCWSTR user_name, + LPCWSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context); + + // HttpOpenRequest function patches. + // HttpOpenRequest documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384233(v=VS.85).aspx + static HINTERNET STDAPICALLTYPE HttpOpenRequestAPatch(HINTERNET connect, + LPCSTR verb, + LPCSTR object_name, + LPCSTR version, + LPCSTR referrer, + LPCSTR* accept_types, + DWORD flags, + DWORD_PTR context); + static HINTERNET STDAPICALLTYPE HttpOpenRequestWPatch(HINTERNET connect, + LPCWSTR verb, + LPCWSTR object_name, + LPCWSTR version, + LPCWSTR referrer, + LPCWSTR* accept_types, + DWORD flags, + DWORD_PTR context); + + // HttpSendRequest function patches. + // HttpSendRequest documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384247(v=VS.85).aspx + static BOOL STDAPICALLTYPE HttpSendRequestAPatch(HINTERNET request, + LPCSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length); + static BOOL STDAPICALLTYPE HttpSendRequestWPatch(HINTERNET request, + LPCWSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length); + + // InternetStatusCallback function patch. + // InternetStatusCallback function documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385121(v=VS.85).aspx + static void CALLBACK InternetStatusCallbackPatch( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length); + + // InternetReadFile function patch. + // InternetReadFile documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385103(v=VS.85).aspx + static BOOL STDAPICALLTYPE InternetReadFilePatch( + HINTERNET file, + LPVOID buffer, + DWORD number_of_bytes_to_read, + LPDWORD number_of_bytes_read); + + // Patches a WinINet function. + // @param name The name of the function to be intercepted. + // @param patch_function The patching helper. You could check the is_patched + // member of this object to see whether the patching operation is + // successful or not. + // @param handler The new function implementation. + void PatchWinINetFunction(const char* name, + app::win::IATPatchFunction* patch_function, + void* handler); + + // Constructs a URL. The method omits the port number if it is the default + // number for the protocol, or it is INTERNET_INVALID_PORT_NUMBER. + // Currently this method is always called within a lock. + // @param https Is it http or https? + // @param server_name The host name of the server. + // @param server_port The port number on the server. + // @param path The path to the target object. + // @param url The returned URL. + // @return Whether the operation is successful or not. + bool ConstructUrl(bool https, + const wchar_t* server_name, + INTERNET_PORT server_port, + const wchar_t* path, + std::wstring* url); + + // Retrieves header information as a number. + // Make the method virtual so that we could mock it for unit tests. + // @param request The request handle. + // @param info_flag Query info flags could be found on: + // http://msdn.microsoft.com/en-us/library/aa385351(v=VS.85).aspx + // @param value The returned value. + // @return Whether the operation is successful or not. + virtual bool QueryHttpInfoNumber(HINTERNET request, + DWORD info_flag, + DWORD* value); + // Retrieves header information as a string. + // Make the method virtual so that we could mock it for unit tests. + // @param request The request handle. + // @param info_flag Query info flags could be found on: + // http://msdn.microsoft.com/en-us/library/aa385351(v=VS.85).aspx + // @param value The returned value. + // @return Whether the operation is successful or not. + virtual bool QueryHttpInfoString(HINTERNET request, + DWORD info_flag, + std::wstring* value); + + // Determines the length of the response body. + // @param request The request handle. + // @param status_code Standard HTTP status code. + // @param length Returns the length of the response body. It is meaningful + // only when type is set to CONTENT_LENGTH_HEADER. + // @param type Returns how the length of the response body is decided. + // @return Whether the operation is successful or not. + bool DetermineMessageLength(HINTERNET request, + DWORD status_code, + DWORD* length, + RequestInfo::MessageLengthType* type); + + // Performs state transition on a request. + // NOTE: this method must be called within a lock. + // @param state The target state. Please note that if it is not a valid + // transition, the request may end up with a state other than the + // target state. + // @param info Information about the request. + void TransitRequestToNextState(RequestInfo::State state, RequestInfo* info); + + // Creates a function stub for the status callback function. + // NOTE: this method must be called within a lock. + // @param original_callback The original callback function. + // @return A patch for the callback function. + INTERNET_STATUS_CALLBACK CreateInternetStatusCallbackStub( + INTERNET_STATUS_CALLBACK original_callback); + + // NOTE: this method must be called within a lock. + // @return Returns true if the service is not functioning, either because + // nobody is interested in the service or because the initialization + // has failed. + bool IsNotRunning() const { + return start_count_ == 0 || initialize_state_ != SUCCEEDED_TO_INITIALIZE; + } + + // Returns true if exactly one (but not both) of the patches has been + // successfully applied. + // @param patch_function_1 A function patch. + // @param patch_function_2 Another function patch. + // @return Returns true if exactly one of them has been successfully applied. + bool HasPatchedOneVersion( + const app::win::IATPatchFunction& patch_function_1, + const app::win::IATPatchFunction& patch_function_2) const { + return (patch_function_1.is_patched() && !patch_function_2.is_patched()) || + (!patch_function_1.is_patched() && patch_function_2.is_patched()); + } + + // Function patches that allow us to intercept WinINet functions. + app::win::IATPatchFunction internet_set_status_callback_a_patch_; + app::win::IATPatchFunction internet_set_status_callback_w_patch_; + app::win::IATPatchFunction internet_connect_a_patch_; + app::win::IATPatchFunction internet_connect_w_patch_; + app::win::IATPatchFunction http_open_request_a_patch_; + app::win::IATPatchFunction http_open_request_w_patch_; + app::win::IATPatchFunction http_send_request_a_patch_; + app::win::IATPatchFunction http_send_request_w_patch_; + app::win::IATPatchFunction internet_read_file_patch_; + + // The funnel for sending webRequest events to the broker. + WebRequestEventsFunnel webrequest_events_funnel_; + + // Used to protect the access to all the following data members. + CComAutoCriticalSection critical_section_; + + // Used to intercept InternetStatusCallback function, which is defined by a + // WinINet client to observe status changes. + FunctionStub* internet_status_callback_stub_; + + // Maps Internet connection handles to ServerInfo instances. + typedef std::map<HINTERNET, ServerInfo> ServerMap; + ServerMap server_map_; + + // Maps HTTP request handles to RequestInfo instances. + typedef std::map<HINTERNET, RequestInfo> RequestMap; + RequestMap request_map_; + + // The number of RequestToStart calls minus the number of RequestToStop calls. + // If the number drops to 0, then the service will be stopped. + int start_count_; + + // Indicates the progress of initialization. + enum InitializeState { + // Initialization hasn't been started. + NOT_INITIALIZED, + // Initialization is happening. + INITIALIZING, + // Initialization has failed. + FAILED_TO_INITIALIZE, + // Initialization has succeeded. + SUCCEEDED_TO_INITIALIZE + }; + InitializeState initialize_state_; + + private: + DISALLOW_COPY_AND_ASSIGN(WebRequestNotifier); +}; + +// A singleton that keeps the WebRequestNotifier used by production code. +class ProductionWebRequestNotifier + : public WebRequestNotifier, + public Singleton<ProductionWebRequestNotifier> { + private: + ProductionWebRequestNotifier() {} + + friend struct DefaultSingletonTraits<ProductionWebRequestNotifier>; + DISALLOW_COPY_AND_ASSIGN(ProductionWebRequestNotifier); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ diff --git a/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc b/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc new file mode 100644 index 0000000..81ee4d0 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc @@ -0,0 +1,340 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit test for WebRequestNotifier class. +#include "ceee/ie/plugin/bho/webrequest_notifier.h" + +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::InSequence; +using testing::MockFunction; +using testing::StrictMock; + +class TestWebRequestNotifier : public WebRequestNotifier { + public: + TestWebRequestNotifier() + : mock_method_(L"GET"), + mock_status_code_(200), + mock_transfer_encoding_present_(false), + mock_content_length_(0), + mock_content_length_present_(false) { + } + virtual ~TestWebRequestNotifier() {} + + virtual WebRequestEventsFunnel& webrequest_events_funnel() { + return mock_webrequest_events_funnel_; + } + + virtual bool QueryHttpInfoString(HINTERNET /*request*/, + DWORD info_flag, + std::wstring* value) { + if (value == NULL) + return false; + if (info_flag == HTTP_QUERY_REQUEST_METHOD) { + *value = mock_method_; + return true; + } else if (mock_transfer_encoding_present_ && + info_flag == HTTP_QUERY_TRANSFER_ENCODING) { + *value = mock_transfer_encoding_; + return true; + } else { + return false; + } + } + + virtual bool QueryHttpInfoNumber(HINTERNET /*request*/, + DWORD info_flag, + DWORD* value) { + if (value == NULL) + return false; + if (info_flag == HTTP_QUERY_STATUS_CODE) { + *value = mock_status_code_; + return true; + } else if (mock_content_length_present_ && + info_flag == HTTP_QUERY_CONTENT_LENGTH) { + *value = mock_content_length_; + return true; + } else { + return false; + } + } + + StrictMock<testing::MockWebRequestEventsFunnel> + mock_webrequest_events_funnel_; + + std::wstring mock_method_; + DWORD mock_status_code_; + std::wstring mock_transfer_encoding_; + bool mock_transfer_encoding_present_; + DWORD mock_content_length_; + bool mock_content_length_present_; + + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestConstructUrl); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestDetermineMessageLength); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestTransitRequestToNextState); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestWinINetPatchHandlers); +}; + +class WebRequestNotifierTestFixture : public testing::Test { + protected: + virtual void SetUp() { + webrequest_notifier_.reset(new TestWebRequestNotifier()); + ASSERT_TRUE(webrequest_notifier_->RequestToStart()); + } + + virtual void TearDown() { + webrequest_notifier_->RequestToStop(); + webrequest_notifier_.reset(NULL); + } + + scoped_ptr<TestWebRequestNotifier> webrequest_notifier_; +}; + +TEST_F(WebRequestNotifierTestFixture, TestConstructUrl) { + std::wstring output; + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + true, L"www.google.com", INTERNET_INVALID_PORT_NUMBER, L"/foobar", + &output)); + EXPECT_STREQ(L"https://www.google.com/foobar", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + false, L"mail.google.com", INTERNET_DEFAULT_HTTP_PORT, L"/index.html", + &output)); + EXPECT_STREQ(L"http://mail.google.com/index.html", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + false, L"docs.google.com", INTERNET_DEFAULT_HTTPS_PORT, L"/login", + &output)); + EXPECT_STREQ(L"http://docs.google.com:443/login", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + true, L"image.google.com", 123, L"/helloworld", &output)); + EXPECT_STREQ(L"https://image.google.com:123/helloworld", output.c_str()); +} + +TEST_F(WebRequestNotifierTestFixture, TestDetermineMessageLength) { + DWORD length = 0; + WebRequestNotifier::RequestInfo::MessageLengthType type = + WebRequestNotifier::RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + HINTERNET request = reinterpret_cast<HINTERNET>(1024); + + // Requests with "HEAD" method don't have a message body in the response. + webrequest_notifier_->mock_method_ = L"HEAD"; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::NO_MESSAGE_BODY, type); + + // Requests with status code 1XX, 204 or 304 don't have a message body in the + // response. + webrequest_notifier_->mock_method_ = L"GET"; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 100, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::NO_MESSAGE_BODY, type); + + // If a Transfer-Encoding header field is present and has any value other than + // "identity", the response body length is variable. + // The Content-Length field is ignored in this case. + webrequest_notifier_->mock_transfer_encoding_present_ = true; + webrequest_notifier_->mock_transfer_encoding_ = L"chunked"; + webrequest_notifier_->mock_content_length_present_ = true; + webrequest_notifier_->mock_content_length_ = 256; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::VARIABLE_MESSAGE_LENGTH, type); + + // If a Content-Length header field is present, the response body length is + // the same as specified in the Content-Length header. + webrequest_notifier_->mock_transfer_encoding_present_ = false; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(256, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::CONTENT_LENGTH_HEADER, type); + + // Otherwise, consider the response body length is variable. + webrequest_notifier_->mock_content_length_present_ = false; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::VARIABLE_MESSAGE_LENGTH, type); +} + +TEST_F(WebRequestNotifierTestFixture, TestTransitRequestToNextState) { + scoped_ptr<WebRequestNotifier::RequestInfo> info( + new WebRequestNotifier::RequestInfo()); + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRedirect(_, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnErrorOccurred(_, _, _, _)); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(check, Call(5)); + } + + // The normal state transition sequence. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REDIRECT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_COMPLETED, info.get()); + check.Call(1); + + // No event is fired after webRequest.onCompleted for any request. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::ERROR_OCCURRED, info.get()); + check.Call(2); + + // No webRequest.onErrorOccurred is fired since the first event for any + // request has to be webRequest.onBeforeRequest. + info.reset(new WebRequestNotifier::RequestInfo()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::ERROR_OCCURRED, info.get()); + check.Call(3); + + // Unexpected next-state will result in webRequest.onErrorOccurred to be + // fired. + info.reset(new WebRequestNotifier::RequestInfo()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_COMPLETED, info.get()); + check.Call(4); + + // No event is fired after webRequest.onErrorOccurred for any request. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + check.Call(5); +} + +TEST_F(WebRequestNotifierTestFixture, TestWinINetPatchHandlers) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRedirect(_, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(1)); + } + + HINTERNET internet = reinterpret_cast<HINTERNET>(512); + HINTERNET server = reinterpret_cast<HINTERNET>(1024); + HINTERNET request = reinterpret_cast<HINTERNET>(2048); + + webrequest_notifier_->mock_method_ = L"GET"; + webrequest_notifier_->mock_status_code_ = 200; + webrequest_notifier_->mock_content_length_ = 256; + webrequest_notifier_->mock_content_length_present_ = true; + + webrequest_notifier_->HandleBeforeInternetConnect(internet); + webrequest_notifier_->HandleAfterInternetConnect( + server, L"www.google.com", INTERNET_DEFAULT_HTTP_PORT, + INTERNET_SERVICE_HTTP); + + webrequest_notifier_->HandleAfterHttpOpenRequest(server, request, "GET", + L"/", 0); + + webrequest_notifier_->HandleBeforeHttpSendRequest(request); + + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_SENDING_REQUEST, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_SENT, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REDIRECT, + "http://www.google.com/index.html", 32); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_SENDING_REQUEST, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_SENT, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_COMPLETE, NULL, 0); + + DWORD number_of_bytes_read = 64; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + number_of_bytes_read = 128; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + number_of_bytes_read = 64; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + + // Since we have read the whole response body, webRequest.onCompleted has been + // sent at this point. + check.Call(1); + + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_HANDLE_CLOSING, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, server, NULL, INTERNET_STATUS_HANDLE_CLOSING, NULL, 0); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/window_message_source.cc b/ceee/ie/plugin/bho/window_message_source.cc new file mode 100644 index 0000000..ec6b0ea --- /dev/null +++ b/ceee/ie/plugin/bho/window_message_source.cc @@ -0,0 +1,216 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window message source implementation. +#include "ceee/ie/plugin/bho/window_message_source.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/win_util.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + +WindowMessageSource::MessageSourceMap WindowMessageSource::message_source_map_; +Lock WindowMessageSource::lock_; + +WindowMessageSource::WindowMessageSource() + : create_thread_id_(::GetCurrentThreadId()), + get_message_hook_(NULL), + call_wnd_proc_ret_hook_(NULL) { +} + +WindowMessageSource::~WindowMessageSource() { + DCHECK(get_message_hook_ == NULL); + DCHECK(call_wnd_proc_ret_hook_ == NULL); + DCHECK(GetEntryFromMap(create_thread_id_) == NULL); +} + +bool WindowMessageSource::Initialize() { + if (!AddEntryToMap(create_thread_id_, this)) + return false; + + get_message_hook_ = ::SetWindowsHookEx(WH_GETMESSAGE, GetMessageHookProc, + NULL, create_thread_id_); + if (get_message_hook_ == NULL) { + TearDown(); + return false; + } + + call_wnd_proc_ret_hook_ = ::SetWindowsHookEx(WH_CALLWNDPROCRET, + CallWndProcRetHookProc, NULL, + create_thread_id_); + if (call_wnd_proc_ret_hook_ == NULL) { + TearDown(); + return false; + } + return true; +} + +void WindowMessageSource::TearDown() { + if (get_message_hook_ != NULL) { + ::UnhookWindowsHookEx(get_message_hook_); + get_message_hook_ = NULL; + } + + if (call_wnd_proc_ret_hook_ != NULL) { + ::UnhookWindowsHookEx(call_wnd_proc_ret_hook_); + call_wnd_proc_ret_hook_ = NULL; + } + + RemoveEntryFromMap(create_thread_id_); +} + +void WindowMessageSource::RegisterSink(Sink* sink) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter == sinks_.end()) + sinks_.push_back(sink); +} + +void WindowMessageSource::UnregisterSink(Sink* sink) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter != sinks_.end()) + sinks_.erase(iter); +} + +// static +LRESULT CALLBACK WindowMessageSource::GetMessageHookProc(int code, + WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION && wparam == PM_REMOVE) { + MSG* message_info = reinterpret_cast<MSG*>(lparam); + if (message_info != NULL) { + if ((message_info->message >= WM_MOUSEFIRST && + message_info->message <= WM_MOUSELAST) || + (message_info->message >= WM_KEYFIRST && + message_info->message <= WM_KEYLAST)) { + WindowMessageSource* source = GetEntryFromMap(::GetCurrentThreadId()); + if (source != NULL) + source->OnHandleMessage(message_info); + } + } + } + + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +void WindowMessageSource::OnHandleMessage(const MSG* message_info) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + DCHECK(message_info != NULL); + MessageType type = IsWithinTabContentWindow(message_info->hwnd) ? + TAB_CONTENT_WINDOW : BROWSER_UI_SAME_THREAD; + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); iter != sinks_.end(); + ++iter) { + (*iter)->OnHandleMessage(type, message_info); + } +} + +// static +LRESULT CALLBACK WindowMessageSource::CallWndProcRetHookProc(int code, + WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION) { + CWPRETSTRUCT* message_info = reinterpret_cast<CWPRETSTRUCT*>(lparam); + if (message_info != NULL && message_info->message == WM_NCDESTROY) { + WindowMessageSource* source = GetEntryFromMap(::GetCurrentThreadId()); + if (source != NULL) + source->OnWindowNcDestroy(message_info->hwnd); + } + } + + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +void WindowMessageSource::OnWindowNcDestroy(HWND window) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + tab_content_window_map_.erase(window); +} + +bool WindowMessageSource::IsWithinTabContentWindow(HWND window) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (window == NULL) + return false; + + // Look up the cache to see whether we have already examined this window + // handle and got the answer. + TabContentWindowMap::const_iterator iter = + tab_content_window_map_.find(window); + if (iter != tab_content_window_map_.end()) + return iter->second; + + // Examine whether the window or one of its ancestors is the tab content + // window. + std::vector<HWND> self_and_ancestors; + bool is_within_tab_content_window = false; + do { + self_and_ancestors.push_back(window); + + if (window_utils::IsWindowClass(window, + windows::kIeTabContentWindowClass)) { + is_within_tab_content_window = true; + break; + } + + window = ::GetAncestor(window, GA_PARENT); + if (window == NULL || !window_utils::IsWindowThread(window)) + break; + + TabContentWindowMap::const_iterator iter = + tab_content_window_map_.find(window); + if (iter != tab_content_window_map_.end()) { + is_within_tab_content_window = iter->second; + break; + } + } while (true); + + // Add the windows that we have examined into the cache. + for (std::vector<HWND>::const_iterator iter = self_and_ancestors.begin(); + iter != self_and_ancestors.end(); ++iter) { + tab_content_window_map_.insert( + std::make_pair<HWND, bool>(*iter, is_within_tab_content_window)); + } + return is_within_tab_content_window; +} + +// static +bool WindowMessageSource::AddEntryToMap(DWORD thread_id, + WindowMessageSource* source) { + DCHECK(source != NULL); + + AutoLock auto_lock(lock_); + MessageSourceMap::const_iterator iter = message_source_map_.find(thread_id); + if (iter != message_source_map_.end()) + return false; + + message_source_map_.insert( + std::make_pair<DWORD, WindowMessageSource*>(thread_id, source)); + return true; +} + +// static +WindowMessageSource* WindowMessageSource::GetEntryFromMap(DWORD thread_id) { + AutoLock auto_lock(lock_); + MessageSourceMap::const_iterator iter = message_source_map_.find(thread_id); + return iter == message_source_map_.end() ? NULL : iter->second; +} + +// static +void WindowMessageSource::RemoveEntryFromMap(DWORD thread_id) { + AutoLock auto_lock(lock_); + message_source_map_.erase(thread_id); +} diff --git a/ceee/ie/plugin/bho/window_message_source.h b/ceee/ie/plugin/bho/window_message_source.h new file mode 100644 index 0000000..baceafa --- /dev/null +++ b/ceee/ie/plugin/bho/window_message_source.h @@ -0,0 +1,103 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window message source implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ +#define CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ + +#include <map> +#include <vector> + +#include "base/basictypes.h" +#include "base/lock.h" + +// A WindowMessageSource instance monitors keyboard and mouse messages on the +// same thread as the one that creates the instance, and fires events to those +// registered sinks. +// +// NOTE: (1) All the non-static methods are supposed to be called on the thread +// that creates the instance. +// (2) Multiple WindowMessageSource instances cannot live in the same +// thread. Only the first one will be successfully initialized. +class WindowMessageSource { + public: + enum MessageType { + // The destination of the message is the content window (or its descendants) + // of a tab. + TAB_CONTENT_WINDOW, + // Otherwise. + BROWSER_UI_SAME_THREAD + }; + + // The interface that event consumers have to implement. + // NOTE: All the callback methods will be called on the thread that creates + // the WindowMessageSource instance. + class Sink { + public: + virtual ~Sink() {} + // Called before a message is handled. + virtual void OnHandleMessage(MessageType type, const MSG* message_info) {} + }; + + WindowMessageSource(); + virtual ~WindowMessageSource(); + + bool Initialize(); + void TearDown(); + + virtual void RegisterSink(Sink* sink); + virtual void UnregisterSink(Sink* sink); + + private: + // Hook procedure for WH_GETMESSAGE. + static LRESULT CALLBACK GetMessageHookProc(int code, + WPARAM wparam, + LPARAM lparam); + void OnHandleMessage(const MSG* message_info); + + // Hook procedure for WH_CALLWNDPROCRET. + static LRESULT CALLBACK CallWndProcRetHookProc(int code, + WPARAM wparam, + LPARAM lparam); + void OnWindowNcDestroy(HWND window); + + // Returns true if the specified window is the tab content window or one of + // its descendants. + bool IsWithinTabContentWindow(HWND window); + + // Adds an entry to the message_source_map_. Returns false if the item was + // already present. + static bool AddEntryToMap(DWORD thread_id, WindowMessageSource* source); + // Retrieves an entry from the message_source_map_. + static WindowMessageSource* GetEntryFromMap(DWORD thread_id); + // Removes an entry from the message_source_map_. + static void RemoveEntryFromMap(DWORD thread_id); + + // The thread that creates this object. + const DWORD create_thread_id_; + // Event consumers. + std::vector<Sink*> sinks_; + + // The handle to the hook procedure of WH_GETMESSAGE. + HHOOK get_message_hook_; + // The handle to the hook procedure of WH_CALLWNDPROCRET. + HHOOK call_wnd_proc_ret_hook_; + + // Caches the information about whether a given window is within the tab + // content window or not. + typedef std::map<HWND, bool> TabContentWindowMap; + TabContentWindowMap tab_content_window_map_; + + // Maintains a map from thread IDs to their corresponding + // WindowMessageSource instances. + typedef std::map<DWORD, WindowMessageSource*> MessageSourceMap; + static MessageSourceMap message_source_map_; + + // Used to protect access to the message_source_map_. + static Lock lock_; + + DISALLOW_COPY_AND_ASSIGN(WindowMessageSource); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ diff --git a/ceee/ie/plugin/scripting/base.js b/ceee/ie/plugin/scripting/base.js new file mode 100644 index 0000000..58401de --- /dev/null +++ b/ceee/ie/plugin/scripting/base.js @@ -0,0 +1,1291 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview Bootstrap for the Google JS Library (Closure) Also includes + * stuff taken from //depot/google3/javascript/lang.js. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/base.js + * + * LOCAL CHANGES: Tagged "CEEE changes" below. + * + * This file has been changed from its original so that the deps.js file is + * not automatically inserted into the page. In the context of CEEE, there + * is no deps.js file. Furthermore, because this file is being injected into + * the page using the Firefox sandbox mechanism, it does not have the security + * rights to write to the document using the write() method. + * + * The reason for injecting base.js into the file is to use the closure-based + * json.js file. If we no longer need json.js, then it would be possible to + * remove this file too. Firefox does not have native json library that can + * be used by unprivileged js code, but it will in version 3.5. + */ + +/** + * @define {boolean} Overridden to true by the compiler when --closure_pass + * or --mark_as_compiled is specified. + */ +var COMPILED = false; + + +/** + * Base namespace for the Closure library. Checks to see goog is + * already defined in the current scope before assigning to prevent + * clobbering if base.js is loaded more than once. + */ +var goog = goog || {}; // Check to see if already defined in current scope + + +/** + * Reference to the global context. In most cases this will be 'window'. + */ +goog.global = this; + + +/** + * @define {boolean} DEBUG is provided as a convenience so that debugging code + * that should not be included in a production js_binary can be easily stripped + * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most + * toString() methods should be declared inside an "if (goog.DEBUG)" conditional + * because they are generally used for debugging purposes and it is difficult + * for the JSCompiler to statically determine whether they are used. + */ +goog.DEBUG = true; + + +/** + * @define {string} LOCALE defines the locale being used for compilation. It is + * used to select locale specific data to be compiled in js binary. BUILD rule + * can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler + * option. + * + * Take into account that the locale code format is important. You should use + * the canonical Unicode format with hyphen as a delimiter. Language must be + * lowercase, Language Script - Capitalized, Region - UPPERCASE. + * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. + * + * See more info about locale codes here: + * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers + * + * For language codes you should use values defined by ISO 693-1. See it here + * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from + * this rule: the Hebrew language. For legacy reasons the old code (iw) should + * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. + */ +// CEEE changes: Changed from 'en' to 'en_US' +goog.LOCALE = 'en_US'; // default to en_US + + +/** + * Indicates whether or not we can call 'eval' directly to eval code in the + * global scope. Set to a Boolean by the first call to goog.globalEval (which + * empirically tests whether eval works for globals). @see goog.globalEval + * @type {boolean?} + * @private + */ +goog.evalWorksForGlobals_ = null; + + +/** + * Creates object stubs for a namespace. When present in a file, goog.provide + * also indicates that the file defines the indicated object. Calls to + * goog.provide are resolved by the compiler if --closure_pass is set. + * @param {string} name name of the object that this file defines. + */ +goog.provide = function(name) { + if (!COMPILED) { + // Ensure that the same namespace isn't provided twice. This is intended + // to teach new developers that 'goog.provide' is effectively a variable + // declaration. And when JSCompiler transforms goog.provide into a real + // variable declaration, the compiled JS should work the same as the raw + // JS--even when the raw JS uses goog.provide incorrectly. + if (goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) { + throw Error('Namespace "' + name + '" already declared.'); + } + + var namespace = name; + while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { + goog.implicitNamespaces_[namespace] = true; + } + } + + goog.exportPath_(name); +}; + + +if (!COMPILED) { + /** + * Namespaces implicitly defined by goog.provide. For example, + * goog.provide('goog.events.Event') implicitly declares + * that 'goog' and 'goog.events' must be namespaces. + * + * @type {Object} + * @private + */ + goog.implicitNamespaces_ = {}; +} + + +/** + * Builds an object structure for the provided namespace path, + * ensuring that names that already exist are not overwritten. For + * example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * Used by goog.provide and goog.exportSymbol. + * @param {string} name name of the object that this file defines. + * @param {Object} opt_object the object to expose at the end of the path. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + * @private + */ +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || goog.global; + var part; + + // Internet Explorer exhibits strange behavior when throwing errors from + // methods externed in this manner. See the testExportSymbolExceptions in + // base_test.html for an example. + if (!(parts[0] in cur) && cur.execScript) { + cur.execScript('var ' + parts[0]); + } + + // Parentheses added to eliminate strict JS warning in Firefox. + while (parts.length && (part = parts.shift())) { + if (!parts.length && goog.isDef(opt_object)) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (cur[part]) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } +}; + + +/** + * Returns an object based on its fully qualified external name. If you are + * using a compilation pass that renames property names beware that using this + * function will not find renamed properties. + * + * @param {string} name The fully qualified name. + * @param {Object} opt_obj The object within which to look; default is + * |goog.global|. + * @return {Object?} The object or, if not found, null. + */ +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split('.'); + var cur = opt_obj || goog.global; + for (var part; part = parts.shift(); ) { + if (cur[part]) { + cur = cur[part]; + } else { + return null; + } + } + return cur; +}; + + +/** + * Globalizes a whole namespace, such as goog or goog.lang. + * + * @param {Object} obj The namespace to globalize. + * @param {Object} opt_global The object to add the properties to. + * @deprecated Properties may be explicitly exported to the global scope, but + * this should no longer be done in bulk. + */ +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for (var x in obj) { + global[x] = obj[x]; + } +}; + + +/** + * Adds a dependency from a file to the files it requires. + * @param {string} relPath The path to the js file. + * @param {Array} provides An array of strings with the names of the objects + * this file provides. + * @param {Array} requires An array of strings with the names of the objects + * this file requires. + */ +goog.addDependency = function(relPath, provides, requires) { + if (!COMPILED) { + var provide, require; + var path = relPath.replace(/\\/g, '/'); + var deps = goog.dependencies_; + for (var i = 0; provide = provides[i]; i++) { + deps.nameToPath[provide] = path; + if (!(path in deps.pathToNames)) { + deps.pathToNames[path] = {}; + } + deps.pathToNames[path][provide] = true; + } + for (var j = 0; require = requires[j]; j++) { + if (!(path in deps.requires)) { + deps.requires[path] = {}; + } + deps.requires[path][require] = true; + } + } +}; + + +/** + * Implements a system for the dynamic resolution of dependencies + * that works in parallel with the BUILD system. Note that all calls + * to goog.require will be stripped by the JSCompiler when the + * --closure_pass option is used. + * @param {string} rule Rule to include, in the form goog.package.part. + */ +goog.require = function(rule) { + + // if the object already exists we do not need do do anything + if (!COMPILED) { + if (goog.getObjectByName(rule)) { + return; + } + var path = goog.getPathFromDeps_(rule); + if (path) { + goog.included_[path] = true; + goog.writeScripts_(); + } else { + // NOTE(nicksantos): We could always throw an error, but this would break + // legacy users that depended on this failing silently. Instead, the + // compiler should warn us when there are invalid goog.require calls. + // For now, we simply give clients a way to turn strict mode on. + if (goog.useStrictRequires) { + throw new Error('goog.require could not find: ' + rule); + } + } + } +}; + + +/** + * Whether goog.require should throw an exception if it fails. + * @type {boolean} + */ +goog.useStrictRequires = false; + + +/** + * Path for included scripts + * @type {string} + */ +goog.basePath = ''; + + +/** + * Null function used for default values of callbacks, etc. + * @type {!Function} + */ +goog.nullFunction = function() {}; + + +/** + * The identity function. Returns its first argument. + * + * @param {*} var_args The arguments of the function. + * @return {*} The first argument. + * @deprecated Use goog.functions.identity instead. + */ +goog.identityFunction = function(var_args) { + return arguments[0]; +}; + + +/** + * When defining a class Foo with an abstract method bar(), you can do: + * + * Foo.prototype.bar = goog.abstractMethod + * + * Now if a subclass of Foo fails to override bar(), an error + * will be thrown when bar() is invoked. + * + * Note: This does not take the name of the function to override as + * an argument because that would make it more difficult to obfuscate + * our JavaScript code. + * + * @throws {Error} when invoked to indicate the method should be + * overridden. + */ +goog.abstractMethod = function() { + throw Error('unimplemented abstract method'); +}; + + +/** + * Adds a {@code getInstance} static method that always return the same instance + * object. + * @param {!Function} ctor The constructor for the class to add the static + * method to. + */ +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor()); + }; +}; + + +if (!COMPILED) { + /** + * Object used to keep track of urls that have already been added. This + * record allows the prevention of circular dependencies. + * @type {Object} + * @private + */ + goog.included_ = {}; + + + /** + * This object is used to keep track of dependencies and other data that is + * used for loading scripts + * @private + * @type {Object} + */ + goog.dependencies_ = { + pathToNames: {}, // 1 to many + nameToPath: {}, // 1 to 1 + requires: {}, // 1 to many + visited: {}, // used when resolving dependencies to prevent us from + // visiting the file twice + written: {} // used to keep track of script files we have written + }; + + + /** + * Tries to detect the base path of the base.js script that bootstraps Closure + * @private + */ + goog.findBasePath_ = function() { + var doc = goog.global.document; + if (typeof doc == 'undefined') { + return; + } + if (goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return; + } else { + // HACKHACK to hide compiler warnings :( + goog.global.CLOSURE_BASE_PATH = null; + } + var scripts = doc.getElementsByTagName('script'); + for (var script, i = 0; script = scripts[i]; i++) { + var src = script.src; + var l = src.length; + if (src.substr(l - 7) == 'base.js') { + goog.basePath = src.substr(0, l - 7); + return; + } + } + }; + + + /** + * Writes a script tag if, and only if, that script hasn't already been added + * to the document. (Must be called at execution time) + * @param {string} src Script source. + * @private + */ + goog.writeScriptTag_ = function(src) { + var doc = goog.global.document; + if (typeof doc != 'undefined' && + !goog.dependencies_.written[src]) { + goog.dependencies_.written[src] = true; + doc.write('<script type="text/javascript" src="' + + src + '"></' + 'script>'); + } + }; + + + /** + * Resolves dependencies based on the dependencies added using addDependency + * and calls writeScriptTag_ in the correct order. + * @private + */ + goog.writeScripts_ = function() { + // the scripts we need to write this time + var scripts = []; + var seenScript = {}; + var deps = goog.dependencies_; + + function visitNode(path) { + if (path in deps.written) { + return; + } + + // we have already visited this one. We can get here if we have cyclic + // dependencies + if (path in deps.visited) { + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + return; + } + + deps.visited[path] = true; + + if (path in deps.requires) { + for (var requireName in deps.requires[path]) { + if (requireName in deps.nameToPath) { + visitNode(deps.nameToPath[requireName]); + } else { + throw Error('Undefined nameToPath for ' + requireName); + } + } + } + + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + } + + for (var path in goog.included_) { + if (!deps.written[path]) { + visitNode(path); + } + } + + for (var i = 0; i < scripts.length; i++) { + if (scripts[i]) { + goog.writeScriptTag_(goog.basePath + scripts[i]); + } else { + throw Error('Undefined script input'); + } + } + }; + + + /** + * Looks at the dependency rules and tries to determine the script file that + * fulfills a particular rule. + * @param {string} rule In the form goog.namespace.Class or project.script. + * @return {string?} Url corresponding to the rule, or null. + * @private + */ + goog.getPathFromDeps_ = function(rule) { + if (rule in goog.dependencies_.nameToPath) { + return goog.dependencies_.nameToPath[rule]; + } else { + return null; + } + }; + + goog.findBasePath_(); + // start CEEE changes {{ + // This file is injected into the page using the Firefox sandbox mechanism. + // There is no generated deps.js file to inject with it. + //goog.writeScriptTag_(goog.basePath + 'deps.js'); + // }} end CEEE changes +} + + + +//============================================================================== +// Language Enhancements +//============================================================================== + + +/** + * This is a "fixed" version of the typeof operator. It differs from the typeof + * operator in such a way that null returns 'null' and arrays return 'array'. + * @param {*} value The value to get the type of. + * @return {string} The name of the type. + */ +goog.typeOf = function(value) { + var s = typeof value; + if (s == 'object') { + if (value) { + // We cannot use constructor == Array or instanceof Array because + // different frames have different Array objects. In IE6, if the iframe + // where the array was created is destroyed, the array loses its + // prototype. Then dereferencing val.splice here throws an exception, so + // we can't use goog.isFunction. Calling typeof directly returns 'unknown' + // so that will work. In this case, this function will return false and + // most array functions will still work because the array is still + // array-like (supports length and []) even though it has lost its + // prototype. + // Mark Miller noticed that Object.prototype.toString + // allows access to the unforgeable [[Class]] property. + // 15.2.4.2 Object.prototype.toString ( ) + // When the toString method is called, the following steps are taken: + // 1. Get the [[Class]] property of this object. + // 2. Compute a string value by concatenating the three strings + // "[object ", Result(1), and "]". + // 3. Return Result(2). + // and this behavior survives the destruction of the execution context. + if (value instanceof Array || // Works quickly in same execution context. + // If value is from a different execution context then + // !(value instanceof Object), which lets us early out in the common + // case when value is from the same context but not an array. + // The {if (value)} check above means we don't have to worry about + // undefined behavior of Object.prototype.toString on null/undefined. + // + // HACK: In order to use an Object prototype method on the arbitrary + // value, the compiler requires the value be cast to type Object, + // even though the ECMA spec explicitly allows it. + (!(value instanceof Object) && + Object.prototype.toString.call( + /** @type {Object} */(value)) == '[object Array]')) { + return 'array'; + } + // HACK: There is still an array case that fails. + // function ArrayImpostor() {} + // ArrayImpostor.prototype = []; + // var impostor = new ArrayImpostor; + // this can be fixed by getting rid of the fast path + // (value instanceof Array) and solely relying on + // (value && Object.prototype.toString.vall(value) === '[object Array]') + // but that would require many more function calls and is not warranted + // unless closure code is receiving objects from untrusted sources. + + // IE in cross-window calls does not correctly marshal the function type + // (it appears just as an object) so we cannot use just typeof val == + // 'function'. However, if the object has a call property, it is a + // function. + if (typeof value.call != 'undefined') { + return 'function'; + } + } else { + return 'null'; + } + + // In Safari typeof nodeList returns 'function', and on Firefox + // typeof behaves similarly for HTML{Applet,Embed,Object}Elements + // and RegExps. We would like to return object for those and we can + // detect an invalid function by making sure that the function + // object has a call method. + } else if (s == 'function' && typeof value.call == 'undefined') { + return 'object'; + } + return s; +}; + + +/** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). Does not use browser native + * Object.propertyIsEnumerable. + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ +goog.propertyIsEnumerableCustom_ = function(object, propName) { + // KJS in Safari 2 is not ECMAScript compatible and lacks crucial methods + // such as propertyIsEnumerable. We therefore use a workaround. + // Does anyone know a more efficient work around? + if (propName in object) { + for (var key in object) { + if (key == propName && + Object.prototype.hasOwnProperty.call(object, propName)) { + return true; + } + } + } + return false; +}; + + +if (Object.prototype.propertyIsEnumerable) { + /** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ + goog.propertyIsEnumerable_ = function(object, propName) { + // In IE if object is from another window, cannot use propertyIsEnumerable + // from this window's Object. Will raise a 'JScript object expected' error. + if (object instanceof Object) { + return Object.prototype.propertyIsEnumerable.call(object, propName); + } else { + return goog.propertyIsEnumerableCustom_(object, propName); + } + }; +} else { + // CEEE changes: Added the conditional above and this case as a bugfix. + goog.propertyIsEnumerable_ = goog.propertyIsEnumerableCustom_; +} + +/** + * Returns true if the specified value is not |undefined|. + * WARNING: Do not use this to test if an object has a property. Use the in + * operator instead. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined. + */ +goog.isDef = function(val) { + return typeof val != 'undefined'; +}; + + +/** + * Returns true if the specified value is |null| + * @param {*} val Variable to test. + * @return {boolean} Whether variable is null. + */ +goog.isNull = function(val) { + return val === null; +}; + + +/** + * Returns true if the specified value is defined and not null + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined and not null. + */ +goog.isDefAndNotNull = function(val) { + return goog.isDef(val) && !goog.isNull(val); +}; + + +/** + * Returns true if the specified value is an array + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArray = function(val) { + return goog.typeOf(val) == 'array'; +}; + + +/** + * Returns true if the object looks like an array. To qualify as array like + * the value needs to be either a NodeList or an object with a Number length + * property. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArrayLike = function(val) { + var type = goog.typeOf(val); + return type == 'array' || type == 'object' && typeof val.length == 'number'; +}; + + +/** + * Returns true if the object looks like a Date. To qualify as Date-like + * the value needs to be an object and have a getFullYear() function. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a like a Date. + */ +goog.isDateLike = function(val) { + return goog.isObject(val) && typeof val.getFullYear == 'function'; +}; + + +/** + * Returns true if the specified value is a string + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a string. + */ +goog.isString = function(val) { + return typeof val == 'string'; +}; + + +/** + * Returns true if the specified value is a boolean + * @param {*} val Variable to test. + * @return {boolean} Whether variable is boolean. + */ +goog.isBoolean = function(val) { + return typeof val == 'boolean'; +}; + + +/** + * Returns true if the specified value is a number + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a number. + */ +goog.isNumber = function(val) { + return typeof val == 'number'; +}; + + +/** + * Returns true if the specified value is a function + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a function. + */ +goog.isFunction = function(val) { + return goog.typeOf(val) == 'function'; +}; + + +/** + * Returns true if the specified value is an object. This includes arrays + * and functions. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an object. + */ +goog.isObject = function(val) { + var type = goog.typeOf(val); + return type == 'object' || type == 'array' || type == 'function'; +}; + + +/** + * Adds a hash code field to an object. The hash code is unique for the + * given object. + * @param {Object} obj The object to get the hash code for. + * @return {number} The hash code for the object. + */ +goog.getHashCode = function(obj) { + // In IE, DOM nodes do not extend Object so they do not have this method. + // we need to check hasOwnProperty because the proto might have this set. + + if (obj.hasOwnProperty && obj.hasOwnProperty(goog.HASH_CODE_PROPERTY_)) { + var hashCode = obj[goog.HASH_CODE_PROPERTY_]; + // CEEE changes: workaround for Chrome bug 1252508. + if (hashCode) { + return hashCode; + } + } + if (!obj[goog.HASH_CODE_PROPERTY_]) { + obj[goog.HASH_CODE_PROPERTY_] = ++goog.hashCodeCounter_; + } + return obj[goog.HASH_CODE_PROPERTY_]; +}; + + +/** + * Removes the hash code field from an object. + * @param {Object} obj The object to remove the field from. + */ +goog.removeHashCode = function(obj) { + // DOM nodes in IE are not instance of Object and throws exception + // for delete. Instead we try to use removeAttribute + if ('removeAttribute' in obj) { + obj.removeAttribute(goog.HASH_CODE_PROPERTY_); + } + /** @preserveTry */ + try { + delete obj[goog.HASH_CODE_PROPERTY_]; + } catch (ex) { + } +}; + + +/** + * {String} Name for hash code property + * @private + */ +goog.HASH_CODE_PROPERTY_ = 'closure_hashCode_'; + + +/** + * @type {number} Counter for hash codes. + * @private + */ +goog.hashCodeCounter_ = 0; + + +/** + * Clone an object/array (recursively) + * @param {Object} proto Object to clone. + * @return {Object} Clone of x;. + */ +goog.cloneObject = function(proto) { + var type = goog.typeOf(proto); + if (type == 'object' || type == 'array') { + if (proto.clone) { + return proto.clone.call(proto); + } + var clone = type == 'array' ? [] : {}; + for (var key in proto) { + clone[key] = goog.cloneObject(proto[key]); + } + return clone; + } + + return proto; +}; + + +/** + * Forward declaration for the clone method. This is necessary until the + * compiler can better support duck-typing constructs as used in + * goog.cloneObject. + * + * @type {Function} + */ +Object.prototype.clone; + + +/** + * Partially applies this function to a particular 'this object' and zero or + * more arguments. The result is a new function with some arguments of the first + * function pre-filled and the value of |this| 'pre-specified'.<br><br> + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones.<br><br> + * + * Also see: {@link #partial}.<br><br> + * + * Note that bind and partial are optimized such that repeated calls to it do + * not create more than one function object, so there is no additional cost for + * something like:<br> + * + * <pre>var g = bind(f, obj); + * var h = partial(g, 1, 2, 3); + * var k = partial(h, a, b, c);</pre> + * + * Usage: + * <pre>var barMethBound = bind(myFunction, myObj, 'arg1', 'arg2'); + * barMethBound('arg3', 'arg4');</pre> + * + * @param {Function} fn A function to partially apply. + * @param {Object} selfObj Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to the function. + * + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.bind = function(fn, selfObj, var_args) { + var boundArgs = fn.boundArgs_; + + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + if (boundArgs) { + args.unshift.apply(args, boundArgs); + } + boundArgs = args; + } + + selfObj = fn.boundSelf_ || selfObj; + fn = fn.boundFn_ || fn; + + var newfn; + var context = selfObj || goog.global; + + if (boundArgs) { + newfn = function() { + // Combine the static args and the new args into one big array + var args = Array.prototype.slice.call(arguments); + args.unshift.apply(args, boundArgs); + return fn.apply(context, args); + }; + } else { + newfn = function() { + return fn.apply(context, arguments); + }; + } + + newfn.boundArgs_ = boundArgs; + newfn.boundSelf_ = selfObj; + newfn.boundFn_ = fn; + + return newfn; +}; + + +/** + * Like bind(), except that a 'this object' is not required. Useful when the + * target function is already bound. + * + * Usage: + * var g = partial(f, arg1, arg2); + * g(arg3, arg4); + * + * @param {Function} fn A function to partially apply. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.partial = function(fn, var_args) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(fn, null); + return goog.bind.apply(null, args); +}; + + +/** + * Copies all the members of a source object to a target object. + * @param {Object} target Target. + * @param {Object} source Source. + * @deprecated Use goog.object.extend instead. + */ +goog.mixin = function(target, source) { + for (var x in source) { + target[x] = source[x]; + } + + // For IE the for-in-loop does not contain any properties that are not + // enumerable on the prototype object (for example, isPrototypeOf from + // Object.prototype) but also it will not include 'replace' on objects that + // extend String and change 'replace' (not that it is common for anyone to + // extend anything except Object). +}; + + +/** + * A simple wrapper for new Date().getTime(). + * + * @return {number} An integer value representing the number of milliseconds + * between midnight, January 1, 1970 and the current time. + */ +goog.now = Date.now || (function() { + return new Date().getTime(); +}); + + +/** + * Evals javascript in the global scope. In IE this uses execScript, other + * browsers use goog.global.eval. If goog.global.eval does not evaluate in the + * global scope (for example, in Safari), appends a script tag instead. + * Throws an exception if neither execScript or eval is defined. + * @param {string} script JavaScript string. + */ +goog.globalEval = function(script) { + if (goog.global.execScript) { + goog.global.execScript(script, 'JavaScript'); + } else if (goog.global.eval) { + // Test to see if eval works + if (goog.evalWorksForGlobals_ == null) { + goog.global.eval('var _et_ = 1;'); + if (typeof goog.global['_et_'] != 'undefined') { + delete goog.global['_et_']; + goog.evalWorksForGlobals_ = true; + } else { + goog.evalWorksForGlobals_ = false; + } + } + + if (goog.evalWorksForGlobals_) { + goog.global.eval(script); + } else { + var doc = goog.global.document; + var scriptElt = doc.createElement('script'); + scriptElt.type = 'text/javascript'; + scriptElt.defer = false; + // Note(pupius): can't use .innerHTML since "t('<test>')" will fail and + // .text doesn't work in Safari 2. Therefore we append a text node. + scriptElt.appendChild(doc.createTextNode(script)); + doc.body.appendChild(scriptElt); + doc.body.removeChild(scriptElt); + } + } else { + throw Error('goog.globalEval not available'); + } +}; + + +/** + * Forward declaration of a type name. + * + * A call of the form + * goog.declareType('goog.MyClass'); + * tells JSCompiler "goog.MyClass is not a hard dependency of this file. + * But it may appear in the type annotations here. This is to assure + * you that the class does indeed exist, even if it's not declared in the + * final binary." + * + * In uncompiled code, does nothing. + * @param {string} typeName The name of the type. + */ +goog.declareType = function(typeName) {}; + + +/** + * A macro for defining composite types. + * + * By assigning goog.typedef to a name, this tells JSCompiler that this is not + * the name of a class, but rather it's the name of a composite type. + * + * For example, + * /** @type {Array|NodeList} / goog.ArrayLike = goog.typedef; + * will tell JSCompiler to replace all appearances of goog.ArrayLike in type + * definitions with the union of Array and NodeList. + * + * Does nothing in uncompiled code. + */ +goog.typedef = true; + + +/** + * Handles strings that are intended to be used as CSS class names. + * + * Without JS Compiler the arguments are simple joined with a hyphen and passed + * through unaltered. + * + * With the JS Compiler the arguments are inlined, e.g: + * var x = goog.getCssName('foo'); + * var y = goog.getCssName(this.baseClass, 'active'); + * becomes: + * var x= 'foo'; + * var y = this.baseClass + '-active'; + * + * If a CSS renaming map is passed to the compiler it will replace symbols in + * the classname. If one argument is passed it will be processed, if two are + * passed only the modifier will be processed, as it is assumed the first + * argument was generated as a result of calling goog.getCssName. + * + * Names are split on 'hyphen' and processed in parts such that the following + * are equivalent: + * var base = goog.getCssName('baseclass'); + * goog.getCssName(base, 'modifier'); + * goog.getCSsName('baseclass-modifier'); + * + * If any part does not appear in the renaming map a warning is logged and the + * original, unobfuscated class name is inlined. + * + * @param {string} className The class name. + * @param {string} opt_modifier A modifier to be appended to the class name. + * @return {string} The class name or the concatenation of the class name and + * the modifier. + */ +goog.getCssName = function(className, opt_modifier) { + return className + (opt_modifier ? '-' + opt_modifier : ''); +}; + + +/** + * Abstract implementation of goog.getMsg for use with localized messages. + * @param {string} str Translatable string, places holders in the form {$foo}. + * @param {Object} opt_values Map of place holder name to value. + * @return {string} message with placeholders filled. + */ +goog.getMsg = function(str, opt_values) { + var values = opt_values || {}; + for (var key in values) { + str = str.replace(new RegExp('\\{\\$' + key + '\\}', 'gi'), values[key]); + } + return str; +}; + + +/** + * Exposes an unobfuscated global namespace path for the given object. + * Note that fields of the exported object *will* be obfuscated, + * unless they are exported in turn via this function or + * goog.exportProperty + * + * <p>Also handy for making public items that are defined in anonymous + * closures. + * + * ex. goog.exportSymbol('Foo', Foo); + * + * ex. goog.exportSymbol('public.path.Foo.staticFunction', + * Foo.staticFunction); + * public.path.Foo.staticFunction(); + * + * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod', + * Foo.prototype.myMethod); + * new public.path.Foo().myMethod(); + * + * @param {string} publicPath Unobfuscated name to export. + * @param {Object} object Object the name should point to. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + */ +goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) { + goog.exportPath_(publicPath, object, opt_objectToExportTo); +}; + + +/** + * Exports a property unobfuscated into the object's namespace. + * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction); + * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod); + * @param {Object} object Object whose static property is being exported. + * @param {string} publicName Unobfuscated name to export. + * @param {Object} symbol Object the name should point to. + */ +goog.exportProperty = function(object, publicName, symbol) { + object[publicName] = symbol; +}; + + +/** + * Inherit the prototype methods from one constructor into another. + * + * Usage: + * <pre> + * function ParentClass(a, b) { } + * ParentClass.prototype.foo = function(a) { } + * + * function ChildClass(a, b, c) { + * ParentClass.call(this, a, b); + * } + * + * goog.inherits(ChildClass, ParentClass); + * + * var child = new ChildClass('a', 'b', 'see'); + * child.foo(); // works + * </pre> + * + * In addition, a superclass' implementation of a method can be invoked + * as follows: + * + * <pre> + * ChildClass.prototype.foo = function(a) { + * ChildClass.superClass_.foo.call(this, a); + * // other code + * }; + * </pre> + * + * @param {Function} childCtor Child class. + * @param {Function} parentCtor Parent class. + */ +goog.inherits = function(childCtor, parentCtor) { + /** @constructor */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; + + +//============================================================================== +// Extending Function +//============================================================================== + + +/** + * @define {boolean} Whether to extend Function.prototype. + * Use --define='goog.MODIFY_FUNCTION_PROTOTYPES=false' to change. + */ +goog.MODIFY_FUNCTION_PROTOTYPES = true; + +if (goog.MODIFY_FUNCTION_PROTOTYPES) { + + + /** + * An alias to the {@link goog.bind()} global function. + * + * Usage: + * var g = f.bind(obj, arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} selfObj Specifies the object to which |this| should point + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the Function on which + * bind() was invoked as a method. + * @deprecated Use the static function goog.bind instead. + */ + Function.prototype.bind = function(selfObj, var_args) { + if (arguments.length > 1) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(this, selfObj); + return goog.bind.apply(null, args); + } else { + return goog.bind(this, selfObj); + } + }; + + + /** + * An alias to the {@link goog.partial()} static function. + * + * Usage: + * var g = f.partial(arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function partial() was + * invoked as a method of. + * @deprecated Use the static function goog.partial instead. + */ + Function.prototype.partial = function(var_args) { + var args = Array.prototype.slice.call(arguments); + args.unshift(this, null); + return goog.bind.apply(null, args); + }; + + + /** + * Inherit the prototype methods from one constructor into another. + * @param {Function} parentCtor Parent class. + * @see goog.inherits + * @deprecated Use the static function goog.inherits instead. + */ + Function.prototype.inherits = function(parentCtor) { + goog.inherits(this, parentCtor); + }; + + + /** + * Mixes in an object's properties and methods into the callee's prototype. + * Basically mixin based inheritance, thus providing an alternative method for + * adding properties and methods to a class' prototype. + * + * <pre> + * function X() {} + * X.mixin({ + * one: 1, + * two: 2, + * three: 3, + * doit: function() { return this.one + this.two + this.three; } + * }); + * + * function Y() { } + * Y.mixin(X.prototype); + * Y.prototype.four = 15; + * Y.prototype.doit2 = function() { return this.doit() + this.four; } + * }); + * + * // or + * + * function Y() { } + * Y.inherits(X); + * Y.mixin({ + * one: 10, + * four: 15, + * doit2: function() { return this.doit() + this.four; } + * }); + * </pre> + * + * @param {Object} source from which to copy properties. + * @see goog.mixin + * @deprecated Use the static function goog.object.extend instead. + */ + Function.prototype.mixin = function(source) { + goog.mixin(this.prototype, source); + }; +}
\ No newline at end of file diff --git a/ceee/ie/plugin/scripting/ceee_bootstrap.js b/ceee/ie/plugin/scripting/ceee_bootstrap.js new file mode 100644 index 0000000..28a0a7d --- /dev/null +++ b/ceee/ie/plugin/scripting/ceee_bootstrap.js @@ -0,0 +1,145 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview this file provides the bootstrap interface between the + * Chrome event and extension bindings JavaScript files, and the CEEE + * native IE interface, as well as initialization hooks for the native + * interface. + */ + + +// Console is diverted to nativeContentScriptApi.Log method. +var console = console || {}; + +// Any function declared native in the Chrome extension bindings files +// is diverted to the ceee namespace to allow the IE JS engine +// to grok the code. +var ceee = ceee || {}; + +(function () { +// Keep a reference to the global environment in +// effect during boostrap script parsing. +var global = this; + +var chromeHidden = {}; +var nativeContentScriptApi = null; + +// Supply a JSON implementation by leeching off closure. +global.JSON = goog.json; +global.JSON.stringify = JSON.serialize; + +ceee.AttachEvent = function (eventName) { + nativeContentScriptApi.AttachEvent(eventName); +}; + +ceee.DetachEvent = function (eventName) { + nativeContentScriptApi.DetachEvent(eventName); +}; + +ceee.OpenChannelToExtension = function (sourceId, targetId, name) { + return nativeContentScriptApi.OpenChannelToExtension(sourceId, + targetId, + name); +}; + +ceee.CloseChannel = function (portId) { + return nativeContentScriptApi.CloseChannel(portId); +}; + +ceee.PortAddRef = function (portId) { + return nativeContentScriptApi.PortAddRef(portId); +}; + +ceee.PortRelease = function (portId) { + return nativeContentScriptApi.PortRelease(portId); +}; + +ceee.PostMessage = function (portId, msg) { + return nativeContentScriptApi.PostMessage(portId, msg); +}; + +ceee.GetChromeHidden = function () { + return chromeHidden; +}; + +// This function is invoked from the native CEEE implementation by name +// to pass in the native CEEE interface implementation at the start of +// script engine initialization. This allows us to provide logging +// and any other required or convenient services during the initialization +// of other boostrap scripts. +ceee.startInit_ = function (nativenativeContentScriptApi, extensionId) { + nativeContentScriptApi = nativenativeContentScriptApi; +}; + +// Last uninitialization callback. +ceee.onUnload_ = function () { + // Dispatch the onUnload event. + chromeHidden.dispatchOnUnload(); + + // Release the native API as very last act. + nativeContentScriptApi = null; +}; + +// This function is invoked from the native CEEE implementation by name +// to pass in the extension ID, and to allow any final initialization of +// the script environment before the content scripts themselves are loaded. +ceee.endInit_ = function (nativenativeContentScriptApi, extensionId) { + chrome.initExtension(extensionId); + + // Provide the native implementation with the the the + // event notification dispatchers. + nativeContentScriptApi.onLoad = chromeHidden.dispatchOnLoad; + nativeContentScriptApi.onUnload = ceee.onUnload_; + + // And the port notification dispatchers. + // function(portId, channelName, tab, extensionId) + nativeContentScriptApi.onPortConnect = chromeHidden.Port.dispatchOnConnect; + + // function(portId) + nativeContentScriptApi.onPortDisconnect = + chromeHidden.Port.dispatchOnDisconnect; + // function(msg, portId) + nativeContentScriptApi.onPortMessage = + chromeHidden.Port.dispatchOnMessage; + + // TODO(siggi@chromium.org): If there is a different global + // environment at this point (i.e. we have cloned the scripting + // engine for a new window) this is where we can restore goog, + // JSON and chrome. + + // Delete the ceee namespace from globals. + delete ceee; +} + +console.log = console.log || function (msg) { + if (nativeContentScriptApi) + nativeContentScriptApi.Log("info", msg); +}; + +console.error = console.error || function (msg) { + if (nativeContentScriptApi) + nativeContentScriptApi.Log("error", msg); +} + +// Provide an indexOf member for arrays if it's not already there +// to satisfy the Chrome extension bindings expectations. +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(elt /*, from*/) { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) { + if (from in this && this[from] === elt) + return from; + } + return -1; + } +}; + +})(); diff --git a/ceee/ie/plugin/scripting/content_script_manager.cc b/ceee/ie/plugin/scripting/content_script_manager.cc new file mode 100644 index 0000000..0080159 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.cc @@ -0,0 +1,384 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager implementation. +#include "ceee/ie/plugin/scripting/content_script_manager.h" + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "base/logging.h" +#include "base/resource_util.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" + +#include "toolband.h" // NOLINT + +namespace { +// The list of bootstrap scripts we need to parse in a new scripting engine. +// We store our content scripts by name, in RT_HTML resources. This allows +// referring them by res: URLs, which makes debugging easier. +struct BootstrapScript { + const wchar_t* name; + // A named function to be called after the script is executed. + const wchar_t* function_name; + std::wstring url; + std::wstring content; +}; + +BootstrapScript bootstrap_scripts[] = { + { L"base.js", NULL }, + { L"json.js", NULL }, + { L"ceee_bootstrap.js", L"ceee.startInit_" }, + { L"event_bindings.js", NULL }, + { L"renderer_extension_bindings.js", L"ceee.endInit_" } +}; + +bool bootstrap_scripts_loaded = false; + +// Load the bootstrap javascript resources to our cache. +bool EnsureBoostrapScriptsLoaded() { + if (bootstrap_scripts_loaded) + return true; + + ceee_module_util::AutoLock lock; + if (bootstrap_scripts_loaded) + return true; + + HMODULE module = _AtlBaseModule.GetResourceInstance(); + + // And construct the base URL. + std::wstring base_url(L"ceee-content://bootstrap/"); + + // Retrieve the resources one by one and convert them to Unicode. + for (int i = 0; i < arraysize(bootstrap_scripts); ++i) { + const wchar_t* name = bootstrap_scripts[i].name; + HRSRC hres_info = ::FindResource(module, name, MAKEINTRESOURCE(RT_HTML)); + if (hres_info == NULL) + return false; + + DWORD data_size = ::SizeofResource(module, hres_info); + HGLOBAL hres = ::LoadResource(module, hres_info); + if (!hres) + return false; + + void* resource = ::LockResource(hres); + if (!resource) + return false; + bool converted = UTF8ToWide(reinterpret_cast<const char*>(resource), + data_size, + &bootstrap_scripts[i].content); + if (!converted) + return false; + + bootstrap_scripts[i].url = StringPrintf(L"%ls%ls", base_url.c_str(), name); + } + + bootstrap_scripts_loaded = true; + return true; +} + +HRESULT InvokeNamedFunction(IScriptHost* script_host, + const wchar_t* function_name, + VARIANT* args, + size_t num_args) { + // Get the named function. + CComVariant function_var; + HRESULT hr = script_host->RunExpression(function_name, &function_var); + if (FAILED(hr)) + return hr; + + // And invoke it with the the params. + if (V_VT(&function_var) != VT_DISPATCH) + return E_UNEXPECTED; + + // Take over the IDispatch pointer. + CComDispatchDriver function_disp; + function_disp.Attach(V_DISPATCH(&function_var)); + V_VT(&function_var) = VT_EMPTY; + V_DISPATCH(&function_var) = NULL; + + return function_disp.InvokeN(static_cast<DISPID>(DISPID_VALUE), + args, + num_args); +} + +} // namespace + +ContentScriptManager::ContentScriptManager() : require_all_frames_(false) { +} + +ContentScriptManager::~ContentScriptManager() { + // TODO(siggi@chromium.org): This mandates teardown prior to + // deletion, is that necessary? + DCHECK(script_host_ == NULL); +} + +HRESULT ContentScriptManager::GetOrCreateScriptHost( + IHTMLDocument2* document, IScriptHost** host) { + if (script_host_ == NULL) { + HRESULT hr = CreateScriptHost(&script_host_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create script host " << com::LogHr(hr); + return hr; + } + + hr = InitializeScriptHost(document, script_host_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to initialize script host " << com::LogHr(hr); + script_host_.Release(); + return hr; + } + + CComQIPtr<IObjectWithSite> script_host_with_site(script_host_); + // Our implementation of script host must always implement IObjectWithSite. + DCHECK(script_host_with_site != NULL); + hr = script_host_with_site->SetSite(document); + DCHECK(SUCCEEDED(hr)); + } + + DCHECK(script_host_ != NULL); + + return script_host_.CopyTo(host); +} + +HRESULT ContentScriptManager::CreateScriptHost(IScriptHost** script_host) { + return ScriptHost::CreateInitializedIID(IID_IScriptHost, script_host); +} + +HRESULT ContentScriptManager::InitializeScriptHost( + IHTMLDocument2* document, IScriptHost* script_host) { + DCHECK(document != NULL); + DCHECK(script_host != NULL); + + CComPtr<IExtensionPortMessagingProvider> messaging_provider; + HRESULT hr = host_->GetExtensionPortMessagingProvider(&messaging_provider); + hr = ContentScriptNativeApi::CreateInitialized(messaging_provider, + &native_api_); + if (FAILED(hr)) + return hr; + + std::wstring extension_id; + host_->GetExtensionId(&extension_id); + DCHECK(extension_id.size()) << + "Need to revisit async loading of enabled extension list."; + + // Execute the bootstrap scripts. + hr = BootstrapScriptHost(script_host, native_api_, extension_id.c_str()); + + CComPtr<IHTMLWindow2> window; + hr = document->get_parentWindow(&window); + if (FAILED(hr)) + return hr; + + // Register the window object and make its members global. + hr = script_host->RegisterScriptObject(L"window", window, true); + + return hr; +} + +HRESULT ContentScriptManager::BootstrapScriptHost(IScriptHost* script_host, + IDispatch* native_api, + const wchar_t* extension_id) { + bool loaded = EnsureBoostrapScriptsLoaded(); + if (!loaded) { + NOTREACHED() << "Unable to load bootstrap scripts"; + return E_UNEXPECTED; + } + + // Note args go in reverse order. + CComVariant args[] = { + extension_id, + native_api + }; + + // Run the bootstrap scripts. + for (int i = 0; i < arraysize(bootstrap_scripts); ++i) { + const wchar_t* url = bootstrap_scripts[i].url.c_str(); + HRESULT hr = script_host->RunScript(url, + bootstrap_scripts[i].content.c_str()); + if (FAILED(hr)) { + NOTREACHED() << "Bootstrap script \"" << url << "\" failed to load"; + return hr; + } + + // Execute the script's named function if it exists. + const wchar_t* function_name = bootstrap_scripts[i].function_name; + if (function_name) { + hr = InvokeNamedFunction(script_host, function_name, args, + arraysize(args)); + if (FAILED(hr)) { + NOTREACHED() << "Named function \"" << function_name << "\" not called"; + return hr; + } + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::LoadCss(const GURL& match_url, + IHTMLDocument2* document) { + // Get the CSS content for all matching user scripts and inject it. + std::string css_content; + HRESULT hr = host_->GetMatchingUserScriptsCssContent(match_url, + require_all_frames_, + &css_content); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get script content " << com::LogHr(hr); + return hr; + } + + if (!css_content.empty()) + return InsertCss(CA2W(css_content.c_str()), document); + + return S_OK; +} + +HRESULT ContentScriptManager::LoadStartScripts(const GURL& match_url, + IHTMLDocument2* document) { + // Run the document end scripts. + return LoadScriptsImpl(match_url, document, UserScript::DOCUMENT_START); +} + +HRESULT ContentScriptManager::LoadEndScripts(const GURL& match_url, + IHTMLDocument2* document) { + // Run the document end scripts. + return LoadScriptsImpl(match_url, document, UserScript::DOCUMENT_END); +} + +HRESULT ContentScriptManager::LoadScriptsImpl(const GURL& match_url, + IHTMLDocument2* document, + UserScript::RunLocation when) { + // Run the document start scripts. + UserScriptsLibrarian::JsFileList js_file_list; + HRESULT hr = host_->GetMatchingUserScriptsJsContent(match_url, + when, + require_all_frames_, + &js_file_list); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get script content " << com::LogHr(hr); + return hr; + } + + // Early out to avoid initializing scripting if we don't need it. + if (js_file_list.size() == 0) + return S_OK; + + for (size_t i = 0; i < js_file_list.size(); ++i) { + hr = ExecuteScript(CA2W(js_file_list[i].content.c_str()), + js_file_list[i].file_path.c_str(), + document); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to inject JS content into page " << com::LogHr(hr); + return hr; + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::ExecuteScript(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document) { + CComPtr<IScriptHost> script_host; + HRESULT hr = GetOrCreateScriptHost(document, &script_host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to retrieve script host " << com::LogHr(hr); + return hr; + } + + hr = script_host->RunScript(file_path, code); + if (FAILED(hr)) { + if (hr == OLESCRIPT_E_SYNTAX) { + // This function is used to execute scripts from extensions. We log + // syntax and runtime errors but we don't return a failing HR as we are + // executing third party code. A syntax or runtime error already causes + // the script host to prompt the user to debug. + LOG(ERROR) << "A syntax or runtime error occured while executing " << + "script " << com::LogHr(hr); + } else { + LOG(ERROR) << "Failed to execute script " << com::LogHr(hr); + return hr; + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::InsertCss(const wchar_t* code, + IHTMLDocument2* document) { + CComPtr<IHTMLDOMNode> head_node; + HRESULT hr = GetHeadNode(document, &head_node); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to retrieve document head node " << com::LogHr(hr); + return hr; + } + + hr = InjectStyleTag(document, head_node, code); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to inject CSS content into page " + << com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT ContentScriptManager::GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** dom_head) { + return DomUtils::GetHeadNode(document, dom_head); +} + +HRESULT ContentScriptManager::InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code) { + return DomUtils::InjectStyleTag(document, head_node, code); +} + +HRESULT ContentScriptManager::Initialize(IFrameEventHandlerHost* host, + bool require_all_frames) { + DCHECK(host != NULL); + DCHECK(host_ == NULL); + host_ = host; + + require_all_frames_ = require_all_frames; + + return S_OK; +} + +HRESULT ContentScriptManager::TearDown() { + if (native_api_ != NULL) { + CComPtr<ICeeeContentScriptNativeApi> native_api; + native_api_.QueryInterface(&native_api); + if (native_api != NULL) { + ContentScriptNativeApi* implementation = + static_cast<ContentScriptNativeApi*>(native_api.p); + // Teardown will release references from ContentScriptNativeApi to + // objects blocking release of BHO. Somehow ContentScriptNativeApi is + // alive after IScriptHost::Close(). + implementation->TearDown(); + } + native_api_.Release(); + } + HRESULT hr = S_OK; + if (script_host_ != NULL) { + hr = script_host_->Close(); + LOG_IF(ERROR, FAILED(hr)) << "ScriptHost::Close failed " << com::LogHr(hr); + + CComQIPtr<IObjectWithSite> script_host_with_site(script_host_); + DCHECK(script_host_with_site != NULL); + hr = script_host_with_site->SetSite(NULL); + DCHECK(SUCCEEDED(hr)); + } + + // TODO(siggi@chromium.org): Kill off open extension ports. + + script_host_.Release(); + + return hr; +} diff --git a/ceee/ie/plugin/scripting/content_script_manager.h b/ceee/ie/plugin/scripting/content_script_manager.h new file mode 100644 index 0000000..f5b64cb --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.h @@ -0,0 +1,112 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager declaration. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ + +#include <mshtml.h> +#include <string> +#include "base/basictypes.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + +// Forward declaration. +class IFrameEventHandlerHost; + +// The content script manager implements CSS and content script injection +// for an extension in a frame. +// It also manages the ScriptHost, its bootstrapping and injecting the +// native API instance to the bootstrap JavaScript code. +class ContentScriptManager { + public: + ContentScriptManager(); + virtual ~ContentScriptManager(); + + // Initialize before first use. + // @param host the frame event handler host we delegate requests to. + // @param require_all_frames Whether to require the all_frames property of the + // matched user scripts to be true. + HRESULT Initialize(IFrameEventHandlerHost* host, bool require_all_frames); + + // Inject CSS for @p match_url into @p document + // Needs to be invoked on READYSTATE_LOADED transition for @p document. + virtual HRESULT LoadCss(const GURL& match_url, IHTMLDocument2* document); + + // Inject start scripts for @p match_url into @p document + // Needs to be invoked on READYSTATE_LOADED transition for @p document. + virtual HRESULT LoadStartScripts(const GURL& match_url, + IHTMLDocument2* document); + + // Inject end scripts for @p match_url into @p document. + // Needs to be invoked on READYSTATE_COMPLETE transition for @p document. + virtual HRESULT LoadEndScripts(const GURL& match_url, + IHTMLDocument2* document); + + // Run the given script code in the document. The @p file_path argument is + // more debugging information, providing context for where the code came + // from. If the given script code has a syntax or runtime error, this + // function will still return S_OK since it is used to run third party code. + virtual HRESULT ExecuteScript(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document); + + // Inject the given CSS code into the document. + virtual HRESULT InsertCss(const wchar_t* code, IHTMLDocument2* document); + + // Release any resources we've acquired or created. + virtual HRESULT TearDown(); + + protected: + // Implementation of script loading Load{Start|End}Scripts. + virtual HRESULT LoadScriptsImpl(const GURL& match_url, + IHTMLDocument2* document, + UserScript::RunLocation when); + + // Retrieves the script host, creating it if does not already exist. + virtual HRESULT GetOrCreateScriptHost(IHTMLDocument2* document, + IScriptHost** host); + + // Create a script host, virtual to make a unittest seam. + virtual HRESULT CreateScriptHost(IScriptHost** host); + + // Load and initialize bootstrap scripts into host. + virtual HRESULT BootstrapScriptHost(IScriptHost* host, + IDispatch* api, + const wchar_t* extension_id); + + // Initializes a newly-created script host. + virtual HRESULT InitializeScriptHost(IHTMLDocument2* document, + IScriptHost* host); + + // Testing seam. + virtual HRESULT GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** head_node); + virtual HRESULT InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code); + + // The script engine that hosts the content scripts. + CComPtr<IScriptHost> script_host_; + + // TODO(siggi@chromium.org): Stash the PageApi instance here for teardown. + + // This is where we get scripts and CSS to inject. + CComPtr<IFrameEventHandlerHost> host_; + + // Whether to require all frames to be true when matching users scripts. + bool require_all_frames_; + + private: + // API accessible from javascript. This reference is required to release + // some resources from ContentScriptManager::Teardown. + // Must be instance of ContentScriptNativeApi. + CComPtr<IDispatch> native_api_; + DISALLOW_COPY_AND_ASSIGN(ContentScriptManager); +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ diff --git a/ceee/ie/plugin/scripting/content_script_manager.rc b/ceee/ie/plugin/scripting/content_script_manager.rc new file mode 100644 index 0000000..7551e5ef --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.rc @@ -0,0 +1,19 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager resources. +#include <winres.h> + +#ifdef APSTUDIO_INVOKED +#error Please edit as text. +#endif + +// These are included as HTML resources to allow referring them +// by res: URLs, which helps debugging. +base.js HTML "base.js" +json.js HTML "json.js" +ceee_bootstrap.js HTML "ceee_bootstrap.js" +event_bindings.js HTML "event_bindings.js" +renderer_extension_bindings.js HTML "renderer_extension_bindings.js" diff --git a/ceee/ie/plugin/scripting/content_script_manager_unittest.cc b/ceee/ie/plugin/scripting/content_script_manager_unittest.cc new file mode 100644 index 0000000..0384875 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager_unittest.cc @@ -0,0 +1,489 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Content script manager implementation unit tests. +#include "ceee/ie/plugin/scripting/content_script_manager.h" + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mshtml_mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::CopyVariantToArgument; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +typedef UserScriptsLibrarian::JsFileList JsFileList; +typedef UserScriptsLibrarian::JsFile JsFile; + +using testing::InstanceCountMixin; +using testing::IActiveScriptSiteMockImpl; +using testing::IActiveScriptSiteDebugMockImpl; + +// An arbitrary valid extension ID. +const wchar_t kExtensionId[] = L"fepbkochiplomghbdfgekenppangbiap"; + +class TestingContentScriptManager: public ContentScriptManager { + public: + // Expose public for testing. + using ContentScriptManager::LoadScriptsImpl; + using ContentScriptManager::GetOrCreateScriptHost; + using ContentScriptManager::InitializeScriptHost; + + MOCK_METHOD1(CreateScriptHost, HRESULT(IScriptHost** host)); + MOCK_METHOD2(GetHeadNode, + HRESULT(IHTMLDocument* document, IHTMLDOMNode** dom_head)); + MOCK_METHOD3(InjectStyleTag, + HRESULT(IHTMLDocument2* document, IHTMLDOMNode* dom_head, + const wchar_t* code)); + MOCK_METHOD2(InsertCss, + HRESULT(const wchar_t* code, IHTMLDocument2* document)); +}; + + +class IScriptHostMockImpl: public IScriptHost { + public: + MOCK_METHOD3(RegisterScriptObject, + HRESULT(const wchar_t* name, IDispatch* disp_obj, bool global)); + MOCK_METHOD2(RunScript, + HRESULT(const wchar_t* file_path, const wchar_t* code)); + MOCK_METHOD3(RunScript, HRESULT(const wchar_t* file_path, + size_t char_offset, + const wchar_t* code)); + MOCK_METHOD2(RunExpression, + HRESULT(const wchar_t* code, VARIANT* result)); + MOCK_METHOD0(Close, HRESULT()); +}; + +class MockDomNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDomNode>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockDomNode) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize() { + return S_OK; + } +}; + +class MockScriptHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockScriptHost>, + public InstanceCountMixin<MockScriptHost>, + public StrictMock<IActiveScriptSiteMockImpl>, + public StrictMock<IActiveScriptSiteDebugMockImpl>, + public IObjectWithSiteImpl<MockScriptHost>, + public StrictMock<IScriptHostMockImpl> { + public: + BEGIN_COM_MAP(MockScriptHost) + COM_INTERFACE_ENTRY(IActiveScriptSite) + COM_INTERFACE_ENTRY(IActiveScriptSiteDebug) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IScriptHost, IScriptHost) + END_COM_MAP() + + HRESULT Initialize(MockScriptHost** script_host) { + *script_host = this; + return S_OK; + } +}; + +class MockWindow + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockWindow>, + public InstanceCountMixin<MockWindow>, + public StrictMock<IHTMLWindow2MockImpl> { + public: + BEGIN_COM_MAP(MockWindow) + COM_INTERFACE_ENTRY(IHTMLWindow2) + END_COM_MAP() + + HRESULT Initialize(MockWindow** self) { + *self = this; + return S_OK; + } +}; + +class MockDispatch + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDispatch>, + public InstanceCountMixin<MockDispatch>, + public StrictMock<testing::IDispatchExMockImpl> { + public: + BEGIN_COM_MAP(MockDispatch) + COM_INTERFACE_ENTRY(IDispatch) + END_COM_MAP() + + HRESULT Initialize(MockDispatch** self) { + *self = this; + return S_OK; + } +}; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public InstanceCountMixin<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + END_COM_MAP() + + HRESULT Initialize(MockDocument** self) { + *self = this; + return S_OK; + } +}; + +class MockFrameEventHandlerAsApiHost + : public testing::MockFrameEventHandlerHostBase< + MockFrameEventHandlerAsApiHost> { + public: + HRESULT Initialize(MockFrameEventHandlerAsApiHost** self) { + *self = this; + return S_OK; + } + + // Always supply ourselves as native API host. + HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) { + GetUnknown()->AddRef(); + *messaging_provider = this; + return S_OK; + } +}; + +class ContentScriptManagerTest: public testing::Test { + public: + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockFrameEventHandlerAsApiHost::CreateInitializedIID( + &frame_host_, IID_IFrameEventHandlerHost, &frame_host_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockScriptHost::CreateInitializedIID( + &script_host_, IID_IScriptHost, &script_host_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockWindow::CreateInitialized(&window_, &window_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockDispatch::CreateInitialized(&function_, &function_keeper_)); + + // Set the document up to return the content window if queried. + EXPECT_CALL(*document_, get_parentWindow(_)) + .WillRepeatedly( + DoAll( + CopyInterfaceToArgument<0>(window_keeper_), + Return(S_OK))); + } + + void TearDown() { + document_ = NULL; + document_keeper_.Release(); + + window_ = NULL; + window_keeper_.Release(); + + script_host_ = NULL; + script_host_keeper_.Release(); + + frame_host_ = NULL; + frame_host_keeper_.Release(); + + function_ = NULL; + function_keeper_.Release(); + + // Test for leakage. + EXPECT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + // Set up to expect scripting initialization. + void ExpectScriptInitialization() { + EXPECT_CALL(*script_host_, + RegisterScriptObject(StrEq(L"window"), _, true)) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*script_host_, + RunScript(testing::StartsWith(L"ceee-content://"), _)) + .Times(5) + .WillRepeatedly(Return(S_OK)); + + // Return the mock function for start/end init. + EXPECT_CALL(*script_host_, RunExpression(StrEq(L"ceee.startInit_"), _)) + .WillOnce( + DoAll( + CopyVariantToArgument<1>(CComVariant(function_keeper_)), + Return(S_OK))); + EXPECT_CALL(*script_host_, RunExpression(StrEq(L"ceee.endInit_"), _)) + .WillOnce( + DoAll( + CopyVariantToArgument<1>(CComVariant(function_keeper_)), + Return(S_OK))); + + EXPECT_CALL(*frame_host_, GetExtensionId(_)).WillOnce(DoAll( + SetArgumentPointee<0>(std::wstring(kExtensionId)), + Return(S_OK))); + + // And expect two invocations. + // TODO(siggi@chromium.org): be more specific? + EXPECT_CALL(*function_, Invoke(_, _, _, _, _, _, _, _)) + .Times(2) + .WillRepeatedly(Return(S_OK)); + } + + void ExpectCreateScriptHost(TestingContentScriptManager* manager) { + EXPECT_CALL(*manager, CreateScriptHost(_)) + .WillOnce( + DoAll( + CopyInterfaceToArgument<0>(script_host_keeper_), + Return(S_OK))); + } + + // Set up to expect NO scripting initialization. + void ExpectNoScriptInitialization() { + EXPECT_CALL(*script_host_, RegisterScriptObject(_, _, _)) + .Times(0); + } + + // Set up to expect a query CSS content. + void ExpectCSSQuery(const GURL& url) { + // Expect CSS query. + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(url, false, _)). + WillOnce(Return(S_OK)); + } + + void SetScriptQueryResults(const GURL& url, + UserScript::RunLocation location, + const JsFileList& js_file_list) { + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsJsContent(url, location, false, _)) + .WillOnce( + DoAll( + SetArgumentPointee<3>(js_file_list), + Return(S_OK))); + } + + protected: + TestingContentScriptManager manager; + + MockFrameEventHandlerAsApiHost* frame_host_; + CComPtr<IFrameEventHandlerHost> frame_host_keeper_; + + MockScriptHost* script_host_; + CComPtr<IScriptHost> script_host_keeper_; + + MockWindow* window_; + CComPtr<IHTMLWindow2> window_keeper_; + + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + // Standin for JS functions. + MockDispatch *function_; + CComPtr<IDispatch> function_keeper_; +}; + +TEST_F(ContentScriptManagerTest, InitializationAndTearDownSucceed) { + ContentScriptManager manager; + + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + manager.TearDown(); +} + +TEST_F(ContentScriptManagerTest, InitializeScripting) { + TestingContentScriptManager manager; + + EXPECT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + ExpectScriptInitialization(); + ASSERT_HRESULT_SUCCEEDED( + manager.InitializeScriptHost(document_, script_host_)); +} + +const GURL kTestUrl( + L"http://www.google.com/search?q=Google+Buys+Iceland"); + +// Verify that we don't initialize scripting when there's nothing to inject. +TEST_F(ContentScriptManagerTest, NoScriptInitializationOnEmptyScripts) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + // No script host creation. + EXPECT_CALL(manager, CreateScriptHost(_)).Times(0); + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_START, JsFileList()); + ASSERT_HRESULT_SUCCEEDED( + manager.LoadStartScripts(kTestUrl, document_)); + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_END, JsFileList()); + ASSERT_HRESULT_SUCCEEDED( + manager.LoadEndScripts(kTestUrl, document_)); + + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +const wchar_t kJsFilePath1[] = L"foo.js"; +const char kJsFileContent1[] = "window.alert('XSS!!');"; +const wchar_t kJsFilePath2[] = L"bar.js"; +const char kJsFileContent2[] = + "window.alert = function () { console.log('gotcha'); }"; + +// Verify that we initialize scripting and inject when there's a URL match. +TEST_F(ContentScriptManagerTest, ScriptInitializationOnUrlMatch) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + JsFileList list; + list.push_back(JsFile()); + JsFile& file1 = list.back(); + file1.file_path = kJsFilePath1; + file1.content = kJsFileContent1; + list.push_back(JsFile()); + JsFile& file2 = list.back(); + file2.file_path = kJsFilePath2; + file2.content = kJsFileContent2; + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_START, list); + + ExpectScriptInitialization(); + ExpectCreateScriptHost(&manager); + const std::wstring content1(UTF8ToWide(kJsFileContent1)); + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath1), StrEq(content1.c_str()))) + .WillOnce(Return(S_OK)); + + const std::wstring content2(UTF8ToWide(kJsFileContent2)); + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath2), StrEq(content2.c_str()))) + .WillOnce(Return(S_OK)); + + // This should initialize scripting and evaluate our script. + ASSERT_HRESULT_SUCCEEDED( + manager.LoadStartScripts(kTestUrl, document_)); + + // Drop the second script. + list.pop_back(); + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_END, list); + + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath1), StrEq(content1.c_str()))) + .WillOnce(Return(S_OK)); + + // This should only evaluate the script. + ASSERT_HRESULT_SUCCEEDED( + manager.LoadEndScripts(kTestUrl, document_)); + + // The script host needs to be shut down on teardown. + EXPECT_CALL(*script_host_, Close()).Times(1); + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +const wchar_t kCssContent[] = L".foo {};"; +// Verify that we inject CSS into the document. +TEST_F(ContentScriptManagerTest, CssInjectionOnUrlMatch) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(kTestUrl, false, _)) + .WillOnce(Return(S_OK)); + + // This should not cause any CSS injection. + ASSERT_HRESULT_SUCCEEDED(manager.LoadCss(kTestUrl, document_)); + + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(kTestUrl, false, _)) + .WillOnce( + DoAll( + SetArgumentPointee<2>(std::string(CW2A(kCssContent))), + Return(S_OK))); + + EXPECT_CALL(manager, + InsertCss(StrEq(kCssContent), document_)) + .WillOnce(Return(S_OK)); + + // We now expect to see the document injected. + ASSERT_HRESULT_SUCCEEDED(manager.LoadCss(kTestUrl, document_)); +} + +const wchar_t kTestCode[] = L"function foo {};"; +const wchar_t kTestFilePath[] = L"TestFilePath"; +TEST_F(ContentScriptManagerTest, ExecuteScript) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_SUCCEEDED(MockDomNode::CreateInitialized(&head_node)); + + ExpectCreateScriptHost(&manager); + ExpectScriptInitialization(); + + EXPECT_CALL(*script_host_, + RunScript(kTestFilePath, kTestCode)) + .WillOnce(Return(S_OK)); + + // We now expect to see the document injected. + ASSERT_HRESULT_SUCCEEDED(manager.ExecuteScript(kTestCode, + kTestFilePath, + document_)); + + // The script host needs to be shut down on teardown. + EXPECT_CALL(*script_host_, Close()).Times(1); + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +TEST_F(ContentScriptManagerTest, InsertCss) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_SUCCEEDED(MockDomNode::CreateInitialized(&head_node)); + + EXPECT_CALL(manager, GetHeadNode(document_, _)) + .WillOnce( + DoAll( + CopyInterfaceToArgument<1>(head_node), + Return(S_OK))); + + EXPECT_CALL(manager, + InjectStyleTag(document_, head_node.p, StrEq(kCssContent))) + .WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + manager.ContentScriptManager::InsertCss(kCssContent, document_)); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/content_script_native_api.cc b/ceee/ie/plugin/scripting/content_script_native_api.cc new file mode 100644 index 0000000..a74d077 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api.cc @@ -0,0 +1,276 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API implementation. +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" + +namespace { + +// DISPID_VALUE is a macro defined to 0, which confuses overloaded Invoke-en. +DISPID kDispidValue = DISPID_VALUE; + +} // namespace + +ContentScriptNativeApi::ContentScriptNativeApi() + : next_local_port_id_(kFirstPortId) { +} + +ContentScriptNativeApi::LocalPort::LocalPort(LocalPortId id) + : state(PORT_UNINITIALIZED), local_id(id), remote_id(kInvalidPortId) { +} + +HRESULT ContentScriptNativeApi::Initialize( + IExtensionPortMessagingProvider *messaging_provider) { + DCHECK(messaging_provider != NULL); + messaging_provider_ = messaging_provider; + return S_OK; +} + +void ContentScriptNativeApi::FinalRelease() { + DCHECK(on_load_ == NULL); + DCHECK(on_unload_ == NULL); + DCHECK(on_port_connect_ == NULL); + DCHECK(on_port_disconnect_ == NULL); + DCHECK(on_port_message_ == NULL); +} + +HRESULT ContentScriptNativeApi::TearDown() { + on_load_.Release(); + on_unload_.Release(); + on_port_connect_.Release(); + on_port_disconnect_.Release(); + on_port_message_.Release(); + messaging_provider_.Release(); + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::Log(BSTR level, BSTR message) { + const wchar_t* str_level = com::ToString(level); + size_t len = ::SysStringLen(level); + if (LowerCaseEqualsASCII(str_level, str_level + len, "info")) { + LOG(INFO) << com::ToString(message); + } else if (LowerCaseEqualsASCII(str_level, str_level + len, "error")) { + LOG(ERROR) << com::ToString(message); + } else { + LOG(WARNING) << com::ToString(message); + } + + return S_OK; +} + + +STDMETHODIMP ContentScriptNativeApi::OpenChannelToExtension(BSTR source_id, + BSTR target_id, + BSTR name, + long* port_id) { + // TODO(siggi@chromium.org): handle connecting to other extensions. + // TODO(siggi@chromium.org): check for the correct source_id here. + if (0 != wcscmp(com::ToString(source_id), com::ToString(target_id))) + return E_UNEXPECTED; + + LocalPortId id = GetNextLocalPortId(); + std::pair<LocalPortMap::iterator, bool> inserted = + local_ports_.insert(std::make_pair(id, LocalPort(id))); + DCHECK(inserted.second && inserted.first != local_ports_.end()); + // Get the port we just inserted. + LocalPort& port = inserted.first->second; + DCHECK_EQ(id, port.local_id); + + std::string extension_id; + bool converted = WideToUTF8(com::ToString(source_id), + ::SysStringLen(source_id), + &extension_id); + DCHECK(converted); + std::string channel_name; + converted = WideToUTF8(com::ToString(name), + ::SysStringLen(name), + &channel_name); + DCHECK(converted); + + // Send off the connection request with our local port ID as cookie. + HRESULT hr = messaging_provider_->OpenChannelToExtension(this, + extension_id, + channel_name, + port.local_id); + DCHECK(SUCCEEDED(hr)); + + port.state = PORT_CONNECTING; + + // TODO(siggi@chromium.org): Clean up on failure. + + *port_id = id; + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::CloseChannel(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "CloseChannel(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PortAddRef(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "PortAddRef(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PortRelease(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "PortRelease(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PostMessage(long port_id, BSTR msg) { + LocalPortMap::iterator it(local_ports_.find(port_id)); + // TODO(siggi@chromium.org): should I expect to get messages to + // defunct port ids? + DCHECK(it != local_ports_.end()); + if (it == local_ports_.end()) + return E_UNEXPECTED; + LocalPort& port = it->second; + + std::string msg_str(WideToUTF8(com::ToString(msg))); + if (port.state == PORT_CONNECTED) { + messaging_provider_->PostMessage(port.remote_id, msg_str); + } else if (port.state == PORT_CONNECTING) { + port.pending_messages.push_back(msg_str); + } else { + LOG(ERROR) << "Unexpected PostMessage for port in state " << port.state; + } + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::AttachEvent(BSTR event_name) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "AttachEvent(" << com::ToString(event_name) << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::DetachEvent(BSTR event_name) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "DetachEvent(" << com::ToString(event_name) << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onLoad(IDispatch* callback) { + on_load_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onUnload(IDispatch* callback) { + on_unload_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortConnect(IDispatch* callback) { + on_port_connect_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortDisconnect(IDispatch* callback) { + on_port_disconnect_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortMessage(IDispatch* callback) { + on_port_message_ = callback; + return S_OK; +} + +void ContentScriptNativeApi::OnChannelOpened(int cookie, + int port_id) { + // The cookie is our local port ID. + LocalPortId local_id = cookie; + LocalPortMap::iterator it(local_ports_.find(local_id)); + DCHECK(it != local_ports_.end()); + + LocalPort& port = it->second; + DCHECK_EQ(local_id, port.local_id); + port.remote_id = port_id; + port.state = PORT_CONNECTED; + + // Remember the mapping so that we can find this port by remote_id. + remote_to_local_port_id_[port.remote_id] = port.local_id; + + // Flush pending messages on this port. + if (port.pending_messages.size() > 0) { + MessageList::iterator it(port.pending_messages.begin()); + MessageList::iterator end(port.pending_messages.end()); + + for (; it != end; ++it) { + messaging_provider_->PostMessage(port.remote_id, *it); + } + } +} + +void ContentScriptNativeApi::OnPostMessage(int port_id, + const std::string& message) { + // Translate the remote port id to a local port id. + RemoteToLocalPortIdMap::iterator it(remote_to_local_port_id_.find(port_id)); + DCHECK(it != remote_to_local_port_id_.end()); + + LocalPortId local_id = it->second; + + // And push the message to the script. + std::wstring message_wide(UTF8ToWide(message)); + CallOnPortMessage(message_wide.c_str(), local_id); +} + +HRESULT ContentScriptNativeApi::CallOnLoad(const wchar_t* extension_id) { + if (on_load_ == NULL) + return E_UNEXPECTED; + + return on_load_.Invoke1(kDispidValue, &CComVariant(extension_id)); +} + +HRESULT ContentScriptNativeApi::CallOnUnload() { + if (on_unload_ == NULL) + return E_UNEXPECTED; + + return on_unload_.Invoke0(kDispidValue); +} + +HRESULT ContentScriptNativeApi::CallOnPortConnect( + long port_id, const wchar_t* channel_name, const wchar_t* tab, + const wchar_t* source_extension_id, const wchar_t* target_extension_id) { + if (on_port_connect_ == NULL) + return E_UNEXPECTED; + + // Note args go in reverse order of declaration for Invoke. + CComVariant args[] = { + target_extension_id, + source_extension_id, + tab, + channel_name, + port_id}; + + return on_port_connect_.InvokeN(kDispidValue, args, arraysize(args)); +} + +HRESULT ContentScriptNativeApi::CallOnPortDisconnect(long port_id) { + if (on_port_disconnect_ == NULL) + return E_UNEXPECTED; + + return on_port_disconnect_.Invoke1(kDispidValue, &CComVariant(port_id)); +} + +HRESULT ContentScriptNativeApi::CallOnPortMessage(const wchar_t* msg, + long port_id) { + if (on_port_message_ == NULL) + return E_UNEXPECTED; + + // Note args go in reverse order of declaration for Invoke. + CComVariant args[] = { port_id, msg }; + + return on_port_message_.InvokeN(kDispidValue, args, arraysize(args)); +} diff --git a/ceee/ie/plugin/scripting/content_script_native_api.h b/ceee/ie/plugin/scripting/content_script_native_api.h new file mode 100644 index 0000000..5c97184 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api.h @@ -0,0 +1,183 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API class declaration. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ + +#include <map> +#include <string> +#include <vector> + +#include "ceee/common/initializing_coclass.h" +#include "toolband.h" // NOLINT + +// Fwd. +class IContentScriptNativeApi; + +class IExtensionPortMessagingProvider: public IUnknown { + public: + // Close all ports opening and opened for this instance. + virtual void CloseAll(IContentScriptNativeApi* instance) = 0; + + // Initiates opening a channel to an extension. + // @param instance the PageApi instance requesting the channel. + // @param extension the id of the extension to open the channel to. + // @param cookie a caller-provided cookie associated with this request. + // @note in the fullness of time, the manager should call + // ChannelOpened, supplying the callback and the assigned port_id. + virtual HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie) = 0; + + // Posts a message from the page to the previously assigned channel + // corresponding to port_id. + virtual HRESULT PostMessage(int port_id, const std::string& message) = 0; +}; + +class IContentScriptNativeApi: public IUnknown { + public: + // Called by host to complete a channel open request. + // @param cookie the cookie previously passed to the host on an + // OpenChannelToExtension invocation. + // @param port_id the port ID assigned to the port by the host. + virtual void OnChannelOpened(int cookie, int port_id) = 0; + + // Called by host on an incoming postMessage. + // @param port_id the host port ID of the destination port. + // @param message the message. + virtual void OnPostMessage(int port_id, const std::string& message) = 0; +}; + +// This class implements the native API provided to content scripts through +// the ceee_bootstrap.js script. The functionality provided here has to be +// safe for any page content to invoke. +// For safety's sake, do not expose any IDispatch-derived interfaces on this +// object other than ICeeeContentScriptNativeApi. +class ContentScriptNativeApi + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ContentScriptNativeApi>, + public IContentScriptNativeApi, + public IDispatchImpl<ICeeeContentScriptNativeApi, + &IID_ICeeeContentScriptNativeApi, + &LIBID_ToolbandLib, + 0xFFFF, // Magic ATL incantation to load + 0xFFFF> { // typelib from our resource. + public: + BEGIN_COM_MAP(ContentScriptNativeApi) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(ICeeeContentScriptNativeApi) + END_COM_MAP() + + ContentScriptNativeApi(); + + HRESULT Initialize(IExtensionPortMessagingProvider* messaging_provider); + void FinalRelease(); + + // @name ICeeeContentScriptNativeApi implementation + // This is the interface presented to the JavaScript + // code in ceee_bootstrap.js. + // @{ + STDMETHOD(Log)(BSTR level, BSTR message); + STDMETHOD(OpenChannelToExtension)(BSTR source_id, + BSTR target_id, + BSTR name, + long* port_id); + STDMETHOD(CloseChannel)(long port_id); + STDMETHOD(PortAddRef)(long port_id); + STDMETHOD(PortRelease)(long port_id); + STDMETHOD(PostMessage)(long port_id, BSTR msg); + STDMETHOD(AttachEvent)(BSTR event_name); + STDMETHOD(DetachEvent)(BSTR event_name); + STDMETHOD(put_onLoad)(IDispatch* callback); + STDMETHOD(put_onUnload)(IDispatch* callback); + STDMETHOD(put_onPortConnect)(IDispatch* callback); + STDMETHOD(put_onPortDisconnect)(IDispatch* callback); + STDMETHOD(put_onPortMessage)(IDispatch* callback); + // @} + + + // @name IContentScriptNativeApi implementation + // @{ + virtual void OnChannelOpened(int cookie, int port_id); + virtual void OnPostMessage(int port_id, const std::string& message); + // @} + + // Typed wrapper functions to call on the respective callbacks. + // @{ + HRESULT CallOnLoad(const wchar_t* extension_id); + HRESULT CallOnUnload(); + HRESULT CallOnPortConnect(long port_id, + const wchar_t* channel_name, + const wchar_t* tab, + const wchar_t* source_extension_id, + const wchar_t* target_extension_id); + HRESULT CallOnPortDisconnect(long port_id); + HRESULT CallOnPortMessage(const wchar_t* msg, long port_id); + // @} + + // Release all resources. + HRESULT TearDown(); + + private: + // Storage for our callback properties. + CComDispatchDriver on_load_; + CComDispatchDriver on_unload_; + CComDispatchDriver on_port_connect_; + CComDispatchDriver on_port_disconnect_; + CComDispatchDriver on_port_message_; + + // The messaging provider takes care of communication transport for us. + CComPtr<IExtensionPortMessagingProvider> messaging_provider_; + + typedef int PortId; + typedef PortId LocalPortId; + typedef PortId RemotePortId; + static const int kInvalidPortId = -1; + static const int kFirstPortId = 2; + + enum LocalPortState { + PORT_UNINITIALIZED, + PORT_CONNECTING, + PORT_CONNECTED, + PORT_CLOSING, + PORT_CLOSED, + }; + + typedef std::vector<std::string> MessageList; + + // State we maintain per port. + class LocalPort { + public: + explicit LocalPort(LocalPortId id); + + LocalPortState state; + LocalPortId local_id; + RemotePortId remote_id; + + // Messages waiting to be posted. + MessageList pending_messages; + }; + + LocalPortId GetNextLocalPortId() { + LocalPortId id = next_local_port_id_; + next_local_port_id_ += 2; + return id; + } + + typedef std::map<LocalPortId, LocalPort> LocalPortMap; + typedef std::map<RemotePortId, LocalPortId> RemoteToLocalPortIdMap; + + // Local state for the ports we handle. + LocalPortMap local_ports_; + // Maps + RemoteToLocalPortIdMap remote_to_local_port_id_; + + LocalPortId next_local_port_id_; +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ diff --git a/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc b/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc new file mode 100644 index 0000000..48b6213 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc @@ -0,0 +1,209 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API implementation. +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace { + +using testing::IDispatchExMockImpl; +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; +using testing::_; +using testing::AllOf; +using testing::DispParamArgEq; +using testing::Field; +using testing::Eq; +using testing::Return; +using testing::StrictMock; +using testing::MockDispatchEx; + +class IExtensionPortMessagingProviderMockImpl + : public IExtensionPortMessagingProvider { + public: + MOCK_METHOD1(CloseAll, void(IContentScriptNativeApi* instance)); + MOCK_METHOD4(OpenChannelToExtension, HRESULT( + IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie)); + MOCK_METHOD2(PostMessage, HRESULT(int port_id, const std::string& message)); +}; + +class MockIExtensionPortMessagingProvider + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockIExtensionPortMessagingProvider>, + public InstanceCountMixin<MockIExtensionPortMessagingProvider>, + public StrictMock<IExtensionPortMessagingProviderMockImpl> { + BEGIN_COM_MAP(MockIExtensionPortMessagingProvider) + END_COM_MAP() + + HRESULT Initialize() { return S_OK; } +}; + +class TestingContentScriptNativeApi + : public ContentScriptNativeApi, + public InitializingCoClass<TestingContentScriptNativeApi> { + public: + // Disambiguate. + using InitializingCoClass<TestingContentScriptNativeApi>::CreateInitialized; + using ContentScriptNativeApi::Initialize; + + HRESULT Initialize(TestingContentScriptNativeApi** self) { + *self = this; + return S_OK; + } +}; + +class ContentScriptNativeApiTest: public testing::Test { + public: + ContentScriptNativeApiTest() : api_(NULL), function_(NULL) { + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + TestingContentScriptNativeApi::CreateInitialized(&api_, &api_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockDispatchEx::CreateInitialized(&function_, &function_keeper_)); + } + + void TearDown() { + if (api_ != NULL) + api_->TearDown(); + + api_ = NULL; + api_keeper_.Release(); + + function_ = NULL; + function_keeper_.Release(); + + ASSERT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + protected: + TestingContentScriptNativeApi* api_; + CComPtr<ICeeeContentScriptNativeApi> api_keeper_; + + MockDispatchEx* function_; + CComPtr<IDispatch> function_keeper_; +}; + +TEST_F(ContentScriptNativeApiTest, ImplementsInterfaces) { + CComPtr<IDispatch> disp; + ASSERT_HRESULT_SUCCEEDED( + api_keeper_->QueryInterface(&disp)); +} + +int kPortId = 42; +const wchar_t* kChannelName = L"Q92FM"; +const wchar_t* kTab = NULL; +const wchar_t* kSourceExtensionId = L"fepbkochiplomghbdfgekenppangbiap"; +const wchar_t* kTargetExtensionId = L"kgeddobpkdopccblmihponcjlbdpmbod"; +const wchar_t* kWideMsg = L"\"JSONified string\""; +const char* kMsg = "\"JSONified string\""; + +const wchar_t* kWideMsg2 = L"\"Other JSONified string\""; +const char* kMsg2 = "\"Other JSONified string\""; + +TEST_F(ContentScriptNativeApiTest, CallUnsetCallbacks) { + ASSERT_HRESULT_FAILED(api_->CallOnLoad(kSourceExtensionId)); + ASSERT_HRESULT_FAILED(api_->CallOnUnload()); + ASSERT_HRESULT_FAILED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); + ASSERT_HRESULT_FAILED(api_->CallOnPortDisconnect(kPortId)); + ASSERT_HRESULT_FAILED(api_->CallOnPortMessage(kWideMsg, kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnLoad) { + ASSERT_HRESULT_FAILED(api_->CallOnLoad(kSourceExtensionId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onLoad(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kSourceExtensionId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnLoad(kSourceExtensionId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnUnload) { + ASSERT_HRESULT_FAILED(api_->CallOnUnload()); + ASSERT_HRESULT_SUCCEEDED(api_->put_onUnload(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnUnload()); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortConnect) { + ASSERT_HRESULT_FAILED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortConnect(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, + kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortDisconnect) { + ASSERT_HRESULT_FAILED(api_->CallOnPortDisconnect(kPortId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortDisconnect(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kPortId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortDisconnect(kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortMessage) { + ASSERT_HRESULT_FAILED(api_->CallOnPortMessage(kWideMsg, kPortId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortMessage(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kWideMsg, kPortId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortMessage(kWideMsg, kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, OnPostMessage) { + MockIExtensionPortMessagingProvider* mock_provider; + ASSERT_HRESULT_SUCCEEDED(MockIExtensionPortMessagingProvider:: + CreateInstance(&mock_provider)); + CComPtr<IExtensionPortMessagingProvider> mock_provider_holder(mock_provider); + EXPECT_HRESULT_SUCCEEDED(api_->Initialize(mock_provider_holder.p)); + // TODO(siggi@chromium.org): Expect the appropriate argument values. + EXPECT_CALL(*mock_provider, OpenChannelToExtension(api_, _, _, _)) + .WillRepeatedly(Return(S_OK)); + CComBSTR source_id(L"SourceId"); + CComBSTR name(L"name"); + long local_port_id1 = 0; + EXPECT_HRESULT_SUCCEEDED(api_->OpenChannelToExtension( + source_id, source_id, name, &local_port_id1)); + long local_port_id2 = 0; + EXPECT_HRESULT_SUCCEEDED(api_->OpenChannelToExtension( + source_id, source_id, name, &local_port_id2)); + + // TODO(siggi@chromium.org): Test pending messages code. + static const int kRemotePortId1 = 42; + static const int kRemotePortId2 = 84; + api_->OnChannelOpened((int)local_port_id2, kRemotePortId2); + api_->OnChannelOpened((int)local_port_id1, kRemotePortId1); + + EXPECT_HRESULT_SUCCEEDED(api_->put_onPortMessage(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kWideMsg, local_port_id1); + api_->OnPostMessage(kRemotePortId1, kMsg); + + function_->ExpectInvoke(DISPID_VALUE, kWideMsg2, local_port_id2); + api_->OnPostMessage(kRemotePortId2, kMsg2); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/json.js b/ceee/ie/plugin/scripting/json.js new file mode 100644 index 0000000..0ef04e4 --- /dev/null +++ b/ceee/ie/plugin/scripting/json.js @@ -0,0 +1,318 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview JSON utility functions. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/json/json.js + * + * LOCAL CHANGES: None. + */ + +goog.provide('goog.json'); +goog.provide('goog.json.Serializer'); + + + +/** + * Tests if a string is an invalid JSON string. This only ensures that we are + * not using any invalid characters + * @param {string} s The string to test. + * @return {boolean} True if the input is a valid JSON string. + * @private + */ +goog.json.isValid_ = function(s) { + // All empty whitespace is not valid. + if (/^\s*$/.test(s)) { + return false; + } + + // This is taken from http://www.json.org/json2.js which is released to the + // public domain. + // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator + // inside strings. We also treat \u2028 and \u2029 as whitespace which they + // are in the RFC but IE and Safari does not match \s to these so we need to + // include them in the reg exps in all places where whitespace is allowed. + // We allowed \x7f inside strings because some tools don't escape it, + // e.g. http://www.json.org/java/org/json/JSONObject.java + + // Parsing happens in three stages. In the first stage, we run the text + // against regular expressions that look for non-JSON patterns. We are + // especially concerned with '()' and 'new' because they can cause invocation, + // and '=' because it can cause mutation. But just to be safe, we want to + // reject all unexpected forms. + + // We split the first stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace all backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + // Don't make these static since they have the global flag. + var backslashesRe = /\\["\\\/bfnrtu]/g; + var simpleValuesRe = + /"[^"\\\n\r\u2028\u2029\x00-\x1f\x80-\x9f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g; + var remainderRe = /^[\],:{}\s\u2028\u2029]*$/; + + return remainderRe.test(s.replace(backslashesRe, '@'). + replace(simpleValuesRe, ']'). + replace(openBracketsRe, '')); +}; + + +/** + * Parses a JSON string and returns the result. This throws an exception if + * the string is an invalid JSON string. + * + * Note that this is very slow on large strings. If you trust the source of + * the string then you should use unsafeParse instead. + * + * @param {*} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.parse = function(s) { + var o = String(s); + if (goog.json.isValid_(o)) { + /** @preserveTry */ + try { + return eval('(' + o + ')'); + } catch (ex) { + } + } + throw Error('Invalid JSON string: ' + o); +}; + + +/** + * Parses a JSON string and returns the result. This uses eval so it is open + * to security issues and it should only be used if you trust the source. + * + * @param {string} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.unsafeParse = function(s) { + return eval('(' + s + ')'); +}; + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.serialize = function(object) { + return new goog.json.Serializer().serialize(object); +}; + + + +/** + * Class that is used to serialize JSON objects to a string. + * @constructor + */ +goog.json.Serializer = function() { +}; + + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object?} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.Serializer.prototype.serialize = function(object) { + var sb = []; + this.serialize_(object, sb); + return sb.join(''); +}; + + +/** + * Serializes a generic value to a JSON string + * @private + * @param {string|number|boolean|undefined|Object|Array} object The object to + * serialize. + * @param {Array} sb Array used as a string builder. + * @throws Error if there are loops in the object graph. + */ +goog.json.Serializer.prototype.serialize_ = function(object, sb) { + switch (typeof object) { + case 'string': + this.serializeString_((/** @type {string} */ object), sb); + break; + case 'number': + this.serializeNumber_((/** @type {number} */ object), sb); + break; + case 'boolean': + sb.push(object); + break; + case 'undefined': + sb.push('null'); + break; + case 'object': + if (object == null) { + sb.push('null'); + break; + } + if (goog.isArray(object)) { + this.serializeArray_(object, sb); + break; + } + // should we allow new String, new Number and new Boolean to be treated + // as string, number and boolean? Most implementations do not and the + // need is not very big + this.serializeObject_(object, sb); + break; + case 'function': + // Skip functions. + break; + default: + throw Error('Unknown type: ' + typeof object); + } +}; + + +/** + * Character mappings used internally for goog.string.quote + * @private + * @type {Object} + */ +goog.json.Serializer.charToJsonCharCache_ = { + '\"': '\\"', + '\\': '\\\\', + '/': '\\/', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + + '\x0B': '\\u000b' // '\v' is not supported in JScript +}; + + +/** + * Regular expression used to match characters that need to be replaced. + * The S60 browser has a bug where unicode characters are not matched by + * regular expressions. The condition below detects such behaviour and + * adjusts the regular expression accordingly. + * @private + * @type {RegExp} + */ +goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ? + /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g; + + +/** + * Serializes a string to a JSON string + * @private + * @param {string} s The string to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeString_ = function(s, sb) { + // The official JSON implementation does not work with international + // characters. + sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) { + // caching the result improves performance by a factor 2-3 + if (c in goog.json.Serializer.charToJsonCharCache_) { + return goog.json.Serializer.charToJsonCharCache_[c]; + } + + var cc = c.charCodeAt(0); + var rv = '\\u'; + if (cc < 16) { + rv += '000'; + } else if (cc < 256) { + rv += '00'; + } else if (cc < 4096) { // \u1000 + rv += '0'; + } + return goog.json.Serializer.charToJsonCharCache_[c] = rv + cc.toString(16); + }), '"'); +}; + + +/** + * Serializes a number to a JSON string + * @private + * @param {number} n The number to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) { + sb.push(isFinite(n) && !isNaN(n) ? n : 'null'); +}; + + +/** + * Serializes an array to a JSON string + * @private + * @param {Array} arr The array to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeArray_ = function(arr, sb) { + var l = arr.length; + sb.push('['); + var sep = ''; + for (var i = 0; i < l; i++) { + sb.push(sep) + this.serialize_(arr[i], sb); + sep = ','; + } + sb.push(']'); +}; + + +/** + * Serializes an object to a JSON string + * @private + * @param {Object} obj The object to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) { + sb.push('{'); + var sep = ''; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + var value = obj[key]; + // Skip functions. + if (typeof value != 'function') { + sb.push(sep); + this.serializeString_(key, sb); + sb.push(':'); + this.serialize_(value, sb); + sep = ','; + } + } + } + sb.push('}'); +};
\ No newline at end of file diff --git a/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc new file mode 100644 index 0000000..4f441a9 --- /dev/null +++ b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc @@ -0,0 +1,479 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// JS unittests for our extension API bindings. +#include <iostream> + +#include "base/path_service.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/string_util.h" + +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include <initguid.h> // NOLINT + +namespace { + +using testing::_; +using testing::DoAll; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; +using testing::IDispatchExMockImpl; + +// This is the chromium buildbot extension id. +const wchar_t kExtensionId[] = L"fepbkochiplomghbdfgekenppangbiap"; +// And the gmail checker. +const wchar_t kAnotherExtensionId[] = L"kgeddobpkdopccblmihponcjlbdpmbod"; +const wchar_t kFileName[] = TEXT(__FILE__); + +// The class and macros belwo make it possible to execute and debug JavaScript +// snippets interspersed with C++ code, which is a humane way to write, but +// particularly to read and debug the unittests below. +// To use this, declare a JavaScript block as follows: +// <code> +// BEGIN_SCRIPT_BLOCK(some_identifier) /* +// window.alert('here I am!' +// */ END_SCRIPT_BLOCK() +// +// HRESULT hr = some_identifier.Execute(script_host); +// </code> +#define BEGIN_SCRIPT_BLOCK(x) ScriptBlock x(TEXT(__FILE__), __LINE__); +#define END_SCRIPT_BLOCK() +class ScriptBlock { + public: + ScriptBlock(const wchar_t* file, size_t line) : file_(file), line_(line) { + } + + HRESULT Execute(IScriptHost* script_host) { + FilePath self_path(file_); + + if (!self_path.IsAbsolute()) { + // Construct the absolute path to this source file. + // The __FILE__ macro may expand to a solution-relative path to the file. + FilePath src_root; + EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &src_root)); + + self_path = src_root.Append(L"ceee") + .Append(L"ie") + .Append(file_); + } + + // Slurp the file. + std::string contents; + if (!file_util::ReadFileToString(self_path, &contents)) + return E_UNEXPECTED; + + // Walk the lines to ours. + std::string::size_type start_pos = 0; + for (size_t i = 0; i < line_; ++i) { + // Find the next newline. + start_pos = contents.find('\n', start_pos); + if (start_pos == contents.npos) + return E_UNEXPECTED; + + // Walk past the newline char. + start_pos++; + } + + // Now find the next occurrence of END_SCRIPT_BLOCK. + std::string::size_type end_pos = contents.find("END_SCRIPT_BLOCK", + start_pos); + if (end_pos == contents.npos) + return E_UNEXPECTED; + + // And walk back to the start of that line. + end_pos = contents.rfind('\n', end_pos); + if (end_pos == contents.npos) + return E_UNEXPECTED; + + CComPtr<IDebugDocumentHelper> doc; + ScriptHost* host = static_cast<ScriptHost*>(script_host); + host->AddDebugDocument(file_, CA2W(contents.c_str()), &doc); + + std::string script = contents.substr(start_pos, end_pos - start_pos); + return host->RunScriptSnippet(start_pos, CA2W(script.c_str()), doc); + } + + private: + const wchar_t* file_; + const size_t line_; +}; + + +class TestingContentScriptNativeApi + : public ContentScriptNativeApi, + public InstanceCountMixin<TestingContentScriptNativeApi>, + public InitializingCoClass<TestingContentScriptNativeApi> { + public: + // Disambiguate. + using InitializingCoClass<TestingContentScriptNativeApi>::CreateInitialized; + + HRESULT Initialize(TestingContentScriptNativeApi** self) { + *self = this; + return S_OK; + } + + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, Log, + HRESULT(BSTR level, BSTR message)); + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, OpenChannelToExtension, + HRESULT(BSTR source_id, BSTR target_id, BSTR name, LONG* port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, CloseChannel, + HRESULT(LONG port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PortAddRef, + HRESULT(LONG port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PortRelease, + HRESULT(LONG port_id)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, PostMessage, + HRESULT(LONG port_id, BSTR msg)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, AttachEvent, + HRESULT(BSTR event_name)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, DetachEvent, + HRESULT(BSTR event_name)); +}; + +class TestingContentScriptManager + : public ContentScriptManager { + public: + // Make accessible for testing. + using ContentScriptManager::BootstrapScriptHost; +}; + +class TestingScriptHost + : public ScriptHost, + public InitializingCoClass<TestingScriptHost>, + public InstanceCountMixin<TestingScriptHost> { + public: + using InitializingCoClass<TestingScriptHost>::CreateInitializedIID; + + HRESULT Initialize(ScriptHost::DebugApplication* debug, + TestingScriptHost** self) { + *self = this; + return ScriptHost::Initialize(debug); + } +}; + +class RendererExtensionBindingsTest: public testing::Test { + public: + RendererExtensionBindingsTest() : api_(NULL), script_host_(NULL) { + } + + static void SetUpTestCase() { + EXPECT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + debug_.Initialize(); + } + + static void TearDownTestCase() { + debug_.Terminate(); + + ::CoUninitialize(); + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + + ASSERT_HRESULT_SUCCEEDED( + TestingScriptHost::CreateInitializedIID(&debug_, + &script_host_, + IID_IScriptHost, + &script_host_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + TestingContentScriptNativeApi::CreateInitialized(&api_, &api_keeper_)); + } + + void TearDown() { + if (api_) { + EXPECT_HRESULT_SUCCEEDED(api_->TearDown()); + api_ = NULL; + api_keeper_.Release(); + } + + script_host_ = NULL; + if (script_host_keeper_ != NULL) + script_host_keeper_->Close(); + script_host_keeper_.Release(); + + EXPECT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + void Initialize() { + ASSERT_HRESULT_SUCCEEDED( + manager_.BootstrapScriptHost(script_host_keeper_, + api_keeper_, + kExtensionId)); + } + + void AssertNameExists(const wchar_t* name) { + CComVariant result; + ASSERT_HRESULT_SUCCEEDED(script_host_->RunExpression(name, &result)); + ASSERT_NE(VT_EMPTY, V_VT(&result)); + } + + void ExpectFirstConnection() { + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + } + void ExpectConnection(const wchar_t* src_extension_id, + const wchar_t* dst_extension_id, + const wchar_t* port_name, + LONG port_id) { + EXPECT_CALL(*api_, + OpenChannelToExtension(StrEq(src_extension_id), + StrEq(dst_extension_id), + StrEq(port_name), + _)) + .WillOnce( + DoAll( + SetArgumentPointee<3>(port_id), + Return(S_OK))); + + EXPECT_CALL(*api_, PortAddRef(port_id)) + .WillOnce(Return(S_OK)); + } + + protected: + TestingContentScriptNativeApi* api_; + CComPtr<ICeeeContentScriptNativeApi> api_keeper_; + TestingContentScriptManager manager_; + + TestingScriptHost* script_host_; + CComPtr<IScriptHost> script_host_keeper_; + + static ScriptHost::DebugApplication debug_; +}; + +ScriptHost::DebugApplication + RendererExtensionBindingsTest::debug_(L"RendererExtensionBindingsTest"); + +TEST_F(RendererExtensionBindingsTest, TestNamespace) { + Initialize(); + + AssertNameExists(L"chrome"); + AssertNameExists(L"chrome.extension"); + AssertNameExists(L"chrome.extension.connect"); + + AssertNameExists(L"JSON"); + AssertNameExists(L"JSON.parse"); +} + +TEST_F(RendererExtensionBindingsTest, GetUrl) { + Initialize(); + + CComVariant result; + + ASSERT_HRESULT_SUCCEEDED( + script_host_->RunExpression( + L"chrome.extension.getURL('foo')", &result)); + + ASSERT_EQ(VT_BSTR, V_VT(&result)); + ASSERT_STREQ(StringPrintf(L"chrome-extension://%ls/foo", + kExtensionId).c_str(), + V_BSTR(&result)); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectDisconnect) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, L"port = chrome.extension.connect()")); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectWithName) { + Initialize(); + const LONG kPortId = 42; + const wchar_t* kPortName = L"A Port Name"; + ExpectConnection(kExtensionId, kExtensionId, kPortName, kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf( + L"port = chrome.extension.connect({name: \"%ls\"});", + kPortName).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectToExtension) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kAnotherExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port = chrome.extension.connect(\"%ls\");", + kAnotherExtensionId).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, PostMessage) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kAnotherExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port = chrome.extension.connect(\"%ls\");", + kAnotherExtensionId).c_str())); + + const wchar_t* kMsg = L"Message in a bottle, yeah!"; + // Note the extra on-the-wire quotes, due to JSON encoding the input. + EXPECT_CALL(*api_, + PostMessage(kPortId, + StrEq(StringPrintf(L"\"%ls\"", kMsg).c_str()))); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port.postMessage(\"%ls\");", kMsg).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, OnConnect) { + Initialize(); + const LONG kPortId = 42; + const wchar_t kPortName[] = L"A port of call"; + BEGIN_SCRIPT_BLOCK(script) /* + function onConnect(port) { + if (port.name == 'A port of call') + console.log('SUCCESS'); + else + console.log(port.name); + }; + chrome.extension.onConnect.addListener(onConnect); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))). + WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + // A 'SUCCESS' log signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(L"SUCCESS"))).Times(1); + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))). + WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, PortAddRef(kPortId)). + WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED( + api_->CallOnPortConnect(kPortId, + kPortName, + L"", + kExtensionId, + kExtensionId)); +} + +TEST_F(RendererExtensionBindingsTest, OnDisconnect) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(script1) /* + var port = chrome.extension.connect() + */ END_SCRIPT_BLOCK() + EXPECT_HRESULT_SUCCEEDED(script1.Execute(script_host_)); + + BEGIN_SCRIPT_BLOCK(script2) /* + port.onDisconnect.addListener(function (port) { + console.log('SUCCESS'); + }); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(script2.Execute(script_host_)); + + // A 'SUCCESS' log signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(L"SUCCESS"))).Times(1); + + EXPECT_HRESULT_SUCCEEDED(api_->CallOnPortDisconnect(kPortId)); +} + +TEST_F(RendererExtensionBindingsTest, OnMessage) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(connect_script) /* + // Connect to our extension. + var port = chrome.extension.connect(); + */ END_SCRIPT_BLOCK() + EXPECT_HRESULT_SUCCEEDED(connect_script.Execute(script_host_)); + + BEGIN_SCRIPT_BLOCK(add_listener_script) /* + // Log the received message to console. + function onMessage(msg, port) { + console.log(msg); + }; + port.onMessage.addListener(onMessage); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(add_listener_script.Execute(script_host_)); + + const wchar_t kMessage[] = L"A message in a bottle, yeah!"; + // The message logged signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(kMessage))).Times(1); + + EXPECT_HRESULT_SUCCEEDED( + api_->CallOnPortMessage(StringPrintf(L"\"%ls\"", kMessage).c_str(), + kPortId)); +} + +TEST_F(RendererExtensionBindingsTest, OnLoad) { + Initialize(); + + BEGIN_SCRIPT_BLOCK(script) /* + var chromeHidden = ceee.GetChromeHidden(); + function onLoad(extension_id) { + console.log(extension_id); + } + chromeHidden.onLoad.addListener(onLoad); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))); + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(kExtensionId))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(api_->CallOnLoad(kExtensionId)); +} + +TEST_F(RendererExtensionBindingsTest, OnUnload) { + Initialize(); + const LONG kPort1Id = 42; + const LONG kPort2Id = 57; + ExpectConnection(kExtensionId, kExtensionId, L"port1", kPort1Id); + ExpectConnection(kExtensionId, kExtensionId, L"port2", kPort2Id); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(script) /* + var port1 = chrome.extension.connect({name: 'port1'}); + var port2 = chrome.extension.connect({name: 'port2'}); + */ END_SCRIPT_BLOCK() + + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + EXPECT_CALL(*api_, PortRelease(kPort1Id)).WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, PortRelease(kPort2Id)).WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, DetachEvent(StrEq(L""))).WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(api_->CallOnUnload()); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc new file mode 100644 index 0000000..1be0ca5 --- /dev/null +++ b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc @@ -0,0 +1,12 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#include <winres.h> + +#ifdef APSTUDIO_INVOKED +#error Please edit as text. +#endif + +// The test needs the CEEE TLB. +1 TYPELIB "toolband.tlb" diff --git a/ceee/ie/plugin/scripting/script_host.cc b/ceee/ie/plugin/scripting/script_host.cc new file mode 100644 index 0000000..e2faf42 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host.cc @@ -0,0 +1,886 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implements our scripting host. + +#include "ceee/ie/plugin/scripting/script_host.h" + +#include <dispex.h> +#include <mshtml.h> +#include <mshtmhst.h> +#include <objsafe.h> + +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" + +#ifndef CMDID_SCRIPTSITE_URL + +// These are documented in MSDN, but not declared in the platform SDK. +// See [http://msdn.microsoft.com/en-us/library/aa769871(VS.85).aspx]. +#define CMDID_SCRIPTSITE_URL 0 +#define CMDID_SCRIPTSITE_HTMLDLGTRUST 1 +#define CMDID_SCRIPTSITE_SECSTATE 2 +#define CMDID_SCRIPTSITE_SID 3 +#define CMDID_SCRIPTSITE_TRUSTEDDOC 4 +#define CMDID_SCRIPTSITE_SECURITY_WINDOW 5 +#define CMDID_SCRIPTSITE_NAMESPACE 6 +#define CMDID_SCRIPTSITE_IURI 7 + +const GUID CGID_ScriptSite = { + 0x3050F3F1, 0x98B5, 0x11CF, 0xBB, 0x82, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x0B }; +#endif // CMDID_SCRIPTSITE_URL + +namespace { +// This class is a necessary wrapper around a text string in +// IDebugDocumentHelper, as one can really only use the deferred +// text mode of the helper if one wants to satisfy the timing +// requirements on notifications imposed by script debuggers. +// +// The timing requirement is this: +// +// It appears that for a debugger to successfully set a breakpoint in +// an ActiveScript engine, the code in question has to have been parsed. +// The VisualStudio and the IE8 debugger both appear to use the events +// IDebugDocumentTextEvents as a trigger point to apply any pending +// breakpoints to the engine. The problem here is that with naive usage of +// the debug document helper, one would simply provide it with the +// text contents of the document at creation, before ParseScriptText +// is performed. This fires the text events too early, which means +// the debugger will not find any code to set breakpoints on. +// To ensure that the debugger finds something to grab on, +// one needs to set or modify the debug document text after +// ParseScriptText, but before execution of the text, such as e.g. +// on the OnEnterScript event to the ActiveScript site. +class DocHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<DocHost>, + public IDebugDocumentHost { + public: + BEGIN_COM_MAP(DocHost) + COM_INTERFACE_ENTRY(IDebugDocumentHost) + END_COM_MAP() + + HRESULT Initialize(const wchar_t* code) { + code_ = code; + return S_OK; + } + + STDMETHOD(GetDeferredText)(DWORD cookie, + WCHAR* text, + SOURCE_TEXT_ATTR* text_attr, + ULONG* num_chars_returned, + ULONG max_chars) { + size_t num_chars = std::min(static_cast<size_t>(max_chars), code_.length()); + *num_chars_returned = num_chars; + memcpy(text, code_.c_str(), num_chars * sizeof(wchar_t)); + + return S_OK; + } + + STDMETHOD(GetScriptTextAttributes)(LPCOLESTR code, + ULONG num_code_chars, + LPCOLESTR delimiter, + DWORD flags, + SOURCE_TEXT_ATTR* attr) { + return E_NOTIMPL; + } + + STDMETHOD(OnCreateDocumentContext)(IUnknown** outer) { + return E_NOTIMPL; + } + + STDMETHOD(GetPathName)(BSTR *long_name, BOOL *is_original_file) { + return E_NOTIMPL; + } + + STDMETHOD(GetFileName)(BSTR *short_name) { + return E_NOTIMPL; + } + STDMETHOD(NotifyChanged)(void) { + return E_NOTIMPL; + } + private: + std::wstring code_; +}; + +HRESULT GetUrlForDocument(IUnknown* unknown, VARIANT* url_out) { + CComQIPtr<IHTMLDocument2> document(unknown); + if (document == NULL) + return E_NOINTERFACE; + + CComBSTR url; + HRESULT hr = document->get_URL(&url); + if (SUCCEEDED(hr)) { + url_out->vt = VT_BSTR; + url_out->bstrVal = url.Detach(); + } else { + DLOG(ERROR) << "Failed to get security url " << com::LogHr(hr); + } + + return hr; +} + +HRESULT GetSIDForDocument(IUnknown* unknown, VARIANT* sid_out) { + CComQIPtr<IServiceProvider> sp(unknown); + if (sp == NULL) + return E_NOINTERFACE; + + CComPtr<IInternetHostSecurityManager> security_manager; + HRESULT hr = sp->QueryService(SID_SInternetHostSecurityManager, + &security_manager); + if (FAILED(hr)) + return hr; + + // This is exactly mimicking observed behavior in IE. + CComBSTR security_id(MAX_SIZE_SECURITY_ID); + DWORD size = MAX_SIZE_SECURITY_ID; + hr = security_manager->GetSecurityId( + reinterpret_cast<BYTE*>(security_id.m_str), &size, 0); + if (SUCCEEDED(hr)) { + sid_out->vt = VT_BSTR; + sid_out->bstrVal = security_id.Detach(); + } else { + DLOG(ERROR) << "Failed to get security manager " << com::LogHr(hr); + } + + return hr; +} + +HRESULT GetWindowForDocument(IUnknown* unknown, VARIANT* window_out) { + CComQIPtr<IServiceProvider> sp(unknown); + if (sp == NULL) + return E_NOINTERFACE; + + CComPtr<IDispatch> window; + HRESULT hr = sp->QueryService(SID_SHTMLWindow, &window); + if (SUCCEEDED(hr)) { + window_out->vt = VT_DISPATCH; + window_out->pdispVal = window.Detach(); + } else { + DLOG(ERROR) << "Failed to get window " << com::LogHr(hr); + } + + return hr; +} + +} // namespace + +// {58E6D2A5-4868-4E49-B3E9-072C845A014A} +const GUID IID_IScriptHost = + { 0x58ECD2A5, 0x4868, 0x4E49, + { 0xB3, 0xE9, 0x07, 0x2C, 0x84, 0x5A, 0x01, 0x4A } }; + +// {f414c260-6ac0-11cf-b6d1-00aa00bbbb58} +const GUID CLSID_JS = + { 0xF414C260, 0x6AC0, 0x11CF, + { 0xB6, 0xD1, 0x00, 0xAA, 0x00, 0xBB, 0xBB, 0x58 } }; + + +ScriptHost::DebugApplication* ScriptHost::default_debug_application_; + + +ScriptHost::ScriptHost() : debug_application_(NULL) { +} + +HRESULT ScriptHost::Initialize(DebugApplication* debug_application) { + debug_application_ = debug_application; + + HRESULT hr = CreateScriptEngine(&script_); + if (FAILED(hr)) { + NOTREACHED(); + return hr; + } + + if (FAILED(hr = script_->SetScriptSite(this))) { + NOTREACHED(); + return hr; + } + + // Get engine's IActiveScriptParse interface, initialize it + script_parse_ = script_; + if (!script_parse_) { + NOTREACHED(); + return E_NOINTERFACE; + } + + if (FAILED(hr = script_parse_->InitNew())) { + NOTREACHED(); + return hr; + } + + // Set the security options of the script engine so it + // queries us for IInternetHostSecurityManager, which + // we delegate to our site. + CComQIPtr<IObjectSafety> script_safety(script_); + if (script_safety == NULL) { + NOTREACHED() << "Script engine does not implement IObjectSafety"; + return E_NOINTERFACE; + } + + hr = script_safety->SetInterfaceSafetyOptions( + IID_IDispatch, INTERFACE_USES_SECURITY_MANAGER, + INTERFACE_USES_SECURITY_MANAGER); + + // Set the script engine into a running state. + hr = script_->SetScriptState(SCRIPTSTATE_CONNECTED); + DCHECK(SUCCEEDED(hr)); + + return hr; +} + +HRESULT ScriptHost::Initialize(DebugApplication* debug_application, + ScriptHost** self) { + *self = this; + return Initialize(debug_application); +} + +HRESULT ScriptHost::Initialize() { + return Initialize(default_debug_application_); +} + +void ScriptHost::FinalRelease() { + DCHECK(script_ == NULL); + debug_application_ = NULL; +} + +HRESULT ScriptHost::RegisterScriptObject(const wchar_t* name, + IDispatch* disp_obj, + bool make_members_global) { + DCHECK(name); + DCHECK(disp_obj); + std::wstring wname = name; + + // Check if the name already exists. + ScriptObjectMap::iterator iter = script_objects_.find(wname); + if (iter != script_objects_.end()) { + return E_ACCESSDENIED; + } + + // Add to the script object map. + CComPtr<IDispatch> disp_obj_ptr(disp_obj); + CAdapt<CComPtr<IDispatch>> disp_obj_adapt(disp_obj_ptr); + script_objects_.insert(std::make_pair(wname, disp_obj_adapt)); + + // Add to the script engine. + DWORD flags = SCRIPTITEM_ISSOURCE | SCRIPTITEM_ISVISIBLE; + if (make_members_global) { + flags |= SCRIPTITEM_GLOBALMEMBERS; + } + script_->AddNamedItem(name, flags); + + return S_OK; +} + +HRESULT ScriptHost::RunScript(const wchar_t* file_path, + const wchar_t* code) { + DCHECK(file_path); + DCHECK(code); + if (!file_path || !code) + return E_POINTER; + + DWORD source_context = 0; + HRESULT hr = GetSourceContext(file_path, code, &source_context); + if (FAILED(hr)) + return hr; + + ScopedExcepInfo ei; + hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, source_context, 0, + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE, NULL, &ei); + // A syntax error is not a CEEE error, so we don't log an error and we return + // it normally so that the caller knows what happened. + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +HRESULT ScriptHost::RunExpression(const wchar_t* code, VARIANT* result) { + DCHECK(code); + if (!code) + return E_POINTER; + + ScopedExcepInfo ei; + HRESULT hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, 0, 0, SCRIPTTEXT_ISEXPRESSION, result, &ei); + // Ignore compilation and runtime errors in the script + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +HRESULT ScriptHost::Close() { + // Close our script host. + HRESULT hr = S_OK; + if (script_) { + // Try to force garbage collection at this time so any objects holding + // reference to native components will be released immediately and dlls + // loaded can be unloaded quickly. + CComQIPtr<IActiveScriptGarbageCollector> script_gc(script_); + if (script_gc != NULL) + script_gc->CollectGarbage(SCRIPTGCTYPE_EXHAUSTIVE); + + hr = script_->Close(); + } + + // Detach all debug documents. + DebugDocMap::iterator iter; + for (iter = debug_docs_.begin(); iter != debug_docs_.end(); iter++) { + // Note that this is IDebugDocumentHelper::Detach and not + // CComPtr::Detach + iter->second.document->Detach(); + } + debug_docs_.clear(); + + script_.Release(); + + return hr; +} + +STDMETHODIMP ScriptHost::GetItemInfo(LPCOLESTR item_name, DWORD return_mask, + IUnknown** item_unknown, + ITypeInfo** item_itypeinfo) { + DCHECK(!(return_mask & SCRIPTINFO_IUNKNOWN) || item_unknown); + DCHECK(!(return_mask & SCRIPTINFO_ITYPEINFO) || item_itypeinfo); + + HRESULT hr = S_OK; + + std::wstring wname = item_name; + ScriptObjectMap::iterator iter = script_objects_.find(wname); + if (iter != script_objects_.end()) { + CComPtr<IDispatch> disp_obj = iter->second.m_T; + + CComPtr<IUnknown> unknown; + if (return_mask & SCRIPTINFO_IUNKNOWN) { + DCHECK(item_unknown); + hr = disp_obj.QueryInterface(&unknown); + } + + CComPtr<ITypeInfo> typeinfo; + if (SUCCEEDED(hr) && return_mask & SCRIPTINFO_ITYPEINFO) { + DCHECK(item_itypeinfo); + hr = disp_obj->GetTypeInfo(0, LANG_NEUTRAL, &typeinfo); + } + + // We have everything ready, return the out args on success. + if (SUCCEEDED(hr)) { + if (return_mask & SCRIPTINFO_IUNKNOWN) { + hr = unknown.CopyTo(item_unknown); + DCHECK(SUCCEEDED(hr)); + } + if (return_mask & SCRIPTINFO_ITYPEINFO) { + hr = typeinfo.CopyTo(item_itypeinfo); + DCHECK(SUCCEEDED(hr)); + } + } + } else { + hr = TYPE_E_ELEMENTNOTFOUND; + } + + return hr; +} + +STDMETHODIMP ScriptHost::GetLCID(LCID *plcid) { + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::GetDocVersionString(BSTR* version) { + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::OnEnterScript() { + DebugDocMap::iterator it(debug_docs_.begin()); + DebugDocMap::iterator end(debug_docs_.end()); + + // It is necessary to defer the document size notifications + // below until after defining all relevant script blocks, + // in order to tickle the debugger at just the right time + // so it can turn around and set any pending breakpoints + // prior to first execution. + for (; it != end; ++it) { + DebugDocInfo& info = it->second; + + if (info.is_new) { + info.is_new = false; + info.document->AddDeferredText(info.len, 0); + } + } + + return S_OK; +} + +STDMETHODIMP ScriptHost::OnLeaveScript() { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnStateChange(SCRIPTSTATE state) { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnScriptTerminate(const VARIANT* result, + const EXCEPINFO* excep_info) { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnScriptError(IActiveScriptError* script_error) { + ScopedExcepInfo ei; + HRESULT hr = script_error->GetExceptionInfo(&ei); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetExceptionInfo. " << + com::LogHr(hr); + + CComBSTR source_line; + hr = script_error->GetSourceLineText(&source_line); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetSourceLineText. " << + com::LogHr(hr); + + DWORD context = 0; + ULONG line_number = 0; + LONG char_pos = 0; + hr = script_error->GetSourcePosition(&context, &line_number, &char_pos); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetSourcePosition. " << + com::LogHr(hr); + + LOG(ERROR) << "Script error occurred: " << + com::ToString(ei.bstrDescription) << ". Source Text: " << + com::ToString(source_line) << ". Context: "<< context << ", line: " << + line_number << ", char pos: " << char_pos; + return S_OK; +} + +STDMETHODIMP ScriptHost::GetDocumentContextFromPosition( + DWORD source_context, ULONG char_offset, ULONG num_chars, + IDebugDocumentContext** debug_doc_context) { + LOG(INFO) << "GetDocumentContextFromPosition(" << source_context << ", " + << char_offset << ", " << num_chars << ")"; + + DebugDocMap::iterator iter; + iter = debug_docs_.find(source_context); + if (iter != debug_docs_.end()) { + DebugDocInfo& info = iter->second; + ULONG start_position = 0; + HRESULT hr = info.document->GetScriptBlockInfo(source_context, + NULL, + &start_position, + NULL); + if (FAILED(hr)) + LOG(ERROR) << "GetScriptBlockInfo failed " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + hr = info.document->CreateDebugDocumentContext( + start_position + char_offset, num_chars, debug_doc_context); + if (FAILED(hr)) + LOG(ERROR) << "GetScriptBlockInfo failed " << com::LogHr(hr); + } + + return hr; + } + + LOG(ERROR) << "No debug document for context " << source_context; + + return E_FAIL; +} + +STDMETHODIMP ScriptHost::GetApplication(IDebugApplication** debug_app) { + if (debug_application_) + return debug_application_->GetDebugApplication(debug_app); + + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::GetRootApplicationNode( + IDebugApplicationNode** debug_app_node) { + DCHECK(debug_app_node); + if (!debug_app_node) + return E_POINTER; + + if (debug_application_ != NULL) { + return debug_application_->GetRootApplicationNode(debug_app_node); + } else { + *debug_app_node = NULL; + return S_OK; + } + + NOTREACHED(); +} + +STDMETHODIMP ScriptHost::OnScriptErrorDebug(IActiveScriptErrorDebug* err, + BOOL* enter_debugger, BOOL* call_on_script_err_when_continuing) { + DCHECK(err); + DCHECK(enter_debugger); + DCHECK(call_on_script_err_when_continuing); + if (!err || !enter_debugger || !call_on_script_err_when_continuing) + return E_POINTER; + + // TODO(ericdingle@chromium.org): internationalization + int ret = ::MessageBox( + NULL, L"A script error occured. Do you want to debug?", + L"Google Chrome Extensions Execution Environment", + MB_ICONERROR | MB_SETFOREGROUND | MB_TASKMODAL | MB_YESNO); + *enter_debugger = (ret == IDYES); + *call_on_script_err_when_continuing = FALSE; + + return S_OK; +} + +STDMETHODIMP ScriptHost::QueryStatus(const GUID* cmd_group, ULONG num_cmds, + OLECMD cmds[], OLECMDTEXT *cmd_text) { + LOG(WARNING) << "QueryStatus " << + CComBSTR(cmd_group ? *cmd_group : GUID_NULL) << ", " << num_cmds; + // We're practically unimplemented. + DLOG(INFO) << "ScriptHost::QueryStatus called"; + return OLECMDERR_E_UNKNOWNGROUP; +}; + +STDMETHODIMP ScriptHost::Exec(const GUID* cmd_group, DWORD cmd_id, + DWORD cmd_exec_opt, VARIANT *arg_in, VARIANT *arg_out) { + LOG(WARNING) << "Exec " << CComBSTR(cmd_group ? *cmd_group : GUID_NULL) << + ", " << cmd_id; + + if (cmd_group && *cmd_group == CGID_ScriptSite) { + switch (cmd_id) { + case CMDID_SCRIPTSITE_URL: + return GetUrlForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_HTMLDLGTRUST: + DLOG(INFO) << "CMDID_SCRIPTSITE_HTMLDLGTRUST"; + break; + case CMDID_SCRIPTSITE_SECSTATE: + DLOG(INFO) << "CMDID_SCRIPTSITE_SECSTATE"; + break; + case CMDID_SCRIPTSITE_SID: + return GetSIDForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_TRUSTEDDOC: + DLOG(INFO) << "CMDID_SCRIPTSITE_TRUSTEDDOC"; + break; + case CMDID_SCRIPTSITE_SECURITY_WINDOW: + return GetWindowForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_NAMESPACE: + DLOG(INFO) << "CMDID_SCRIPTSITE_NAMESPACE"; + break; + case CMDID_SCRIPTSITE_IURI: + DLOG(INFO) << "CMDID_SCRIPTSITE_IURI"; + break; + default: + DLOG(INFO) << "ScriptHost::Exec unknown command " << cmd_id; + break; + } + } + + return OLECMDERR_E_UNKNOWNGROUP; +} + +HRESULT ScriptHost::CreateScriptEngine(IActiveScript** script) { + return script_.CoCreateInstance(CLSID_JS, NULL, CLSCTX_INPROC_SERVER); +} + +HRESULT ScriptHost::GetSourceContext(const wchar_t* file_path, + const wchar_t* code, + DWORD* source_context) { + DCHECK(debug_application_ != NULL); + HRESULT hr = S_OK; + CComPtr<IDebugDocumentHelper> helper; + hr = debug_application_->CreateDebugDocumentHelper(file_path, + code, + 0, + &helper); + size_t len = lstrlenW(code); + if (SUCCEEDED(hr) && helper != NULL) { + hr = helper->DefineScriptBlock(0, len, script_, FALSE, source_context); + DCHECK(SUCCEEDED(hr)); + + DebugDocInfo info; + info.is_new = true; + info.len = len; + info.document = helper; + debug_docs_.insert(std::make_pair(*source_context, info)); + } + + return hr; +} + +HRESULT ScriptHost::AddDebugDocument(const wchar_t* file_path, + const wchar_t* code, + IDebugDocumentHelper** doc) { + DCHECK(debug_application_ != NULL); + return debug_application_->CreateDebugDocumentHelper(file_path, + code, + TEXT_DOC_ATTR_READONLY, + doc); +} + +HRESULT ScriptHost::RunScriptSnippet(size_t start_offset, + const wchar_t* code, + IDebugDocumentHelper* doc) { + DWORD source_context = 0; + if (doc) { + size_t len = lstrlenW(code); + HRESULT hr = doc->DefineScriptBlock(start_offset, + len, + script_, + FALSE, + &source_context); + + if (SUCCEEDED(hr)) { + DebugDocInfo info; + info.document = doc; + info.len = start_offset + len; + info.is_new = true; + + debug_docs_.insert(std::make_pair(source_context, info)); + } else { + LOG(ERROR) << "Failed to define a script block " << com::LogHr(hr); + LOG(ERROR) << "Script: " << code; + } + } + + ScopedExcepInfo ei; + HRESULT hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, source_context, 0, + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE, NULL, &ei); + + // Ignore compilation and runtime errors in the script + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +ScriptHost::DebugApplication::DebugApplication(const wchar_t* application_name) + : application_name_(application_name), + debug_app_cookie_(kInvalidDebugAppCookie), + initialization_count_(0) { +} + +ScriptHost::DebugApplication::~DebugApplication() { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK_EQ(0U, initialization_count_); +} + +void ScriptHost::DebugApplication::RegisterDebugApplication() { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK(debug_app_cookie_ == kInvalidDebugAppCookie); + + // Don't need to lock as this MUST be single-threaded and first use. + CComPtr<IProcessDebugManager> manager; + HRESULT hr = CreateProcessDebugManager(&manager); + + CComPtr<IDebugApplication> application; + if (SUCCEEDED(hr)) + hr = manager->CreateApplication(&application); + + if (SUCCEEDED(hr)) + hr = application->SetName(application_name_); + + DWORD cookie = 0; + if (SUCCEEDED(hr)) + hr = manager->AddApplication(application, &cookie); + + if (FAILED(hr)) { + LOG(INFO) << "ScriptHost debug initialization failed: " << com::LogHr(hr); + return; + } + + debug_manager_ = manager; + debug_application_ = application; + debug_app_cookie_ = cookie; +} + +void ScriptHost::DebugApplication::Initialize() { + AutoLock lock(lock_); + + ++initialization_count_; + + if (initialization_count_ == 1) + RegisterDebugApplication(); +} + +void ScriptHost::DebugApplication::Initialize( + IUnknown* debug_application_provider) { + AutoLock lock(lock_); + + ++initialization_count_; + + if (initialization_count_ == 1) { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK(debug_app_cookie_ == kInvalidDebugAppCookie); + + CComPtr<IDebugApplication> debug_app; + HRESULT hr = debug_application_provider->QueryInterface(&debug_app); + if (FAILED(hr) || debug_app == NULL) { + CComPtr<IServiceProvider> sp; + hr = debug_application_provider->QueryInterface(&sp); + if (SUCCEEDED(hr) && sp != NULL) + hr = sp->QueryService(IID_IDebugApplication, &debug_app); + } + + if (debug_app != NULL) + debug_application_ = debug_app; + else + RegisterDebugApplication(); + } +} + +void ScriptHost::DebugApplication::Initialize( + IProcessDebugManager* manager, IDebugApplication* app) { + AutoLock lock(lock_); + // This function is exposed for testing only. + DCHECK_EQ(0U, initialization_count_); + + DCHECK_EQ(static_cast<IUnknown*>(NULL), debug_manager_); + DCHECK_EQ(static_cast<IUnknown*>(NULL), debug_application_); + DCHECK_EQ(kInvalidDebugAppCookie, debug_app_cookie_); + + ++initialization_count_; + + debug_manager_ = manager; + debug_application_ = app; +} + + +void ScriptHost::DebugApplication::Terminate() { + AutoLock lock(lock_); + DCHECK_GT(initialization_count_, (size_t)0); + --initialization_count_; + + if (initialization_count_ == 0) { + if (debug_manager_ != NULL) { + if (debug_app_cookie_ != kInvalidDebugAppCookie) { + HRESULT hr = debug_manager_->RemoveApplication(debug_app_cookie_); + DCHECK(SUCCEEDED(hr)); + } + } + + debug_manager_.Release(); + debug_application_.Release(); + debug_app_cookie_ = kInvalidDebugAppCookie; + } +} + +HRESULT ScriptHost::DebugApplication::GetDebugApplication( + IDebugApplication** app) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) + return E_NOTIMPL; + + return debug_application_.CopyTo(app); +} + +HRESULT ScriptHost::DebugApplication::GetRootApplicationNode( + IDebugApplicationNode** debug_app_node) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) { + *debug_app_node = NULL; + return S_OK; + } + + return debug_application_->GetRootNode(debug_app_node); +} + +HRESULT ScriptHost::DebugApplication::CreateDebugDocumentHelper( + const wchar_t* long_name, const wchar_t* code, TEXT_DOC_ATTR attributes, + IDebugDocumentHelper** helper) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) + return S_OK; + + // Find the last forward or backward slash in the long name, + // and construct a short name from the rest - the base name. + std::wstring name(long_name); + size_t pos = name.find_last_of(L"\\/"); + const wchar_t* short_name = long_name; + if (pos != name.npos && long_name[pos + 1] != '\0') + short_name = long_name + pos + 1; + + CComPtr<IDebugDocumentHelper> doc; + HRESULT hr = CreateDebugDocumentHelper(&doc); + if (SUCCEEDED(hr)) + hr = doc->Init(debug_application_, short_name, long_name, attributes); + + // Wrap the text in a document host. + CComPtr<IDebugDocumentHost> host; + if (SUCCEEDED(hr)) + hr = DocHost::CreateInitialized(code, &host); + if (SUCCEEDED(hr)) + hr = doc->SetDebugDocumentHost(host); + if (SUCCEEDED(hr)) + hr = doc->Attach(NULL); + + if (SUCCEEDED(hr)) { + DCHECK(doc != NULL); + + *helper = doc.Detach(); + } + + return hr; +} + +HRESULT ScriptHost::DebugApplication::CreateDebugDocumentHelper( + IDebugDocumentHelper** helper) { + // Create the debug document. + if (debug_manager_ != NULL) + return debug_manager_->CreateDebugDocumentHelper(NULL, helper); + + // As it turns out, it's better to create a debug document + // from the same DLL server as issued the debug manager, so let's + // try and accomodate. Get the class object function from the + // in-process instance. + HMODULE pdm = ::GetModuleHandle(L"pdm.dll"); + LPFNGETCLASSOBJECT pdm_get_class_object = NULL; + if (pdm != NULL) + pdm_get_class_object = reinterpret_cast<LPFNGETCLASSOBJECT>( + ::GetProcAddress(pdm, "DllGetClassObject")); + + // Fallback to plain CoCreateInstance if we didn't get the function. + if (!pdm_get_class_object) { + LOG(WARNING) << "CreateDebugDocumentHelper falling back to " + "CoCreateInstance"; + return ::CoCreateInstance(CLSID_CDebugDocumentHelper, + NULL, + CLSCTX_INPROC_SERVER, + IID_IDebugDocumentHelper, + reinterpret_cast<void**>(helper)); + } + + // Create a debug helper. + CComPtr<IClassFactory> factory; + HRESULT hr = pdm_get_class_object(CLSID_CDebugDocumentHelper, + IID_IClassFactory, + reinterpret_cast<void**>(&factory)); + if (SUCCEEDED(hr)) { + DCHECK(factory != NULL); + hr = factory->CreateInstance(NULL, + IID_IDebugDocumentHelper, + reinterpret_cast<void**>(helper)); + } + + return hr; +} + +HRESULT ScriptHost::DebugApplication::CreateProcessDebugManager( + IProcessDebugManager** manager) { + return ::CoCreateInstance(CLSID_ProcessDebugManager, + NULL, + CLSCTX_INPROC_SERVER, + IID_IProcessDebugManager, + reinterpret_cast<void**>(manager)); +} diff --git a/ceee/ie/plugin/scripting/script_host.h b/ceee/ie/plugin/scripting/script_host.h new file mode 100644 index 0000000..553d142 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host.h @@ -0,0 +1,317 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A host for Microsoft's JScript engine that we use to create a separate +// engine for our content scripts so that they do not run in the same +// context/namespace as the scripts that are part of the page. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ + +// The Platform SDK version of this header is newer than the +// Active Script SDK, and we use some newer interfaces. +#include <activscp.h> +#include <atlbase.h> +#include <atlcom.h> +#include <dispex.h> +#include <docobj.h> +#include <map> +#include <string> + +#include "base/lock.h" +#include "base/logging.h" +#include "third_party/activscp/activdbg.h" +#include "ceee/common/initializing_coclass.h" + +#ifndef OLESCRIPT_E_SYNTAX +#define OLESCRIPT_E_SYNTAX 0x80020101 +#endif + +extern const GUID IID_IScriptHost; + +// Interface for ScriptHost needed for unit testing. +class IScriptHost : public IUnknown { + public: + // Registers an IDispatch script object with the script host. + // @param name The name the object will have inside the script host. + // @param disp_obj The IDispatch object to register. + // @param make_members_global Whether or not to make the object's members + // global. + virtual HRESULT RegisterScriptObject(const wchar_t* name, + IDispatch* disp_obj, + bool make_members_global) = 0; + + // Run the specified script in the script host. + // @param file_path The name/path to the file for debugging. + // @param code The code to be executed. + virtual HRESULT RunScript(const wchar_t* file_path, const wchar_t* code) = 0; + + // Run the specified expression in the script host and get its return value. + // @param code The code to be executed. + // @param result A variant to write the result to. + virtual HRESULT RunExpression(const wchar_t* code, VARIANT* result) = 0; + + // Close the script host and release resources. + virtual HRESULT Close() = 0; +}; + +// Implementation of ScriptHost class. +// +// This implements the requisite IActiveScript{Debug} interfaces necessary +// to host a active script engine and to integrate with script debugging. +// It also exposes IServiceProvider and IOleCommandTarget, which serve the +// purpose of declaring the script code origin to the IE DOM. When our scripts +// invoke on the IE DOM through IDispatchEx::InvokeEx, the last parameter is +// a service provider that leads back to this host. The IE DOM implementation +// will query this service provider for SID_GetScriptSite, and acquire an +// IOleCommandTarget on it, which in turn gets interrogated about the scripts +// origin and security properties. +class ATL_NO_VTABLE ScriptHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ScriptHost>, + public IActiveScriptSite, + public IActiveScriptSiteDebug, + public IServiceProviderImpl<ScriptHost>, + public IObjectWithSiteImpl<ScriptHost>, + public IOleCommandTarget, + public IScriptHost { + public: + BEGIN_COM_MAP(ScriptHost) + COM_INTERFACE_ENTRY(IActiveScriptSite) + COM_INTERFACE_ENTRY(IActiveScriptSiteDebug) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IOleCommandTarget) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IScriptHost, IScriptHost) + END_COM_MAP() + BEGIN_SERVICE_MAP(ScriptHost) + SERVICE_ENTRY(SID_GetScriptSite) + // We delegate to our site object. This allows the site to provide + // SID_SInternetHostSecurityManager, which can govern ActiveX control + // creation and the like. + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + ScriptHost(); + + // Fwd. + class DebugApplication; + + // Sets the default debug application script host instaces + // will use unless provided with another specific instance. + // Note: a DebugApplication instance set here must outlive all + // ScriptHost instances in this process. + static void set_default_debug_application(DebugApplication* debug) { + default_debug_application_ = debug; + } + static DebugApplication* default_debug_application() { + return default_debug_application_; + } + + // Initialize with a debug application and return a pointer to self. + // @param debug_application the debug application on whose behalf we're + // running. + // @param self returns a referenceless pointer to new instance. + HRESULT Initialize(DebugApplication* debug_application, ScriptHost** self); + + // Initialize with a debug application. + // @param debug_application the debug application on whose behalf we're + // running. + HRESULT Initialize(DebugApplication* debug_application); + + HRESULT Initialize(); + + void FinalRelease(); + + // IScriptHost methods. + HRESULT RegisterScriptObject(const wchar_t* name, IDispatch* disp_obj, + bool make_members_global); + HRESULT RunScript(const wchar_t* file_path, const wchar_t* code); + HRESULT RunExpression(const wchar_t* code, VARIANT* result); + HRESULT Close(); + + // IActiveScriptSite methods. + STDMETHOD(GetItemInfo)(LPCOLESTR item_name, DWORD return_mask, + IUnknown** item_iunknown, ITypeInfo** item_itypeinfo); + STDMETHOD(GetLCID)(LCID *plcid); + STDMETHOD(GetDocVersionString)(BSTR* version); + STDMETHOD(OnEnterScript)(); + STDMETHOD(OnLeaveScript)(); + STDMETHOD(OnStateChange)(SCRIPTSTATE state); + STDMETHOD(OnScriptTerminate)(const VARIANT* result, + const EXCEPINFO* excep_info); + STDMETHOD(OnScriptError)(IActiveScriptError* script_error); + + // IActiveScriptSiteDebug methods. + STDMETHOD(GetDocumentContextFromPosition)(DWORD source_context, + ULONG char_offset, ULONG num_chars, + IDebugDocumentContext** debug_doc_context); + STDMETHOD(GetApplication)(IDebugApplication** debug_app); + STDMETHOD(GetRootApplicationNode)(IDebugApplicationNode** debug_app_node); + STDMETHOD(OnScriptErrorDebug)(IActiveScriptErrorDebug* error_debug, + BOOL* enter_debugger, BOOL* call_on_script_err_when_continuing); + + + // @name IOleCommandTarget methods + // @{ + STDMETHOD(QueryStatus)(const GUID* cmd_group, + ULONG num_cmds, + OLECMD cmds[], + OLECMDTEXT *cmd_text); + STDMETHOD(Exec)(const GUID* cmd_group, + DWORD cmd_id, + DWORD cmd_exec_opt, + VARIANT* arg_in, + VARIANT* arg_out); + // @} + + // @name Debug-only functionality. + // @( + // Create a debug document for @p code, which originates from @p file + // and return it in @doc. + // @param file_path a file path or URL to code. + // @param code document code containing JavaScript snippets. + // @param doc on success returns the new debug document. + HRESULT AddDebugDocument(const wchar_t* file_path, + const wchar_t* code, + IDebugDocumentHelper** doc); + + // Run a JavaScript snippet from a previously declared document. + // @param start_offset character offset from start of document to + // @p code. + // @param code a code snippet from a document previously declared + // to AddDebugDocument. + // @param doc a debug document previously received from AddDebugDocument. + HRESULT RunScriptSnippet(size_t start_offset, + const wchar_t* code, + IDebugDocumentHelper* doc); + // @} + + protected: + // Virtual methods to inject dependencies for unit testing. + virtual HRESULT CreateScriptEngine(IActiveScript** script); + + private: + struct ScopedExcepInfo : public EXCEPINFO { + public: + ScopedExcepInfo() { + bstrSource = NULL; + bstrDescription = NULL; + bstrHelpFile = NULL; + } + ~ScopedExcepInfo() { + if (bstrSource) { + ::SysFreeString(bstrSource); + bstrSource = NULL; + } + if (bstrDescription) { + ::SysFreeString(bstrDescription); + bstrDescription = NULL; + } + if (bstrHelpFile) { + ::SysFreeString(bstrHelpFile); + bstrHelpFile = NULL; + } + } + }; + + HRESULT GetSourceContext(const wchar_t* file_path, + const wchar_t* code, + DWORD* source_context); + + // The JScript script engine. NULL until Initialize(). + CComPtr<IActiveScript> script_; + + // The JScript parser. NULL until Initialize(). + CComQIPtr<IActiveScriptParse> script_parse_; + + // A map of wstring to IDispatch pointers to hold registered script objects. + // Empty on Initialize(). Filled using calls to RegisterScriptObject(). + typedef std::map<std::wstring, CAdapt<CComPtr<IDispatch> > > ScriptObjectMap; + ScriptObjectMap script_objects_; + + // A map of source contexts to debug document helpers used for debugging. + // Empty on Initialize(). Filled using calls to CreateDebugDoc(). + // Resources released and emptied on Close(). + struct DebugDocInfo { + bool is_new; + size_t len; + CComPtr<IDebugDocumentHelper> document; + }; + typedef std::map<DWORD, DebugDocInfo> DebugDocMap; + DebugDocMap debug_docs_; + + // Our debug application state. + DebugApplication* debug_application_; + + // Default debug application state, which is used if no other state is + // provided at initialization time. + static DebugApplication* default_debug_application_; +}; + +class ScriptHost::DebugApplication { + public: + DebugApplication(const wchar_t* application_name); + ~DebugApplication(); + + // Best-effort initialize process script debugging. + // Every call to this function must be matched with a call to Terminate(). + void Initialize(); + + // Best-effort initialize process script debugging. + // @param manager_or_provider either implements IDebugApplication or + // IServiceProvider. Both methods will be tried in turn to aquire + // an IDebugApplication. If both methods fail, this will fall back + // to initialization by creating a new debug application. + void Initialize(IUnknown* debug_application_provider); + + // Exposed for testing only, needs a corresponding call to Terminate. + void Initialize(IProcessDebugManager* manager, IDebugApplication* app); + + // Terminate script debugging, call once for every call to Initialize(). + void Terminate(); + + // Creates a debug document helper with the given long name, containing code. + HRESULT CreateDebugDocumentHelper(const wchar_t* long_name, + const wchar_t* code, + TEXT_DOC_ATTR attributes, + IDebugDocumentHelper** helper); + // Retrieve the debug application. + HRESULT GetDebugApplication(IDebugApplication** application); + // Retrieve root application node. + HRESULT GetRootApplicationNode(IDebugApplicationNode** debug_app_node); + + private: + // Virtual for testing. + virtual HRESULT CreateProcessDebugManager(IProcessDebugManager** manager); + virtual HRESULT CreateDebugDocumentHelper(IDebugDocumentHelper** helper); + + // Creates and registers a new debug application for us. + void RegisterDebugApplication(); // Under lock_. + + // Protects all members below. + ::Lock lock_; // Our containing class has a Lock method. + + // Number of initialization calls. + size_t initialization_count_; + + // The debug manager, non-NULL only if we register + // our own debug application. + CComPtr<IProcessDebugManager> debug_manager_; + + // Registration cookie for debug_manager_ registration of debug_application_. + DWORD debug_app_cookie_; + + // The debug application to be used for debugging. NULL until Initialize(). + CComPtr<IDebugApplication> debug_application_; + + static const DWORD kInvalidDebugAppCookie = 0; + + // The application name we register. + const wchar_t* application_name_; +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ diff --git a/ceee/ie/plugin/scripting/script_host_unittest.cc b/ceee/ie/plugin/scripting/script_host_unittest.cc new file mode 100644 index 0000000..4e52874 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host_unittest.cc @@ -0,0 +1,519 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Script host implementation unit tests. + +#include "ceee/ie/plugin/scripting/script_host.h" + +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::InstanceCountMixin; +using testing::MockDispatchEx; +using testing::MockIServiceProvider; + +using testing::_; +using testing::AddRef; +using testing::AllOf; +using testing::CopyInterfaceToArgument; +using testing::DispParamArgEq; +using testing::DoAll; +using testing::Eq; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::StrEq; + +class MockIHTMLWindow2 + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<IHTMLWindow2MockImpl>, + public InstanceCountMixin<MockIHTMLWindow2> { + public: + BEGIN_COM_MAP(MockIHTMLWindow2) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLWindow2) + COM_INTERFACE_ENTRY(IHTMLFramesCollection2) + END_COM_MAP() + + HRESULT Initialize(MockIHTMLWindow2** self) { + *self = this; + return S_OK; + } +}; + +class MockProcessDebugManager + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IProcessDebugManagerMockImpl>, + public InstanceCountMixin<MockProcessDebugManager> { + public: + BEGIN_COM_MAP(MockProcessDebugManager) + COM_INTERFACE_ENTRY(IProcessDebugManager) + END_COM_MAP() + + HRESULT Initialize(MockProcessDebugManager** debug_manager) { + *debug_manager = this; + return S_OK; + } +}; + +class MockDebugApplication + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IDebugApplicationMockImpl>, + public InstanceCountMixin<MockDebugApplication> { + public: + BEGIN_COM_MAP(MockDebugApplication) + COM_INTERFACE_ENTRY(IDebugApplication) + END_COM_MAP() + + HRESULT Initialize(MockDebugApplication** debug_application) { + *debug_application = this; + return S_OK; + } +}; + +class MockDebugDocumentHelper + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IDebugDocumentHelperMockImpl>, + public InstanceCountMixin<MockDebugDocumentHelper> { + public: + BEGIN_COM_MAP(MockDebugDocumentHelper) + COM_INTERFACE_ENTRY(IDebugDocumentHelper) + END_COM_MAP() + + HRESULT Initialize(MockDebugDocumentHelper** debug_document) { + *debug_document = this; + return S_OK; + } +}; + +const wchar_t* kDebugApplicationName = L"ScriptHostTest"; + +class TestingDebugApplication : public ScriptHost::DebugApplication { + public: + TestingDebugApplication() + : ScriptHost::DebugApplication(kDebugApplicationName) { + } + + MOCK_METHOD1(CreateProcessDebugManager, + HRESULT(IProcessDebugManager** manager)); +}; + +class MockIActiveScriptAndParse + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IActiveScriptMockImpl>, + public StrictMock<testing::IActiveScriptParseMockImpl>, + public StrictMock<testing::IObjectSafetyMockImpl>, + public InstanceCountMixin<MockIActiveScriptAndParse> { + public: + BEGIN_COM_MAP(MockIActiveScriptAndParse) + COM_INTERFACE_ENTRY(IActiveScript) + COM_INTERFACE_ENTRY(IActiveScriptParse) + COM_INTERFACE_ENTRY(IObjectSafety) + END_COM_MAP() + + HRESULT Initialize(MockIActiveScriptAndParse** script) { + *script = this; + return S_OK; + } +}; + +class TestingScriptHost + : public ScriptHost, + public InstanceCountMixin<TestingScriptHost> { + public: + HRESULT Initialize(ScriptHost::DebugApplication* debug, + IActiveScript* script) { + my_script_ = script; + ScriptHost::Initialize(debug); + return S_OK; + } + + private: + HRESULT CreateDebugManager(IProcessDebugManager** debug_manager) { + return my_debug_manager_.CopyTo(debug_manager); + } + HRESULT CreateScriptEngine(IActiveScript** script) { + return my_script_.CopyTo(script); + } + + CComPtr<IProcessDebugManager> my_debug_manager_; + CComPtr<IActiveScript> my_script_; +}; + +const DISPID kDispId = 5; +const wchar_t* kDispObjName = L"tpain"; +const wchar_t* kDispObjName2 = L"akon"; +const wchar_t* kJsFilePath = L"liljon.js"; +const wchar_t* kJsCode = L"alert('WWHHHATTT? OOOKKAAYY.')"; +const DWORD kAddFlags = SCRIPTITEM_ISSOURCE | SCRIPTITEM_ISVISIBLE; +const DWORD kAddGlobalFlags = kAddFlags | SCRIPTITEM_GLOBALMEMBERS; +const DWORD kSourceContext = 123456; +const DWORD kUnknownSourceContext = 654321; +const DWORD kNoSourceContext = 0; +const DWORD kScriptFlags = + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE; +const DWORD kExpressionFlags = SCRIPTTEXT_ISEXPRESSION; +const ULONG kCharOffset = 1234; +const ULONG kNumChars = 4321; + +class ScriptHostTest : public testing::Test { + public: + ScriptHostTest() : mock_debug_manager_(NULL), mock_debug_application_(NULL), + mock_script_(NULL), debug_(kDebugApplicationName) { + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockProcessDebugManager>:: + CreateInitializedIID(&mock_debug_manager_, + IID_IProcessDebugManager, + &debug_manager_)); + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDebugApplication>:: + CreateInitializedIID(&mock_debug_application_, + IID_IDebugApplication, + &debug_application_)); + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIActiveScriptAndParse>::CreateInitializedIID( + &mock_script_, IID_IActiveScript, &script_)); + + debug_.Initialize(debug_manager_, debug_application_); + } + + void ExpectEngineInitialization() { + EXPECT_CALL(*mock_script_, SetScriptSite(_)).WillOnce( + Return(S_OK)); + EXPECT_CALL(*mock_script_, InitNew()).WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_script_, SetScriptState(SCRIPTSTATE_CONNECTED)).WillOnce( + Return(S_OK)); + EXPECT_CALL(*mock_script_, SetInterfaceSafetyOptions( + IID_IDispatch, INTERFACE_USES_SECURITY_MANAGER, + INTERFACE_USES_SECURITY_MANAGER)).WillOnce(Return(S_OK)); + } + + void TearDown() { + if (script_host_) { + EXPECT_CALL(*mock_script_, Close()).WillOnce(Return(S_OK)); + + script_host_->Close(); + } + + mock_debug_manager_ = NULL; + debug_manager_.Release(); + + mock_debug_application_ = NULL; + debug_application_.Release(); + + mock_script_ = NULL; + script_.Release(); + + script_host_.Release(); + + debug_.Terminate(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateScriptHost() { + ExpectEngineInitialization(); + + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<TestingScriptHost>::CreateInitializedIID( + &debug_, script_, IID_IScriptHost, &script_host_)); + } + + void ExpectCreateDebugDocumentHelper( + MockDebugDocumentHelper** mock_debug_document) { + CComPtr<IDebugDocumentHelper> debug_document; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDebugDocumentHelper>::CreateInitialized( + mock_debug_document, &debug_document)); + + EXPECT_CALL(*mock_debug_manager_, CreateDebugDocumentHelper(NULL, _)) + .WillOnce(DoAll(CopyInterfaceToArgument<1>(debug_document), + Return(S_OK))); + + EXPECT_CALL(**mock_debug_document, + Init(Eq(debug_application_), StrEq(kJsFilePath), StrEq(kJsFilePath), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(**mock_debug_document, Attach(NULL)).WillOnce(Return(S_OK)); + EXPECT_CALL(**mock_debug_document, SetDebugDocumentHost(_)).WillOnce( + Return(S_OK)); + EXPECT_CALL(**mock_debug_document, + DefineScriptBlock(_, _, Eq(script_), _, _)) + .WillOnce(DoAll(SetArgumentPointee<4>(kSourceContext), Return(S_OK))); + + // Detach will be called on this document when the script host is closed + EXPECT_CALL(**mock_debug_document, Detach()).WillOnce(Return(S_OK)); + } + + void ExpectParseScriptText(DWORD source_context, DWORD flags) { + EXPECT_CALL(*mock_script_, ParseScriptText(StrEq(kJsCode), NULL, NULL, NULL, + source_context, 0, flags, _, _)) + .WillOnce(Return(S_OK)); + } + + MockProcessDebugManager* mock_debug_manager_; + CComPtr<IProcessDebugManager> debug_manager_; + MockDebugApplication* mock_debug_application_; + CComPtr<IDebugApplication> debug_application_; + MockIActiveScriptAndParse* mock_script_; + CComPtr<IActiveScript> script_; + CComPtr<IScriptHost> script_host_; + + ScriptHost::DebugApplication debug_; +}; + +TEST_F(ScriptHostTest, DebugApplicationDefaultInitialize) { + TestingDebugApplication debug; + + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_manager_), + Return(S_OK))); + + EXPECT_CALL(*mock_debug_manager_, CreateApplication(_)).WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_application_), Return(S_OK))); + EXPECT_CALL(*mock_debug_application_, SetName(StrEq(kDebugApplicationName))) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_debug_manager_, AddApplication( + static_cast<IDebugApplication*>(debug_application_), _)) + .WillOnce(DoAll(SetArgumentPointee<1>(42), Return(S_OK))); + + debug.Initialize(); + + // We expect the script host to undo debug app registration. + EXPECT_CALL(*mock_debug_manager_, RemoveApplication(42)) + .WillOnce(Return(S_OK)); + debug.Terminate(); +} + +// Test initializing debugging with a service provider. +TEST_F(ScriptHostTest, DebugApplicationInitializeWithServiceProvider) { + TestingDebugApplication debug; + + MockIServiceProvider* sp_keeper; + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIServiceProvider>::CreateInitialized( + &sp_keeper, &sp)); + + + EXPECT_CALL(*sp_keeper, + QueryService(IID_IDebugApplication, IID_IDebugApplication, _)) + .WillOnce(DoAll( + AddRef(debug_manager_.p), + SetArgumentPointee<2>(static_cast<void*>(debug_manager_)), + Return(S_OK))); + + // Initialization with a service provider that yields + // a debug application should only query service, and + // not try to create a debug manager. + EXPECT_CALL(debug, CreateProcessDebugManager(_)).Times(0); + + debug.Initialize(sp); + + // And there should be no app unregistration. + debug.Terminate(); +} + +TEST_F(ScriptHostTest, DebugApplicationInitializeWithEmptyServiceProvider) { + TestingDebugApplication debug; + + MockIServiceProvider* sp_keeper; + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIServiceProvider>::CreateInitialized( + &sp_keeper, &sp)); + + + EXPECT_CALL(*sp_keeper, + QueryService(IID_IDebugApplication, IID_IDebugApplication, _)) + .WillOnce(Return(E_NOINTERFACE)); + + // Initialization with a service provider that yields + // no debug application should go the default route. + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_manager_), + Return(S_OK))); + + EXPECT_CALL(*mock_debug_manager_, CreateApplication(_)).WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_application_), Return(S_OK))); + EXPECT_CALL(*mock_debug_application_, SetName(StrEq(kDebugApplicationName))) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_debug_manager_, AddApplication( + static_cast<IDebugApplication*>(debug_application_), _)) + .WillOnce(DoAll(SetArgumentPointee<1>(42), Return(S_OK))); + + debug.Initialize(sp); + + // And the registration should be undone. + EXPECT_CALL(*mock_debug_manager_, RemoveApplication(42)) + .WillOnce(Return(S_OK)); + debug.Terminate(); +} + +TEST_F(ScriptHostTest, DebugApplicationInitFailure) { + TestingDebugApplication debug; + + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce(Return(REGDB_E_CLASSNOTREG)); + + debug.Initialize(); + + CComPtr<IDebugDocumentHelper> helper; + EXPECT_HRESULT_SUCCEEDED( + debug.CreateDebugDocumentHelper(kJsFilePath, + kJsCode, + 0, + &helper)); + EXPECT_TRUE(helper == NULL); + + // This must fail when debugging is not present. + CComPtr<IDebugApplication> debug_app; + EXPECT_HRESULT_FAILED(debug.GetDebugApplication(&debug_app)); + ASSERT_TRUE(debug_app == NULL); + + // Whereas this should succeed, but return a NULL node. + CComPtr<IDebugApplicationNode> root_node; + EXPECT_HRESULT_SUCCEEDED(debug.GetRootApplicationNode(&root_node)); + ASSERT_TRUE(root_node == NULL); + + debug.Terminate(); +} + +TEST_F(ScriptHostTest, QueryInterface) { + CreateScriptHost(); + + CComQIPtr<IActiveScriptSite> script_site(script_host_); + ASSERT_TRUE(script_site != NULL); + + CComQIPtr<IActiveScriptSiteDebug> script_site_debug(script_host_); + ASSERT_TRUE(script_site_debug != NULL); +} + +TEST_F(ScriptHostTest, RegisterScriptObject) { + CreateScriptHost(); + + MockIHTMLWindow2* mock_dispatch_obj; + CComPtr<IHTMLWindow2> dispatch_obj; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIHTMLWindow2>::CreateInitialized( + &mock_dispatch_obj, &dispatch_obj)); + + // Add a non global script object. + EXPECT_CALL(*mock_script_, AddNamedItem(StrEq(kDispObjName), kAddFlags)) + .WillOnce(Return(S_OK)); + HRESULT hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, + false); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Add a global script object. + EXPECT_CALL(*mock_script_, + AddNamedItem(StrEq(kDispObjName2), kAddGlobalFlags)) + .WillOnce(Return(S_OK)); + hr = script_host_->RegisterScriptObject(kDispObjName2, dispatch_obj, + true); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Add a duplicate named object. + hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, false); + EXPECT_EQ(hr, E_ACCESSDENIED); +} + +TEST_F(ScriptHostTest, RegisterScriptObjectAndGetItemInfo) { + CreateScriptHost(); + + MockDispatchEx* mock_dispatch_obj; + CComPtr<IDispatchEx> dispatch_obj; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized( + &mock_dispatch_obj, &dispatch_obj)); + + // Add a non global script object. + EXPECT_CALL(*mock_script_, AddNamedItem(StrEq(kDispObjName), kAddFlags)) + .WillOnce(Return(S_OK)); + HRESULT hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, + false); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Make sure we return the object when it's called for. + EXPECT_CALL(*mock_dispatch_obj, GetTypeInfo(0, LANG_NEUTRAL, _)) + .WillOnce(Return(S_OK)); + CComQIPtr<IActiveScriptSite> script_site(script_host_); + ASSERT_TRUE(script_site != NULL); + + CComPtr<IUnknown> item_iunknown; + CComPtr<ITypeInfo> item_itypeinfo; + hr = script_site->GetItemInfo(kDispObjName, + SCRIPTINFO_IUNKNOWN | SCRIPTINFO_ITYPEINFO, + &item_iunknown, &item_itypeinfo); + ASSERT_HRESULT_SUCCEEDED(hr); + EXPECT_EQ(dispatch_obj, item_iunknown); +} + +TEST_F(ScriptHostTest, RunScript) { + CreateScriptHost(); + + MockDebugDocumentHelper* mock_debug_document; + ExpectCreateDebugDocumentHelper(&mock_debug_document); + ExpectParseScriptText(kSourceContext, kScriptFlags); + + HRESULT hr = script_host_->RunScript(kJsFilePath, kJsCode); + ASSERT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(ScriptHostTest, RunExpression) { + CreateScriptHost(); + + ExpectParseScriptText(kNoSourceContext, kExpressionFlags); + + CComVariant dummy; + HRESULT hr = script_host_->RunExpression(kJsCode, &dummy); + ASSERT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(ScriptHostTest, RunScriptAndGetDocumentContextFromPosition) { + CreateScriptHost(); + + MockDebugDocumentHelper* mock_debug_document; + ExpectCreateDebugDocumentHelper(&mock_debug_document); + ExpectParseScriptText(kSourceContext, kScriptFlags); + + script_host_->RunScript(kJsFilePath, kJsCode); + + CComQIPtr<IActiveScriptSiteDebug> script_site_debug(script_host_); + ASSERT_TRUE(script_site_debug != NULL); + + EXPECT_CALL(*mock_debug_document, + GetScriptBlockInfo(kSourceContext, NULL, _, NULL)) + .WillOnce(DoAll(SetArgumentPointee<2>(kCharOffset), Return(S_OK))); + EXPECT_CALL(*mock_debug_document, + CreateDebugDocumentContext(2 * kCharOffset, kNumChars, _)) + .WillOnce(Return(S_OK)); + + + // Call with a known source context. + CComPtr<IDebugDocumentContext> dummy_debug_document_context; + HRESULT hr = script_site_debug->GetDocumentContextFromPosition( + kSourceContext, kCharOffset, kNumChars, &dummy_debug_document_context); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Call with an unknown source context. + hr = script_site_debug->GetDocumentContextFromPosition( + kUnknownSourceContext, kCharOffset, kNumChars, + &dummy_debug_document_context); + ASSERT_TRUE(hr == E_FAIL); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/scripting.gyp b/ceee/ie/plugin/scripting/scripting.gyp new file mode 100644 index 0000000..d7593e7 --- /dev/null +++ b/ceee/ie/plugin/scripting/scripting.gyp @@ -0,0 +1,94 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + '../../../../ceee/common.gypi', + ], + 'targets': [ + { + 'target_name': 'scripting', + 'type': 'static_library', + 'dependencies': [ + 'javascript_bindings', + '../../common/common.gyp:ie_common', + '../../common/common.gyp:ie_common_settings', + '../toolband/toolband.gyp:toolband_idl', + '../../../../base/base.gyp:base', + '../../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + 'base.js', + 'ceee_bootstrap.js', + 'json.js', + 'content_script_manager.cc', + 'content_script_manager.h', + 'content_script_manager.rc', + 'content_script_native_api.cc', + 'content_script_native_api.h', + '../../common/precompile.cc', + '../../common/precompile.h', + 'script_host.cc', + 'script_host.h', + 'userscripts_librarian.cc', + 'userscripts_librarian.h', + 'userscripts_docs.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'javascript_bindings', + 'type': 'none', + 'variables': { + 'chrome_renderer_path' : '../../../../chrome/renderer', + 'input_js_files': [ + '<(chrome_renderer_path)/resources/event_bindings.js', + '<(chrome_renderer_path)/resources/renderer_extension_bindings.js', + ], + 'output_js_files': [ + '<(SHARED_INTERMEDIATE_DIR)/event_bindings.js', + '<(SHARED_INTERMEDIATE_DIR)/renderer_extension_bindings.js', + ], + }, + 'sources': [ + 'transform_native_js.py', + '<@(input_js_files)', + ], + 'actions': [ + { + 'action_name': 'transform_native_js', + 'msvs_cygwin_shell': 0, + 'msvs_quote_cmd': 0, + 'inputs': [ + '<@(_sources)', + ], + 'outputs': [ + '<@(output_js_files)', + ], + 'action': [ + '<@(python)', + 'transform_native_js.py', + '<@(input_js_files)', + '-o', + '<(SHARED_INTERMEDIATE_DIR)', + ], + }, + ], + # Make sure our dependents can refer to the transformed + # files from their .rc file(s). + 'direct_dependent_settings': { + 'resource_include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + ] +} diff --git a/ceee/ie/plugin/scripting/transform_native_js.py b/ceee/ie/plugin/scripting/transform_native_js.py new file mode 100644 index 0000000..b77ffc7 --- /dev/null +++ b/ceee/ie/plugin/scripting/transform_native_js.py @@ -0,0 +1,53 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +'''A GYP script action file to transform native JS function declarations. + +Any line in the input script of the form + "native function <NAME>(<ARGS>);" +will be transformed to + "var <NAME> = ceee.<NAME>; // function(<ARGS>)" +''' + +import optparse +import os.path +import re +import sys + +_NATIVE_FUNCTION_RE = re.compile( + 'native\s+function\s+(?P<name>\w+)\((?P<args>[^)]*)\)\s*;') + + +def TransformFile(input_file, output_file): + '''Transform native functions in input_file, write to output_file''' + # Slurp the input file. + contents = open(input_file, "r").read() + + repl = 'var \g<name> = ceee.\g<name>; // function(\g<args>)' + contents = _NATIVE_FUNCTION_RE.sub(repl, contents,) + + # Write the output file. + open(output_file, "w").write(contents) + + +def GetOptionParser(): + parser = optparse.OptionParser(description=__doc__) + parser.add_option('-o', dest='output_dir', + help='Output directory') + return parser + +def Main(): + parser = GetOptionParser() + (opts, args) = parser.parse_args() + if not opts.output_dir: + parser.error('You must provide an output directory') + + for input_file in args: + output_file = os.path.join(opts.output_dir, os.path.basename(input_file)) + TransformFile(input_file, output_file) + + return 0 + + +if __name__ == '__main__': + sys.exit(Main()) diff --git a/ceee/ie/plugin/scripting/userscripts_docs.h b/ceee/ie/plugin/scripting/userscripts_docs.h new file mode 100644 index 0000000..c9aded9 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_docs.h @@ -0,0 +1,66 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ // Mainly for lint +#define CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ + +/** @page UserScriptsDoc Detailed documentation of the UserScriptsLibrarian. + +@section UserScriptsLibrarianIntro Introduction + +We need to be able to load the user scripts information (including url pattern +matching info) from an extension manifest file and make it available for +insertion in a WEB page as needed. + +@section IE + +We already have a class (ExtensionManifest) that reads information from a +manifest file that was first needed to load the toolstrip info. So we need to +add code in there to load the information related to user scripts. + +Since this class has a one to one relationship with a manifest file, we need +another one to hold on all of the scripts from all the extensions that we will +eventually read from and make them accessible to the page API to load the ones +that match the current URL. + +So this other class (UserScriptsLibrarian) has a method to add UserScripts and +to retrieve the CSS and JavaScript content of the ones that match a given URL. +It reuses the UserScript object. + +TODO(siggi@chromium.org): Unbranch this code; a lot of it was taken +from (and adapted) the UserScriptSlave class which is used to apply +user scripts to a given WebKit frame. + +Our version simply returns the JavaScript code and CSS as text and let the +PageApi class take care of the insertion in the browser script engine. + +The PageAPI code must react to the earliest event after the creation of the +HTML document so that we can inject the CSS content before the page is rendered. +For the user script, we have not yet found a way to inject the scripts that +specify the start location. + +@section Firefox + +TODO: The implementation has changed, so we should update this doc. + +@section IEFF For both IE and Firefox + +The JavaScript code returned by the functions extracting them from the user +scripts contain the proper heading to emulate the Greasemonkey API +(taken from greasemonkey_api.js) for running scripts marked as stand alone +(i.e., an extension without a public key), and it also wraps them +individually within an anonymous function. + +The code taking care of injecting the code in chromium (in UserScriptSlave) +concatenate all the scripts of a dictionary entry in the +manifest file, and executes them in a separate context. +@note However, this is being changed: http://crbug.com/22110. + +We don't have this capacity yet but the code must be written in a way that it +will be easy to add this later on, so the methods returning the script code +should only concatenate together the scripts of a single dictionary entry at a +time. The CSS styles can all be bundled up in a single string though. +**/ + +#endif // CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ diff --git a/ceee/ie/plugin/scripting/userscripts_librarian.cc b/ceee/ie/plugin/scripting/userscripts_librarian.cc new file mode 100644 index 0000000..adeb4e3 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian.cc @@ -0,0 +1,119 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to manage user scripts. + +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/string_util.h" + +UserScriptsLibrarian::UserScriptsLibrarian() { +} + +UserScriptsLibrarian::~UserScriptsLibrarian() { +} + +HRESULT UserScriptsLibrarian::AddUserScripts( + const UserScriptList& user_scripts) { + // Note that we read the userscript files from disk every time that a frame + // loads (one BHO and one librarian per frame). This offers us the advantage + // of not having to reload scripts into memory when an extension autoupdates + // in Chrome. If we decide to cache these scripts in memory, then we will need + // a Chrome extension autoupdate automation notification. + + size_t i = user_scripts_.size(); + user_scripts_.insert(user_scripts_.end(), user_scripts.begin(), + user_scripts.end()); + for (; i < user_scripts_.size(); ++i) { + UserScript& user_script = user_scripts_[i]; + UserScript::FileList& js_scripts = user_script.js_scripts(); + LoadFiles(&js_scripts); + UserScript::FileList& css_scripts = user_script.css_scripts(); + LoadFiles(&css_scripts); + } + + return S_OK; +} + +bool UserScriptsLibrarian::HasMatchingUserScripts(const GURL& url) const { + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + if (user_script.MatchesUrl(url)) + return true; + } + return false; +} + +HRESULT UserScriptsLibrarian::GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) const { + DCHECK(css_content); + if (!css_content) + return E_POINTER; + + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + if (!user_script.MatchesUrl(url) || + (require_all_frames && !user_script.match_all_frames())) + continue; + + for (size_t j = 0; j < user_script.css_scripts().size(); ++j) { + const UserScript::File& file = user_script.css_scripts()[j]; + *css_content += file.GetContent().as_string(); + } + } + + return S_OK; +} + +HRESULT UserScriptsLibrarian::GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, bool require_all_frames, + JsFileList* js_file_list) { + DCHECK(js_file_list); + if (!js_file_list) + return E_POINTER; + + if (!user_scripts_.empty()) { + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + + // TODO(ericdingle@chromium.org): Remove the fourth and fifth + // conditions once DOCUMENT_IDLE is supported. + if (!user_script.MatchesUrl(url) || + (require_all_frames && !user_script.match_all_frames()) || + user_script.run_location() != location && + (user_script.run_location() != UserScript::DOCUMENT_IDLE || + location != UserScript::DOCUMENT_END)) + continue; + + for (size_t j = 0; j < user_script.js_scripts().size(); ++j) { + const UserScript::File& file = user_script.js_scripts()[j]; + + js_file_list->push_back(JsFile()); + JsFile& js_file = (*js_file_list)[js_file_list->size()-1]; + js_file.file_path = + file.extension_root().Append(file.relative_path()).value(); + js_file.content = file.GetContent().as_string(); + } + } + } + + return S_OK; +} + +void UserScriptsLibrarian::LoadFiles(UserScript::FileList* file_list) { + for (size_t i = 0; i < file_list->size(); ++i) { + UserScript::File& script_file = (*file_list)[i]; + // The content may have been set manually (e.g., for unittests). + // So we first check if they need to be loaded from the file, or not. + if (script_file.GetContent().empty()) { + std::string content; + file_util::ReadFileToString( + script_file.extension_root().Append( + script_file.relative_path()), &content); + script_file.set_content(content); + } + } +} diff --git a/ceee/ie/plugin/scripting/userscripts_librarian.h b/ceee/ie/plugin/scripting/userscripts_librarian.h new file mode 100644 index 0000000..e554a08 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian.h @@ -0,0 +1,73 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to maintain user scripts and their matching URLs. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ + +#include <windows.h> + +#include <string> +#include <vector> + +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + + +// The manager of UserScripts responsible for loading them from an extension +// and then returning the ones that match a given URL. +// TODO(mad@chromium.org): Find a way to reuse code from +// chrome/renderer/user_script_slave. +class UserScriptsLibrarian { + public: + typedef struct { + std::wstring file_path; + std::string content; + } JsFile; + typedef std::vector<JsFile> JsFileList; + + UserScriptsLibrarian(); + ~UserScriptsLibrarian(); + + // Adds a list of users scripts to our current list. + HRESULT AddUserScripts(const UserScriptList& user_scripts); + + // Identifies if we have any userscript that would match the given URL. + bool HasMatchingUserScripts(const GURL& url) const; + + // Retrieve the CSS content from user scripts that match the given URL. + // @param url The URL to match. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param css_content The single stream of CSS content. + HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, + bool require_all_frames, + std::string* css_content) const; + + // Retrieve the JS content from user scripts that match the given URL. + // @param url The URL to match. + // @param location The location where the scripts will be run at. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param js_file_list A map of file names to JavaScript content to allow the + // caller to apply them individually (e.g., each with their own script + // engine). + HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + JsFileList* js_file_list); + + private: + // A helper function to load the content of a script file if it has not + // already been done, or explicitly set. + void LoadFiles(UserScript::FileList* file_list); + + UserScriptList user_scripts_; + DISALLOW_COPY_AND_ASSIGN(UserScriptsLibrarian); +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ diff --git a/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc b/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc new file mode 100644 index 0000000..0a3a22b --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc @@ -0,0 +1,332 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ExtensionManifest. + +#include <atlconv.h> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kExtensionId[] = "abcdefghijklmnopqabcdefghijklmno"; +const char kCssContent[] = "body:after {content: \"The End\";}"; +const char kJsContent[] = "alert('red');"; +const char kUrlPattern1[] = "http://madlymad.com/*"; +const char kUrlPattern2[] = "https://superdave.com/*"; +const char kUrl1[] = "http://madlymad.com/index.html"; +const char kUrl2[] = "https://superdave.com/here.blue"; +const char kUrl3[] = "http://not.here.com/there.where"; +const wchar_t kJsPath1[] = L"script1.js"; +const wchar_t kJsPath2[] = L"script2.js"; +const wchar_t kCssFileName[] = L"CssFile.css"; + +TEST(UserScriptsLibrarianTest, Empty) { + testing::LogDisabler no_dchecks; + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(UserScriptList()); + + std::string css; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(), true, &css)); + EXPECT_TRUE(css.empty()); + + UserScriptsLibrarian::JsFileList js; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(), UserScript::DOCUMENT_START, true, &js)); + EXPECT_TRUE(js.empty()); +} + +TEST(UserScriptsLibrarianTest, SingleScript) { + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + // Set up the librarian. + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Matching URL + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Non matching URL + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl2), false, &css_content)); + EXPECT_TRUE(css_content.empty()); + + // Matching URL and run location. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Matching URL and non matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_END, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Non matching URL and matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Non matching URL and non matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_END, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); +} + +TEST(UserScriptsLibrarianTest, MultipleScript) { + testing::LogDisabler no_dchecks; + + // Set up one UserScript object + URLPattern url_pattern1(UserScript::kValidUserScriptSchemes); + url_pattern1.Parse(kUrlPattern1); + + UserScript user_script1; + user_script1.add_url_pattern(url_pattern1); + user_script1.set_extension_id(kExtensionId); + + user_script1.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script1.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script1.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script1.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list1; + user_script_list1.push_back(user_script1); + + // Set up a second UserScript object + URLPattern url_pattern2(UserScript::kValidUserScriptSchemes); + url_pattern2.Parse(kUrlPattern2); + + UserScript user_script2; + user_script2.add_url_pattern(url_pattern2); + user_script2.set_extension_id(kExtensionId); + + user_script2.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file2 = user_script2.js_scripts().back(); + js_file2.set_content(kJsContent); + + user_script_list1.push_back(user_script2); + + // Set up the librarian. + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list1); + + // Matching URL + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(css_content.c_str(), kCssContent); + + // Matching URL and non matching location + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Matching URL and location + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_END, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Matching URL and location, shouldn't have extension init + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_END, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); +} + +TEST(UserScriptsLibrarianTest, SingleScriptFromFile) { + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + FilePath extension_folder; + EXPECT_TRUE(file_util::GetTempDir(&extension_folder)); + FilePath css_path(extension_folder.Append(kCssFileName)); + + user_script.css_scripts().push_back(UserScript::File( + extension_folder, FilePath(kCssFileName), GURL())); + + FILE* temp_file = file_util::OpenFile(css_path, "w"); + EXPECT_TRUE(temp_file != NULL); + + fwrite(kCssContent, ::strlen(kCssContent), 1, temp_file); + file_util::CloseFile(temp_file); + temp_file = NULL; + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + EXPECT_TRUE(file_util::Delete(css_path, false)); + + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_EQ(0, js_content_list.size()); +} + +TEST(UserScriptsLibrarianTest, MatchAllFramesFalse) { + // all_frames is set to false. We should only get user scripts back + // when we pass false for require_all_frames to GetMatching*. + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Get CSS with require_all_frames set to false. + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get CSS with require_all_frames set to true. + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), true, &css_content)); + EXPECT_TRUE(css_content.empty()); + + // Get JS with require_all_frames set to false. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Get JS with require_all_frames set to true. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, true, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); +} + +TEST(UserScriptsLibrarianTest, MatchAllFramesTrue) { + // all_frames is set to true. We should get user scripts back + // when we pass any value for require_all_frames to GetMatching*. + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + user_script.set_match_all_frames(true); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Get CSS with require_all_frames set to false. + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get CSS with require_all_frames set to true. + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), true, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get JS with require_all_frames set to false. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Get JS with require_all_frames set to true. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, true, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); +} + +} // namespace diff --git a/ceee/ie/plugin/toolband/resource.h b/ceee/ie/plugin/toolband/resource.h new file mode 100644 index 0000000..d090e40 --- /dev/null +++ b/ceee/ie/plugin/toolband/resource.h @@ -0,0 +1,35 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Resource constants for CEEE toolband. +#ifndef CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ + + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by toolband.rc +// +#define IDS_PROJNAME 100 +#define IDR_IE 101 +#define IDR_BROWSERHELPEROBJECT 102 +#define IDR_TOOL_BAND 103 +#define IDR_GREASEMONKEY_API_JS 105 +#define IDR_EXECUTOR 106 +#define IDR_EXECUTOR_CREATOR 107 +#define IDR_NO_EXTENSION 108 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 109 +#define _APS_NEXT_COMMAND_VALUE 32768 +#define _APS_NEXT_CONTROL_VALUE 201 +#define _APS_NEXT_SYMED_VALUE 109 +#endif +#endif + + +#endif // CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ diff --git a/ceee/ie/plugin/toolband/tool_band.cc b/ceee/ie/plugin/toolband/tool_band.cc new file mode 100644 index 0000000..b8dfcea --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.cc @@ -0,0 +1,628 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband implementation. +#include "ceee/ie/plugin/toolband/tool_band.h" + +#include <atlsafe.h> +#include <atlstr.h> +#include <shlguid.h> + +#include "base/debug/trace_event.h" +#include "base/file_path.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/automation/automation_constants.h" +#include "chrome_frame/com_message_event.h" + +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO ToolBand::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_bstr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BSTR } }; +_ATL_FUNC_INFO ToolBand::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO ToolBand::handler_type_bstrarray_= + { CC_STDCALL, VT_EMPTY, 1, { VT_ARRAY | VT_BSTR } }; +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_variantref_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_VARIANT | VT_BYREF } }; + +ToolBand::ToolBand() + : already_tried_installing_(false), + own_line_flag_(false), + already_checked_own_line_flag_(false), + listening_to_browser_events_(false), + band_id_(0), + is_quitting_(false), + current_width_(64), + current_height_(25) { + TRACE_EVENT_BEGIN("ceee.toolband", this, ""); +} + +ToolBand::~ToolBand() { + TRACE_EVENT_END("ceee.toolband", this, ""); +} + +HRESULT ToolBand::FinalConstruct() { + return S_OK; +} + +void ToolBand::FinalRelease() { +} + +STDMETHODIMP ToolBand::SetSite(IUnknown* site) { + typedef IObjectWithSiteImpl<ToolBand> SuperSite; + + // From experience we know the site may be set multiple times. + // Let's ignore second and subsequent set or unset. + if (NULL != site && NULL != m_spUnkSite.p || + NULL == site && NULL == m_spUnkSite.p) { + // TODO(siggi@chromium.org) log this. + return S_OK; + } + + if (NULL == site) { + // We're being torn down. + Teardown(); + } + + HRESULT hr = SuperSite::SetSite(site); + if (FAILED(hr)) + return hr; + + if (NULL != site) { + // We're being initialized. + hr = Initialize(site); + + // Release the site in case of failure. + if (FAILED(hr)) + SuperSite::SetSite(NULL); + } + + return hr; +} + +STDMETHODIMP ToolBand::ShowDW(BOOL show) { + ShowWindow(show ? SW_SHOW : SW_HIDE); + if (show) { + // Report that the toolband is being shown, so that the BHO + // knows it doesn't need to explicitly make it visible. + ToolBandVisibility::ReportToolBandVisible(web_browser_); + } + // Unless ShowDW changes are explicitly being ignored (e.g. if the + // BHO is forcing the toolband to be visible via + // ShowBrowserBar), or unless the toolband is closing on quit, then we assume + // a call to ShowDW reflects the user's toolband visibility choice, modifiable + // through the View -> Toolbars menu in IE. We track this choice here. + if (!ceee_module_util::GetIgnoreShowDWChanges() && !is_quitting_) { + ceee_module_util::SetOptionToolbandIsHidden(show == FALSE); + } + return S_OK; +} + +STDMETHODIMP ToolBand::CloseDW(DWORD reserved) { + // Indicates to ShowDW() that the tool band is being closed, as opposed to + // being explicitly hidden by the user. + is_quitting_ = true; + return ShowDW(FALSE); +} + +STDMETHODIMP ToolBand::ResizeBorderDW(LPCRECT border, + IUnknown* toolband_site, + BOOL reserved) { + DCHECK(FALSE); // Not used for toolbands. + return E_NOTIMPL; +} + +STDMETHODIMP ToolBand::GetBandInfo(DWORD band_id, + DWORD view_mode, + DESKBANDINFO* deskband_info) { + band_id_ = band_id; + + // We're only registered as a horizontal band. + DCHECK(view_mode == DBIF_VIEWMODE_NORMAL); + + if (!deskband_info) + return E_POINTER; + + if (deskband_info->dwMask & DBIM_MINSIZE) { + deskband_info->ptMinSize.x = current_width_; + deskband_info->ptMinSize.y = current_height_; + } + + if (deskband_info->dwMask & DBIM_MAXSIZE) { + deskband_info->ptMaxSize.x = -1; + deskband_info->ptMaxSize.y = -1; + } + + if (deskband_info->dwMask & DBIM_INTEGRAL) { + deskband_info->ptIntegral.x = 1; + deskband_info->ptIntegral.y = 1; + } + + if (deskband_info->dwMask & DBIM_ACTUAL) { + // By not setting, we just use the default. + // deskband_info->ptActual.x = 7000; + deskband_info->ptActual.y = current_height_; + } + + if (deskband_info->dwMask & DBIM_TITLE) { + // Title is empty. + deskband_info->wszTitle[0] = 0; + } + + if (deskband_info->dwMask & DBIM_MODEFLAGS) { + deskband_info->dwModeFlags = DBIMF_NORMAL /* | DBIMF_TOPALIGN */; + + if (ShouldForceOwnLine()) { + deskband_info->dwModeFlags |= DBIMF_BREAK; + } + } + + if (deskband_info->dwMask & DBIM_BKCOLOR) { + // Use the default background color by removing this flag. + deskband_info->dwMask &= ~DBIM_BKCOLOR; + } + return S_OK; +} + +STDMETHODIMP ToolBand::GetWindow(HWND* window) { + *window = m_hWnd; + return S_OK; +} + +STDMETHODIMP ToolBand::ContextSensitiveHelp(BOOL enter_mode) { + LOG(INFO) << "ContextSensitiveHelp"; + return E_NOTIMPL; +} + +STDMETHODIMP ToolBand::GetClassID(CLSID* clsid) { + *clsid = GetObjectCLSID(); + return S_OK; +} + +STDMETHODIMP ToolBand::IsDirty() { + return S_FALSE; // Never dirty for now. +} + +STDMETHODIMP ToolBand::Load(IStream* stream) { + return S_OK; // Loading is no-op. +} + +STDMETHODIMP ToolBand::Save(IStream* stream, BOOL clear_dirty) { + return S_OK; // Saving is no-op. +} + +STDMETHODIMP ToolBand::GetSizeMax(ULARGE_INTEGER* size) { + size->QuadPart = 0ULL; // We're frugal. + return S_OK; +} + +STDMETHODIMP ToolBand::GetWantsPrivileged(boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP ToolBand::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP ToolBand::GetChromeProfileName(BSTR* profile_name) { + *profile_name = ::SysAllocString( + ceee_module_util::GetBrokerProfileNameForIe()); + return S_OK; +} + +STDMETHODIMP ToolBand::GetExtensionApisToAutomate(BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +HRESULT ToolBand::Initialize(IUnknown* site) { + TRACE_EVENT_INSTANT("ceee.toolband.initialize", this, ""); + + CComQIPtr<IServiceProvider> service_provider = site; + DCHECK(service_provider); + if (service_provider == NULL) { + return E_FAIL; + } + + HRESULT hr = InitializeAndShowWindow(site); + + if (FAILED(hr)) { + LOG(ERROR) << "Toolband failed to initalize its site window: " << + com::LogHr(hr); + return hr; + } + + // Store the web browser, used to report toolband visibility to + // the BHO. Also required to get navigate2 notification. + hr = service_provider->QueryService( + SID_SWebBrowserApp, IID_IWebBrowser2, + reinterpret_cast<void**>(&web_browser_)); + + DCHECK(SUCCEEDED(hr)); + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get web browser: 0x" << std::hex << hr; + return hr; + } else if (ShouldForceOwnLine()) { + // This may seem odd, but event subscription is required + // only to clear 'own line' flag later (see OnIeNavigateComplete2) + hr = HostingBrowserEvents::DispEventAdvise(web_browser_, + &DIID_DWebBrowserEvents2); + listening_to_browser_events_ = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr)) << + "DispEventAdvise on web browser failed. Error: " << hr; + // Non-critical functionality. If fails in the field, just move on. + } + + return S_OK; +} + +HRESULT ToolBand::InitializeAndShowWindow(IUnknown* site) { + CComPtr<IOleWindow> site_window; + HRESULT hr = site->QueryInterface(&site_window); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get site window: " << com::LogHr(hr); + return hr; + } + + DCHECK(NULL != site_window.p); + hr = site_window->GetWindow(&parent_window_.m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get parent window handle: " << com::LogHr(hr); + return hr; + } + DCHECK(parent_window_); + if (!parent_window_) + return E_FAIL; + + if (NULL == Create(parent_window_)) + return E_FAIL; + + BOOL shown = ShowWindow(SW_SHOW); + DCHECK(shown); + + return hr; +} + +HRESULT ToolBand::Teardown() { + TRACE_EVENT_INSTANT("ceee.toolband.teardown", this, ""); + + if (IsWindow()) { + // Teardown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + } + + if (web_browser_ && listening_to_browser_events_) { + HostingBrowserEvents::DispEventUnadvise(web_browser_, + &DIID_DWebBrowserEvents2); + } + listening_to_browser_events_ = false; + + return S_OK; +} + +void ToolBand::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +LRESULT ToolBand::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CAxHostWindow::CreateInstance(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create ActiveX host window. " << com::LogHr(hr); + return 1; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = chrome_frame_.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create the Chrome Frame instance. " << + com::LogHr(hr); + return 1; + } + + // And attach it to our window. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host. " << + com::LogHr(hr); + return 1; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink. " << com::LogHr(hr); + } + + return 0; +} + +void ToolBand::OnPaint(CDCHandle dc) { + RECT rc = {}; + if (GetUpdateRect(&rc, FALSE)) { + PAINTSTRUCT ps = {}; + BeginPaint(&ps); + + BOOL ret = GetClientRect(&rc); + DCHECK(ret); + CString text; + text.Format(L"Google CEEE. No Chrome Frame found. Instance: 0x%p. ID: %d!)", + this, band_id_); + ::DrawText(ps.hdc, text, -1, &rc, DT_SINGLELINE | DT_BOTTOM | DT_CENTER); + + EndPaint(&ps); + } +} + +void ToolBand::OnSize(UINT type, CSize size) { + LOG(INFO) << "ToolBand::OnSize(" << type << ", " + << size.cx << "x" << size.cy << ")"; + CWindow chrome_window = ::GetWindow(m_hWnd, GW_CHILD); + if (!chrome_window) { + LOG(ERROR) << "Failed to retrieve Chrome Frame window"; + return; + } + + BOOL resized = chrome_window.ResizeClient(size.cx, size.cy); + DCHECK(resized); +} + +STDMETHODIMP_(void) ToolBand::OnCfReadyStateChanged(LONG state) { + DLOG(INFO) << "OnCfReadyStateChanged(" << state << ")"; + + if (state == READYSTATE_COMPLETE) { + extension_path_ = ceee_module_util::GetExtensionPath(); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_) && + ceee_module_util::NeedToInstallExtension()) { + LOG(INFO) << "Installing extension: \"" << extension_path_ << "\""; + chrome_frame_->installExtension(CComBSTR(extension_path_.c_str())); + } else { + // In the case where we don't have a CRX (or we don't need to install it), + // we must ask for the currently enabled extension before we can decide + // what we need to do. + chrome_frame_->getEnabledExtensions(); + } + } +} + +STDMETHODIMP_(void) ToolBand::OnCfMessage(IDispatch* event) { + VARIANT origin = {VT_NULL}; + HRESULT hr = event->Invoke(ComMessageEvent::DISPID_MESSAGE_EVENT_ORIGIN, + IID_NULL, 0, DISPATCH_PROPERTYGET, 0, &origin, 0, 0); + if (FAILED(hr) || origin.vt != VT_BSTR) { + DLOG(WARNING) << __FUNCTION__ << ": unable to discern message origin."; + return; + } + + VARIANT data_bstr = {VT_NULL}; + hr = event->Invoke(ComMessageEvent::DISPID_MESSAGE_EVENT_DATA, + IID_NULL, 0, DISPATCH_PROPERTYGET, 0, &data_bstr, 0, 0); + if (FAILED(hr) || data_bstr.vt != VT_BSTR) { + DLOG(INFO) << __FUNCTION__ << ": no message data. Origin:" + << origin.bstrVal; + return; + } + DLOG(INFO) << __FUNCTION__ << ": Origin: " << origin.bstrVal + << ", Data: " << data_bstr.bstrVal; + CString data(data_bstr); + + // Handle CEEE-specific messages. + // TODO(skare@google.com): If we will need this for more than one + // message, consider making responses proper JSON. + + // ceee_getCurrentWindowId: chrome.windows.getCurrent workaround. + CString message; + if (data == L"ceee_getCurrentWindowId") { + HWND browser_window = 0; + web_browser_->get_HWND(reinterpret_cast<long*>(&browser_window)); + bool is_ieframe = window_utils::IsWindowClass(browser_window, + windows::kIeFrameWindowClass); + if (is_ieframe) { + message.Format(L"ceee_getCurrentWindowId %d", browser_window); + } else { + DCHECK(is_ieframe); + LOG(WARNING) << "Could not find IE Frame window."; + message = L"ceee_getCurrentWindowId -1"; + } + } + + if (!message.IsEmpty()) { + chrome_frame_->postMessage(CComBSTR(message), origin); + } +} + +void ToolBand::StartExtension(const wchar_t* base_dir) { + if (!LoadManifestFile(base_dir, &extension_url_)) { + LOG(ERROR) << "No extension found"; + } else { + HRESULT hr = chrome_frame_->put_src(CComBSTR(extension_url_.c_str())); + DCHECK(SUCCEEDED(hr)); + LOG_IF(WARNING, FAILED(hr)) << "IChromeFrame::put_src returned: " << + com::LogHr(hr); + } +} + +STDMETHODIMP_(void) ToolBand::OnCfExtensionReady(BSTR path, int response) { + TRACE_EVENT_INSTANT("ceee.toolband.oncfextensionready", this, ""); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath( + FilePath(extension_path_)); + } + + // Now list enabled extensions so that we can properly start it whether + // it's a CRX file or an exploded folder. + // + // Note that we do this even if Chrome says installation failed, + // as that is the error code it uses when we try to install an + // older version of the extension than it already has, which happens + // on overinstall when Chrome has already auto-updated. + // + // If it turns out no extension is installed, we will handle that + // error in the OnCfGetEnabledExtensionsComplete callback. + chrome_frame_->getEnabledExtensions(); +} + +STDMETHODIMP_(void) ToolBand::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* extension_directories) { + CComSafeArray<BSTR> directories; + directories.Attach(extension_directories); // MUST DETACH BEFORE RETURNING + + // TODO(joi@chromium.org) Handle multiple extensions. + if (directories.GetCount() > 0) { + // If our extension_path is not a CRX, it MUST be the same as the installed + // extension path which would be an exploded extension. + // If you get this DCHECK, you may have changed your registry settings to + // debug with an exploded extension, but you didn't uninstall the previous + // extension, either via the Chrome UI or by simply wiping out your + // profile folder. + DCHECK(ceee_module_util::IsCrxOrEmpty(extension_path_) || + extension_path_ == std::wstring(directories.GetAt(0))); + StartExtension(directories.GetAt(0)); + } else if (!ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // We have an extension path that isn't a CRX and we don't have any + // enabled extension, so we must load the exploded extension from this + // given path. WE MUST DO THIS BEFORE THE NEXT ELSE IF because it assumes + // a CRX file. + chrome_frame_->loadExtension(CComBSTR(extension_path_.c_str())); + } else if (!already_tried_installing_ && !extension_path_.empty()) { + // We attempt to install the .crx file from the CEEE folder; in the + // default case this will happen only once after installation. + // It may seem redundant with OnCfReadyStateChanged; this is in case the + // user deleted the extension but the registry stayed the same. + already_tried_installing_ = true; + chrome_frame_->installExtension(CComBSTR(extension_path_.c_str())); + } else { + // Hide the browser bar as fast as we can. + // Set the current height of the bar to 0, so that if the user manually + // shows the bar, it will not be visible on screen. + current_height_ = 0; + + // Ask IE to reload all info for this toolband. + CComPtr<IOleCommandTarget> cmd_target; + HRESULT hr = GetSite(IID_IOleCommandTarget, + reinterpret_cast<void**>(&cmd_target)); + if (SUCCEEDED(hr)) { + CComVariant band_id(static_cast<int>(band_id_)); + hr = cmd_target->Exec(&CGID_DeskBand, DBID_BANDINFOCHANGED, + OLECMDEXECOPT_DODEFAULT, &band_id, NULL); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to Execute DBID_BANDINFOCHANGED. Error Code: " + << com::LogHr(hr); + } + } else { + LOG(ERROR) << "Failed to obtain OleCommandTarget. Error Code: " + << com::LogHr(hr); + } + } + directories.Detach(); +} + +STDMETHODIMP_(void) ToolBand::OnIeNavigateComplete2(IDispatch* dispatch, + VARIANT* url) { + // The flag is cleared on navigation complete since at this point we are + // certain the process of placing the toolband has been completed. + // Doing it in GetBandInfo proved premature as many queries are expeced. + ClearForceOwnLineFlag(); + + // We need to clear that flag just once. Now that's done, unadvise. + DCHECK(web_browser_ != NULL); + if (web_browser_ && listening_to_browser_events_) { + HostingBrowserEvents::DispEventUnadvise(web_browser_, + &DIID_DWebBrowserEvents2); + listening_to_browser_events_ = false; + } +} + +bool ToolBand::LoadManifestFile(const std::wstring& base_dir, + std::string* toolband_url) { + DCHECK(toolband_url); + FilePath toolband_extension_path; + toolband_extension_path = FilePath(base_dir); + + if (toolband_extension_path.empty()) { + // Expected case if no extensions registered/found. + return false; + } + + ExtensionManifest manifest; + HRESULT hr = manifest.ReadManifestFile(toolband_extension_path, true); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to read manifest at \"" << + toolband_extension_path.value() << "\", error " << com::LogHr(hr); + return false; + } + + const std::vector<std::string>& toolstrip_names( + manifest.GetToolstripFileNames()); + if (!toolstrip_names.empty()) { + *toolband_url = "chrome-extension://"; + *toolband_url += manifest.extension_id(); + *toolband_url += "/"; + // TODO(mad@chromium.org): For now we only load the first one we + // find, we may want to stack them at one point... + *toolband_url += toolstrip_names[0]; + } + + return true; +} + +bool ToolBand::ShouldForceOwnLine() { + if (!already_checked_own_line_flag_) { + own_line_flag_ = ceee_module_util::GetOptionToolbandForceReposition(); + already_checked_own_line_flag_ = true; + } + + return own_line_flag_; +} + +void ToolBand::ClearForceOwnLineFlag() { + if (own_line_flag_ || !already_checked_own_line_flag_) { + own_line_flag_ = false; + already_checked_own_line_flag_ = true; + ceee_module_util::SetOptionToolbandForceReposition(false); + } +} diff --git a/ceee/ie/plugin/toolband/tool_band.h b/ceee/ie/plugin/toolband/tool_band.h new file mode 100644 index 0000000..33ad61c --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.h @@ -0,0 +1,247 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband implementation. +#ifndef CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcom.h> +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlwin.h> +#include <atlmisc.h> +#include <exdispid.h> +#include <shobjidl.h> +#include <list> +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/toolband/resource.h" + +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +class DictionaryValue; +class PageApi; +class ToolBand; +class DictionaryValue; +class Value; + +typedef IDispEventSimpleImpl<0, ToolBand, &DIID_DIChromeFrameEvents> + ChromeFrameEvents; + +typedef IDispEventSimpleImpl<1, ToolBand, &DIID_DWebBrowserEvents2> + HostingBrowserEvents; + +// Implements an IE toolband which gets instantiated for every IE browser tab +// and renders by hosting chrome frame as an ActiveX control. +class ATL_NO_VTABLE ToolBand : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<ToolBand, &CLSID_ToolBand>, + public IObjectWithSiteImpl<ToolBand>, + public IServiceProviderImpl<ToolBand>, + public IChromeFramePrivileged, + public IDeskBand, + public IPersistStream, + public ChromeFrameEvents, + public HostingBrowserEvents, + public CWindowImpl<ToolBand> { + public: + ToolBand(); + ~ToolBand(); + + DECLARE_REGISTRY_RESOURCEID(IDR_TOOL_BAND) + + BEGIN_COM_MAP(ToolBand) + COM_INTERFACE_ENTRY(IDeskBand) + COM_INTERFACE_ENTRY(IDockingWindow) + COM_INTERFACE_ENTRY(IOleWindow) + COM_INTERFACE_ENTRY(IPersist) + COM_INTERFACE_ENTRY(IPersistStream) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(ToolBand) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + + BEGIN_SINK_MAP(ToolBand) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + OnCfGetEnabledExtensionsComplete, &handler_type_bstrarray_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONMESSAGE, + OnCfMessage, &handler_type_idispatch_) + SINK_ENTRY_INFO(1, DIID_DWebBrowserEvents2, + DISPID_NAVIGATECOMPLETE2, + OnIeNavigateComplete2, &handler_type_idispatch_variantref_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + HRESULT FinalConstruct(); + void FinalRelease(); + + BEGIN_MSG_MAP(ToolBand) + MSG_WM_CREATE(OnCreate) + MSG_WM_PAINT(OnPaint) + MSG_WM_SIZE(OnSize) + END_MSG_MAP() + + // @name IObjectWithSite overrides. + STDMETHOD(SetSite)(IUnknown *site); + + // @name IDockingWindow implementation. + // @{ + STDMETHOD(ShowDW)(BOOL show); + STDMETHOD(CloseDW)(DWORD reserved); + STDMETHOD(ResizeBorderDW)(LPCRECT border, IUnknown *toolband_site, + BOOL reserved); + // @} + + // @name IDeskBand implementation. + STDMETHOD(GetBandInfo)(DWORD band_id, DWORD view_mode, + DESKBANDINFO *deskband_info); + + // @name IOleWindow implementation. + // @{ + STDMETHOD(GetWindow)(HWND *window); + STDMETHOD(ContextSensitiveHelp)(BOOL enter_mode); + // @} + + // @name IPersist implementation. + STDMETHOD(GetClassID)(CLSID *clsid); + + // @name IPersistStream implementation. + // @{ + STDMETHOD(IsDirty)(); + STDMETHOD(Load)(IStream *stream); + STDMETHOD(Save)(IStream *stream, BOOL clear_dirty); + STDMETHOD(GetSizeMax)(ULARGE_INTEGER *size); + // @} + + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean *wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR *args); + STDMETHOD(GetChromeProfileName)(BSTR *args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR *args); + // @} + + + // @name ChromeFrame event handlers + // @{ + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfGetEnabledExtensionsComplete)( + SAFEARRAY* extension_directories); + STDMETHOD_(void, OnCfMessage)(IDispatch* event); + STDMETHOD_(void, OnIeNavigateComplete2)(IDispatch* dispatch, VARIANT* url); + // @} + + protected: + // Our window maintains a refcount on us for the duration of its lifetime. + // The self-reference is managed with those two methods. + virtual void OnFinalMessage(HWND window); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + // Loads the manifest from file and retrieves the URL to the extension. + // @returns true on success, false on failure to read the manifest or URL. + bool LoadManifestFile(const std::wstring& base_dir, + std::string* toolband_url); + + // @name Message handlers. + // @{ + void OnPaint(CDCHandle dc); + void OnSize(UINT type, CSize size); + // @} + + private: + // Initializes the toolband to the given site. + // Called from SetSite. + HRESULT Initialize(IUnknown *site); + // Tears down an initialized toolband. + // Called from SetSite. + HRESULT Teardown(); + + // Handles the dispatching of command received from the User/UI context. + HRESULT DispatchUserCommand(const DictionaryValue& dict, + scoped_ptr<Value>* return_value); + + // Parses the manifest and navigates CF to the toolband URL. + void StartExtension(const wchar_t* base_dir); + + // Subroutine of general initialization. Extracted to make testable. + virtual HRESULT InitializeAndShowWindow(IUnknown* site); + + // The OwnLine flag indicates that the toolband should request from the IE + // host to put it in its own space (and not behind whatever toolband might + // have been installed first). + bool ShouldForceOwnLine(); + void ClearForceOwnLineFlag(); + + // The web browser that initialized this toolband. + CComPtr<IWebBrowser2> web_browser_; + // Our parent window, yielded by our site's IOleWindow. + CWindow parent_window_; + // Our band id, provided by GetBandInfo. + DWORD band_id_; + + // The minimum size the toolband should take. + LONG current_width_; + LONG current_height_; + + // The URL to our extension. + std::string extension_url_; + + // Our Chrome frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // Indicates whether CloseDW() is being called on this tool band. + bool is_quitting_; + + // True if we noticed that no extensions are enabled and requested + // to install one. + bool already_tried_installing_; + + // Flag purpose: see comments to ShouldForceOwnLine + // for efficiency we read only once (thus the second flag). + bool own_line_flag_; + bool already_checked_own_line_flag_; + + // Listening to DIID_DWebBrowserEvents2 is optional. For registration + // purposes we have to know if we are listening, though. + bool listening_to_browser_events_; + + // Filesystem path to the .crx we will install, or the empty string, or + // (if not ending in .crx) the path to an exploded extension directory to + // load. + std::wstring extension_path_; + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_; + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_idispatch_bstr_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_bstrarray_; + static _ATL_FUNC_INFO handler_type_idispatch_variantref_; + + DISALLOW_COPY_AND_ASSIGN(ToolBand); +}; + +#endif // CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ diff --git a/ceee/ie/plugin/toolband/tool_band.rgs b/ceee/ie/plugin/toolband/tool_band.rgs new file mode 100644 index 0000000..0a7ec34 --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.rgs @@ -0,0 +1,20 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B}' = s 'Google Chrome Extensions Execution Environment' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + } + } +} +HKLM { + NoRemove Software { + NoRemove Microsoft { + NoRemove 'Internet Explorer' { + NoRemove Toolbar { + val '{2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B}' = s 'Google Chrome Extensions Execution Environment' + } + } + } + } +} diff --git a/ceee/ie/plugin/toolband/tool_band_unittest.cc b/ceee/ie/plugin/toolband/tool_band_unittest.cc new file mode 100644 index 0000000..cc8c3ca --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band_unittest.cc @@ -0,0 +1,363 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband unit tests. +#include "ceee/ie/plugin/toolband/tool_band.h" + +#include <exdisp.h> +#include <shlguid.h> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/ie/testing/mock_browser_and_friends.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::GetConnectionCount; +using testing::InstanceCountMixin; +using testing::MockDispatchEx; +using testing::Return; +using testing::StrictMock; +using testing::TestBrowser; +using testing::TestBrowserSite; + +// Makes ToolBand testable - circumvents InitializeAndShowWindow. +class TestingToolBand + : public ToolBand, + public InstanceCountMixin<TestingToolBand>, + public InitializingCoClass<TestingToolBand> { + public: + HRESULT Initialize(TestingToolBand** self) { + *self = this; + return S_OK; + } + private: + virtual HRESULT InitializeAndShowWindow(IUnknown* site) { + return S_OK; // This aspect is not tested. + } +}; + +class ToolBandTest: public testing::Test { + public: + ToolBandTest() : tool_band_(NULL), site_(NULL), browser_(NULL) { + } + + ~ToolBandTest() { + } + + virtual void SetUp() { + // Create the instance to test. + ASSERT_HRESULT_SUCCEEDED( + TestingToolBand::CreateInitialized(&tool_band_, &tool_band_with_site_)); + tool_band_with_site_ = tool_band_; + + ASSERT_TRUE(tool_band_with_site_ != NULL); + } + + virtual void TearDown() { + tool_band_ = NULL; + tool_band_with_site_.Release(); + + site_ = NULL; + site_keeper_.Release(); + + browser_ = NULL; + browser_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateSite() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, &site_keeper_)); + } + + void CreateBrowser() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser_, &browser_keeper_)); + + if (site_) + site_->browser_ = browser_keeper_; + } + + bool ToolbandHasSite() { + // Check whether ToolBand has a site set. + CComPtr<IUnknown> site; + if (SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&site)))) { + return true; + } + + // If GetSite failed and site != NULL, we are seeing things. + DCHECK(site == NULL); + return false; + } + + static void PrepareDeskBandInfo(DESKBANDINFO* pdinfo_for_test) { + memset(pdinfo_for_test, 0, sizeof(*pdinfo_for_test)); + + // What I really care in this test is DBIM_MODEFLAGS, but if there + // are weird interactions here, we want to be warned. + pdinfo_for_test->dwMask = DBIM_MODEFLAGS | DBIM_MAXSIZE | DBIM_MINSIZE | + DBIM_TITLE | DBIM_INTEGRAL; + } + + static const wchar_t* kUrl1; + + testing::TestBrowserSite* site_; + CComPtr<IUnknown> site_keeper_; + + TestBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + TestingToolBand* tool_band_; + CComPtr<IObjectWithSite> tool_band_with_site_; + + // the purpose of this mock is to redirect registry calls + StrictMock<testing::MockCeeeModuleUtils> ceee_module_utils_; +}; + +const wchar_t* ToolBandTest::kUrl1 = L"http://www.google.com"; + + +// Setting the ToolBand site with a non-service provider fails. +TEST_F(ToolBandTest, SetSiteWithNoServiceProviderFails) { + testing::LogDisabler no_dchecks; + + // Create an object that doesn't implement IServiceProvider. + MockDispatchEx* site = NULL; + CComPtr<IUnknown> site_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&site, + &site_keeper)); + // Setting a site that doesn't implement IServiceProvider fails. + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with no browser fails. +TEST_F(ToolBandTest, SetSiteWithNullBrowserFails) { + testing::LogDisabler no_dchecks; + + CreateSite(); + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with a non-browser fails. +TEST_F(ToolBandTest, SetSiteWithNonBrowserFails) { + testing::LogDisabler no_dchecks; + + CreateSite(); + // Endow the site with a non-browser service. + MockDispatchEx* mock_non_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_non_browser, + &site_->browser_)); + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with a browser that doesn't implement the +// DIID_DWebBrowserEvents2 still works. +TEST_F(ToolBandTest, SetSiteWithNoEventsWorksAnyway) { + // We need to quash dcheck here, too (see: ToolBand::Initialize). + testing::LogDisabler no_dchecks; + CreateSite(); + CreateBrowser(); + + // Disable the connection point. + browser_->no_events_ = true; + + // Successful SetSite always calls GetOptionToolbandForceReposition + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(ToolbandHasSite()); +} + +TEST_F(ToolBandTest, SetSiteWithBrowserSucceeds) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site.IsEqualObject(site_keeper_)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, SetSiteEstablishesConnectionWhenRequired) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site.IsEqualObject(site_keeper_)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); + + // And check that the connection was severed. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); +} + +TEST_F(ToolBandTest, NavigationCompleteResetsFlagAndUnadvises) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + EXPECT_CALL(ceee_module_utils_, + SetOptionToolbandForceReposition(false)).Times(1); + + // First navigation triggers (single) registry check and unadivising. + // After that things stay quiet. + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, NormalRunDoesntTriggerLineBreak) { + CreateSite(); + CreateBrowser(); + + // Expected sequence of actions: + // 1) initialization will trigger registry check + // 2) invocations if GetBandInfo do not trigger registry check + // 3) since spoofed registry says 'do not reposition', there should be no + // DBIMF_BREAK flag set in the structure. + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + DESKBANDINFO dinfo_for_test; + PrepareDeskBandInfo(&dinfo_for_test); + + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + // Take another pass and result should be the same. + PrepareDeskBandInfo(&dinfo_for_test); + + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, NewInstallationTriggersLineBreak) { + CreateSite(); + CreateBrowser(); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + DESKBANDINFO dinfo_for_test; + + // Expected sequence of actions: + // 1) invocation of 'GetBandInfo' will trigger registry check. + // 2) subsequent invocations do not trigger registry check, but the answer + // should also be 'line break' until navigation is completed; + // navigation completed is emulated by a call to FireOnNavigateComplete + // after that, the break flag is not returned. + + PrepareDeskBandInfo(&dinfo_for_test); + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + EXPECT_CALL(ceee_module_utils_, + SetOptionToolbandForceReposition(false)).Times(1); + + ASSERT_TRUE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + PrepareDeskBandInfo(&dinfo_for_test); + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +} // namespace diff --git a/ceee/ie/plugin/toolband/toolband.def b/ceee/ie/plugin/toolband/toolband.def new file mode 100644 index 0000000..9c9cc5b --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.def @@ -0,0 +1,13 @@ +; Copyright (c) 2010 The Chromium Authors. All rights reserved. +; Use of this source code is governed by a BSD-style license that can be +; found in the LICENSE file. +; +; ie.def : Declares the module exports. + +LIBRARY "ceee_ie.DLL" + +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE diff --git a/ceee/ie/plugin/toolband/toolband.gyp b/ceee/ie/plugin/toolband/toolband.gyp new file mode 100644 index 0000000..551092d --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.gyp @@ -0,0 +1,140 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + ], + 'targets': [ + { + # This target builds Chrome Frame's IDL file to our + # shared intermediate directory + 'target_name': 'chrome_tab_idl', + 'type': 'none', + 'msvs_settings': { + 'VCMIDLTool': { + 'OutputDirectory': '<(SHARED_INTERMEDIATE_DIR)', + }, + }, + 'sources': [ + '../../../../chrome_frame/chrome_tab.idl', + ], + # Add the output dir for those who depend on us. + 'direct_dependent_settings': { + 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + { + 'target_name': 'ceee_ie_lib', + 'type': 'static_library', + 'dependencies': [ + 'chrome_tab_idl', + '../../common/common.gyp:ie_common_settings', + '../../../../base/base.gyp:base', + '../../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + '../../common/precompile.cc', + '../../common/precompile.h', + 'tool_band.cc', + 'tool_band.h', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'ceee_ie', + 'type': 'shared_library', + 'dependencies': [ + 'ceee_ie_lib', + 'ie_toolband_common', + 'toolband_idl', + '../bho/bho.gyp:bho', + '../scripting/scripting.gyp:scripting', + '../../common/common.gyp:ie_common_settings', + '../../common/common.gyp:ie_guids', + '../../../../base/base.gyp:base', + '../../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../../ceee/common/common.gyp:ceee_common', + '<(DEPTH)/chrome/chrome.gyp:chrome_version_header', + ], + 'sources': [ + '../../common/precompile.cc', + '../../common/precompile.h', + 'resource.h', + 'tool_band.rgs', + 'toolband.def', + 'toolband.rc', + 'toolband_module.cc', + '../bho/browser_helper_object.rgs', + '../executor.rgs', + '../executor_creator.rgs', + '../scripting/content_script_manager.rc', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + 'include_dirs': [ + # Allows us to include .tlb and .h files generated + # from our .idl without undue trouble + '$(IntDir)', + ], + 'msvs_settings': { + 'VCLinkerTool': { + 'OutputFile': '$(OutDir)/servers/$(ProjectName).dll', + }, + }, + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'ie_toolband_common', + 'type': 'static_library', + 'dependencies': [ + '../../../../chrome_frame/crash_reporting/' + 'crash_reporting.gyp:crash_report', + '../../../../base/base.gyp:base', + '../../../../breakpad/breakpad.gyp:breakpad_handler', + ], + 'sources': [ + 'toolband_module_reporting.cc', + 'toolband_module_reporting.h', + ], + }, + { + 'target_name': 'toolband_idl', + 'type': 'none', + 'sources': [ + '../../broker/broker_lib.idl', + 'toolband.idl', + ], + 'msvs_settings': { + 'VCMIDLTool': { + 'OutputDirectory': '<(SHARED_INTERMEDIATE_DIR)', + 'DLLDataFileName': '$(InputName)_dlldata.c', + }, + }, + # Add the output dir for those who depend on us. + 'direct_dependent_settings': { + 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + ] +} diff --git a/ceee/ie/plugin/toolband/toolband.idl b/ceee/ie/plugin/toolband/toolband.idl new file mode 100644 index 0000000..4164945 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.idl @@ -0,0 +1,370 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Interface and object declarations for CEEE IE toolband. +import "oaidl.idl"; +import "ocidl.idl"; + +[ + object, + uuid(07995791-0923-4c4e-B861-AA3B933A31CC), + dual, + local, // no marshaling for this interface + nonextensible, + pointer_default(unique) +] +// Native interface to content scripts. This interface is exposed to +// ceee_boostrap.js, and is used to satisfy the same contract as +// Chrome's native functions declared in event_bindings.js and +// renderer_extension_bindings.js. Additionally this interface exposes +// properties for the dispatch functions declared in the same files. +// At any given time, this interface should to be synonymous with what you'd +// see by searching the two above files for the strings "dispatchOn" and +// "native function". +interface ICeeeContentScriptNativeApi : IDispatch { + // Supports JS console.log and console.error. + HRESULT Log([in] BSTR level, [in] BSTR message); + + // Port-related functions. + HRESULT OpenChannelToExtension([in] BSTR source_id, + [in] BSTR target_id, + [in] BSTR name, + [out, retval] long* port_id); + HRESULT CloseChannel([in] long port_id); + HRESULT PortAddRef([in] long port_id); + HRESULT PortRelease([in] long port_id); + HRESULT PostMessage([in] long port_id, [in] BSTR msg); + + // Event-related functions. + HRESULT AttachEvent([in] BSTR event_name); + HRESULT DetachEvent([in] BSTR event_name); + + // Event notification callbacks to script. + [propput] HRESULT onLoad(IDispatch* callback); + [propput] HRESULT onUnload(IDispatch* callback); + + // Port notification callbacks. + [propput] HRESULT onPortConnect(IDispatch* callback); + [propput] HRESULT onPortDisconnect(IDispatch* callback); + [propput] HRESULT onPortMessage(IDispatch* callback); +}; + +// An invalid tab Id. This is declared here so that both the BHO and the broker +// knows about it. +const int kInvalidChromeSessionId = -1; + +typedef long CeeeWindowHandle; + +typedef enum tagCeeeTabStatus { + kCeeeTabStatusLoading = 0, + kCeeeTabStatusComplete = 1, +} CeeeTabStatus; + +// Information about a tab. +typedef struct tagCeeeTabInfo { + BSTR url; + BSTR title; + CeeeTabStatus status; + BSTR fav_icon_url; +} CeeeTabInfo; + +typedef enum tagCeeeTabCodeType { + kCeeeTabCodeTypeCss = 0, + kCeeeTabCodeTypeJs = 1, +} CeeeTabCodeType; + +// Information about a window. +typedef struct { + BOOL focused; + RECT rect; + // We use a BSTR to dynamically allocate a list of couple (HWND, index). + // These are stored as a JSON list of longs, the even indexes are for ids + // and their associated odd numbers are the tab index. + // This value can be NULL if the caller didn't request to populate tabs. + BSTR tab_list; +} CeeeWindowInfo; + +// Information about an HTTP cookie. +typedef struct { + BSTR name; + BSTR value; + BSTR domain; + BOOL host_only; + BSTR path; + BOOL secure; + BOOL http_only; + BOOL session; + double expiration_date; + BSTR store_id; +} CeeeCookieInfo; + +[ + object, + uuid(8DEEECC5-7B49-482d-99F8-2109EA5F2618), + nonextensible, + helpstring("ICeeeWindowExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeWindowExecutor : IUnknown { + // Initializes the executor to work with the given CeeeWindowHandle. + // + // @param hwnd The HWND of the window the executor represents. + HRESULT Initialize([in] CeeeWindowHandle hwnd); + + // Returns information about the window represented by this executor. + // + // @param populate_tabs Specifies whether we want to receive the list of tabs. + // @param window_info Where to return the info about the window. The + // @p tab_list field are only set if @p populate_tabs is + // true. + HRESULT GetWindow([in] BOOL populate_tabs, + [out, ref] CeeeWindowInfo* window_info); + + // Returns the list of tabs of this window. We return both the tab HWND and + // the tab index (encoded in the @p tab_list BSTR) so that our callers don't + // need an extra IPC to get the tab index later on. + // + // @param tab_list Where to return the tab identifiers in the same format as + // described for tagCeeeWindowInfo::tab_ids. + HRESULT GetTabs([out, retval] BSTR* tab_list); + + // Updates the window with the given set of parameters. + // + // @param left The new left position of the window. -1 to leave unchanged. + // @param top The new top position of the window. -1 to leave unchanged. + // @param width The new width of the window. -1 to leave unchanged. + // @param height The new height of the window. -1 to leave unchanged. + // @param window_info Where to return the new info about the updated window. + HRESULT UpdateWindow( + [in] long left, [in] long top, [in] long width, [in] long height, + [out, ref] CeeeWindowInfo* window_info); + + // Close the window represented by our FrameExecutor. + // + // @retval S_OK We could successfully and silently removed the window. + // @retval S_FALSE We failed to <b>silently</b> remove the window, + // so the caller should try other alternatives (e.g., + // posting WM_CLOSE to the window). + HRESULT RemoveWindow(); + + // Returns the index of the given tab. + // + // @param tab The window handle (HWND) of the tab we want the index of. + // @param index Where to return the index. + HRESULT GetTabIndex([in] CeeeWindowHandle tab, [out, ref] long* index); + + // Moves the tab specified by @p tab to the index identified by @p index. + // + // @param tab The window handle (HWND) of the tab to move. + // @param index Where to move the tab. + HRESULT MoveTab([in] CeeeWindowHandle tab, [in] long index); + + // Removes the specified tab. + // + // @param tab The window handle (HWND) of the tab to be removed. + HRESULT RemoveTab([in] CeeeWindowHandle tab); + + // Selects the specified tab. + // + // @param tab The window handle (HWND) of the tab to be selected. + HRESULT SelectTab([in] CeeeWindowHandle tab); +}; + +[ + object, + uuid(C7FF41BA-72D5-4086-8B5A-2EF4FD12E0FE), + nonextensible, + helpstring("ICeeeTabExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeTabExecutor : IUnknown { + // Initializes the executor to work with the given CeeeWindowHandle. + // + // @param hwnd The HWND of the tab the executor represents. + HRESULT Initialize([in] CeeeWindowHandle hwnd); + + // Returns information about the tab represented by the TabExecutor in + // @p tab_info structure used to return the information. + // + // @param tab_info Where to return the tab information. + // + // @rvalue S_OK Success + // @return Other failure HRESULTs may also be returned in case of cascading + // errors. + HRESULT GetTabInfo([out, ref] CeeeTabInfo* tab_info); + + // Navigate to the given url from the given properties. + // + // @param url The URL where to navigate the tab to. Can NOT be NULL. + // @param flags Specifies the type of navigation based on the + // BrowserNavConstants enum values. + // @param target Specifies the navigation target (e.g., _top or _blank). + // + // @rvalue S_OK Success + // S_FALSE Nothing needed to be done since the URL was already set. + // @return Other failure HRESULTs may also be returned in case of cascading + // errors. + HRESULT Navigate([in] BSTR url, [in] long flags, [in] BSTR target); + + // Execute or insert code inside a tab. + // + // @param code The code to execute or insert. + // @param file A path to the file that contains the script to execute. + // This path is relative to the extension root. + // @param all_frames If true, applies to the top level frame as well as + // contained iframes. Otherwise, applies onlt to the + // top level frame. + HRESULT InsertCode([in] BSTR code, + [in] BSTR file, + [in] BOOL all_frames, + [in] CeeeTabCodeType type); +}; + +[ + object, + uuid(07630967-D7FB-4745-992F-28614930D9A3), + nonextensible, + helpstring("ICeeeCookieExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeCookieExecutor : IUnknown { + // Returns information about the cookie identified by the @c name field of + // the @p cookie_info structure used to return the information. + // + // @param url The URL with which the cookie to retrieve is associated. + // @param name The name of the cookie to retrieve. + // @param cookie_info Where to return the cookie information. + HRESULT GetCookie([in] BSTR url, [in] BSTR name, + [out, ref] CeeeCookieInfo* cookie_info); + + // Registers the executor's process as a known cookie store; used to indicate + // that the cookie store ID has been issued for this process and may be used + // in other cookie APIs. + // This API is used to ensure that stale cookie store IDs don't inadvertently + // match new cookie store processes. This may happen because IE derives the + // cookie store ID from the IE process ID, which may be recycled by Windows. + // The first time a cookie store ID is issued for an IE process, this + // RegisterCookieStore function should be called to indicate that the IE + // process may now be selected by a user-provided store ID. All cookie APIs + // should verify that CookieStoreIsRegistered() returns S_OK before matching + // a user-provided cookie store ID to an IE process. + HRESULT RegisterCookieStore(); + + // Returns S_OK if the executor's process has been registered as a cookie + // store, S_FALSE if not. All cookie API implementations must ensure this + // call returns S_OK before accessing a cookie store via a user-provided + // cookie store ID; if it doesn't, the store ID is stale. + HRESULT CookieStoreIsRegistered(); +}; + +[ + object, + uuid(276D47E8-1692-4a21-907D-948D170E4330), + nonextensible, + helpstring("ICeeeInfobarExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeInfobarExecutor : IUnknown { + // Stores the id of our extension. + // @param extension_id The id of the extension. + HRESULT SetExtensionId([in] BSTR extension_id); + + // Creates infobar and opens @p url in it. Translates relative path to the + // absolute path using "chrome-extension://extension_id" prefix where + // extension_id is the id set by SetExtensionId() call. + // @param url The URL the infobar window should be navigated to. + // @param window_handle Where to return the handle of the window in which + // this infobar was created. + HRESULT ShowInfobar([in] BSTR url, + [out, ref] CeeeWindowHandle* window_handle); + + // Notifies infobar about OnBeforeNavigate2 event for the browser top frame. + // @param url The URL the top frame is about to navigate to. + HRESULT OnTopFrameBeforeNavigate([in] BSTR url); +}; + +[ + object, + uuid(BBB10A7B-DB0D-4f1a-8669-65378DAD0C99), + nonextensible, + helpstring("ICeeeExecutorCreator Interface"), + pointer_default(unique), + local +] +// Creates an executor in a destination thread, and registers it in the +// CeeeBroker. + +// Used to instantiate a CeeeExecutor. +interface ICeeeExecutorCreator : IUnknown { + // Creates a CeeeExecutor for the given @p thread_id. + // + // @param thread_id The identifier of the destination thread where we want + // an executor to be creared. + // @param window The window handle (HWND) the new executor represents. + HRESULT CreateWindowExecutor([in] long thread_id, + [in] CeeeWindowHandle window); + + // Teardown what was left hanging while waiting for the + // new executor to be registered for the given @p thread_id. + // + // @param thread_id The identifier of the destination thread for which we want + // to tear down our infrastructure. + HRESULT Teardown([in] long thread_id); +}; + +[ + uuid(7C09079D-F9CB-4E9E-9293-D224B071D8BA), + version(1.0), + helpstring("Google CEEE 1.0 Type Library") +] +library ToolbandLib { + importlib("stdole2.tlb"); + + // include type info in .tlb + interface ICEEEContentScriptNativeApi; + interface ICeeeTabExecutor; + interface ICeeeWindowExecutor; + + [ + uuid(E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC), + helpstring("BrowserHelperObject Class") + ] + coclass BrowserHelperObject { + [default] interface IUnknown; + }; + [ + uuid(2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B), + helpstring("ToolBand Class") + ] + coclass ToolBand { + [default] interface IUnknown; + }; + [ + uuid(4A562910-2D54-4e98-B87F-D4A7F5F5D0B9), + helpstring("CEEE Executor Creator Class") + ] + coclass CeeeExecutorCreator { + [default] interface IUnknown; + }; + [ + uuid(057FCFE3-F872-483d-86B0-0430E375E41F), + helpstring("CEEE Executor Class") + ] + coclass CeeeExecutor { + [default] interface IUnknown; + interface ICeeeTabExecutor; + interface ICeeeWindowExecutor; + interface ICeeeCookieExecutor; + interface ICeeeInfobarExecutor; + }; +}; diff --git a/ceee/ie/plugin/toolband/toolband.rc b/ceee/ie/plugin/toolband/toolband.rc new file mode 100644 index 0000000..d1319c2 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.rc @@ -0,0 +1,116 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Microsoft Visual C++ generated resource script. +// +#include "ceee/ie/plugin/toolband/resource.h" +#include "version.h" + +// See winuser.h. +#define RT_HTML 23 + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +# error Don't open this in the GUI, it'll be massacred on save. +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION CHROME_VERSION + PRODUCTVERSION CHROME_VERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Google Inc." + VALUE "FileDescription", "Google Chrome Extensions Execution Environment for IE." + VALUE "FileVersion", CHROME_VERSION_STRING + VALUE "LegalCopyright", COPYRIGHT_STRING + VALUE "InternalName", "ceee_ie.dll" + VALUE "OriginalFilename", "ceee_ie.dll" + VALUE "ProductName", "Google Chrome Extensions Execution Environment" + VALUE "ProductVersion", CHROME_VERSION_STRING + + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +IDR_BROWSERHELPEROBJECT REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\browser_helper_object.rgs" +IDR_EXECUTOR REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\executor.rgs" +IDR_EXECUTOR_CREATOR REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\executor_creator.rgs" +IDR_TOOL_BAND REGISTRY "tool_band.rgs" + +///////////////////////////////////////////////////////////////////////////// +// +// BINDATA +// + +IDR_GREASEMONKEY_API_JS BINDATA "..\\..\\chrome\\renderer\\resources\\greasemonkey_api.js" + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PROJNAME "IE" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +1 TYPELIB "toolband.tlb" + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/ceee/ie/plugin/toolband/toolband_module.cc b/ceee/ie/plugin/toolband/toolband_module.cc new file mode 100644 index 0000000..2d2c735 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module.cc @@ -0,0 +1,408 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of ATL module object and DLL exports. + +#include "base/at_exit.h" +#include "base/atomic_ref_count.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/logging_win.h" +#include "base/thread.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/install_utils.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/browser_helper_object.h" +#include "ceee/ie/plugin/bho/executor.h" +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" +#include "ceee/ie/plugin/toolband/tool_band.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/windows_constants.h" +#include "chrome/common/url_constants.h" + +#include "toolband.h" // NOLINT + +namespace { + +const wchar_t kLogFileName[] = L"ceee.log"; + +// {73213C1A-C369-4740-A75C-FA849E6CE540} +static const GUID kCeeeIeLogProviderName = + { 0x73213c1a, 0xc369, 0x4740, + { 0xa7, 0x5c, 0xfa, 0x84, 0x9e, 0x6c, 0xe5, 0x40 } }; + +// This is the Script Debugging state for all script engines we instantiate. +ScriptHost::DebugApplication debug_application(L"CEEE"); + +} // namespace + +// Object entries go here instead of with each object, so that we can move +// the objects in a lib, and also to decrease the amount of magic. +OBJECT_ENTRY_AUTO(CLSID_BrowserHelperObject, BrowserHelperObject) +OBJECT_ENTRY_AUTO(CLSID_ToolBand, ToolBand) +OBJECT_ENTRY_AUTO(CLSID_CeeeExecutorCreator, CeeeExecutorCreator) +OBJECT_ENTRY_AUTO(CLSID_CeeeExecutor, CeeeExecutor) + +class ToolbandModule : public CAtlDllModuleT<ToolbandModule> { + public: + ToolbandModule(); + ~ToolbandModule(); + + DECLARE_LIBID(LIBID_ToolbandLib) + + // Needed to make sure we call Init/Term outside the loader lock. + HRESULT DllCanUnloadNow(); + HRESULT DllGetClassObject(REFCLSID clsid, REFIID iid, void** object); + void Init(); + void Term(); + bool module_initialized() const { + return module_initialized_; + } + + // Fires an event to the broker, so that the call can be made with an + // instance of a broker proxy that was CoCreated in the worker thread. + void FireEventToBroker(const std::string& event_name, + const std::string& event_args); + + private: + class ComWorkerThread : public base::Thread { + public: + ComWorkerThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + + // Called by FireEventTask so that the broker we instantiate in the + // worker thread can be used. + void FireEventToBroker(BSTR event_name, BSTR event_args); + protected: + CComPtr<ICeeeBroker> broker_; + static const int kMaxNumberOfRetries = 5; + static const int64 kRetryDelayMs = 10; + int current_number_of_retries_; + }; + + class FireEventTask : public Task { + public: + FireEventTask(ComWorkerThread* worker_thread, + const std::string& event_name, + const std::string& event_args) + : worker_thread_(worker_thread), + event_name_(event_name.c_str()), + event_args_(event_args.c_str()) { + } + FireEventTask(ComWorkerThread* worker_thread, + const BSTR event_name, + const BSTR event_args) + : worker_thread_(worker_thread), + event_name_(event_name), + event_args_(event_args) { + } + virtual void Run() { + worker_thread_->FireEventToBroker(event_name_, event_args_); + } + private: + ComWorkerThread* worker_thread_; + CComBSTR event_name_; + CComBSTR event_args_; + }; + // We only start the thread on first use. If we would start it on + // initialization, when our DLL is loaded into the broker process, + // it would try to start this thread which tries to CoCreate a Broker + // and this could cause a complex deadlock... + void EnsureThreadStarted(); + + // We use a pointer so that we can make sure we only destroy the object + // when the thread is properly stopped. Otherwise, we would get a DCHECK + // if the thread is killed before we get to Stop it when DllCanUnloadNow + // returns S_OK, which happens when the application quits with live objects, + // this causes the destructor to DCHECK. + ComWorkerThread* worker_thread_; + base::AtExitManager at_exit_; + bool module_initialized_; + bool crash_reporting_initialized_; + + int worker_thread_ref_count_; + + friend void ceee_module_util::AddRefModuleWorkerThread(); + friend void ceee_module_util::ReleaseModuleWorkerThread(); + + void IncThreadRefCount(); + void DecThreadRefCount(); +}; + +ToolbandModule::ToolbandModule() + : crash_reporting_initialized_(false), + module_initialized_(false), + worker_thread_(NULL) { + wchar_t logfile_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(logfile_path), logfile_path); + ::PathAppend(logfile_path, kLogFileName); + + // It seems we're obliged to initialize the current command line + // before initializing logging. This feels a little strange for + // a plugin. + CommandLine::Init(0, NULL); + + logging::InitLogging( + logfile_path, + logging::LOG_TO_BOTH_FILE_AND_SYSTEM_DEBUG_LOG, + logging::LOCK_LOG_FILE, + logging::APPEND_TO_OLD_LOG_FILE); + + // Initialize ETW logging. + logging::LogEventProvider::Initialize(kCeeeIeLogProviderName); + + // Initialize control hosting. + BOOL initialized = AtlAxWinInit(); + DCHECK(initialized); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + ScriptHost::set_default_debug_application(&debug_application); +} + +ToolbandModule::~ToolbandModule() { + ScriptHost::set_default_debug_application(NULL); + + // Just leave thread as is. Releasing interface from this thread may hang IE. + DCHECK(worker_thread_ref_count_ == 0); + DCHECK(worker_thread_ == NULL); + + // Uninitialize control hosting. + BOOL uninitialized = AtlAxWinTerm(); + DCHECK(uninitialized); + + logging::CloseLogFile(); +} + +HRESULT ToolbandModule::DllCanUnloadNow() { + HRESULT hr = CAtlDllModuleT<ToolbandModule>::DllCanUnloadNow(); + if (hr == S_OK) { + // We must protect our data member against concurrent calls to check if we + // can be unloaded. We must also making the call to Term within the lock + // to make sure we don't try to re-initialize in case a new + // DllGetClassObject would occur in the mean time, in another thread. + m_csStaticDataInitAndTypeInfo.Lock(); + if (module_initialized_) { + Term(); + } + m_csStaticDataInitAndTypeInfo.Unlock(); + } + return hr; +} + +HRESULT ToolbandModule::DllGetClassObject(REFCLSID clsid, REFIID iid, + void** object) { + // Same comment as above in ToolbandModule::DllCanUnloadNow(). + m_csStaticDataInitAndTypeInfo.Lock(); + if (!module_initialized_) { + Init(); + } + m_csStaticDataInitAndTypeInfo.Unlock(); + return CAtlDllModuleT<ToolbandModule>::DllGetClassObject(clsid, iid, object); +} + +void ToolbandModule::Init() { + crash_reporting_initialized_ = InitializeCrashReporting(); + module_initialized_ = true; +} + +void ToolbandModule::Term() { + if (worker_thread_ != NULL) { + // It is OK to call Stop on a thread even when it isn't running. + worker_thread_->Stop(); + delete worker_thread_; + worker_thread_ = NULL; + } + if (crash_reporting_initialized_) { + bool crash_reporting_deinitialized = ShutdownCrashReporting(); + DCHECK(crash_reporting_deinitialized); + crash_reporting_initialized_ = false; + } + module_initialized_ = false; +} + +void ToolbandModule::IncThreadRefCount() { + m_csStaticDataInitAndTypeInfo.Lock(); + DCHECK_GE(worker_thread_ref_count_, 0); + worker_thread_ref_count_++; + m_csStaticDataInitAndTypeInfo.Unlock(); +} + +void ToolbandModule::DecThreadRefCount() { + ComWorkerThread* thread = NULL; + + m_csStaticDataInitAndTypeInfo.Lock(); + // If we're already at 0, we have a problem, so we check if we're >=. + DCHECK_GT(worker_thread_ref_count_, 0); + + // If this was our last reference, we delete the thread. This is okay even if + // we increment the count again, because the thread is created on the "first" + // FireEventToBroker, thus it will be created again if needed. + if (--worker_thread_ref_count_ == 0) { + if (worker_thread_ != NULL) { + // Store the worker_thread to a temporary pointer. It will be freed later. + thread = worker_thread_; + worker_thread_ = NULL; + } + } + m_csStaticDataInitAndTypeInfo.Unlock(); + + // Clean the thread after the unlock to be certain we don't get a deadlock + // (the CriticalSection could be used in the worker thread). + if (thread) { + // It is OK to call Stop on a thread even when it isn't running. + thread->Stop(); + delete thread; + } +} + +void ToolbandModule::EnsureThreadStarted() { + m_csStaticDataInitAndTypeInfo.Lock(); + if (worker_thread_ == NULL) { + worker_thread_ = new ComWorkerThread; + // The COM worker thread must be a UI thread so that it can pump windows + // messages and allow COM to handle cross apartment calls. + worker_thread_->StartWithOptions(base::Thread::Options(MessageLoop::TYPE_UI, + 0)); // stack_size + } + m_csStaticDataInitAndTypeInfo.Unlock(); +} + +void ToolbandModule::FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + EnsureThreadStarted(); + DCHECK(worker_thread_ != NULL); + MessageLoop* message_loop = worker_thread_->message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, + new FireEventTask(worker_thread_, event_name, event_args)); + } else { + LOG(ERROR) << "Trying to post a message before the COM worker thread is" + "completely initialized and ready."; + } +} + + +ToolbandModule::ComWorkerThread::ComWorkerThread() + : base::Thread("CEEE-COM Worker Thread"), + current_number_of_retries_(0) { +} + +void ToolbandModule::ComWorkerThread::Init() { + ::CoInitializeEx(0, COINIT_MULTITHREADED); + HRESULT hr = broker_.CoCreateInstance(CLSID_CeeeBroker); + DCHECK(SUCCEEDED(hr)) << "Failed to create broker. " << com::LogHr(hr); +} + +void ToolbandModule::ComWorkerThread::CleanUp() { + broker_.Release(); + ::CoUninitialize(); +} + +void ToolbandModule::ComWorkerThread::FireEventToBroker(BSTR event_name, + BSTR event_args) { + DCHECK(broker_ != NULL); + if (broker_ != NULL) { + HRESULT hr = broker_->FireEvent(event_name, event_args); + if (SUCCEEDED(hr)) { + current_number_of_retries_ = 0; + return; + } + // If the server is busy (which can happen if it is calling in as we try to + // to call out to it), then we should retry a few times a little later. + if (current_number_of_retries_ < kMaxNumberOfRetries && message_loop()) { + ++current_number_of_retries_; + LOG(WARNING) << "Retrying Broker FireEvent Failure. " << com::LogHr(hr); + message_loop()->PostDelayedTask(FROM_HERE, + new FireEventTask(this, event_name, event_args), kRetryDelayMs); + } else { + current_number_of_retries_ = 0; + DCHECK(SUCCEEDED(hr)) << "Broker FireEvent Failed. " << com::LogHr(hr); + } + } +} + +ToolbandModule module; + +void ceee_module_util::AddRefModuleWorkerThread() { + module.IncThreadRefCount(); +} +void ceee_module_util::ReleaseModuleWorkerThread() { + module.DecThreadRefCount(); +} + +void ceee_module_util::FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + module.FireEventToBroker(event_name, event_args); +} + +void ceee_module_util::Lock() { + module.m_csStaticDataInitAndTypeInfo.Lock(); +} + +void ceee_module_util::Unlock() { + module.m_csStaticDataInitAndTypeInfo.Unlock(); +} + +LONG ceee_module_util::LockModule() { + return module.Lock(); +} + +LONG ceee_module_util::UnlockModule() { + return module.Unlock(); +} + + +// DLL Entry Point +extern "C" BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, + LPVOID reserved) { + // Prevent us from being loaded by older versions of the shell. + if (reason == DLL_PROCESS_ATTACH) { + wchar_t main_exe[MAX_PATH] = { 0 }; + ::GetModuleFileName(NULL, main_exe, arraysize(main_exe)); + + // We don't want to be loaded in the explorer process. + _wcslwr_s(main_exe, arraysize(main_exe)); + if (wcsstr(main_exe, windows::kExplorerModuleName)) + return FALSE; + } + + return module.DllMain(reason, reserved); +} + +// Used to determine whether the DLL can be unloaded by OLE +STDAPI DllCanUnloadNow(void) { + return module.DllCanUnloadNow(); +} + +// Returns a class factory to create an object of the requested type +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { + return module.DllGetClassObject(rclsid, riid, ppv); +} + +// DllRegisterServer - Adds entries to the system registry +// +// This is not the actual entrypoint; see the define right below this +// function, which keeps us safe from ever forgetting to check for +// the --enable-ceee flag. +STDAPI DllRegisterServerImpl(void) { + // registers object, typelib and all interfaces in typelib + HRESULT hr = module.DllRegisterServer(); + return hr; +} + +CEEE_DEFINE_DLL_REGISTER_SERVER() + +// DllUnregisterServer - Removes entries from the system registry +STDAPI DllUnregisterServer(void) { + // We always allow unregistration, even if no --enable-ceee install flag. + HRESULT hr = module.DllUnregisterServer(); + return hr; +} diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting.cc b/ceee/ie/plugin/toolband/toolband_module_reporting.cc new file mode 100644 index 0000000..c1d5f58 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of CEEE plugin's wrapper around common crash reporting. + +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" + +// Well known SID for the system principal. +const wchar_t kSystemPrincipalSid[] = L"S-1-5-18"; + +// Returns the custom info structure based on the dll in parameter and the +// process type. +google_breakpad::CustomClientInfo* GetCustomInfo() { + // TODO(jeffbailey@google.com): Put in a real version. + // (bb3143594). + static google_breakpad::CustomInfoEntry ver_entry(L"ver", L"Ver.Goes.Here"); + static google_breakpad::CustomInfoEntry prod_entry(L"prod", L"CEEE_IE"); + static google_breakpad::CustomInfoEntry plat_entry(L"plat", L"Win32"); + static google_breakpad::CustomInfoEntry type_entry(L"ptype", L"ie_plugin"); + static google_breakpad::CustomInfoEntry entries[] = { + ver_entry, prod_entry, plat_entry, type_entry }; + static google_breakpad::CustomClientInfo custom_info = { + entries, arraysize(entries) }; + return &custom_info; +} + +bool InitializeCrashReporting() { + if (!ceee_module_util::GetCollectStatsConsent()) + return false; + + // Get the alternate dump directory. We use the temp path. + FilePath temp_directory; + if (!file_util::GetTempDir(&temp_directory) || temp_directory.empty()) { + return false; + } + + bool result = InitializeVectoredCrashReporting( + false, kSystemPrincipalSid, temp_directory.value(), GetCustomInfo()); + DCHECK(result) << "Failed initialize crashreporting."; + return result; +} + +bool ShutdownCrashReporting() { + return ShutdownVectoredCrashReporting(); +} diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting.h b/ceee/ie/plugin/toolband/toolband_module_reporting.h new file mode 100644 index 0000000..9b3aad9 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting.h @@ -0,0 +1,23 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A wrapper around common crash reporting code to manage reporting for CEEE +// plugin. + +#ifndef CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ + +#include "chrome_frame/crash_reporting/crash_report.h" + +extern const wchar_t kSystemPrincipalSid[]; + +// Intialize crash reporting for the Toolband Plugin. Specific parameters +// here include using the temp directory for dumps, using the system-wide +// install ID, and customized client info. +bool InitializeCrashReporting(); + +// Shut down crash reporting for CEEE plug-in. +bool ShutdownCrashReporting(); + +#endif // CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc b/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc new file mode 100644 index 0000000..dfbf904 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband module reporting unit tests. +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/testing/utils/mock_static.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::Return; +using testing::StrictMock; + +MOCK_STATIC_CLASS_BEGIN(MockToolbandModuleReporting) + MOCK_STATIC_INIT_BEGIN(MockToolbandModuleReporting) + MOCK_STATIC_INIT2(ceee_module_util::GetCollectStatsConsent, + GetCollectStatsConsent); + MOCK_STATIC_INIT(InitializeVectoredCrashReporting); + MOCK_STATIC_INIT_END() + + MOCK_STATIC0(bool, , GetCollectStatsConsent); + MOCK_STATIC4(bool, , InitializeVectoredCrashReporting, bool, + const wchar_t*, + const std::wstring&, + google_breakpad::CustomClientInfo*); +MOCK_STATIC_CLASS_END(MockToolbandModuleReporting) + +TEST(ToolbandModuleReportingTest, InitializeCrashReportingWithoutConsent) { + StrictMock<MockToolbandModuleReporting> mock; + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(mock, InitializeVectoredCrashReporting(_, _, _, _)) + .Times(0); + + InitializeCrashReporting(); +} + +TEST(ToolbandModuleReportingTest, InitializeCrashReportingWithConsent) { + StrictMock<MockToolbandModuleReporting> mock; + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(true)); + + EXPECT_CALL(mock, InitializeVectoredCrashReporting(_, _, _, _)) + .Times(1) + .WillOnce(Return(true)); + + InitializeCrashReporting(); +} + +} // namespace diff --git a/ceee/ie/testing/ie_unittest_main.cc b/ceee/ie/testing/ie_unittest_main.cc new file mode 100644 index 0000000..2317472 --- /dev/null +++ b/ceee/ie/testing/ie_unittest_main.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Main function for common unittests. +#include <atlbase.h> +#include <atlcom.h> +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "ceee/testing/utils/gflag_utils.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "toolband.h" // NOLINT + +// Stub for unittesting. +namespace ceee_module_util { + +LONG LockModule() { + return 0; +} +LONG UnlockModule() { + return 0; +} +void Lock() { +} +void Unlock() { +} +void AddRefModuleWorkerThread() { +} +void ReleaseModuleWorkerThread() { +} + +// Fires an event to the broker, so that the call can be made with an +// instance of a broker proxy that was CoCreated in the worker thread. +void FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + // Must get some work done so that the function can be mocked. + // Otherwise, it would be too short to be side stepped... + if (event_name == event_args) { + CHECK(event_name == event_args); + } else { + CHECK(event_name != event_args); + } +} + +} // namespace ceee_module_util + + +// We're testing ATL code that requires a module object. +class ObligatoryModule: public CAtlDllModuleT<ObligatoryModule> { + public: + DECLARE_LIBID(LIBID_ToolbandLib); +}; + +ObligatoryModule g_obligatory_atl_module; + +// Run these tests under page heap. +const DWORD kGFlags = FLG_USER_STACK_TRACE_DB | FLG_HEAP_PAGE_ALLOCS; + +int main(int argc, char **argv) { + // Disabled, bb2560934. + // if (!IsDebuggerPresent()) + // testing::RelaunchWithGFlags(kTestGFlags); + + base::AtExitManager at_exit; + CommandLine::Init(argc, argv); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + testing::InitGoogleMock(&argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/ceee/ie/testing/mediumtest_ie_common.cc b/ceee/ie/testing/mediumtest_ie_common.cc new file mode 100644 index 0000000..1b2fd45 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_common.cc @@ -0,0 +1,267 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A common test fixture for testing against a captive shell browser +// control instance - which is as close to habitating the belly of the IE +// beast as can be done with a modicum of safety and sanitation. +#include "ceee/ie/testing/mediumtest_ie_common.h" + +#include <atlcrack.h> +#include <atlsync.h> +#include <atlwin.h> +#include <exdisp.h> +#include <exdispid.h> +#include <mshtmdid.h> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "base/utf_string_conversions.h" +#include "gtest/gtest.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + + +namespace testing { + +// A test resource that's a simple single-resource page. +const wchar_t* kSimplePage = L"simple_page.html"; +// A test resource that's a frameset referencing the two frames below. +const wchar_t* kTwoFramesPage = L"two_frames.html"; +const wchar_t* kFrameOne = L"frame_one.html"; +const wchar_t* kFrameTwo = L"frame_two.html"; +// Another test resource that's a frameset referencing the two frames below. +const wchar_t* kAnotherTwoFramesPage = L"another_two_frames.html"; +const wchar_t* kAnotherFrameOne = L"another_frame_one.html"; +const wchar_t* kAnotherFrameTwo = L"another_frame_two.html"; + +// A test resource that's a top-level frameset with two nested iframes +// the inner one of which refers frame_one.html. +const wchar_t* kDeepFramesPage = L"deep_frames.html"; +const wchar_t* kLevelOneFrame = L"level_one_frame.html"; +const wchar_t* kLevelTwoFrame = L"level_two_frame.html"; + +// A test resource that have javascript generate frames that look orphan. +const wchar_t* kOrphansPage = L"orphans.html"; + +std::set<READYSTATE> BrowserEventSinkBase::seen_states_; + +// Constructs a res: url to the test resource in our module. +std::wstring GetTestUrl(const wchar_t* resource_name) { + FilePath path; + EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &path)); + path = path.Append(FILE_PATH_LITERAL("ceee")) + .Append(FILE_PATH_LITERAL("ie")) + .Append(FILE_PATH_LITERAL("testing")) + .Append(FILE_PATH_LITERAL("test_data")) + .Append(resource_name); + + return UTF8ToWide(net::FilePathToFileURL(path).spec()); +} + +// Returns the path to our temp folder. +std::wstring GetTempPath() { + FilePath temp; + EXPECT_TRUE(PathService::Get(base::DIR_TEMP, &temp)); + + return temp.value(); +} + +_ATL_FUNC_INFO handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO handler_type_idispatch_variantptr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BYREF | VT_VARIANT } }; + + +void BrowserEventSinkBase::GetDescription(std::string* description) const { + description->clear(); + description->append("TestBrowserEventSink"); +} + +HRESULT BrowserEventSinkBase::Initialize(IWebBrowser2* browser) { + browser_ = browser; + HRESULT hr = DispEventAdvise(browser_); + if (SUCCEEDED(hr)) { + hr = AtlAdvise(browser_, + GetUnknown(), + IID_IPropertyNotifySink, + &prop_notify_cookie_); + } + + return hr; +} + +void BrowserEventSinkBase::TearDown() { + EXPECT_HRESULT_SUCCEEDED(DispEventUnadvise(browser_)); + EXPECT_HRESULT_SUCCEEDED(AtlUnadvise(browser_, + IID_IPropertyNotifySink, + prop_notify_cookie_)); +} + +STDMETHODIMP BrowserEventSinkBase::OnChanged(DISPID changed_property) { + ATLTRACE("OnChanged(%d)\n", changed_property); + + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + browser_->get_ReadyState(&ready_state); + ATLTRACE("READYSTATE: %d\n", ready_state); + seen_states_.insert(ready_state); + + return S_OK; +} + +STDMETHODIMP BrowserEventSinkBase::OnRequestEdit(DISPID changed_property) { + ATLTRACE("OnRequestEdit(%d)\n", changed_property); + return S_OK; +} + +STDMETHODIMP_(void) BrowserEventSinkBase::OnNavigateComplete( + IDispatch* browser_disp, VARIANT* url_var) { +} + +STDMETHODIMP_(void) BrowserEventSinkBase::OnDocumentComplete( + IDispatch* browser_disp, VARIANT* url) { +} + +void ShellBrowserTestBase::SetUpTestCase() { + // CoInitialize(NULL) serves two purposes here: + // 1. if we're running in a non-initialized apartment, it initializes it. + // 2. we need to be in an STA to use the shell browser, and if we're + // for whatever reason running in an MTA, it will fail. + ASSERT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + ASSERT_TRUE(AtlAxWinInit()); +} + +void ShellBrowserTestBase::TearDownTestCase() { + EXPECT_TRUE(AtlAxWinTerm()); + ::CoUninitialize(); +} + +void ShellBrowserTestBase::SetUp() { + ASSERT_TRUE(Create(HWND_MESSAGE) != NULL); + + // Create the webbrowser control and get the AX host. + ASSERT_HRESULT_SUCCEEDED( + AtlAxCreateControl(CComBSTR(CLSID_WebBrowser), + m_hWnd, + NULL, + &host_)); + + // Get the control's top-level IWebBrowser. + CComPtr<IUnknown> control; + ASSERT_HRESULT_SUCCEEDED(AtlAxGetControl(m_hWnd, &control)); + ASSERT_HRESULT_SUCCEEDED(control->QueryInterface(&browser_)); + + ASSERT_HRESULT_SUCCEEDED(CreateEventSink(browser_)); +} + +void ShellBrowserTestBase::TearDown() { + // Navigating the browser to a folder creates a non-webbrowser document, + // which shakes off all our frame handlers. + EXPECT_TRUE(NavigateBrowser(GetTempPath())); + + // Tear down the rest of our stuff. + if (event_sink_) + event_sink_->TearDown(); + + event_sink_keeper_.Release(); + host_.Release(); + browser_.Release(); + + // Should have retained no objects past this point. + EXPECT_EQ(0, InstanceCountMixinBase::all_instance_count()); + if (InstanceCountMixinBase::all_instance_count() > 0) { + InstanceCountMixinBase::LogLeakedInstances(); + } + + // Finally blow away the host window. + if (IsWindow()) + DestroyWindow(); +} + +bool ShellBrowserTestBase::NavigateBrowser(const std::wstring& url) { + CComVariant empty; + HRESULT hr = browser_->Navigate2(&CComVariant(url.c_str()), + &empty, &empty, + &empty, &empty); + EXPECT_HRESULT_SUCCEEDED(hr); + if (FAILED(hr)) + return false; + + return WaitForReadystateComplete(); +} + +bool ShellBrowserTestBase::WaitForReadystateComplete() { + return WaitForReadystate(READYSTATE_COMPLETE); +} + +bool ShellBrowserTestBase::WaitForReadystateLoading() { + return WaitForReadystate(READYSTATE_LOADING); +} + +bool ShellBrowserTestBase::WaitForReadystateWithTimerId( + READYSTATE waiting_for, UINT_PTR timer_id) { + while (true) { + if (!browser_ || !event_sink_) + return false; + + // Is the browser in the required state now? + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = browser_->get_ReadyState(&ready_state); + if (FAILED(hr)) + return false; + + if (ready_state == waiting_for) { + event_sink_->remove_state(waiting_for); + return true; + } + + // Has the state been seen? + if (event_sink_->has_state(waiting_for)) { + event_sink_->remove_state(waiting_for); + return true; + } + + MSG msg = {}; + if (!GetMessage(&msg, 0, 0, 0)) { + // WM_QUIT. + return false; + } + + if (msg.message == WM_TIMER && + msg.hwnd == NULL && + msg.wParam == timer_id) { + // Timeout. + return false; + } + + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + +bool ShellBrowserTestBase::WaitForReadystate(READYSTATE waiting_for) { + if (!event_sink_) + return false; + + // Clear the seen states. + event_sink_->clear_states(); + + // Bound the wait by setting a timer. + UINT_PTR timer_id = ::SetTimer(NULL, + 0, + wait_timeout_ms_, + NULL); + EXPECT_NE(0, timer_id); + bool ret = WaitForReadystateWithTimerId(waiting_for, timer_id); + EXPECT_TRUE(::KillTimer(NULL, timer_id)); + + return ret; +} + +} // namespace testing diff --git a/ceee/ie/testing/mediumtest_ie_common.h b/ceee/ie/testing/mediumtest_ie_common.h new file mode 100644 index 0000000..048e620 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_common.h @@ -0,0 +1,204 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A common test fixture for testing against a captive shell browser +// control instance - which is as close to habitating the belly of the IE +// beast as can be done with a modicum of safety and sanitation. +#ifndef CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ +#define CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ + +#include <atlbase.h> +#include <atlcrack.h> +#include <atlwin.h> +#include <exdisp.h> +#include <exdispid.h> +#include <mshtmdid.h> + +#include <set> +#include <string> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace testing { + +// A test resource that's a simple single-resource page. +extern const wchar_t* kSimplePage; +// A test resource that's a frameset referencing the two frames below. +extern const wchar_t* kTwoFramesPage; +extern const wchar_t* kFrameOne; +extern const wchar_t* kFrameTwo; + +// Another copy of above on new URLs. +extern const wchar_t* kAnotherTwoFramesPage; +extern const wchar_t* kAnotherFrameOne; +extern const wchar_t* kAnotherFrameTwo; + +// A test resource that's a top-level frameset with two nested iframes +// the inner one of which refers frame_one.html. +extern const wchar_t* kDeepFramesPage; +extern const wchar_t* kLevelOneFrame; +extern const wchar_t* kLevelTwoFrame; + +// A test resource that have javascript generate frames that look orphan. +extern const wchar_t* kOrphansPage; + +// Constructs a file: url to the test file our source directory. +std::wstring GetTestUrl(const wchar_t* resource_name); + +// Returns the path to our temp folder. +std::wstring GetTempPath(); + +extern _ATL_FUNC_INFO handler_type_idispatch_; +extern _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + +// Base class that implements the rudiments of sinking events from +// an IWebBrowser. +class BrowserEventSinkBase + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<BrowserEventSinkBase>, + public InstanceCountMixin<BrowserEventSinkBase>, + public IDispEventSimpleImpl<0, + BrowserEventSinkBase, + &DIID_DWebBrowserEvents2>, + public IPropertyNotifySink { + public: + BEGIN_COM_MAP(BrowserEventSinkBase) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + END_COM_MAP() + + BEGIN_SINK_MAP(BrowserEventSinkBase) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, + OnNavigateComplete, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, + OnDocumentComplete, &handler_type_idispatch_variantptr_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + BrowserEventSinkBase() : prop_notify_cookie_(-1) { + } + + virtual void GetDescription(std::string* description) const; + HRESULT Initialize(IWebBrowser2* browser); + + void TearDown(); + + // @name IPropertyNotifySink implementation. + // @{ + STDMETHOD(OnChanged)(DISPID changed_property); + STDMETHOD(OnRequestEdit)(DISPID changed_property); + // @} + + // @name DWebBrowserEvents event handler implementation. + // Override these as needed. + // @{ + STDMETHOD_(void, OnNavigateComplete)(IDispatch* browser_disp, + VARIANT* url_var); + STDMETHOD_(void, OnDocumentComplete)(IDispatch* browser_disp, VARIANT* url); + + static bool has_state(READYSTATE state) { + return seen_states_.find(state) != seen_states_.end(); + } + static void add_state(READYSTATE state) { + seen_states_.insert(state); + } + static void remove_state(READYSTATE state) { + seen_states_.erase(state); + } + static void clear_states() { + seen_states_.clear(); + } + + private: + // This bitset records the readystate values seen in OnChange. + // The point of this is to make the WaitForReadyState function + // below work in the case where the browser takes multiple + // readystate changes while processing a single event. This + // will occur in the refresh case, where the top-level browser + // and individual document objects transition from COMPLETE to + // LOADING and then back to COMPLETE on processing a single event. + static std::set<READYSTATE> seen_states_; + + // Advise cookie for property notify sink. + DWORD prop_notify_cookie_; + + CComPtr<IWebBrowser2> browser_; +}; + +// Test fixture base class. +class ShellBrowserTestBase + : public testing::Test, + public CWindowImpl<ShellBrowserTestBase> { + public: + // Default timeout for pumping events. + static const UINT kWaitTimeoutMs = 2000; + + ShellBrowserTestBase() + : wait_timeout_ms_(kWaitTimeoutMs) { + } + + static void SetUpTestCase(); + static void TearDownTestCase(); + + void SetUp(); + void TearDown(); + + // Navigates the browser to the given URL and waits for + // the top-level browser's readystate to reach complete. + bool NavigateBrowser(const std::wstring& url); + + // Wait for the top-level browser's readystate to reach + // READYSTATE_COMPLETE, READYSTATE_LOADING or wait_for, + // respectively. + bool WaitForReadystateComplete(); + bool WaitForReadystateLoading(); + bool WaitForReadystate(READYSTATE wait_for); + + BEGIN_MSG_MAP_EX(BrowserEventTest) + END_MSG_MAP() + + protected: + bool WaitForReadystateWithTimerId(READYSTATE waiting_for, UINT_PTR timer_id); + virtual HRESULT CreateEventSink(IWebBrowser2* browser) = 0; + + // Timeout for pumping messages in WaitForReadyState. + UINT wait_timeout_ms_; + + CComPtr<IUnknown> host_; + CComPtr<IWebBrowser2> browser_; + + BrowserEventSinkBase* event_sink_; + CComPtr<IUnknown> event_sink_keeper_; +}; + +template <class EventSinkImpl> +class ShellBrowserTestImpl: public ShellBrowserTestBase { + public: + protected: + virtual HRESULT CreateEventSink(IWebBrowser2* browser) { + EventSinkImpl* sink; + HRESULT hr = EventSinkImpl::CreateInitialized(&sink, + browser, + &event_sink_keeper_); + event_sink_ = sink; + return hr; + } + + // Handy accessor to retrieve the event sink by actual type. + EventSinkImpl* event_sink() const { + return static_cast<EventSinkImpl*>(event_sink_); + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ diff --git a/ceee/ie/testing/mediumtest_ie_main.cc b/ceee/ie/testing/mediumtest_ie_main.cc new file mode 100644 index 0000000..b79fe46 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_main.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Main function for large IE tests. +#include <atlbase.h> +#include <atlcom.h> +#include "base/at_exit.h" +#include "base/command_line.h" +#include "ceee/testing/utils/gflag_utils.h" +#include "ceee/testing/utils/nt_internals.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +// Stub for unittesting. +namespace ceee_module_util { + +LONG LockModule() { + return 0; +} +LONG UnlockModule() { + return 0; +} +void Lock() { +} +void Unlock() { +} +void AddRefModuleWorkerThread() { +} +void ReleaseModuleWorkerThread() { +} +void FireEventToBroker(const std::string& event_name, + const std::string& event_args) { +} + +} // namespace ceee_module_util + + +// We're testing some ATL code that requires a module object. +class ObligatoryModule: public CAtlDllModuleT<ObligatoryModule> { +}; + +ObligatoryModule g_obligatory_atl_module; + +const DWORD kTestGFlags = FLG_USER_STACK_TRACE_DB | + FLG_ENABLE_HANDLE_EXCEPTIONS | + FLG_ENABLE_CLOSE_EXCEPTIONS | + FLG_HEAP_VALIDATE_PARAMETERS; + +int main(int argc, char **argv) { + // Disabled, bb2560934. + // if (!IsDebuggerPresent()) + // testing::RelaunchWithGFlags(kTestGFlags); + + testing::InitGoogleMock(&argc, argv); + testing::InitGoogleTest(&argc, argv); + + // Obligatory Chrome base initialization. + CommandLine::Init(argc, argv); + base::AtExitManager at_exit_manager; + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + return RUN_ALL_TESTS(); +} diff --git a/ceee/ie/testing/mock_broker_and_friends.h b/ceee/ie/testing/mock_broker_and_friends.h new file mode 100644 index 0000000..7c02845 --- /dev/null +++ b/ceee/ie/testing/mock_broker_and_friends.h @@ -0,0 +1,359 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of Broker and related objects. +#ifndef CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ +#define CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +#include "broker_lib.h" // NOLINT +#include "toolband.h" // NOLINT + +namespace testing { + +class MockBrokerImpl : public ICeeeBroker { + public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, Execute, HRESULT(BSTR, BSTR*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, FireEvent, HRESULT(BSTR, BSTR)); +}; + +class MockBrokerRegistrarImpl : public ICeeeBrokerRegistrar { + public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, RegisterWindowExecutor, + HRESULT(long, IUnknown*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, UnregisterExecutor, HRESULT(long)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, RegisterTabExecutor, + HRESULT(long, IUnknown*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetTabIdForHandle, + HRESULT(long, CeeeWindowHandle)); +}; + +class MockBroker + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockBroker>, + public InstanceCountMixin<MockBroker>, + public StrictMock<MockBrokerRegistrarImpl>, + public StrictMock<MockBrokerImpl> { + public: + BEGIN_COM_MAP(MockBroker) + COM_INTERFACE_ENTRY(ICeeeBrokerRegistrar) + COM_INTERFACE_ENTRY(ICeeeBroker) + END_COM_MAP() + + HRESULT Initialize(MockBroker** self) { + *self = this; + return S_OK; + } +}; + +class MockExecutorIUnknown + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockExecutorIUnknown>, + public InstanceCountMixin<MockExecutorIUnknown>, + public IObjectWithSiteImpl<MockExecutorIUnknown> { + public: + BEGIN_COM_MAP(MockExecutorIUnknown) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockExecutorIUnknown** self) { + *self = this; + return S_OK; + } +}; + +class MockCeeeWindowExecutorImpl : public ICeeeWindowExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Initialize, HRESULT(CeeeWindowHandle)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetWindow, + HRESULT(BOOL, CeeeWindowInfo*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabs, HRESULT(BSTR*)); + MOCK_METHOD5_WITH_CALLTYPE(__stdcall, UpdateWindow, HRESULT(long, long, + long, long, CeeeWindowInfo*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, RemoveWindow, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetTabIndex, HRESULT(CeeeWindowHandle, + long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, MoveTab, HRESULT(CeeeWindowHandle, + long)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, RemoveTab, HRESULT(CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SelectTab, HRESULT(CeeeWindowHandle)); +}; + +class MockWindowExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockWindowExecutor>, + public InstanceCountMixin<MockWindowExecutor>, + public IObjectWithSiteImpl<MockWindowExecutor>, + public StrictMock<MockCeeeWindowExecutorImpl> { + public: + BEGIN_COM_MAP(MockWindowExecutor) + COM_INTERFACE_ENTRY(ICeeeWindowExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockWindowExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockCeeeTabExecutorImpl : public ICeeeTabExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Initialize, HRESULT(CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabInfo, + HRESULT(CeeeTabInfo*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, Navigate, HRESULT(BSTR, long, BSTR)); + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, InsertCode, HRESULT( + BSTR, BSTR, BOOL, CeeeTabCodeType)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PopupizeFrameWindow, HRESULT(long)); +}; + +class MockTabExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockTabExecutor>, + public InstanceCountMixin<MockTabExecutor>, + public IObjectWithSiteImpl<MockTabExecutor>, + public StrictMock<MockCeeeTabExecutorImpl> { + public: + BEGIN_COM_MAP(MockTabExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockTabExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockTabEventsFunnel : public TabEventsFunnel { + public: + MOCK_METHOD4(OnMoved, HRESULT(HWND tab, int window_id, int from_index, + int to_index)); + MOCK_METHOD1(OnRemoved, HRESULT(HWND tab)); + MOCK_METHOD2(OnSelectionChanged, HRESULT(HWND tab, int window_id)); + MOCK_METHOD3(OnCreated, HRESULT(HWND tab, BSTR url, bool completed)); + MOCK_METHOD3(OnUpdated, HRESULT(HWND tab, BSTR url, + READYSTATE ready_state)); + MOCK_METHOD2(OnTabUnmapped, HRESULT(HWND tab, int tab_id)); +}; + +class MockCeeeCookieExecutorImpl : public ICeeeCookieExecutor { + public: + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetCookie, + HRESULT(BSTR, BSTR, CeeeCookieInfo*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, RegisterCookieStore, HRESULT()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, CookieStoreIsRegistered, HRESULT()); +}; + +class MockCookieExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockCookieExecutor>, + public InstanceCountMixin<MockCookieExecutor>, + public IObjectWithSiteImpl<MockCookieExecutor>, + public StrictMock<MockCeeeCookieExecutorImpl> { + public: + BEGIN_COM_MAP(MockCookieExecutor) + COM_INTERFACE_ENTRY(ICeeeCookieExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockCookieExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockCookieEventsFunnel : public CookieEventsFunnel { + public: + MOCK_METHOD2(OnChanged, HRESULT(bool removed, + const cookie_api::CookieInfo& cookie)); +}; + +class MockCeeeInfobarExecutorImpl : public ICeeeInfobarExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetExtensionId, HRESULT(BSTR)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, ShowInfobar, + HRESULT(BSTR, CeeeWindowHandle*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnTopFrameBeforeNavigate, + HRESULT(BSTR)); +}; + +class MockInfobarExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockInfobarExecutor>, + public InstanceCountMixin<MockInfobarExecutor>, + public IObjectWithSiteImpl<MockInfobarExecutor>, + public StrictMock<MockCeeeInfobarExecutorImpl> { + public: + BEGIN_COM_MAP(MockInfobarExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockInfobarExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockTabInfobarExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockTabInfobarExecutor>, + public InstanceCountMixin<MockTabInfobarExecutor>, + public IObjectWithSiteImpl<MockTabInfobarExecutor>, + public StrictMock<MockCeeeTabExecutorImpl>, + public StrictMock<MockCeeeInfobarExecutorImpl> { + public: + BEGIN_COM_MAP(MockTabInfobarExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockTabInfobarExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockApiDispatcher : public ApiDispatcher { + public: + MOCK_METHOD2(HandleApiRequest, void(BSTR, BSTR*)); + MOCK_METHOD2(RegisterInvocation, void(const char* function_name, + InvocationFactory factory)); + MOCK_METHOD3(RegisterEphemeralEventHandler, + void(const char*, EphemeralEventHandler, InvocationResult*)); + MOCK_METHOD3(GetExecutor, void(HWND, REFIID, void**)); + MOCK_METHOD2(FireEvent, void(BSTR event_name, BSTR event_args)); + + MOCK_CONST_METHOD1(GetTabHandleFromId, HWND(int)); + MOCK_CONST_METHOD1(GetWindowHandleFromId, HWND(int)); + MOCK_CONST_METHOD1(GetTabIdFromHandle, int(HWND)); + MOCK_CONST_METHOD1(GetWindowIdFromHandle, int(HWND)); +}; + +// A mock class for an API invocation class that derives from ApiResultCreator, +// defined in ceee\ie\broker\api_dispatcher.h. This class enables the injection +// of a mock API result instance when the API is invoked. +template<class ResultType, class MockResultType, class BaseClass> +class MockApiInvocation : public BaseClass { + public: + MockApiInvocation() : request_id_(kRequestId) {} + + // Calls the ContinueExecution method of the base class, using the mock + // invocation result and mock API dispatcher. + HRESULT CallContinueExecution(const std::string& input_args) { + HRESULT hr = ContinueExecution(input_args, invocation_result_.get(), + GetDispatcher()); + + // NOTE: the ContinueExecution method has already deleted the invocation + // result if its return is not S_FALSE. In that case, we have to release + // the pointer so that we don't delete the same object twice. + if (hr != S_FALSE) + invocation_result_.release(); + + return hr; + } + + // We need to create the results before we get asked for one + // so that we can set expectations on it. And we need to allocate them + // because the callers of CreateApiResult take ownership of the memory. + void AllocateNewResult(int request_id) { + request_id_ = request_id; + invocation_result_.reset(new StrictMock<MockResultType>(request_id)); + } + virtual ResultType* CreateApiResult(int request_id) { + EXPECT_EQ(request_id, request_id_); + EXPECT_NE(static_cast<ResultType*>(NULL), invocation_result_.get()); + return invocation_result_.release(); // The caller becomes the owner. + } + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + // public so that the tests can set expectations on them. + scoped_ptr<StrictMock<MockResultType> > invocation_result_; + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + int request_id_; +}; + +class MockWebNavigationEventsFunnel : public WebNavigationEventsFunnel { + public: + MOCK_METHOD5(OnBeforeNavigate, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp)); + MOCK_METHOD4(OnBeforeRetarget, HRESULT(CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp)); + MOCK_METHOD6(OnCommitted, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp)); + MOCK_METHOD4(OnCompleted, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp)); + MOCK_METHOD4(OnDOMContentLoaded, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp)); + MOCK_METHOD5(OnErrorOccurred, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp)); +}; + +class MockWebRequestEventsFunnel : public WebRequestEventsFunnel { + public: + MOCK_METHOD5(OnBeforeRedirect, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp)); + MOCK_METHOD6(OnBeforeRequest, HRESULT(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp)); + MOCK_METHOD4(OnCompleted, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp)); + MOCK_METHOD4(OnErrorOccurred, HRESULT(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp)); + MOCK_METHOD4(OnHeadersReceived, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp)); + MOCK_METHOD4(OnRequestSent, HRESULT(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp)); +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ diff --git a/ceee/ie/testing/mock_browser_and_friends.h b/ceee/ie/testing/mock_browser_and_friends.h new file mode 100644 index 0000000..76db63c --- /dev/null +++ b/ceee/ie/testing/mock_browser_and_friends.h @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of TestBrowser and related objects. +#ifndef CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ +#define CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ + +#include <string> + +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" + +namespace testing { + +class MockIOleWindow : public IOleWindow { + public: + // Simple IOleWindow implementation here, no need for a MockXxxImpl. + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetWindow, HRESULT(HWND* window)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, ContextSensitiveHelp, HRESULT(BOOL)); +}; + +class TestBrowserSite + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowserSite>, + public InitializingCoClass<TestBrowserSite>, + public IServiceProviderImpl<TestBrowserSite>, + public StrictMock<MockIOleWindow> { + public: + BEGIN_COM_MAP(TestBrowserSite) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IOleWindow) + END_COM_MAP() + + BEGIN_SERVICE_MAP(TestDumbSite) + if (browser_ && (guidService == SID_SWebBrowserApp || + guidService == SID_SShellBrowser)) { + return browser_->QueryInterface(riid, ppvObject); + } + END_SERVICE_MAP() + + HRESULT Initialize(TestBrowserSite** self) { + *self = this; + return S_OK; + } + + public: + CComPtr<IDispatch> browser_; +}; + +class TestBrowser + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowser>, + public InitializingCoClass<TestBrowser>, + public StrictMock<IWebBrowser2MockImpl>, + public IConnectionPointImpl<TestBrowser, &DIID_DWebBrowserEvents2>, + public IConnectionPointContainerImpl<TestBrowser> { + public: + typedef IConnectionPointImpl<TestBrowser, &DIID_DWebBrowserEvents2> + WebBrowserEvents; + + BEGIN_COM_MAP(TestBrowser) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IWebBrowser) + COM_INTERFACE_ENTRY(IWebBrowserApp) + COM_INTERFACE_ENTRY(IWebBrowser2) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(TestBrowser) + CONNECTION_POINT_ENTRY(DIID_DWebBrowserEvents2) + END_CONNECTION_POINT_MAP() + + // Override from IConnectionPointContainerImpl. + STDMETHOD(FindConnectionPoint)(REFIID iid, IConnectionPoint** cp) { + typedef IConnectionPointContainerImpl<TestBrowser> CPC; + + if (iid == DIID_DWebBrowserEvents2 && no_events_) + return CONNECT_E_NOCONNECTION; + + return CPC::FindConnectionPoint(iid, cp); + } + + void FireOnNavigateComplete(IDispatch* browser, VARIANT *url) { + CComVariant args[] = { 0, browser }; + args[0].vt = VT_BYREF | VT_BSTR; + args[0].pvarVal = url; + + testing::FireEvent(static_cast<WebBrowserEvents*>(this), + DISPID_NAVIGATECOMPLETE2, + arraysize(args), + args); + } + + TestBrowser() : no_events_(false) { + } + + HRESULT Initialize(TestBrowser** self) { + *self = this; + return S_OK; + } + + public: + // If true, won't return the WebBrowserEvents connection point. + bool no_events_; +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ diff --git a/ceee/ie/testing/mock_chrome_frame_host.h b/ceee/ie/testing/mock_chrome_frame_host.h new file mode 100644 index 0000000..8ef07ff --- /dev/null +++ b/ceee/ie/testing/mock_chrome_frame_host.h @@ -0,0 +1,62 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of ChromeFrameHost. +#ifndef CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ +#define CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ + +#include <string> +#include "ceee/ie/common/chrome_frame_host.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace testing { + +class IChromeFrameHostMockImpl : public IChromeFrameHost { + public: + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, SetAsChromeFrameMaster, void()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetChromeProfileName, + void(const wchar_t* chrome_profile_name)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetUrl, HRESULT(BSTR url)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, StartChromeFrame, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, PostMessage, + HRESULT(BSTR message, BSTR target)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, TearDown, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetEventSink, + void(IChromeFrameHostEvents* event_sink)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, InstallExtension, + HRESULT(BSTR crx_path)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, LoadExtension, + HRESULT(BSTR extension_dir)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, GetEnabledExtensions, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetExtensionApisToAutomate, + HRESULT(BSTR* enabled_functions)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetSessionId, HRESULT(int*)); +}; + +// A mock implementation of ChromeFrameHost. +class MockChromeFrameHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<StrictMock<MockChromeFrameHost> >, + public InstanceCountMixin<MockChromeFrameHost>, + public StrictMock<IChromeFrameHostMockImpl> { + public: + BEGIN_COM_MAP(MockChromeFrameHost) + COM_INTERFACE_ENTRY_IID(IID_IChromeFrameHost, IChromeFrameHost) + END_COM_MAP() + + HRESULT Initialize() { + return S_OK; + } + + HRESULT Initialize(MockChromeFrameHost** self) { + *self = this; + return S_OK; + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ diff --git a/ceee/ie/testing/mock_frame_event_handler_host.h b/ceee/ie/testing/mock_frame_event_handler_host.h new file mode 100644 index 0000000..18e9572 --- /dev/null +++ b/ceee/ie/testing/mock_frame_event_handler_host.h @@ -0,0 +1,81 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of ChromeFrameHost. +#ifndef CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ +#define CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ + +#include <string> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "gmock/gmock.h" + +namespace testing { + +class IFrameEventHandlerHostMockImpl : public IFrameEventHandlerHost { + public: + MOCK_METHOD3(AttachBrowser, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler)); + MOCK_METHOD3(DetachBrowser, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler)); + MOCK_METHOD1(GetTopLevelBrowser, HRESULT(IWebBrowser2** browser)); + MOCK_METHOD1(GetReadyState, HRESULT(READYSTATE* ready_state)); + MOCK_METHOD1(OnReadyStateChanged, HRESULT(READYSTATE ready_state)); + MOCK_METHOD3(GetMatchingUserScriptsCssContent, + HRESULT(const GURL& url, bool require_all_frames, + std::string* css_content)); + MOCK_METHOD4(GetMatchingUserScriptsJsContent, + HRESULT(const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list)); + MOCK_METHOD1(GetExtensionId, HRESULT(std::wstring* extension_id)); + MOCK_METHOD1(GetExtensionPath, HRESULT(std::wstring* extension_path)); + MOCK_METHOD1(GetExtensionPortMessagingProvider, + HRESULT(IExtensionPortMessagingProvider** messaging_provider)); + MOCK_METHOD4(InsertCode, HRESULT(BSTR, BSTR, BOOL, CeeeTabCodeType)); +}; + +class IExtensionPortMessagingProviderMockImpl + : public IExtensionPortMessagingProvider { + public: + MOCK_METHOD1(CloseAll, void(IContentScriptNativeApi* instance)); + MOCK_METHOD4(OpenChannelToExtension, + HRESULT(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie)); + MOCK_METHOD2(PostMessage, HRESULT(int port_id, const std::string& message)); +}; + +template<class BaseClass> +class MockFrameEventHandlerHostBase + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<BaseClass>, + public InstanceCountMixin<BaseClass>, + public StrictMock<IFrameEventHandlerHostMockImpl>, + public StrictMock<IExtensionPortMessagingProviderMockImpl> { + public: + BEGIN_COM_MAP(MockFrameEventHandlerHostBase) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandlerHost, IFrameEventHandlerHost) + END_COM_MAP() +}; + +class MockFrameEventHandlerHost + : public MockFrameEventHandlerHostBase<MockFrameEventHandlerHost> { + public: + HRESULT Initialize(MockFrameEventHandlerHost** self) { + *self = this; + return S_OK; + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ diff --git a/ceee/ie/testing/precompile.cc b/ceee/ie/testing/precompile.cc new file mode 100644 index 0000000..975a615 --- /dev/null +++ b/ceee/ie/testing/precompile.cc @@ -0,0 +1,6 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile generator file. +#include "ceee/ie/testing/precompile.h" diff --git a/ceee/ie/testing/precompile.h b/ceee/ie/testing/precompile.h new file mode 100644 index 0000000..ef85751 --- /dev/null +++ b/ceee/ie/testing/precompile.h @@ -0,0 +1,19 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile header for IE CEEE unittests. + +#ifndef CEEE_IE_TESTING_PRECOMPILE_H_ +#define CEEE_IE_TESTING_PRECOMPILE_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <atlstr.h> + +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#endif // CEEE_IE_TESTING_PRECOMPILE_H_ diff --git a/ceee/ie/testing/test_data/another_frame_one.html b/ceee/ie/testing/test_data/another_frame_one.html new file mode 100644 index 0000000..a97a903 --- /dev/null +++ b/ceee/ie/testing/test_data/another_frame_one.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another Frame One</title></head> +<body> +<h1>Another Frame One</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/another_frame_two.html b/ceee/ie/testing/test_data/another_frame_two.html new file mode 100644 index 0000000..fbd64e6 --- /dev/null +++ b/ceee/ie/testing/test_data/another_frame_two.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another Frame Two</title></head> +<body> +<h1>Another Frame Two</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/another_two_frames.html b/ceee/ie/testing/test_data/another_two_frames.html new file mode 100644 index 0000000..339f133 --- /dev/null +++ b/ceee/ie/testing/test_data/another_two_frames.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another two frames in a frameset</title></head> +<frameset cols="50%,50%"> + <frame src="another_frame_one.html" /> + <frame src="another_frame_two.html" /> +</frameset> +</html> diff --git a/ceee/ie/testing/test_data/deep_frames.html b/ceee/ie/testing/test_data/deep_frames.html new file mode 100644 index 0000000..a89090c --- /dev/null +++ b/ceee/ie/testing/test_data/deep_frames.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Deep frames</title></head> +<frameset cols="50%,50%"> + <frame src="level_one_frame.html" name="deep_frames" /> +</frameset> +</html> diff --git a/ceee/ie/testing/test_data/frame_one.html b/ceee/ie/testing/test_data/frame_one.html new file mode 100644 index 0000000..3bba64e --- /dev/null +++ b/ceee/ie/testing/test_data/frame_one.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Frame One</title></head> +<body> +<h1>Frame One</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/frame_two.html b/ceee/ie/testing/test_data/frame_two.html new file mode 100644 index 0000000..e4bdd8c --- /dev/null +++ b/ceee/ie/testing/test_data/frame_two.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Frame Two</title></head> +<body> +<h1>Frame Two</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/level_one_frame.html b/ceee/ie/testing/test_data/level_one_frame.html new file mode 100644 index 0000000..4422375 --- /dev/null +++ b/ceee/ie/testing/test_data/level_one_frame.html @@ -0,0 +1,14 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Level One Frame</title></head> +<body> + <h1>Level One Frame</h1> + <iframe width="100%" height="400px" src="level_two_frame.html" + name="level_one" /> +</body> +</html> diff --git a/ceee/ie/testing/test_data/level_two_frame.html b/ceee/ie/testing/test_data/level_two_frame.html new file mode 100644 index 0000000..0c84867 --- /dev/null +++ b/ceee/ie/testing/test_data/level_two_frame.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Level Two Frame</title></head> +<body> + <h1>Level Two Frame</h1> + <iframe width="100%" height="400px" src="frame_one.html" name="level_two"/> +</body> +</html> diff --git a/ceee/ie/testing/test_data/orphans.html b/ceee/ie/testing/test_data/orphans.html new file mode 100644 index 0000000..65fcf10 --- /dev/null +++ b/ceee/ie/testing/test_data/orphans.html @@ -0,0 +1,72 @@ +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> + <head> + <script> +var isIECache = "unset"; +function isIE() { + if (isIECache == "unset") { + isIECache = 0; + try { + if (navigator.appName == "Microsoft Internet Explorer") { + var a = navigator.userAgent, b = /MSIE ([0-9]{1,}[.0-9]{0,})/; + if (b.exec(a) != null) { + rv = parseFloat(RegExp.$1); + if (rv > 0) + isIECache = rv; + } + } + } catch (c) { + } + } + return isIECache; +} + +function ie7Mode() { + if (isIE()) + if (document.compatMode == "BackCompat") + return true; + else if (isIE() < 8) + return true; + else if (document.documentMode > 0 && document.documentMode < 8) + return true; + return false; +} + +function go() { + var temp_frame = document.createElement("iframe"); + temp_frame.src="#BlaBlaBla"; + var j = document.getElementById("segmentDiv"); + j.appendChild(temp_frame); + var inner_content = null; + if (temp_frame.contentDocument) { + inner_content = temp_frame.contentDocument; + } else if (temp_frame.contentWindow) { + inner_content = temp_frame.contentWindow; + } + if (inner_content) { + var inner_document = null; + if (ie7Mode()) + inner_document = inner_content.window.document; + else { + inner_content.open(); + inner_content.close(); + inner_document = inner_content; + } + inner_document.write('<html><body><div id="iframeDIV" ></div></body></html>'); + inner_document.close(); + + inner_document.getElementById("iframeDIV").innerHTML = '<iframe src="./frame_one.html"></iframe>'; + } else { + alert('no content!'); + } +} + </script> + </head> + <body> + <div id="segmentDiv"></div> + <script> go(); </script> + </body> +</html> diff --git a/ceee/ie/testing/test_data/simple_page.html b/ceee/ie/testing/test_data/simple_page.html new file mode 100644 index 0000000..74fd506 --- /dev/null +++ b/ceee/ie/testing/test_data/simple_page.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Simple Page</title></head> +<body> +<h1>This is the simplest possible page.</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/two_frames.html b/ceee/ie/testing/test_data/two_frames.html new file mode 100644 index 0000000..dd8b0c4 --- /dev/null +++ b/ceee/ie/testing/test_data/two_frames.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Two frames in a frameset</title></head> +<frameset cols="50%,50%"> + <frame src="frame_one.html" /> + <frame src="frame_two.html" /> +</frameset> +</html> |