diff options
Diffstat (limited to 'ceee/ie/broker')
48 files changed, 11479 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 |