diff options
Diffstat (limited to 'ceee/ie')
192 files changed, 41211 insertions, 0 deletions
diff --git a/ceee/ie/broker/api_dispatcher.cc b/ceee/ie/broker/api_dispatcher.cc new file mode 100644 index 0000000..218a198 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher.cc @@ -0,0 +1,306 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Dispatcher and registry for Chrome Extension APIs. + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/broker/infobar_api_module.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/broker/webnavigation_api_module.h" +#include "ceee/ie/broker/webrequest_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "chrome/browser/automation/extension_automation_constants.h" + +namespace keys = extension_automation_constants; + +bool ApiDispatcher::IsRunningInSingleThread() { + return thread_id_ == ::GetCurrentThreadId(); +} + + +void ApiDispatcher::HandleApiRequest(BSTR message_text, BSTR* response) { + DCHECK(IsRunningInSingleThread()); + DCHECK(message_text); + + scoped_ptr<Value> base_value(base::JSONReader::Read(CW2A(message_text).m_psz, + false)); + DCHECK(base_value.get() && base_value->GetType() == Value::TYPE_DICTIONARY); + if (!base_value.get() || base_value->GetType() != Value::TYPE_DICTIONARY) { + return; + } + DictionaryValue* value = static_cast<DictionaryValue*>(base_value.get()); + + std::string function_name; + if (!value->GetString(keys::kAutomationNameKey, &function_name)) { + DCHECK(false); + return; + } + + std::string args_string; + if (!value->GetString(keys::kAutomationArgsKey, &args_string)) { + DCHECK(false); + return; + } + + base::JSONReader reader; + scoped_ptr<Value> args(reader.JsonToValue(args_string, false, false)); + DCHECK(args.get() && args->GetType() == Value::TYPE_LIST); + if (!args.get() || args->GetType() != Value::TYPE_LIST) { + return; + } + ListValue* args_list = static_cast<ListValue*>(args.get()); + + int request_id = -1; + if (!value->GetInteger(keys::kAutomationRequestIdKey, &request_id)) { + DCHECK(false); + return; + } + DLOG(INFO) << "Request: " << request_id << ", for: " << function_name; + FactoryMap::const_iterator iter = factories_.find(function_name); + DCHECK(iter != factories_.end()); + if (iter == factories_.end()) { + return; + } + + scoped_ptr<Invocation> invocation(iter->second()); + DCHECK(invocation.get()); + invocation->Execute(*args_list, request_id); +} + +void ApiDispatcher::FireEvent(BSTR event_name, BSTR event_args) { + DCHECK(IsRunningInSingleThread()); + DLOG(INFO) << "ApiDispatcher::FireEvent. " << event_name << " - " << + event_args; + std::string event_args_str(CW2A(event_args).m_psz); + std::string event_name_str(CW2A(event_name).m_psz); + // Start by going through the permanent event handlers map. + PermanentEventHandlersMap::const_iterator iter = + permanent_event_handlers_.find(event_name_str); + bool fire_event = true; + std::string converted_event_args; + if (iter != permanent_event_handlers_.end()) { + fire_event = iter->second(event_args_str, &converted_event_args, this); + } else { + // Some events don't need a handler and + // have all the info already available. + converted_event_args = event_args_str; + } + + if (fire_event) { + // The notification message is an array containing event name as a string + // at index 0 and then a JSON encoded string of the other arguments. + ListValue message; + message.Append(Value::CreateStringValue(event_name_str)); + + // Event messages expect a string for the arguments. + message.Append(Value::CreateStringValue(converted_event_args)); + + std::string message_str; + base::JSONWriter::Write(&message, false, &message_str); + ChromePostman::GetInstance()->PostMessage(CComBSTR(message_str.c_str()), + CComBSTR(keys::kAutomationBrowserEventRequestTarget)); + } + + // We do this after firing the event, to be in the same order as Chrome. + // And to allow reusing the args conversion from the permanent handler. + EphemeralEventHandlersMap::iterator map_iter = + ephemeral_event_handlers_.find(event_name_str); + if (map_iter != ephemeral_event_handlers_.end() && + !map_iter->second.empty()) { + // We must work with a copy since other handlers might get registered + // as we call the current set of handlers (some of them chain themselves). + EphemeralEventHandlersList handlers_list; + map_iter->second.swap(handlers_list); + // Swap emptied our current list, so we will need to add back the items + // that are not affected by this call (the ones that we wouldn't want + // to remove from the list just yet return S_FALSE). This scheme is much + // simpler than trying to keep up with the live list as it may change. + EphemeralEventHandlersList::iterator list_iter = handlers_list.begin(); + for (;list_iter != handlers_list.end(); ++list_iter) { + if (list_iter->handler(converted_event_args, + list_iter->user_data, this) == S_FALSE) { + map_iter->second.push_back(*list_iter); + } + } + } +} + +void ApiDispatcher::GetExecutor(HWND window, REFIID iid, void** executor) { + DWORD thread_id = ::GetWindowThreadProcessId(window, NULL); + HRESULT hr = Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>::get()-> + GetExecutor(thread_id, window, iid, executor); + DLOG_IF(INFO, FAILED(hr)) << "Failed to get executor for window: " << + window << ". In thread: " << thread_id << ". " << com::LogHr(hr); +} + + +HWND ApiDispatcher::GetTabHandleFromId(int tab_id) const { + return Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + GetTabHandleFromId(tab_id); +} + +HWND ApiDispatcher::GetWindowHandleFromId(int window_id) const { + return reinterpret_cast<HWND>(window_id); +} + +int ApiDispatcher::GetTabIdFromHandle(HWND tab_handle) const { + return Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + GetTabIdFromHandle(tab_handle); +} + +int ApiDispatcher::GetWindowIdFromHandle(HWND window_handle) const { + return reinterpret_cast<int>(window_handle); +} + + +void ApiDispatcher::SetTabIdForHandle(long tab_id, HWND handle) { + Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + SetTabIdForHandle(tab_id, handle); +} + +void ApiDispatcher::DeleteTabHandle(HWND handle) { + Singleton<ExecutorsManager, ExecutorsManager::SingletonTraits>::get()-> + DeleteTabHandle(handle); +} + +void ApiDispatcher::RegisterInvocation(const char* function_name, + InvocationFactory factory) { + DCHECK(function_name && factory); + // Re-registration is not expected. + DCHECK(function_name && factories_.find(function_name) == factories_.end()); + if (function_name && factory) + factories_[function_name] = factory; +} + +void ApiDispatcher::RegisterPermanentEventHandler( + const char* event_name, PermanentEventHandler event_handler) { + DCHECK(event_name && event_handler); + // Re-registration is not expected. + DCHECK(event_name && permanent_event_handlers_.find(event_name) == + permanent_event_handlers_.end()); + if (event_name && event_handler) + permanent_event_handlers_[event_name] = event_handler; +} + +void ApiDispatcher::RegisterEphemeralEventHandler( + const char* event_name, + EphemeralEventHandler event_handler, + InvocationResult* user_data) { + DCHECK(IsRunningInSingleThread()); + // user_data could be NULL, we don't care. + DCHECK(event_handler != NULL); + if (event_handler != NULL) { + ephemeral_event_handlers_[event_name].push_back( + EphemeralEventHandlerTuple(event_handler, user_data)); + } +} + +ApiDispatcher::InvocationResult::~InvocationResult() { + // We must have posted the response before we died. + DCHECK(request_id_ == kNoRequestId); +} + +// A quick helper function to reuse the call to ChromePostman. +void ApiDispatcher::InvocationResult::PostResponseToChromeFrame( + const char* response_key, const std::string& response_str) { + DictionaryValue response_dict; + response_dict.SetInteger(keys::kAutomationRequestIdKey, request_id_); + response_dict.SetString(response_key, response_str); + + std::string response; + base::JSONWriter::Write(&response_dict, false, &response); + ChromePostman::GetInstance()->PostMessage( + CComBSTR(response.c_str()), CComBSTR(keys::kAutomationResponseTarget)); +} + +// Once an invocation completes successfully, it must call this method on its +// result object to send it back to Chrome. +void ApiDispatcher::InvocationResult::PostResult() { + // We should never post results twice, or for non requested results. + DCHECK(request_id_ != kNoRequestId); + if (request_id_ != kNoRequestId) { + std::string result_str; + // Some invocations don't return values. We can set an empty string here. + if (value_.get() != NULL) + base::JSONWriter::Write(value_.get(), false, &result_str); + PostResponseToChromeFrame(keys::kAutomationResponseKey, result_str); + // We should only post once! + request_id_ = kNoRequestId; + } +} + +// Invocations can use this method to post an error to Chrome when it +// couldn't complete the invocation successfully. +void ApiDispatcher::InvocationResult::PostError(const std::string& error) { + LOG(ERROR) << error; + // Event handlers reuse InvocationResult code without a requestId, + // so don't DCHECK as in PostResult here. + // TODO(mad@chromium.org): Might be better to use a derived class instead. + if (request_id_ != kNoRequestId) { + PostResponseToChromeFrame(keys::kAutomationErrorKey, error); + // We should only post once! + request_id_ = kNoRequestId; + } +} + +void ApiDispatcher::InvocationResult::SetValue(const char* name, Value* value) { + DCHECK(name != NULL && value != NULL); + scoped_ptr<Value> value_holder(value); + if (name == NULL || value == NULL) + return; + + if (temp_values_.get() == NULL) { + temp_values_.reset(new DictionaryValue); + } + + // scoped_ptr::release() lets go of its ownership. + temp_values_->Set(name, value_holder.release()); +} + +const Value* ApiDispatcher::InvocationResult::GetValue(const char* name) { + if (temp_values_.get() == NULL) + return NULL; + + Value* value = NULL; + bool success = temp_values_->Get(name, &value); + // Make sure that success means NotNull and vice versa. + DCHECK((value != NULL || !success) && (success || value == NULL)); + return value; +} + +ApiDispatcher* ApiDispatcher::InvocationResult::GetDispatcher() { + return ProductionApiDispatcher::get(); +} + +ApiDispatcher* ApiDispatcher::Invocation::GetDispatcher() { + return ProductionApiDispatcher::get(); +} + +// Function registration preprocessor magic. See api_registration.h for details. +#ifdef REGISTER_ALL_API_FUNCTIONS +#error Must not include api_registration.h previously in this compilation unit. +#endif // REGISTER_ALL_API_FUNCTIONS +#define PRODUCTION_API_DISPATCHER +#define REGISTER_TAB_API_FUNCTIONS() tab_api::RegisterInvocations(this) +#define REGISTER_WINDOW_API_FUNCTIONS() window_api::RegisterInvocations(this) +#define REGISTER_COOKIE_API_FUNCTIONS() cookie_api::RegisterInvocations(this) +#define REGISTER_INFOBAR_API_FUNCTIONS() infobar_api::RegisterInvocations(this) +#define REGISTER_WEBNAVIGATION_API_FUNCTIONS() \ + webnavigation_api::RegisterInvocations(this) +#define REGISTER_WEBREQUEST_API_FUNCTIONS() \ + webrequest_api::RegisterInvocations(this) +#include "ceee/ie/common/api_registration.h" + +ProductionApiDispatcher::ProductionApiDispatcher() { + REGISTER_ALL_API_FUNCTIONS(); +} diff --git a/ceee/ie/broker/api_dispatcher.h b/ceee/ie/broker/api_dispatcher.h new file mode 100644 index 0000000..db13110 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher.h @@ -0,0 +1,377 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Dispatcher and registry for Chrome Extension APIs. + +#ifndef CEEE_IE_BROKER_API_DISPATCHER_H_ +#define CEEE_IE_BROKER_API_DISPATCHER_H_ + +#include <list> +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "base/values.h" + +#include "broker_lib.h" // NOLINT + +class ExecutorsManager; + +// Keeps a registry of all API Invocation implementations and implements +// the logic needed to deserialize API Invocation requests, dispatch them, +// and serialize and return the response. +class ApiDispatcher { + public: + ApiDispatcher() : thread_id_(0) {} + virtual ~ApiDispatcher() {} + + // Dispatches a Chrome Extensions API request and sends back the response. + // + // @param message_text The raw JSON-encoded text of the API request over + // the automation interface. + // @param response Where to return the JSON-encoded response. + virtual void HandleApiRequest(BSTR message_text, BSTR* response); + + // Fire the given event message to Chrome Frame and potentially use it + // to complete pending extension API execution. + // + // @param event_name The name of the event to fire. + // @param event_args The JSON encoded event arguments. + virtual void FireEvent(BSTR event_name, BSTR event_args); + + // This class holds on the result and can be derived from to generate results + // that are either specific to tabs or windows for example. + class InvocationResult { + public: + static const int kNoRequestId = -1; + explicit InvocationResult(int request_id) + : request_id_(request_id) { + } + virtual ~InvocationResult(); + + // Post the response string from our current result to Chrome Frame. + virtual void PostResult(); + + // Post the given error string to Chrome Frame. + virtual void PostError(const std::string& error); + + // Identifies if we are pending a post (for which we need a request id). + virtual bool Pending() const { return request_id_ != kNoRequestId; } + + // Returns the result of the API invocation as a value object which + // is still owned by this object - the caller does not take ownership. + // May return NULL if execution didn't produce any results yet. + const Value* value() const { + return value_.get(); + } + void set_value(Value* value) { + value_.reset(value); + } + + // Sets a value in a dictionary. The Value pointer is kept in the + // dictionary which takes ownership so the caller should NOT deallocate it. + // Even in case of errors, the value will be deallocated. + // This value can then be retrieved by GetValue below, this can be useful + // to keep information in the InvocationResult during async handling. + virtual void SetValue(const char* name, Value* value); + + // Returns a value set by SetValue above. Note that the value is only + // valid during the lifetime of the InvocationResult object or until another + // value is set for the same name. Also, the returned value should NOT + // be deallocated by the caller, it will get deallocated on destruction + // of the object, or when another value is set for the same name. + // Returns NULL if no value of this name have been previously set. + virtual const Value* GetValue(const char* name); + + protected: + // A unit test seam. + virtual ApiDispatcher* GetDispatcher(); + + // A helper function to post the given response to Chrome Frame. + virtual void PostResponseToChromeFrame(const char* response_key, + const std::string& response_str); + // Where to store temporary values. + scoped_ptr<DictionaryValue> temp_values_; + + // Where to store the result value. + scoped_ptr<Value> value_; + + // Invocation request identifier. + int request_id_; + + DISALLOW_COPY_AND_ASSIGN(InvocationResult); + }; + + // Base class for API Invocation Execution registered with this object. + // A new execution object is created for each API invocation call even though + // the state data is stored in the classes deriving from InvocationResult + // (declared above). This is to allow easier testing of the Invocation + // implementation by having virtual methods like GetDispatcher to be used + // as a test seam, or other specific methods (like + // ApiResultCreator<>::CreateApiResult). Otherwise, Invocation::Execute + // could be a static callback as the event handlers described below. + class Invocation { + public: + Invocation() {} + virtual ~Invocation() {} + + // Called when a request to invoke the execution of an API is received. + // + // @param args The list value object for the arguments passed. + // @param request_id The identifier of the request being executed. + virtual void Execute(const ListValue& args, int request_id) = 0; + + protected: + // Unit test seam. + virtual ApiDispatcher* GetDispatcher(); + + DISALLOW_COPY_AND_ASSIGN(Invocation); + }; + + // The Invocation factory pointers to be stored in the factory map. + typedef Invocation* (*InvocationFactory)(); + + // The permanent event handlers are stored in a specific map and don't get + // removed after they are called. They are also registered without user data. + // The handler can stop the broadcast of the event by returning false. + typedef bool (*PermanentEventHandler)(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); + + // The ephemeral event handlers are stored in a specific map and they get + // removed after a successfully completed call or an error (a successful call + // is one which returns S_OK, S_FALSE is returned to identify that this + // set of arguments is not the one we were waiting for, so we need to wait + // some more, and returning a FAILED HRESULT causes the handler to be removed + // from the queue). + // EphemeralEventHandlers can specify user data to be passed back to them + // when the event occurs. The dynamic data allocation and release is the + // responsibility of the caller/handler pair, the dispatcher only keeps the + // pointer and passes it back to the handler as is without any post-cleanup. + // Also note that the Ephemeral event handlers are called after the Permanent + // handlers which may have had the chance to augment the content of the + // arguments which are passed to the ephemeral handlers. So these are useful + // for asynchronous invocation completion. + // We also pass in the ApiDispatcher for ease of test mocking. + typedef HRESULT (*EphemeralEventHandler)(const std::string& input_args, + InvocationResult* user_data, + ApiDispatcher* dispatcher); + + // Registers a factory for an API Invocation of a given name. Note that + // registering the same name more than once is not permitted. + // + // @param function_name The name of the function handled by the Invocation. + // @param factory The factory function to call to create a new instance of + // the Invocation type to handle this function. + virtual void RegisterInvocation(const char* function_name, + InvocationFactory factory); + + // Registers a permanent handler for an event of a given name. Note that + // registering the same name more than once is not permitted. + // + // @param event_name The name of the event to handle. + // @param event_handler The event handler to call to handle the event. + virtual void RegisterPermanentEventHandler( + const char* event_name, PermanentEventHandler event_handler); + + // Registers an ephemeral handler for an event of a given name. Note that + // registering the same name more than once is permitted. When an event is + // fired, all ephemeral handlers are called after the permanent one if + // one was registered. + // + // @param event_name The name of the event to handle. + // @param event_handler The event handler to call to handle the event. + // @param user_data The data that has to be passed back to the handler. + virtual void RegisterEphemeralEventHandler( + const char* event_name, + EphemeralEventHandler event_handler, + InvocationResult* user_data); + + // Sets the thread id of the ApiInvocation thread so that we can make sure + // that we are only ran from that thread. Only used for debug purposes. + virtual void SetApiInvocationThreadId(DWORD thread_id) { + thread_id_ = thread_id; + } + + // Fetch the appropriate executor to execute code for the given window. + // + // @param window The window for which we want an executor. + // @param iid The identifier of the interface we want to work with. + // @param executor Where to return the requested executor interface pointer. + virtual void GetExecutor(HWND window, REFIID iid, void** executor); + + // Return a tab handle associated with the id. + // + // @param tab_id The tab identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if tab_id isn't + // found). + virtual HWND GetTabHandleFromId(int tab_id) const; + + // Return a window handle associated with the id. + // + // @param window_id The window identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if window_id isn't + // found). + virtual HWND GetWindowHandleFromId(int window_id) const; + + // Return a tab id associated with the HWND. + // + // @param tab_handle The tab HWND. + // @return The corresponding tab id (or 0 if tab_handle isn't found). + virtual int GetTabIdFromHandle(HWND tab_handle) const; + + // Return a window id associated with the HWND. + // + // @param window_handle The window HWND. + // @return The corresponding window id (or 0 if window_handle isn't found). + virtual int GetWindowIdFromHandle(HWND window_handle) const; + + // Register the relation between a tab_id and a HWND. + virtual void SetTabIdForHandle(long tab_id, HWND tab_handle); + + // Unregister the HWND and its corresponding tab_id. + virtual void DeleteTabHandle(HWND handle); + + protected: + typedef std::map<std::string, InvocationFactory> FactoryMap; + FactoryMap factories_; + + typedef std::map<std::string, PermanentEventHandler> + PermanentEventHandlersMap; + PermanentEventHandlersMap permanent_event_handlers_; + + // For ephemeral event handlers we also need to keep the user data. + struct EphemeralEventHandlerTuple { + EphemeralEventHandlerTuple( + EphemeralEventHandler handler, + InvocationResult* user_data) + : handler(handler), user_data(user_data) {} + EphemeralEventHandler handler; + InvocationResult* user_data; + }; + + // We can have a list of ephemeral event handlers for a given event name. + typedef std::vector<EphemeralEventHandlerTuple> + EphemeralEventHandlersList; + typedef std::map<std::string, EphemeralEventHandlersList> + EphemeralEventHandlersMap; + EphemeralEventHandlersMap ephemeral_event_handlers_; + + // The thread we are running into. + DWORD thread_id_; + + // Make sure this is always called from the same thread, + bool IsRunningInSingleThread(); + + DISALLOW_COPY_AND_ASSIGN(ApiDispatcher); +}; + +// Convenience InvocationFactory implementation. +template <class InvocationType> +ApiDispatcher::Invocation* NewApiInvocation() { + return new InvocationType(); +} + +// A singleton that initializes and keeps the ApiDispatcher used by production +// code. +class ProductionApiDispatcher : public ApiDispatcher, + public Singleton<ProductionApiDispatcher> { + private: + // This ensures no construction is possible outside of the class itself. + friend struct DefaultSingletonTraits<ProductionApiDispatcher>; + DISALLOW_IMPLICIT_CONSTRUCTORS(ProductionApiDispatcher); +}; + +// A convenience class that can be derived from by API function classes instead +// of ApiDispatcher::Invocation. Mainly benefits ease of testing, so that we +// can specify a mocked result object. +template<class ResultType = ApiDispatcher::InvocationResult> +class ApiResultCreator : public ApiDispatcher::Invocation { + protected: + // Allocates a new instance of ResultType. The caller of this function takes + // ownership of this instance and is responsible for freeing it. + virtual ResultType* CreateApiResult(int request_id) { + return new ResultType(request_id); + } +}; + +template<class InvocationBase> +class IterativeApiResult : public InvocationBase { + public: + explicit IterativeApiResult(int request_id) + : InvocationBase(request_id), result_accumulator_(new ListValue()) { + } + + // Unlike the base class, this hack subverts the process by accumulating + // responses (positive and errors) rather than posting them right away. + virtual void PostResult() { + DCHECK(value() != NULL); + DCHECK(result_accumulator_ != NULL); + + if (result_accumulator_ != NULL) + result_accumulator_->Append(value_.release()); + } + + virtual void PostError(const std::string& error) { + error_accumulator_.push_back(error); + } + + // Finally post whatever was reported as result. + virtual void FlushAllPosts() { + if (AllFailed()) { + CallRealPostError(error_accumulator_.back()); + } else { + // Post success as per base class implementation. Declare all is fine + // even if !AllSucceeded. + // TODO(motek@google.com): Perhaps we should post a different + // message post for 'not quite ok, but almost'? + set_value(result_accumulator_.release()); + CallRealPostResult(); + } + error_accumulator_.clear(); + } + + bool AllSucceeded() const { + DCHECK(result_accumulator_ != NULL); + return result_accumulator_ != NULL && !result_accumulator_->empty() && + error_accumulator_.empty(); + } + + bool AllFailed() const { + DCHECK(result_accumulator_ != NULL); + return !error_accumulator_.empty() && + (result_accumulator_ == NULL || result_accumulator_->empty()); + } + + bool IsEmpty() const { + DCHECK(result_accumulator_ != NULL); + return (result_accumulator_ == NULL || result_accumulator_->empty()) && + error_accumulator_.empty(); + } + + const std::string LastError() const { + if (!error_accumulator_.empty()) + return error_accumulator_.back(); + return std::string(); + } + + private: + // Redirected invocations of base class's 'post function' create test seam. + virtual void CallRealPostResult() { + InvocationBase::PostResult(); + } + virtual void CallRealPostError(const std::string& error) { + InvocationBase::PostError(error); + } + + scoped_ptr<ListValue> result_accumulator_; + std::list<std::string> error_accumulator_; +}; + + +#endif // CEEE_IE_BROKER_API_DISPATCHER_H_ diff --git a/ceee/ie/broker/api_dispatcher_docs.h b/ceee/ie/broker/api_dispatcher_docs.h new file mode 100644 index 0000000..ae6b711 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher_docs.h @@ -0,0 +1,420 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ + +/** @page ApiDispatcherDoc Detailed documentation of the ApiDispatcher. + +@section ApiDispatcherIntro Introduction + +The API Dispatcher implemented by the ApiDispatcher class +is used by the CeeeBroker object to dispatch execution of Api calls +coming from the "Chrome Extensions Execution Environment" or CEEE. + +It keeps a registry of all API invocation implementations and takes care of +the logic needed to de-serialize API function requests, dispatch them, +and then serialize & return the response. + +One of the main complexities of the API invocation implementations is that some +of them need to interact with windows, and these interactions must happen in +the same thread that created the window. So the dispatcher need to delegate +calls to Executor objects that they get from the ExecutorsManager. + +So the implementation of these APIs are actually split between the classes +inheriting from ApiDispatcher::Invocation and the CeeeExecutor COM object. + +Another complexity is the fact that some of the APIs can not be completed +synchronously (e.g., the tab creation is an asynchronous process), so the +dispatcher must remember the pending execution and get them completed when a +given event is fired. + +And to make it even more fun, there might be two concurrent API invocation +that depend on the same event being fired (e.g., GetSelectedTab is called +while a new tab is being created as selected, but the asynchronous selection +isn't completed yet... pfffewww). So the list of pending asynchronous execution +must allow more than one to be executed when a given event is fired. + +@section InvocationFactory Invocation Factory + +The ApiDispatcher uses the @link ApiDispatcher::Invocation Invocation @endlink +nested class as a base for all API invocation implementation so that it can +easily dispatch to them. It keeps pointers to registered static functions +(called Invocation factories) that can be called to instantiation a new +Invocation object in the @link ApiDispatcher::factories_ factories_ @endlink +@link ApiDispatcher::FactoryMap map@endlink. + +When @link ApiDispatcher::HandleApiRequest HandleApiRequest @endlink is called, +it looks up the Invocation Factory to find the Invocation object to execute with +the data that it de-serialized from the JSON string it was given. + +@subsection InvocationExecution Invocation Execution + +The Invocation base class has an @link ApiDispatcher::Invocation::Execute +Execute @endlink pure virtual method that is to be called by HandleApiRequest. +This method receives the argument values, as well as a pointer to a string +where to return the response. + +When the execution can be completed synchronously, the result (or the error +message in case of failure) is written in the @link +ApiDispatcher::Invocation::result_ result_ @endlink member variable +(or @link ApiDispatcher::Invocation::error_ error_ @endlink for errors). If the +execution must be completed asynchronously, a new AsynchronousHandler object +is instantiated and stored in the @link +ApiDispatcher::Invocation::async_handler_ async_handler_ @endlink data member +so that the dispatcher can find it. + +@subsection ExecutorsDelegation Delegating to Executors + +When an Invocation implementation needs to interact with windows, it can do so +by delegating the execution to an Executor object via the ICeeeTabExecutor +or ICeeeWindowExecutor interface pointer that it can get from a call to +ApiDispatcher::Invocation::GetExecutor which gets it from the Executors Manager. + +@section EventHandling Event Handling + +Event handling actually serves two purposes in the API Dispatcher, 1) handle +events (of course) and 2) complete asynchronous executions. Events are received +by the @link ApiDispatcher::FireEvent FireEvent @endlink member function. + +@subsection HandlingEvents Handling Events + +Events are being fired by the browser and must be propagated to extensions via +the Chrome Frame. The @link EventDispatchingDoc Event Dispatching @endlink +documentation describes how the EventFunnel derived classes can be used to help +fire events. Once they got through the funnel, the events get to the API +Dispatcher, which is responsible to post them to Chrome Frame. + +We do the event propagation to Chrome Frame from the API dispatcher so that +we can also deal with the completion of the asynchronous API invocation at the +same time... the same place... + +We also reuse some of the code written for API invocation since some of the +events are posted with the same information that is returned to the API +invocation callbacks (e.g., tabs.create and tabs.onCreated). + +So the ApiDispatcher::Invocation derived classes can be used to handle events +by filling in the blanks in the arguments posted with the event (because some of +these arguments can be filled by the source of the event, we only need to fill +in the blanks that were left because, for example, the information must be +fetched from other threads than the one from where the event come from). Those +classes that can be used to handle events must expose a static method with the +syntax defined as ApiDispatcher::EventHandler and register it by calling +ApiDispatcher::RegisterEventHandler so that it get stored in the +ApiDispatcher::EventHandlersMap. + +When ApiDispatcher::FireEvent is called, the event handlers map is looked up to +see if a handler was registered for the given event. If so, the event handler +is called to fill in the blanks in the event arguments. Then the newly filled +argumentes can be posted with the event. + +@subsection AsynchronousResponse Asynchronous API Invocation response + +When an API invocation can't be completed synchronously, a new instance of an +ApiDispatcher::AsynchronousHandler object is stored in the +ApiDispatcher::AsyncHandlersMap. These objects are associated to a given +event (which can be identified by calling the +ApiDispatcher::AsyncHandlersMap::event_name member function). So when an event +is fired, the map is looked up for the list of handlers registered for the +given event name (because there can be more than one handler registerd for a +given name, for the cases of concurrent API invocations, and also for cases +where more than one invocation need to wait for the same event, e.g., the +tabs.create and tabs.getSelected APIs). + +Since there may be more than one handler for a given event, and we may have +pending handlers for more than one event with the same name, we need to be able +to properly match the events with their pending handlers. To identify if an +event is the one a specific handler is waiting for, we have to ask the handlers +if they recognize it as theirs. Even if they do, we may still need to let +other handlers process the same event. This is why a handler can specify that +it is a pass-through handler (i.e., it doesn't swallow the event, it can let +other handlers use it, even multiple instances of itself can all use the same +one, as long as they recognize it as theirs). If a handler recognize an event +as its own, and isn't a pass-through handler, then, we must stop looking for +more handlers. + +This means that all pass-through handlers must be placed at the beginning of +the list of handlers for a given event, and all the other ones (the +non-pass-through handlers) must be put at the end. + +@section InvocationImplementation Invocation Implementation + +The implementation of API invocations are done by classes derived from the +Invocation nested class of the ApiDispatcher class. Here is the current list of +those derived classes and some details about their implementation. + +@subsection WindowsApi Windows API + +Here is the list of invocations implementing the Windows API. +You can also read a more detailed information about the Chrome Extension +Windows api here: http://code.google.com/chrome/extensions/windows.html + +@subsubsection GetWindow + +This invocation receives a single argument which is the identifier of the window +to return information for. The window identifier is simply the window's HWND +value. We only provide such information for IEFrame windows (so we validate +that before continuing) and since we must get this information from the thread +that created the window, we delegate this call to the @link +ICeeeWindowExecutor::GetWindow @endlink method of the +ICeeeWindowExecutor interface. + +The returned information contains the following: +<table> + <tr><td>type</td><td>Info</td></tr> + <tr><td>int</td><td>Window identifier</td></tr> + <tr><td>bool</td><td>Focused</td></tr> + <tr><td>int</td><td>Left position</td></tr> + <tr><td>int</td><td>Top position</td></tr> + <tr><td>int</td><td>Width</td></tr> + <tr><td>int</td><td>Height</td></tr> +</table> + +@subsubsection GetCurrentWindow + +This invocation does the same thing as @ref GetWindow except that it doesn't +receive a windows identifier argument (it doesn't receive any argument). Instead +it must find the current window from where the invocation started from. + +@note We currently don't have a way to find the window from which the invocation +came from, so until we do, we return the top most IE Frame window as +@ref GetLastFocusedWindow does. + +@subsubsection GetLastFocusedWindow + +This invocation return information about the last focused IE window, which it +gets by calling @link window_api::WindowApiResult::TopIeWindow +TopIeWindow@endlink, which finds it by calling +window_utils::FindDescendentWindow with NULL as the parent (which means to ask +for top level windows). + +@subsubsection CreateWindowFunc + +This invocation creates a new window with the parameters specified in the +received argument and navigate it to an optionally provided URL or about:blank +otherwise. It creates the new window by simply calling the +ICeeeTabExecutor::Navigate method with a _blank target and the +navOpenInNewWindow flag. + +It then needs to find that window (by diff'ing the list of IEFrame windows +before and after the navigation), and then it can apply the provided parameters. + +The provided parameters are as follows (and are all optional): +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>int</td><td>Left position</td></tr> +<tr><td>int</td><td>Top position</td></tr> +<tr><td>int</td><td>Width</td></tr> +<tr><td>int</td><td>Height</td></tr> +</table> + +The application of the window parameters must be done in the same thread that +created the window, so we must delegate this task to the @link +ICeeeWindowExecutor::UpdateWindow UpdateWindow @endlink method of +the ICeeeWindowExecutor interface. This UpdateWindow method also returns +information about the updated window so that we can return it. The returned +information is the same as the one returned by the @ref GetWindow method +described above. + +@subsubsection UpdateWindow + +This invocation updates an existing window. So it must be given the window +identifier, as well as the new parameters to update the window with. + +It simply reuses the same code as the @ref CreateWindowFunc invocation, except +that it doesn't create a new window, it modifies the one specified by the +identifier, after validating that it is indeed an IEFrame window. + +@subsubsection RemoveWindow + +This invocation simply closes the window specified as a windows identifier +argument. It simply delegates the call to the @link +ICeeeWindowExecutor::RemoveWindow RemoveWindow @endlink method of the +ICeeeWindowExecutor interface. + +@subsubsection GetAllWindows + +This invocation returns an array of window information, one for each of the +current IEFrame window. It also fills an extra array of tabs information if the +populate flag is set in the single (and optional, defaults to false) argument. + +The information returned for each window is the same as in the @ref GetWindow +invocation. The information for each tab (if requested) is provided as an extra +entry in the window information in the form of an array of tab information. The +tab information is the same as the one returned by the @ref GetTab invocation +described below. + +It delegates the call to the @link ICeeeWindowExecutor::GetAllWindows +GetAllWindows @endlink method of the ICeeeWindowExecutor interface. This +methods can't return all the information about every tab since the windows for +these tabs may have been created in other threads/processes. So it only returns +the identifiers of these tabs. Then we must delegate to different executors for +each of the tabs potentially created in different threads/process by calling the +@link ICeeeTabExecutor::GetTab GetTab @endlink method of the +ICeeeTabExecutor interface as is done for the @ref GetTab API described +below. We do the same in @ref GetAllTabsInWindow described below. + +@subsection TabsApi Tabs API + +Here is the list of invocations implementing the Tabs API. +You can also read a more detailed information about the Chrome Extension +Tabs api here: http://code.google.com/chrome/extensions/tabs.html + +@subsubsection GetTab + +This invocation receives a single argument which is the identifier of the tab +to return information for. The tab identifier is the HWND value of the window +of class "TabWindowClass" that holds on the real tab window. + +We only provide such information for these windows (so we validate +that before continuing). Also, we must get some of the information from the +thread that created the tab window, and some other information are only +available via the tab window manager which lives in the thread that created the +frame window. So we must delegate this call in two phases, the first +one to the @link ICeeeTabExecutor::GetTab GetTab @endlink method of the +ICeeeTabExecutor interface which returns the following information: +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>string</td><td>Title</td></tr> +<tr><td>string</td><td>Loading/Complete status</td></tr> +<tr><td>string</td><td>favIconUrl (not supported yet in CEEE)</td></tr> +</table> + +The information above is retrivied via the HTML Document and Web Browser +associated to the given tab, and these must be fetched and interacted with from +the same thread that created the tab window. + +So we are left with the following: +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>int</td><td>Tab identifier</td></tr> +<tr><td>int</td><td>Window identifier</td></tr> +<tr><td>bool</td><td>Selected state</td></tr> +<tr><td>int</td><td>Tab index</td></tr> +</table> + +Most of the other information about the tab can be retrieved from any thread +by simply using Win32 calls. The tab identifier is the HWND of the window of +class "TabWindowClass", the window identifier is simply the HWND of +the top level parent of the tab window, and the selected state can be determined +by the visibility of the window which we can get from anywhere. + +The last piece of information, the tab index, must be retrieved from the tab +window manager. We must get another executor so that we can +run it in the appropriate thread. We do this via the @link +ICeeeTabExecutor::GetTabIndex GetTabIndex @endlink method of the +ICeeeTabExecutor interface. + +@subsubsection GetSelectedTab + +This invocation does the same thing as @ref GetTab except that it doesn't +receive a tab identifier argument, it gets the information of the currently +selected tab instead. But it can receive an optional window identifier argument, +specifying which window's selected tab we want. If no window identifier is +specified, we use the current window. The current window is the same as the one +returned from @ref GetCurrentWindow. + +@subsubsection GetAllTabsInWindow + +This invocation returns the information for all the tabs of a given window. If +no window identifier is provided as an optional argument, then the current +window is used instead. The returned information is an array of tab information. +The tab information is the same as the one returned by @ref GetTab. + +We delegate the first part of this call to the +@link ICeeeTabExecutor::GetAllTabs GetAllTabs @endlink method of the +ICeeeTabExecutor interface which gets the list of tabs from the tab window +manager in the frame window thread. Then we need to sequentially call different +instances of executors to call their @ref GetTab method, since they were not +created in the same thread where GetAllTabs needs to be executed. + +@subsubsection CreateTab + +This invocation creates a new tab with the parameters specified in the received +argument and navigate it to an optionally provided URL or about:blank otherwise. + +The new tab is to be created in a specified window or the current window if the +optional window identifier is not provided as an argument. The new tab +is created by first finding an existing tab in the any window and then +calling ICeeeTabExecutor::Navigate with the appropriate URL with the _blank +target and the navOpenInNewTab or navOpenInBackgroundTab whether the selected +bool optional parameter is set to true or not (defaults to true). + +The new tab may also need to be moved to a specific index if the optional index +parameter argument was provided. This can be done using the tab window manager +so must be done in the appropriate thread by delegating this task to the @link +ICeeeTabExecutor::MoveTab MoveTab @endlink method of the +ICeeeTabExecutor interface. + +And finally, the same tab information as from @ref GetTab must be returned. +Since some of this information isn't available right after the call to Navigate, +we must handle the rest of the CreateTab API invocation Asynchronously. We +register the async handler to be called when the tabs.onCreated event is fired. +Then, we know that all the information is available to return the tab info. + +The provided parameters are as follows (and are all optional): +<table> +<tr><td>type</td><td>Info</td></tr> +<tr><td>int</td><td>Parent window identifier</td></tr> +<tr><td>int</td><td>Index</td></tr> +<tr><td>string</td><td>URL</td></tr> +<tr><td>bool</td><td>Selected</td></tr> +</table> + +@note <b>TODO(mad@chromium.org)</b>: We currently can't get the tab id of the +newly created tab since it is created asynchronously. So we will need to return +this info asynchronously once we have the support for asynchronous events. + +@subsubsection UpdateTab + +This invocation updates an existing tab. So it must be given the tab identifier, +as well as the new parameters to update the tab with. + +If the optional URL parameter is provided as an argument, then we must call +ICeeeTabExecutor::Navigate with this new URL (if it is different from the +current one). + +If the optional selected parameter is provided, then we must use the tab window +manager to select the tab. But we must do it in the IE Frame thread so we must +delegate the tab selection to the @link ICeeeTabExecutor::SelectTab +SelectTab @endlink method of another instance of +the executor object implementing the ICeeeTabExecutor interface, since it +must be execute in the thread that created the frame window, this is the one we +must use when accessing the ITabWindowManager interface. + +No information is returned from UpdateTab. + +@subsubsection MoveTab + +This invocation moves the tab from one index to another. So it must receive the +identifier of the tab, as well as the destination index. An optional window +identifier can also be provided if we want to specify in which window we want +to move the tab. When not specified, the window where the tab currently lies is +used (and it is the only case that is currently supported in CEEE). + +This code is delegated to the @link ICeeeTabExecutor::MoveTab MoveTab +@endlink method of the ICeeeTabExecutor interface. + +No information is returned from MoveTab. + +@subsubsection RemoveTab + +This invocation closes a tab identified by the single mandatory parameter +argument. The removal of the tab is delegated to the ITabWindowManager interface +so it must be executed in the IEFrame thread by calling the @link +ICeeeTabExecutor::RemoveTab RemoveTab @endlink method on +the ICeeeTabExecutor interface. + +@note <b>TODO(mad@chromium.org)</b>: An alternative to having the RemoveTab and +MoveTab and SelectTab methods on the ICeeeTabExecutor interface, would be to +simply have a GetTabWindowManager method which returns a proxy to the +ITabWindowManager interface that we can safely use from the Broker process. + +No information is returned from RemoveTab. + +**/ + +#endif // CEEE_IE_BROKER_API_DISPATCHER_DOCS_H_ diff --git a/ceee/ie/broker/api_dispatcher_unittest.cc b/ceee/ie/broker/api_dispatcher_unittest.cc new file mode 100644 index 0000000..1987733 --- /dev/null +++ b/ceee/ie/broker/api_dispatcher_unittest.cc @@ -0,0 +1,275 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ApiDispatcher. + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + + +namespace { + +namespace keys = extension_automation_constants; + +using testing::_; +using testing::StrEq; +using testing::StrictMock; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class MockInvocation + : public ApiDispatcher::Invocation { + public: + MOCK_METHOD2(Execute, void(const ListValue& args, int request_id)); + + // A factory for the registry. + static ApiDispatcher::Invocation* TestingInstance() { + ApiDispatcher::Invocation* return_value = testing_instance_.release(); + testing_instance_.reset(new MockInvocation); + return return_value; + } + + static scoped_ptr<MockInvocation> testing_instance_; +}; + +scoped_ptr<MockInvocation> MockInvocation::testing_instance_; + + +TEST(ApiDispatcher, InvalidRegistrationNoCrash) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation(NULL, NewApiInvocation<MockInvocation>); + dispatcher.RegisterInvocation("hi", NULL); +} + +TEST(ApiDispatcher, DuplicateRegistrationNoCrash) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("hi", NewApiInvocation<MockInvocation>); + dispatcher.RegisterInvocation("hi", NewApiInvocation<MockInvocation>); +} + +// Encodes a request in JSON. Optionally skip encoding one of the required +// keys. +std::string MakeRequest(const char* function_name, const Value* args, + int request_id, const std::string& skip_key = "") { + std::string json; + DictionaryValue value; + if (skip_key != keys::kAutomationNameKey) + value.SetString(keys::kAutomationNameKey, function_name); + if (skip_key != keys::kAutomationArgsKey) { + std::string json; + if (args) { + base::JSONWriter::Write(args, false, &json); + } + value.SetString(keys::kAutomationArgsKey, json); + } + if (skip_key != keys::kAutomationRequestIdKey) + value.SetInteger(keys::kAutomationRequestIdKey, request_id); + base::JSONWriter::Write(&value, false, &json); + return json; +} + +static const int kRequestId = 42; + +class ApiDispatcherTests : public testing::Test { + public: + virtual void SetUp() { + // We must create the testing instance before the factory returns it + // so that we can set expectations on it beforehand. + MockInvocation::testing_instance_.reset(new MockInvocation); + } + virtual void TearDown() { + // We don't want to wait for the static instance to get destroyed, + // it will be too late and will look like we leaked. + MockInvocation::testing_instance_.reset(NULL); + } +}; + + +TEST_F(ApiDispatcherTests, BasicDispatching) { + testing::LogDisabler no_dchecks; + + scoped_ptr<ListValue> args1(new ListValue()); + args1->Append(Value::CreateStringValue("hello world")); + scoped_ptr<ListValue> args2(new ListValue()); + args2->Append(Value::CreateIntegerValue(711)); + scoped_ptr<ListValue> args3(new ListValue()); + args3->Append(Value::CreateBooleanValue(true)); + DictionaryValue* dict_value = new DictionaryValue(); + dict_value->SetString("foo", "moo"); + dict_value->SetInteger("blat", 42); + scoped_ptr<ListValue> args4(new ListValue()); + args4->Append(dict_value); + + ListValue* values[] = { args1.get(), args2.get(), args3.get(), args4.get() }; + + for (int i = 0; i < arraysize(values); ++i) { + ListValue* test_value = values[i]; + EXPECT_CALL(*MockInvocation::testing_instance_, + Execute(ValuesEqual(test_value), kRequestId)).Times(1); + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("DoStuff", &MockInvocation::TestingInstance); + std::string json = MakeRequest("DoStuff", test_value, kRequestId); + dispatcher.HandleApiRequest(CComBSTR(json.c_str()), NULL); + } +} + +TEST_F(ApiDispatcherTests, ErrorHandlingDispatching) { + testing::LogDisabler no_dchecks; + + ApiDispatcher dispatcher; + dispatcher.RegisterInvocation("DoStuff", &MockInvocation::TestingInstance); + + std::string test_data[] = { + MakeRequest("DoStuff", NULL, 42, keys::kAutomationNameKey), + MakeRequest("DoStuff", NULL, 43, keys::kAutomationArgsKey), + MakeRequest("DoStuff", NULL, 44, keys::kAutomationRequestIdKey), + MakeRequest("NotRegistered", NULL, 45), + }; + + for (int i = 0; i < arraysize(test_data); ++i) { + EXPECT_CALL(*MockInvocation::testing_instance_, Execute(_, _)).Times(0); + CComBSTR json(test_data[i].c_str()); + dispatcher.HandleApiRequest(json, NULL); + } +} + +class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); +}; + +TEST(ApiDispatcher, PostResult) { + testing::LogDisabler no_dchecks; + + // Simply instantiating the postman makes it the single instance + // available to all clients. + CComObjectStackEx< StrictMock< MockChromePostman > > postman; + + // We use different (numbered) result instances, because they are not + // meant to post more than once (and we validate that too). + ApiDispatcher::InvocationResult result1(kRequestId); + + // First check that we can correctly create an error response. + static const char* kError = "error"; + DictionaryValue expected_value; + expected_value.SetInteger(keys::kAutomationRequestIdKey, kRequestId); + expected_value.SetString(keys::kAutomationErrorKey, kError); + + std::string expected_response; + base::JSONWriter::Write(&expected_value, false, &expected_response); + + CComBSTR request_name(keys::kAutomationResponseTarget); + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + result1.PostError(kError); + + // PostResult and PostError must not call the postman anymore, + // and StrictMock will validate it. + result1.PostResult(); + result1.PostError(kError); + + // Now check that we can create an empty response when there is no value. + expected_value.Remove(keys::kAutomationErrorKey, NULL); + expected_value.SetString(keys::kAutomationResponseKey, ""); + base::JSONWriter::Write(&expected_value, false, &expected_response); + + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + ApiDispatcher::InvocationResult result2(kRequestId); + result2.PostResult(); + // These must not get to PostMan, and StrictMock will validate it. + result2.PostError(""); + result2.PostResult(); + + // And now check that we can create an full response. + ApiDispatcher::InvocationResult result3(kRequestId); + result3.set_value(Value::CreateIntegerValue(42)); + std::string result_str; + base::JSONWriter::Write(result3.value(), false, &result_str); + + expected_value.SetString(keys::kAutomationResponseKey, result_str); + base::JSONWriter::Write(&expected_value, false, &expected_response); + + EXPECT_CALL(postman, PostMessage( + StrEq(CComBSTR(expected_response.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + result3.PostResult(); + // These must not get to PostMan, and StrictMock will validate it. + result3.PostResult(); + result3.PostError(""); +} + +bool EventHandler1(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + EXPECT_STREQ(input_args.c_str(), "EventHandler1Args"); + EXPECT_TRUE(converted_args != NULL); + *converted_args = "EventHandler1ConvertedArgs"; + return true; +} + +bool EventHandler2(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + EXPECT_STREQ(input_args.c_str(), "EventHandler2Args"); + EXPECT_TRUE(converted_args != NULL); + *converted_args = "EventHandler2ConvertedArgs"; + return true; +} + +bool EventHandler3(const std::string& input_args, std::string* converted_args, + ApiDispatcher* dispatcher) { + return false; +} + +TEST(ApiDispatcher, PermanentEventHandler) { + testing::LogDisabler no_dchecks; + + // Simply instantiating the postman makes it the single instance + // available to all clients. + CComObjectStackEx< StrictMock< MockChromePostman > > postman; + ApiDispatcher dispatcher; + dispatcher.RegisterPermanentEventHandler("Event1", EventHandler1); + dispatcher.RegisterPermanentEventHandler("Event2", EventHandler2); + dispatcher.RegisterPermanentEventHandler("Event3", EventHandler3); + + ListValue message1; + message1.Append(Value::CreateStringValue("Event1")); + message1.Append(Value::CreateStringValue("EventHandler1ConvertedArgs")); + std::string message1_str; + base::JSONWriter::Write(&message1, false, &message1_str); + CComBSTR request_name(keys::kAutomationBrowserEventRequestTarget); + EXPECT_CALL(postman, PostMessage(StrEq(CComBSTR(message1_str.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + dispatcher.FireEvent(CComBSTR("Event1"), CComBSTR("EventHandler1Args")); + + ListValue message2; + message2.Append(Value::CreateStringValue("Event2")); + message2.Append(Value::CreateStringValue("EventHandler2ConvertedArgs")); + std::string message2_str; + base::JSONWriter::Write(&message2, false, &message2_str); + EXPECT_CALL(postman, PostMessage(StrEq(CComBSTR(message2_str.c_str()).m_str), + StrEq(request_name.m_str))).Times(1); + dispatcher.FireEvent(CComBSTR("Event2"), CComBSTR("EventHandler2Args")); + + // There shouldn't be a post when the event handler returns false. + dispatcher.FireEvent(CComBSTR("Event3"), CComBSTR("")); +} + +// TODO(mad@chromium.org): Add tests for the EphemeralEventHandlers. + +} // namespace diff --git a/ceee/ie/broker/api_module_constants.cc b/ceee/ie/broker/api_module_constants.cc new file mode 100644 index 0000000..a24a830d --- /dev/null +++ b/ceee/ie/broker/api_module_constants.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Constants used by the API modules. + +#include "ceee/ie/broker/api_module_constants.h" + +namespace api_module_constants { + +const char kInvalidArgumentsError[] = "Invalid arguments."; +const char kInternalErrorError[] = "Internal error."; + +} // namespace api_module_constants diff --git a/ceee/ie/broker/api_module_constants.h b/ceee/ie/broker/api_module_constants.h new file mode 100644 index 0000000..0555531 --- /dev/null +++ b/ceee/ie/broker/api_module_constants.h @@ -0,0 +1,18 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Constants used by the API modules. + +#ifndef CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ +#define CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ + +namespace api_module_constants { + +// Errors. +extern const char kInvalidArgumentsError[]; +extern const char kInternalErrorError[]; + +} // namespace api_module_constants + +#endif // CEEE_IE_BROKER_API_MODULE_CONSTANTS_H_ diff --git a/ceee/ie/broker/api_module_util.cc b/ceee/ie/broker/api_module_util.cc new file mode 100644 index 0000000..96c8568 --- /dev/null +++ b/ceee/ie/broker/api_module_util.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utilities used by the API modules. + +#include "ceee/ie/broker/api_module_util.h" + +namespace api_module_util { + +bool GetListFromJsonString(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + DCHECK(list != NULL); + scoped_ptr<Value> input_args_val(base::JSONReader::Read(input_list_args, + true)); + DCHECK(input_args_val != NULL && input_args_val->IsType(Value::TYPE_LIST)); + if (input_args_val == NULL || !input_args_val->IsType(Value::TYPE_LIST)) + return false; + list->reset(static_cast<ListValue*>(input_args_val.release())); + return true; +} + +Value* GetListAndFirstValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + if (!GetListFromJsonString(input_list_args, list)) + return NULL; + Value* value = NULL; + if (!(*list)->Get(0, &value) || value == NULL) { + DCHECK(false) << "Input arguments are not a non-empty list."; + } + return value; +} + +bool GetListAndIntegerValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list, + int* int_value) { + Value* value = GetListAndFirstValue(input_list_args, list); + DCHECK(value != NULL && value->IsType(Value::TYPE_INTEGER)); + if (value == NULL || !value->IsType(Value::TYPE_INTEGER)) { + return false; + } + return value->GetAsInteger(int_value); +} + +DictionaryValue* GetListAndDictionaryValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list) { + Value* value = GetListAndFirstValue(input_list_args, list); + DCHECK(value != NULL && value->IsType(Value::TYPE_DICTIONARY)); + if (value == NULL || !value->IsType(Value::TYPE_DICTIONARY)) { + return NULL; + } + return static_cast<DictionaryValue*>(value); +} + +bool GetListAndDictIntValue(const std::string& input_list_args, + const char* dict_value_key_name, + scoped_ptr<ListValue>* list, + int* int_value) { + DictionaryValue* dict = GetListAndDictionaryValue(input_list_args, list); + if (dict == NULL) + return false; + return dict->GetInteger(dict_value_key_name, int_value); +} + +} // namespace api_module_util diff --git a/ceee/ie/broker/api_module_util.h b/ceee/ie/broker/api_module_util.h new file mode 100644 index 0000000..e8bc9c2 --- /dev/null +++ b/ceee/ie/broker/api_module_util.h @@ -0,0 +1,101 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utilities used by the API modules. + +#ifndef CEEE_IE_BROKER_API_MODULE_UTIL_H_ +#define CEEE_IE_BROKER_API_MODULE_UTIL_H_ + +#include <string> + +#include "toolband.h" //NOLINT + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "ceee/ie/broker/api_dispatcher.h" + +namespace api_module_util { + +// Helper function to fetch a list from a JSON string. +bool GetListFromJsonString(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string and return its +// first Value. Note that the Value allocation is owned by the list. +// Returns NULL on failures. +Value* GetListAndFirstValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string and return its +// first Value as an integer. Returns false on failures. +bool GetListAndIntegerValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list, + int* int_value); + +// Helper function to fetch a list from a JSON string and return its +// first Value as a DictionaryValue. Note that the DictionaryValue +// allocation is owned by the list. Returns NULL on failures. +DictionaryValue* GetListAndDictionaryValue(const std::string& input_list_args, + scoped_ptr<ListValue>* list); + +// Helper function to fetch a list from a JSON string, get its +// first Value as a DictionaryValue and extracts an int from the dict using +// the provided key name. Returns false on failures. +bool GetListAndDictIntValue(const std::string& input_list_args, + const char* dict_value_key_name, + scoped_ptr<ListValue>* list, + int* int_value); + +// A function that can be used as a permanent event handler, which converts the +// tab window handle in the input arguments into the corresponding tab ID. +// @tparam tab_id_key_name The key name for the tab ID. +// @param input_args A list of arguments in the form of a JSON string. The first +// argument is a dictionary. It contains the key tab_id_key_name, whose +// corresponding value is the tab window handle. +// @param converted_args On success returns a JSON string, in which the tab +// window handle has been replaced with the actual tab ID; otherwise +// returns input_args. +// @param dispatcher The dispatcher used to query tab IDs using tab window +// handles. +template<const char* tab_id_key_name> +bool ConvertTabIdInDictionary(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + if (converted_args == NULL || dispatcher == NULL) { + NOTREACHED(); + return false; + } + // Fail safe... + *converted_args = input_args; + + scoped_ptr<ListValue> input_list; + DictionaryValue* input_dict = GetListAndDictionaryValue(input_args, + &input_list); + if (input_dict == NULL) { + LOG(ERROR) + << "Failed to get the details object from the list of arguments."; + return false; + } + + int tab_handle = -1; + bool success = input_dict->GetInteger(tab_id_key_name, &tab_handle); + if (!success) { + LOG(ERROR) << "Failed to get " << tab_id_key_name << " property."; + return false; + } + + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + int tab_id = kInvalidChromeSessionId; + if (tab_window != INVALID_HANDLE_VALUE) { + tab_id = dispatcher->GetTabIdFromHandle(tab_window); + } + input_dict->SetInteger(tab_id_key_name, tab_id); + + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; +} + +} // namespace api_module_util + +#endif // CEEE_IE_BROKER_API_MODULE_UTIL_H_ diff --git a/ceee/ie/broker/broker.cc b/ceee/ie/broker/broker.cc new file mode 100644 index 0000000..24ed0da --- /dev/null +++ b/ceee/ie/broker/broker.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ICeeeBroker implementation + +#include "ceee/ie/broker/broker.h" + +#include "base/logging.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/common/ceee_module_util.h" + + +HRESULT CeeeBroker::FinalConstruct() { + // So that we get a pointer to the ExecutorsManager and let tests override it. + executors_manager_ = Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>::get(); + api_dispatcher_ = ProductionApiDispatcher::get(); + return S_OK; +} + +void CeeeBroker::OnAddConnection(bool first_lock) { + if (first_lock) + ceee_module_util::LockModule(); +} + +void CeeeBroker::OnReleaseConnection(bool last_unlock, + bool last_unlock_releases) { + if (last_unlock) + ceee_module_util::UnlockModule(); + IExternalConnectionImpl<CeeeBroker>::OnReleaseConnection( + last_unlock, last_unlock_releases); +} + +STDMETHODIMP CeeeBroker::Execute(BSTR function, BSTR* response) { + // This is DEPRECATED and we should use ChromePostman (see FireEvent). + api_dispatcher_->HandleApiRequest(function, response); + return S_OK; +} + +STDMETHODIMP CeeeBroker::FireEvent(BSTR event_name, BSTR event_args) { + ChromePostman::GetInstance()->FireEvent(event_name, event_args); + return S_OK; +} + +STDMETHODIMP CeeeBroker::RegisterWindowExecutor(long thread_id, + IUnknown* executor) { + // TODO(mad@chromium.org): Add security check here. + return executors_manager_->RegisterWindowExecutor(thread_id, executor); +} + +STDMETHODIMP CeeeBroker::UnregisterExecutor(long thread_id) { + // TODO(mad@chromium.org): Add security check here. + return executors_manager_->RemoveExecutor(thread_id); +} + +STDMETHODIMP CeeeBroker::RegisterTabExecutor(long thread_id, + IUnknown* executor) { + // TODO(mad@chromium.org): Implement the proper manual/secure registration. + return executors_manager_->RegisterTabExecutor(thread_id, executor); +} + +STDMETHODIMP CeeeBroker::SetTabIdForHandle(long tab_id, + CeeeWindowHandle handle) { + // TODO(mad@chromium.org): Add security check here. + DCHECK(tab_id != kInvalidChromeSessionId && + handle != reinterpret_cast<CeeeWindowHandle>(INVALID_HANDLE_VALUE)); + executors_manager_->SetTabIdForHandle(tab_id, reinterpret_cast<HWND>(handle)); + return S_OK; +} diff --git a/ceee/ie/broker/broker.gyp b/ceee/ie/broker/broker.gyp new file mode 100644 index 0000000..422eeac --- /dev/null +++ b/ceee/ie/broker/broker.gyp @@ -0,0 +1,105 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'broker', + 'type': 'static_library', + 'dependencies': [ + '../common/common.gyp:ie_common', + '../common/common.gyp:ie_common_settings', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../../../base/base.gyp:base', + '../../../build/temp_gyp/googleurl.gyp:googleurl', + '../../../ceee/common/common.gyp:initializing_coclass', + '../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + 'api_dispatcher.cc', + 'api_dispatcher.h', + 'api_dispatcher_docs.h', + 'api_module_constants.cc', + 'api_module_constants.h', + 'api_module_util.cc', + 'api_module_util.h', + 'broker.cc', + 'broker.h', + 'broker_docs.h', + 'chrome_postman.cc', + 'chrome_postman.h', + 'common_api_module.cc', + 'common_api_module.h', + 'cookie_api_module.cc', + 'cookie_api_module.h', + 'event_dispatching_docs.h', + 'executors_manager.cc', + 'executors_manager.h', + 'executors_manager_docs.h', + 'infobar_api_module.cc', + 'infobar_api_module.h', + '../common/precompile.cc', + '../common/precompile.h', + 'tab_api_module.cc', + 'tab_api_module.h', + 'webnavigation_api_module.cc', + 'webnavigation_api_module.h', + 'webrequest_api_module.cc', + 'webrequest_api_module.h', + 'window_api_module.cc', + 'window_api_module.h', + 'window_events_funnel.cc', + 'window_events_funnel.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../common/precompile.cc', + 'msvs_precompiled_header': '../common/precompile.h', + }, + }, + }, + { + # IF YOU CHANGE THIS TARGET NAME YOU MUST UPDATE: + # ceee_module_util::kCeeeBrokerModuleName + 'target_name': 'ceee_broker', + 'type': 'executable', + 'sources': [ + 'broker.rgs', + 'broker_module.cc', + 'broker_module.rc', + 'broker_module.rgs', + 'broker_module_util.h', + 'resource.h' + ], + 'dependencies': [ + 'broker', + '../common/common.gyp:ie_common_settings', + '../common/common.gyp:ie_guids', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../../../base/base.gyp:base', + '../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../ceee/common/common.gyp:ceee_common', + '<(DEPTH)/chrome/chrome.gyp:chrome_version_header', + '<(DEPTH)/chrome_frame/chrome_frame.gyp:chrome_frame_ie', # for GUIDs. + ], + 'msvs_settings': { + 'VCLinkerTool': { + 'OutputFile': '$(OutDir)/servers/$(ProjectName).exe', + # Set /SUBSYSTEM:WINDOWS since this is not a command-line program. + 'SubSystem': '2', + }, + }, + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + ] +} diff --git a/ceee/ie/broker/broker.h b/ceee/ie/broker/broker.h new file mode 100644 index 0000000..8132e8a --- /dev/null +++ b/ceee/ie/broker/broker.h @@ -0,0 +1,70 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// ICeeeBroker implementation. + +#ifndef CEEE_IE_BROKER_BROKER_H_ +#define CEEE_IE_BROKER_BROKER_H_ + +#include <atlbase.h> +#include <atlcom.h> + +#include "ceee/ie/broker/resource.h" + +#include "broker_lib.h" // NOLINT + +class ExecutorsManager; +class ApiDispatcher; + +// The CEEE user Broker is a COM object exposed from an executable server +// so that we can call it to execute code in destination threads of other +// processes, even if they run at a hight integrity level. + +// Entry point to execute code in specific destination threads. +class ATL_NO_VTABLE CeeeBroker + : public CComObjectRootEx<CComMultiThreadModel>, + public CComCoClass<CeeeBroker, &CLSID_CeeeBroker>, + public ICeeeBroker, + public ICeeeBrokerRegistrar, + public IExternalConnectionImpl<CeeeBroker> { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_BROKER) + + DECLARE_NOT_AGGREGATABLE(CeeeBroker) + BEGIN_COM_MAP(CeeeBroker) + COM_INTERFACE_ENTRY(IExternalConnection) + COM_INTERFACE_ENTRY(ICeeeBrokerRegistrar) + COM_INTERFACE_ENTRY(ICeeeBroker) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + // To set get our pointer to the ExecutorsManager and let tests override it. + virtual HRESULT FinalConstruct(); + + // @name ICeeeBroker implementation. + // @{ + STDMETHOD(Execute)(BSTR function, BSTR* response); + STDMETHOD(FireEvent)(BSTR event_name, BSTR event_args); + // @} + + // @name ICeeeBrokerRegistrar implementation. + // @{ + STDMETHOD(RegisterWindowExecutor)(long thread_id, IUnknown* executor); + STDMETHOD(RegisterTabExecutor)(long thread_id, IUnknown* executor); + STDMETHOD(UnregisterExecutor)(long thread_id); + STDMETHOD(SetTabIdForHandle)(long tab_id, CeeeWindowHandle handle); + // @} + + // IExternalConnectionImpl overrides + void OnAddConnection(bool first_lock); + void OnReleaseConnection(bool last_unlock, bool last_unlock_releases); + + protected: + // A pointer to single instance objects, or seams set for unittests. + ExecutorsManager * executors_manager_; + ApiDispatcher* api_dispatcher_; +}; + +#endif // CEEE_IE_BROKER_BROKER_H_ diff --git a/ceee/ie/broker/broker.rgs b/ceee/ie/broker/broker.rgs new file mode 100644 index 0000000..65b012b --- /dev/null +++ b/ceee/ie/broker/broker.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove {6D88A70D-2218-4466-BBD6-87AB563811A2} = s 'Google CEEE Broker' { + ForceRemove 'Programmable' + LocalServer32 = s '%MODULE%' + 'TypeLib' = s '{45B783D0-8040-49a6-A719-84E320AAB3C5}' + } + } +} diff --git a/ceee/ie/broker/broker_docs.h b/ceee/ie/broker/broker_docs.h new file mode 100644 index 0000000..5cc1220 --- /dev/null +++ b/ceee/ie/broker/broker_docs.h @@ -0,0 +1,61 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_BROKER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_BROKER_DOCS_H_ + +/** @page BrokerDoc Detailed documentation of the Broker process. + +@section BrokerIntro Introduction + +Since Internet Explorer can have multiple processes and run them at different +integrity level, we use a separate process to take care of integrity level +elevation when needed. This Broker process is also used as the sole +communication point with a Chrome Frame instance for Chrome Extension API +invocation to be executed in IE as well as events coming from IE and going back +to Chrome. + +@subsection ApiInvocation API Invocation + +The Broker must register a Chrome Frame Events sink to receive the private +messages used to redirect Chrome Extension API Invocations. But this has to be +done in a non-blocking single threaded way. So we must create a Single Thread +Apartment (STA) into which an instance of the ChromeFrameHost class will run, so +it can receive the private messages safely. + +Once an API Invocation message is passed to the Broker via the Chrome Frame +Host, the request is simply added to a global queue of API Invocation, waiting +to be processed by a worker thread running in the Multi Thread Apartment (MTA). + +Running the API invocation mostly likely need to execute code in the IE process, +but we can't block the STA for those calls, this is why we need to delegate +these calls to a work thread while the Chrome Frame Host STA can run free. + +Once an API invocation is picked up from the queue, and processed (by making +calls to instances of the CeeeExecutor running in the IE process and managed +by the ExecutorsManager), then the API response can be sent back to Chrome, via +the Chrome Frame Host, in the exact same way that we would send Chrome Extension +Events back to Chrome as described below. + +@subsection EventDispatching Event Dispatching + +When an event occurs in IE, that Chrome Extension might be interested in, the +code running in IE fires an event on the ICeeeBroker interface and one of +the RPC threads, running in the MTA catch it. We process all that is needed +to fill the event data from the RPC thread, and then add the event to the +global queue of events that are to be dispatched to Chrome via the Chrome Frame +Host running in the STA. + +So the Chrome Frame Host thread must wake up when events are added to the queue +and simply pass them to Chrome Frame before going back to sleep. Waiting for +further API invocations, or more events. + +Since the API invocation responses need to go through the Chrome Frame Host +in the exact same way as the events (except for a different target string), we +can use the exact same mechanism to send back API invocation response as we do +for events. + +**/ + +#endif // CEEE_IE_BROKER_BROKER_DOCS_H_ diff --git a/ceee/ie/broker/broker_lib.idl b/ceee/ie/broker/broker_lib.idl new file mode 100644 index 0000000..ddb2ece --- /dev/null +++ b/ceee/ie/broker/broker_lib.idl @@ -0,0 +1,103 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Interface and object declarations for CEEE IE user broker. +import "exdisp.idl"; +import "oaidl.idl"; +import "ocidl.idl"; + +typedef long CeeeWindowHandle; + +[ + object, + uuid(A767CE3D-D65B-458f-B66A-CDB10B1F2DF6), + nonextensible, + helpstring("ICeeeBroker Interface"), + pointer_default(unique), + oleautomation +] +// Can execute code in specific threads of other process, even at higher +// integrity level than the calling process. + +// Allows the execution of code in destination threads. +interface ICeeeBroker : IUnknown { + // This method is DEPRECATED. + // Execute the requested message in the appropriate process/thread. + // + // @param function The JSON encoded function message to process. + // @param response Where to return the JSON encoded response. + HRESULT Execute([in] BSTR function, [out] BSTR* response); + + // Fire the given event message to Chrome Frame and potentially use it + // to complete asynchronous extension API execution. + // + // @param event_name The name of the event to fire. + // @param event_args The JSON encoded event arguments. + HRESULT FireEvent([in] BSTR event_name, [in] BSTR event_args); +} + +[ + object, + uuid(1821043a-a496-4d0e-9b50-1e0599237016), + nonextensible, + helpstring("ICeeeBrokerRegistrar Interface"), + pointer_default(unique), + oleautomation +] +// Access to the broker registrar to un/register a CeeeExecutor or a +// Chrome Frame Host running in an STA. +interface ICeeeBrokerRegistrar : IUnknown { + // Register the Window Executor @p executor to be used for the given + // @p thread_id. + // + // @param thread_id The identifier of the thread where @p executor runs. + // @param executor The executor to register. It must expose the + // ICeeeWindowExecutor interface. + HRESULT RegisterWindowExecutor([in] long thread_id, [in] IUnknown* executor); + + // TODO(mad@chromium.org): Implement the proper manual/secure + // registration. Register a tab executor. This version bypasses the + // checks to make sure the broker is the one which initiated the + // registration. + // + // @param thread_id The identifier of the thread where @p executor runs. + // @param executor The executor to register. It must expose the + // ICeeeTabExecutor interface. + HRESULT RegisterTabExecutor([in] long thread_id, [in] IUnknown* executor); + + // Unregister @p executor for the given @p thread_id. + // + // @param thread_id The identifier of the thread for which we want to + // unregister the executor of. + HRESULT UnregisterExecutor([in] long thread_id); + + // TODO(hansl@google.com): Remove this and implement it in + // RegisterTabExecutor. Make sure the Tab isn't registered before a TabId + // is available. + // Link a tab_id with a related BHO handle. There is a strict one to one + // relation between tab_ids and handles. + // + // @param tab_id The Chrome TabId related to the tab. + // @param handle The HWND of the BHO for this TabId. + HRESULT SetTabIdForHandle([in] long tab_id, [in] CeeeWindowHandle handle); +}; + +[ + uuid(45B783D0-8040-49a6-A719-84E320AAB3C5), + version(1.0), + helpstring("CEEE Broker 1.0 Type Library") +] +library CeeeBrokerLib { + interface ICeeeBrokerRegistrar; + interface ICeeePostman; + + [ + uuid(6D88A70D-2218-4466-BBD6-87AB563811A2), + helpstring("CEEE Broker Class") + ] + coclass CeeeBroker { + [default] interface ICeeeBroker; + }; +}; diff --git a/ceee/ie/broker/broker_module.cc b/ceee/ie/broker/broker_module.cc new file mode 100644 index 0000000..38b8529 --- /dev/null +++ b/ceee/ie/broker/broker_module.cc @@ -0,0 +1,194 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of ATL module object for EXE module. + +#include <atlbase.h> +#include <atlhost.h> + +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "base/logging_win.h" +#include "ceee/ie/broker/broker.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/broker/resource.h" +#include "ceee/ie/broker/window_events_funnel.h" +#include "ceee/ie/common/crash_reporter.h" +#include "ceee/common/com_utils.h" +#include "chrome/common/url_constants.h" + +namespace { + +const wchar_t kLogFileName[] = L"CeeeBroker.log"; + +// {6E3D6168-1DD2-4edb-A183-584C2C66E96D} +const GUID kCeeeBrokerLogProviderName = + { 0x6e3d6168, 0x1dd2, 0x4edb, + { 0xa1, 0x83, 0x58, 0x4c, 0x2c, 0x66, 0xe9, 0x6d } }; + +} // namespace + +// Object entries go here instead of with each object, so that we can keep +// the objects in a lib, and also to decrease the amount of magic. +OBJECT_ENTRY_AUTO(__uuidof(CeeeBroker), CeeeBroker) + +class CeeeBrokerModule : public CAtlExeModuleT<CeeeBrokerModule> { + public: + CeeeBrokerModule(); + ~CeeeBrokerModule(); + + DECLARE_LIBID(LIBID_CeeeBrokerLib) + static HRESULT WINAPI UpdateRegistryAppId(BOOL register) throw(); + + // We have our own version so that we can explicitly specify + // that we want to be in a multi threaded apartment. + static HRESULT InitializeCom(); + + // Prevent COM objects we don't own to control our lock count. + // To properly manage our lifespan, yet still be able to control the + // lifespan of the ChromePostman's thread, we must only rely on the + // CeeeBroker implementation of the IExternalConnection interface + // as well as the ExecutorsManager map content to decide when to die. + virtual LONG Lock() { + return 1; + } + virtual LONG Unlock() { + return 1; + } + + // We prevent access to the module lock count from objects we don't + // own by overriding the Un/Lock methods above. But when we want to + // access the module lock count, we do it from here, and bypass our + // override. These are the entry points that only our code accesses. + LONG LockModule() { + return CAtlExeModuleT<CeeeBrokerModule>::Lock(); + } + LONG UnlockModule() { + return CAtlExeModuleT<CeeeBrokerModule>::Unlock(); + } + + HRESULT PostMessageLoop(); + HRESULT PreMessageLoop(int show); + private: + // We maintain a postman COM object on the stack so that we can + // properly initialize and terminate it at the right time. + CComObjectStackEx<ChromePostman> chrome_postman_; + CrashReporter crash_reporter_; + base::AtExitManager at_exit_; +}; + +CeeeBrokerModule module; + +extern "C" int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int nShowCmd) { + return module.WinMain(nShowCmd); +} + +CeeeBrokerModule::CeeeBrokerModule() + : crash_reporter_(L"ceee_broker") { + TRACE_EVENT_BEGIN("ceee.broker", this, ""); + + wchar_t logfile_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(logfile_path), logfile_path); + ::PathAppend(logfile_path, kLogFileName); + + // It seems we're obliged to initialize the current command line + // before initializing logging. + CommandLine::Init(0, NULL); + + logging::InitLogging( + logfile_path, + logging::LOG_TO_BOTH_FILE_AND_SYSTEM_DEBUG_LOG, + logging::LOCK_LOG_FILE, + logging::APPEND_TO_OLD_LOG_FILE); + + // Initialize ETW logging. + logging::LogEventProvider::Initialize(kCeeeBrokerLogProviderName); + + // Initialize control hosting. + BOOL initialized = AtlAxWinInit(); + DCHECK(initialized); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + crash_reporter_.InitializeCrashReporting(false); +} + +HRESULT CeeeBrokerModule::InitializeCom() { + HRESULT hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (FAILED(hr)) + return hr; + + // We need to initialize security before setting global options. + hr = ::CoInitializeSecurity(NULL, + -1, + NULL, + NULL, + // Clients must identify. + RPC_C_IMP_LEVEL_IDENTIFY, + // And we identify. + RPC_C_IMP_LEVEL_IDENTIFY, + NULL, + // We don't want low integrity to be able to + // instantiate arbitrary objects in our process. + EOAC_NO_CUSTOM_MARSHAL, + NULL); + DCHECK(SUCCEEDED(hr)); + + // Ensure the marshaling machinery doesn't eat our crashes. + CComPtr<IGlobalOptions> options; + hr = options.CoCreateInstance(CLSID_GlobalOptions); + if (SUCCEEDED(hr)) { + hr = options->Set(COMGLB_EXCEPTION_HANDLING, COMGLB_EXCEPTION_DONOT_HANDLE); + } + DLOG_IF(WARNING, FAILED(hr)) << "IGlobalOptions::Set failed " + << com::LogHr(hr); + + // The above is best-effort, don't bail on error. + return S_OK; +} + +HRESULT CeeeBrokerModule::PreMessageLoop(int show) { + // It's important to initialize the postman BEFORE we make the Broker + // and the event funnel available, since we may get requests to execute + // API invocation or Fire events before the postman is ready to handle them. + chrome_postman_.Init(); + WindowEventsFunnel::Initialize(); + return CAtlExeModuleT<CeeeBrokerModule>::PreMessageLoop(show); +} + +HRESULT CeeeBrokerModule::PostMessageLoop() { + HRESULT hr = CAtlExeModuleT<CeeeBrokerModule>::PostMessageLoop(); + Singleton<ExecutorsManager, + ExecutorsManager::SingletonTraits>()->Terminate(); + WindowEventsFunnel::Terminate(); + chrome_postman_.Term(); + return hr; +} + +CeeeBrokerModule::~CeeeBrokerModule() { + crash_reporter_.ShutdownCrashReporting(); + logging::CloseLogFile(); + + TRACE_EVENT_END("ceee.broker", this, ""); +} + +HRESULT WINAPI CeeeBrokerModule::UpdateRegistryAppId(BOOL reg) throw() { + return com::ModuleRegistrationWithoutAppid(IDR_BROKER_MODULE, reg); +} + +namespace ceee_module_util { + +LONG LockModule() { + return module.LockModule(); +} + +LONG UnlockModule() { + return module.UnlockModule(); +} + +} // namespace diff --git a/ceee/ie/broker/broker_module.rc b/ceee/ie/broker/broker_module.rc new file mode 100644 index 0000000..43af9fe --- /dev/null +++ b/ceee/ie/broker/broker_module.rc @@ -0,0 +1,104 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Microsoft Visual C++ generated resource script. +// +#include "ceee/ie/broker/resource.h" +#include "version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +#error Don't open this in the GUI, it'll be massacred on save. +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION CHROME_VERSION + PRODUCTVERSION CHROME_VERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Google Inc." + VALUE "FileDescription", "Internet Explorer User Broker for Google CEEE" + VALUE "FileVersion", CHROME_VERSION_STRING + VALUE "LegalCopyright", COPYRIGHT_STRING + VALUE "InternalName", "ceee_broker.exe" + VALUE "OriginalFilename", "ceee_broker.exe" + VALUE "ProductName", "Google Chrome Extensions Execution Environment" + VALUE "ProductVersion", CHROME_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +IDR_BROKER_MODULE REGISTRY "broker_module.rgs" +IDR_BROKER REGISTRY "broker.rgs" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PROJNAME "ceee_broker" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +1 TYPELIB "broker_lib.tlb" + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/ceee/ie/broker/broker_module.rgs b/ceee/ie/broker/broker_module.rgs new file mode 100644 index 0000000..dfd0d75 --- /dev/null +++ b/ceee/ie/broker/broker_module.rgs @@ -0,0 +1,17 @@ +HKLM { + NoRemove SOFTWARE { + NoRemove Microsoft { + NoRemove 'Internet Explorer' { + NoRemove 'Low Rights' { + NoRemove ElevationPolicy { + ForceRemove {F2DE0DE9-098E-48a7-92C2-53B89A272BF4} { + val Policy = d 3 + val AppName = s '%MODULE_BASENAME%' + val AppPath = s '%MODULE_PATH%' + } + } + } + } + } + } +} diff --git a/ceee/ie/broker/broker_module_util.h b/ceee/ie/broker/broker_module_util.h new file mode 100644 index 0000000..effcd99 --- /dev/null +++ b/ceee/ie/broker/broker_module_util.h @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Utility functions to be called on the Broker Module object. + +#ifndef CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ +#define CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ + +namespace ceee_module_util { + +// Locks the module to prevent it from being terminated. +// +// @return The new module ref count, but only for debugging purposes. +LONG LockModule(); + +// Unlocks the module to allow it to be terminated appropriately. +// +// @return The new module ref count, but only for debugging purposes. +LONG UnlockModule(); + +} // namespace + +#endif // CEEE_IE_BROKER_BROKER_MODULE_UTIL_H__ diff --git a/ceee/ie/broker/broker_unittest.cc b/ceee/ie/broker/broker_unittest.cc new file mode 100644 index 0000000..aec7453 --- /dev/null +++ b/ceee/ie/broker/broker_unittest.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for user broker. + +#include "base/logging.h" +#include "ceee/ie/broker/broker.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::Return; + +// Lets us test protected members and mock certain calls. +class MockExecutorsManager : public ExecutorsManager { + public: + // true for no thread. + MockExecutorsManager() : ExecutorsManager(true) { + } + MOCK_METHOD2(RegisterWindowExecutor, HRESULT(ThreadId thread_id, + IUnknown* executor)); + MOCK_METHOD2(RegisterTabExecutor, HRESULT(ThreadId thread_id, + IUnknown* executor)); + MOCK_METHOD1(RemoveExecutor, HRESULT(ThreadId thread_id)); +}; + +class TestBroker: public CeeeBroker { + public: + // Prevents the instantiation of the ExecutorsManager, ApiDispatcher + // singletons. + HRESULT FinalConstruct() { + return S_OK; + } + // Set our own ExecutorsManager mock. + void SetExecutorsManager(ExecutorsManager* executors_manager) { + executors_manager_ = executors_manager; + } + + // Set our own ApiDispatcher mock. + void SetApiDisptacher(ApiDispatcher* api_dispatcher) { + api_dispatcher_ = api_dispatcher; + } +}; + +TEST(CeeeBroker, All) { + CComObject<TestBroker>* broker = NULL; + CComObject<TestBroker>::CreateInstance(&broker); + CComPtr<TestBroker> broker_keeper(broker); + + MockExecutorsManager executors_manager; + broker->SetExecutorsManager(&executors_manager); + + testing::MockApiDispatcher api_dispatcher; + broker->SetApiDisptacher(&api_dispatcher); + + EXPECT_CALL(api_dispatcher, HandleApiRequest(NULL, NULL)).Times(1); + EXPECT_EQ(S_OK, broker->Execute(NULL, NULL)); + + EXPECT_CALL(executors_manager, RegisterWindowExecutor(0, NULL)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->RegisterWindowExecutor(0, NULL)); + + EXPECT_CALL(executors_manager, RegisterTabExecutor(0, NULL)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->RegisterTabExecutor(0, NULL)); + + EXPECT_CALL(executors_manager, RemoveExecutor(0)). + WillOnce(Return(S_OK)); + EXPECT_EQ(S_OK, broker->UnregisterExecutor(0)); +} + +} // namespace diff --git a/ceee/ie/broker/chrome_postman.cc b/ceee/ie/broker/chrome_postman.cc new file mode 100644 index 0000000..59f2ba7 --- /dev/null +++ b/ceee/ie/broker/chrome_postman.cc @@ -0,0 +1,300 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ChromePostman implementation. +#include "ceee/ie/broker/chrome_postman.h" + +#include "base/string_util.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/common/api_registration.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/common/url_constants.h" +#include "ceee/common/com_utils.h" + +namespace ext = extension_automation_constants; + +namespace { + // The maximum number of times we should try to start Frame. + const unsigned int kMaxFrameResetCount = 5; +} + +class ChromeFrameMessageTask : public Task { + public: + explicit ChromeFrameMessageTask( + ChromePostman::ChromePostmanThread* thread_object, + BSTR message, BSTR target) + : thread_object_(thread_object), + message_(message), + target_(target) { + } + + virtual void Run() { + thread_object_->PostMessage(message_, target_); + } + private: + ChromePostman::ChromePostmanThread* thread_object_; + CComBSTR message_; + CComBSTR target_; +}; + + +class ChromeFrameResetTask : public Task { + public: + explicit ChromeFrameResetTask( + ChromePostman::ChromePostmanThread* thread_object) + : thread_object_(thread_object) { + } + + virtual void Run() { + thread_object_->ResetChromeFrame(); + } + private: + ChromePostman::ChromePostmanThread* thread_object_; +}; + + +class ApiExecutionTask : public Task { + public: + explicit ApiExecutionTask(BSTR message) : message_(message) {} + + virtual void Run() { + ProductionApiDispatcher::get()->HandleApiRequest(message_, NULL); + } + private: + CComBSTR message_; +}; + +class FireEventTask : public Task { + public: + FireEventTask(BSTR event_name, BSTR event_args) + : event_name_(event_name), event_args_(event_args) {} + + virtual void Run() { + ProductionApiDispatcher::get()->FireEvent(event_name_, event_args_); + } + private: + CComBSTR event_name_; + CComBSTR event_args_; +}; + + +ChromePostman* ChromePostman::single_instance_ = NULL; +ChromePostman::ChromePostman() { + DCHECK(single_instance_ == NULL); + single_instance_ = this; + frame_reset_count_ = 0; +} + +ChromePostman::~ChromePostman() { + DCHECK(single_instance_ == this); + single_instance_ = NULL; +} + +void ChromePostman::Init() { + // The postman thread must be a UI thread so that it can pump windows + // messages and allow COM to handle cross apartment calls. + + chrome_postman_thread_.StartWithOptions( + base::Thread::Options(MessageLoop::TYPE_UI, 0)); + api_worker_thread_.Start(); +} + +void ChromePostman::Term() { + api_worker_thread_.Stop(); + chrome_postman_thread_.Stop(); +} + +void ChromePostman::PostMessage(BSTR message, BSTR target) { + MessageLoop* message_loop = chrome_postman_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask( + FROM_HERE, new ChromeFrameMessageTask(&chrome_postman_thread_, + message, target)); + } else { + LOG(ERROR) << "Trying to post a message before the postman thread is" + "completely initialized and ready."; + } +} + +void ChromePostman::FireEvent(BSTR event_name, BSTR event_args) { + MessageLoop* message_loop = api_worker_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, + new FireEventTask(event_name, event_args)); + } else { + LOG(ERROR) << "Trying to post a message before the API worker thread is" + "completely initialized and ready."; + } +} + +HRESULT ChromePostman::OnCfReadyStateChanged(LONG state) { + if (state == READYSTATE_COMPLETE) { + // If the page is fully loaded, we reset the count to 0 to ensure we restart + // it if it's the user's fault (no max count). + DLOG(INFO) << "frame_reset_count_ reset"; + frame_reset_count_ = 0; + } + return S_OK; +} + +HRESULT ChromePostman::OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target) { + if (CComBSTR(target) == ext::kAutomationRequestTarget) { + MessageLoop* message_loop = api_worker_thread_.message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, new ApiExecutionTask(msg)); + } else { + LOG(ERROR) << "Trying to post a message before the API worker thread is" + "completely initialized and ready."; + } + } + return S_OK; +} + +HRESULT ChromePostman::OnCfExtensionReady(BSTR path, int response) { + return S_OK; +} + +HRESULT ChromePostman::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* tab_delimited_paths) { + return S_OK; +} + +HRESULT ChromePostman::OnCfGetExtensionApisToAutomate(BSTR* functions_enabled) { + DCHECK(functions_enabled != NULL); + std::vector<std::string> function_names; +#define REGISTER_API_FUNCTION(func) \ + function_names.push_back(func##Function::function_name()) + REGISTER_ALL_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // There is a special case with CreateWindow that is explained in the + // class comments. + function_names.push_back(CreateWindowFunction::function_name()); + std::string function_names_delim = JoinString(function_names, ','); + *functions_enabled = CComBSTR(function_names_delim.c_str()).Detach(); + + // CF is asking for the list of extension APIs to automate so the + // automation channel is ready. Set a dummy URL so we get the OnLoad + // callback. + // + // The current function call should come to us on the COM thread so we can + // just call the ChromeFrameHost directly from here. + CComPtr<IChromeFrameHost> host; + chrome_postman_thread_.GetHost(&host); + return host->SetUrl(CComBSTR(chrome::kAboutBlankURL)); + + return S_OK; +} + +HRESULT ChromePostman::OnCfChannelError() { + MessageLoop* message_loop = chrome_postman_thread_.message_loop(); + if (message_loop) { + frame_reset_count_++; + LOG(INFO) << "frame_reset_count_ = " << frame_reset_count_; + + // No use staying alive if Chrome Frame can't start. + CHECK(frame_reset_count_ < kMaxFrameResetCount) + << "Trying to reset Chrome Frame too many times already. Something's " + "wrong."; + + message_loop->PostTask(FROM_HERE, + new ChromeFrameResetTask(&chrome_postman_thread_)); + } else { + LOG(ERROR) << "Trying to reset Chrome Frame before the postman thread is" + "completely initialized and ready."; + } + return S_OK; +} + +ChromePostman::ChromePostmanThread::ChromePostmanThread() + : base::Thread("ChromePostman") { +} + +void ChromePostman::ChromePostmanThread::Init() { + HRESULT hr = ::CoInitializeEx(0, COINIT_APARTMENTTHREADED); + DCHECK(SUCCEEDED(hr)) << "Can't Init COM??? " << com::LogHr(hr); + + hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); +} + +HRESULT ChromePostman::ChromePostmanThread::InitializeChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + HRESULT hr = CreateChromeFrameHost(); + DCHECK(SUCCEEDED(hr) && chrome_frame_host_ != NULL); + if (FAILED(hr) || chrome_frame_host_ == NULL) { + LOG(ERROR) << "Failed to get chrome frame host " << com::LogHr(hr); + return com::AlwaysError(hr); + } + + DCHECK(ChromePostman::GetInstance() != NULL); + chrome_frame_host_->SetEventSink(ChromePostman::GetInstance()); + chrome_frame_host_->SetChromeProfileName( + ceee_module_util::GetBrokerProfileNameForIe()); + hr = chrome_frame_host_->StartChromeFrame(); + DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to start chrome frame " << com::LogHr(hr); + return hr; + } + return hr; +} + +HRESULT ChromePostman::ChromePostmanThread::CreateChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + return ChromeFrameHost::CreateInitializedIID(IID_IChromeFrameHost, + &chrome_frame_host_); +} + +void ChromePostman::ChromePostmanThread::CleanUp() { + TeardownChromeFrameHost(); + ::CoUninitialize(); +} + +void ChromePostman::ChromePostmanThread::TeardownChromeFrameHost() { + DCHECK(thread_id() == ::GetCurrentThreadId()); + if (chrome_frame_host_) { + chrome_frame_host_->SetEventSink(NULL); + HRESULT hr = chrome_frame_host_->TearDown(); + DCHECK(SUCCEEDED(hr)) << "ChromeFrameHost TearDown failed " << + com::LogHr(hr); + chrome_frame_host_.Release(); + } +} + +void ChromePostman::ChromePostmanThread::ResetChromeFrame() { + TeardownChromeFrameHost(); + HRESULT hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); +} + +void ChromePostman::ChromePostmanThread::PostMessage(BSTR message, + BSTR target) { + DCHECK(thread_id() == ::GetCurrentThreadId()); + HRESULT hr = chrome_frame_host_->PostMessage(message, target); + DCHECK(SUCCEEDED(hr)) << "ChromeFrameHost PostMessage failed " << + com::LogHr(hr); +} + +void ChromePostman::ChromePostmanThread::GetHost(IChromeFrameHost** host) { + DCHECK(thread_id() == ::GetCurrentThreadId()); + chrome_frame_host_.CopyTo(host); +} + +ChromePostman::ApiInvocationWorkerThread::ApiInvocationWorkerThread() + : base::Thread("ApiInvocationWorker") { +} + +void ChromePostman::ApiInvocationWorkerThread::Init() { + ::CoInitializeEx(0, COINIT_MULTITHREADED); + ProductionApiDispatcher::get()->SetApiInvocationThreadId( + ::GetCurrentThreadId()); +} + +void ChromePostman::ApiInvocationWorkerThread::CleanUp() { + ::CoUninitialize(); + ProductionApiDispatcher::get()->SetApiInvocationThreadId(0); +} diff --git a/ceee/ie/broker/chrome_postman.h b/ceee/ie/broker/chrome_postman.h new file mode 100644 index 0000000..538abd7 --- /dev/null +++ b/ceee/ie/broker/chrome_postman.h @@ -0,0 +1,133 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Broke Postman implementation. + +#ifndef CEEE_IE_BROKER_CHROME_POSTMAN_H_ +#define CEEE_IE_BROKER_CHROME_POSTMAN_H_ + +#include "base/singleton.h" +#include "base/thread.h" +#include "ceee/ie/common/chrome_frame_host.h" + +#include "broker_lib.h" // NOLINT + +// The Broker Postman singleton object wrapping the ChromeFrameHost +// class so that it can receive API invocation from Chrome Frame, and also +// dispatch response and events to it. +// +// Since Chrome Frame must be access from a Single Thread Apartment, this +// object must only call it in an STA, so the PostMessage method must post a +// task to do so in the STA thread. +// +// But when Chrome Frame calls into this object, we can't block and thus +// the API invocations must be queued so that the API Dispatcher can +// fetch them from the queue, handle them appropriately in the MTA and +// post back the asynchronous response to Chrome Frame via our +// PostMessage method. + +class ChromePostman + : public CComObjectRootEx<CComMultiThreadModel>, + public IChromeFrameHostEvents { + public: + BEGIN_COM_MAP(ChromePostman) + END_COM_MAP() + + ChromePostman(); + virtual ~ChromePostman(); + + // This posts a tasks to our STA thread so that it posts the given message + // to Chrome Frame. + virtual void PostMessage(BSTR message, BSTR target); + + // This posts a tasks to the Api Invocation thread to fire the given event. + virtual void FireEvent(BSTR event_name, BSTR event_args); + + // This creates both an STA and an MTA threads. We must make sure we only call + // Chrome Frame from this STA. And since we can't block Chrome Frame we use + // the MTA thread to executes API Invocation we receive from Chrome Frame. + // We also create and initialize Chrome Framer from here. + virtual void Init(); + + // To cleanly terminate the threads, and our hooks into Chrome Frame. + virtual void Term(); + + // Returns our single instance held by the module. + static ChromePostman* GetInstance() { return single_instance_; } + + class ChromePostmanThread : public base::Thread { + public: + ChromePostmanThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + + // Posts the message to our instance of Chrome Frame. + // THIS CAN ONLY BE CALLED FROM OUR THREAD, and we DCHECK it. + void PostMessage(BSTR message, BSTR target); + + // Retrieves the Chrome Frame host; should only be called from the + // postman thread. + void GetHost(IChromeFrameHost** host); + + // Resets Chrome Frame by tearing it down and restarting it. + // We use this when we receive a channel error meaning Chrome has died. + void ResetChromeFrame(); + + protected: + // Creates and initializes the chrome frame host. + // CAN ONLY BE CALLED FROM THE STA! + HRESULT InitializeChromeFrameHost(); + + // Isolate the creation of the host so we can overload it to mock + // the Chrome Frame Host in our tests. + // CAN ONLY BE CALLED FROM THE STA! + virtual HRESULT CreateChromeFrameHost(); + + // Tears down the Chrome Frame host. + void TeardownChromeFrameHost(); + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<IChromeFrameHost> chrome_frame_host_; + }; + + protected: + // Messages received from Chrome Frame are sent to the API dispatcher via + // a task posted to our MTA thread. + HRESULT OnCfReadyStateChanged(LONG state); + HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target); + HRESULT OnCfExtensionReady(BSTR path, int response); + HRESULT OnCfGetEnabledExtensionsComplete(SAFEARRAY* tab_delimited_paths); + HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled); + HRESULT OnCfChannelError(); + + class ApiInvocationWorkerThread : public base::Thread { + public: + ApiInvocationWorkerThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + }; + + // The STA in which we run. + ChromePostmanThread chrome_postman_thread_; + + // The MTA thread to which we post API Invocations and Fired Events. + ApiInvocationWorkerThread api_worker_thread_; + + // The number of times we tried to launch ChromeFrame. + int frame_reset_count_; + + // "in the end, there should be only one!" :-) + static ChromePostman* single_instance_; +}; + +#endif // CEEE_IE_BROKER_CHROME_POSTMAN_H_ diff --git a/ceee/ie/broker/common_api_module.cc b/ceee/ie/broker/common_api_module.cc new file mode 100644 index 0000000..096003b --- /dev/null +++ b/ceee/ie/broker/common_api_module.cc @@ -0,0 +1,146 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementations for common functions for various API implementation modules. + +#include "ceee/ie/broker/common_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> // Must be included AFTER base. + +#include "base/string_number_conversions.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + +namespace ext = extension_tabs_module_constants; + +namespace common_api { + +bool CommonApiResult::IsTabWindowClass(HWND window) { + return window_utils::IsWindowClass(window, windows::kIeTabWindowClass); +} + +bool CommonApiResult::IsIeFrameClass(HWND window) { + return window_utils::IsWindowClass(window, windows::kIeFrameWindowClass); +} + +HWND CommonApiResult::TopIeWindow() { + HWND window = NULL; + if (window_utils::FindDescendentWindow(NULL, windows::kIeFrameWindowClass, + true, &window)) { + return window; + } + NOTREACHED() << "How come we can't find a Top IE Window???"; + return NULL; +} + +void CommonApiResult::SetResultFromWindowInfo( + HWND window, const CeeeWindowInfo& window_info, bool populate_tabs) { + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + dict->SetInteger(ext::kIdKey, window_id); + dict->SetBoolean(ext::kFocusedKey, window_info.focused != FALSE); + + dict->SetInteger(ext::kLeftKey, window_info.rect.left); + dict->SetInteger(ext::kTopKey, window_info.rect.top); + dict->SetInteger(ext::kWidthKey, window_info.rect.right - + window_info.rect.left); + dict->SetInteger(ext::kHeightKey, window_info.rect.bottom - + window_info.rect.top); + if (populate_tabs) { + DCHECK(window_info.tab_list != NULL); + Value* tab_list_value = CreateTabList(window_info.tab_list); + // DCHECK yet continue if we get a NULL tab list since we may get here + // before it is available. + DCHECK(tab_list_value != NULL); + if (!tab_list_value) + tab_list_value = Value::CreateNullValue(); + dict->Set(ext::kTabsKey, tab_list_value); + } + + dict->SetBoolean(ext::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + + // TODO(mad@chromium.org): for now, always setting to "normal" since we don't + // yet have a way to tell if the window is a popup or not. + dict->SetString(ext::kWindowTypeKey, ext::kWindowTypeValueNormal); + + DCHECK(value_ == NULL); + value_.reset(dict.release()); +} + +Value* CommonApiResult::CreateTabList(BSTR tab_list) { + // No need for a request_id. + tab_api::GetAllTabsInWindowResult result(kNoRequestId); + result.Execute(tab_list); + const Value* tab_list_value = result.value(); + // No DCHECK as this will happen if we try to update a window + // that has been closed. + if (tab_list_value) { + return tab_list_value->DeepCopy(); + } + LOG(WARNING) << "Can't get info for tab_ids: '" << tab_list << "'."; + return NULL; +} + +bool CommonApiResult::CreateWindowValue(HWND window, bool populate_tabs) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + if (window_utils::WindowHasNoThread(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + if (!IsIeFrameClass(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get window info."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + common_api::WindowInfo window_info; + HRESULT hr = executor->GetWindow(populate_tabs, &window_info); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't get info for window: " << std::hex << window << + ". " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + SetResultFromWindowInfo(window, window_info, populate_tabs); + return true; +} + +WindowInfo::WindowInfo() { + focused = FALSE; + rect.bottom = -1; + rect.top = -1; + rect.left = -1; + rect.right = -1; + tab_list = NULL; +} + +WindowInfo::~WindowInfo() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(tab_list); +} + +} // namespace common_api diff --git a/ceee/ie/broker/common_api_module.h b/ceee/ie/broker/common_api_module.h new file mode 100644 index 0000000..c4b6961 --- /dev/null +++ b/ceee/ie/broker/common_api_module.h @@ -0,0 +1,71 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common functions for various API implementation modules. + +#ifndef CEEE_IE_BROKER_COMMON_API_MODULE_H_ +#define CEEE_IE_BROKER_COMMON_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "toolband.h" // NOLINT + +namespace common_api { + +class CommonApiResult : public ApiDispatcher::InvocationResult { + public: + explicit CommonApiResult(int request_id) + : ApiDispatcher::InvocationResult(request_id) { + } + + // Returns true if the given window is an IE "server" window, i.e. a tab. + static bool IsTabWindowClass(HWND window); + + // Returns true if the given window is a top-level IE window. + static bool IsIeFrameClass(HWND window); + + // Returns the IE frame window at the top of the Z-order. This will generally + // be the last window used or the new window just created. + // @return The HWND of the top IE window. + static HWND TopIeWindow(); + + // Build the result_ value from the provided window info. It will set the + // value if it is currently NULL, otherwise it assumes it is a ListValue and + // adds a new Value to it. + // @param window The window handle + // @param window_info The info about the window to create a new value for. + // @param populate_tabs To specify if we want to populate the tabs info. + virtual void SetResultFromWindowInfo(HWND window, + const CeeeWindowInfo& window_info, + bool populate_tabs); + + // Creates a list value of all tabs in the given list. + // @param tab_list A list of HWND and index of the tabs for which we want to + // create a value JSON encoded as a list of (id, index) pairs. + // @Return A ListValue containing the individual Values for each tab info. + virtual Value* CreateTabList(BSTR tab_list); + + // Creates a value for the given window, populating tabs if requested. + // Sets value_ with the appropriate value content, or resets it in case of + // errors. Also calls PostError() if there is an error and returns false. + // The @p window parameter contrasts with CreateTabValue because we most + // often use this function with HWND gotten without Ids (ie. from + // TopIeWindow()). This was not the case with CreateTabValue. + // @param window The identifier of the window for which to create the value. + // @param populate_tabs To specify if we want to populate the tabs info. + virtual bool CreateWindowValue(HWND window, bool populate_tabs); +}; + +// Helper class to handle the data cleanup. +class WindowInfo : public CeeeWindowInfo { + public: + WindowInfo(); + ~WindowInfo(); +}; + +} // namespace common_api + +#endif // CEEE_IE_BROKER_COMMON_API_MODULE_H_ diff --git a/ceee/ie/broker/cookie_api_module.cc b/ceee/ie/broker/cookie_api_module.cc new file mode 100644 index 0000000..2e7d59b --- /dev/null +++ b/ceee/ie/broker/cookie_api_module.cc @@ -0,0 +1,450 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation. + +#include "ceee/ie/broker/cookie_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" + +namespace keys = extension_cookies_api_constants; + +namespace cookie_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); }\ + while (false) + REGISTER_COOKIE_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // Now register the permanent event handler. + dispatcher->RegisterPermanentEventHandler(keys::kOnChanged, + CookieChanged::EventHandler); +} + +bool CookieApiResult::CreateCookieValue(const CookieInfo& cookie_info) { + scoped_ptr<DictionaryValue> cookie(new DictionaryValue()); + cookie->SetString(keys::kNameKey, com::ToString(cookie_info.name)); + cookie->SetString(keys::kValueKey, com::ToString(cookie_info.value)); + cookie->SetString(keys::kDomainKey, com::ToString(cookie_info.domain)); + cookie->SetBoolean(keys::kHostOnlyKey, cookie_info.host_only == TRUE); + cookie->SetString(keys::kPathKey, com::ToString(cookie_info.path)); + cookie->SetBoolean(keys::kSecureKey, cookie_info.secure == TRUE); + cookie->SetBoolean(keys::kHttpOnlyKey, cookie_info.http_only == TRUE); + cookie->SetBoolean(keys::kSessionKey, cookie_info.session == TRUE); + if (cookie_info.session == FALSE) + cookie->SetReal(keys::kExpirationDateKey, cookie_info.expiration_date); + cookie->SetString(keys::kStoreIdKey, com::ToString(cookie_info.store_id)); + DCHECK(value() == NULL); + set_value(cookie.release()); + return true; +} + +HRESULT CookieApiResult::GetCookieInfo( + const std::string& url, const std::string& name, HWND window, + CookieInfo* cookie_info) { + // Get a tab window child of the cookie store window, so that we can properly + // access session cookies. + HWND tab_window = NULL; + if (!window_utils::FindDescendentWindow( + window, windows::kIeTabWindowClass, false, &tab_window) || + tab_window == NULL) { + PostError("Failed to get tab window for a given cookie store."); + return E_FAIL; + } + + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(tab_window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get cookie info."; + PostError("Internal Error while getting cookie info."); + return E_FAIL; + } + + HRESULT hr = executor->GetCookie( + CComBSTR(url.data()), CComBSTR(name.data()), cookie_info); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get cookie info." << com::LogHr(hr); + PostError("Internal Error while getting cookie info."); + } + return hr; +} + +bool CookieApiResult::CreateCookieStoreValue(DWORD process_id, + const WindowSet& windows) { + scoped_ptr<DictionaryValue> cookie_store(new DictionaryValue()); + std::ostringstream store_id_stream; + store_id_stream << process_id; + // For IE CEEE, we use a string representation of the IE process ID as the + // cookie store ID. + cookie_store->SetString(keys::kIdKey, store_id_stream.str()); + DCHECK(windows.size()); + if (windows.size() == 0) { + PostError(api_module_constants::kInternalErrorError); + return false; + } + WindowSet::const_iterator iter = windows.begin(); + // First register the cookie store once per process. + if (FAILED(RegisterCookieStore(*iter))) + return false; + // Now collect all tab IDs from each frame window into a single list. + scoped_ptr<ListValue> tab_ids(new ListValue()); + for (; iter != windows.end(); ++iter) { + if (!AppendToTabIdList(*iter, tab_ids.get())) { + PostError(api_module_constants::kInternalErrorError); + return false; + } + } + cookie_store->Set(keys::kTabIdsKey, tab_ids.release()); + set_value(cookie_store.release()); + PostResult(); + return true; +} + +HRESULT CookieApiResult::RegisterCookieStore(HWND window) { + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to register a cookie store."; + PostError(api_module_constants::kInternalErrorError); + return E_FAIL; + } + HRESULT hr = executor->RegisterCookieStore(); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't register cookie store. " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + } + return hr; +} + +HRESULT CookieApiResult::CookieStoreIsRegistered(HWND window) { + CComPtr<ICeeeCookieExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeCookieExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to register a cookie store."; + PostError(api_module_constants::kInternalErrorError); + return E_FAIL; + } + HRESULT hr = executor->CookieStoreIsRegistered(); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Error accessing cookie store. " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + } + return hr; +} + +bool CookieApiResult::AppendToTabIdList(HWND window, ListValue* tab_ids) { + CComPtr<ICeeeWindowExecutor> executor; + GetDispatcher()->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get window tabs."; + return false; + } + CComBSTR tab_ids_string; + HRESULT hr = executor->GetTabs(&tab_ids_string); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't get tabs for window: " << std::hex << window << + ". " << com::LogHr(hr); + return false; + } + scoped_ptr<ListValue> tabs_list; + if (!api_module_util::GetListFromJsonString(CW2A(tab_ids_string).m_psz, + &tabs_list)) { + NOTREACHED() << "Invalid tabs list BSTR: " << tab_ids_string; + return false; + } + size_t num_values = tabs_list->GetSize(); + if (num_values % 2 != 0) { + // Values should come in pairs, one for the id and another one for the + // index. + NOTREACHED() << "Invalid tabs list BSTR: " << tab_ids_string; + return false; + } + for (size_t i = 0; i < num_values; i += 2) { + int tab_id = 0; + if (tabs_list->GetInteger(i, &tab_id)) + tab_ids->Append(Value::CreateIntegerValue(tab_id)); + } + return true; +} + +void CookieApiResult::FindAllProcessesAndWindows( + ProcessWindowMap* all_windows) { + DCHECK(all_windows); + WindowSet ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + if (ie_frame_windows.empty()) + return; + + WindowSet::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + DWORD process_id = 0; + // Skip over windows with no thread. + if (::GetWindowThreadProcessId(*iter, &process_id) == 0) + continue; + DCHECK(process_id); + + if (process_id != 0) + (*all_windows)[process_id].insert(*iter); + } +} + +HWND CookieApiResult::GetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + DWORD store_process_id = 0; + std::istringstream store_id_stream(store_id); + store_id_stream >> store_process_id; + if (!store_process_id) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidStoreIdError, store_id)); + return NULL; + } + + WindowSet ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + + WindowSet::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + DWORD process_id = 0; + if (::GetWindowThreadProcessId(*iter, &process_id) != 0 && + process_id == store_process_id) { + if (allow_unregistered_store) + return *iter; + HRESULT hr = CookieStoreIsRegistered(*iter); + // If the above call failed, an error has already been posted. + if (FAILED(hr)) { + return NULL; + } else if (hr == S_OK) { + return *iter; + } + } + } + // Matching window not found. + PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidStoreIdError, store_id)); + return NULL; +} + +void GetCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + + DictionaryValue* details; + if (!args.GetDictionary(0, &details)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + std::string url; + if (!details->GetString(keys::kUrlKey, &url)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + if (!GURL(url).is_valid()) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidUrlError, url)); + return; + } + // TODO(cindylau@chromium.org): Add extension host permissions + // checks against the URL. + + std::string name; + if (!details->GetString(keys::kNameKey, &name)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + HWND cookie_store_window = NULL; + if (details->HasKey(keys::kStoreIdKey)) { + std::string store_id; + if (!details->GetString(keys::kStoreIdKey, &store_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + cookie_store_window = result->GetWindowFromStoreId(store_id, false); + // If no window was found, an error has already been posted. + if (!cookie_store_window) + return; + } else { + // The store ID was unspecified or isn't a registered cookie store + // ID. Use the current execution context's cookie store by default. + // TODO(cindylau@chromium.org): We currently don't have a way to + // get the current execution context, so we are using the top IE + // window for now. + cookie_store_window = window_api::WindowApiResult::TopIeWindow(); + } + DCHECK(cookie_store_window); + + CookieInfo cookie_info; + HRESULT hr = result->GetCookieInfo(url, name, cookie_store_window, + &cookie_info); + // If the call failed, an error has already been posted. + if (FAILED(hr)) + return; + if (hr == S_OK) { + DCHECK(WideToASCII(com::ToString(cookie_info.name)) == name); + if (!result->CreateCookieValue(cookie_info)) + return; + } + result->PostResult(); +} + +void GetAllCookies::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void SetCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void RemoveCookie::Execute(const ListValue& args, int request_id) { + scoped_ptr<CookieApiResult> result(CreateApiResult(request_id)); + result->PostError("Not implemented."); +} + +void GetAllCookieStores::Execute(const ListValue& args, int request_id) { + scoped_ptr<IterativeCookieApiResult> result(CreateApiResult(request_id)); + + // TODO(cindylau@chromium.org): Restrict incognito (InPrivate) + // windows if incognito is not enabled for the extension. Right now + // CEEE has no mechanism to discover the extension's + // incognito-enabled setting, so adding support here is premature. + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + if (all_windows.empty()) { + result->FlushAllPosts(); + return; + } + + CookieApiResult::ProcessWindowMap::const_iterator iter = all_windows.begin(); + for (; iter != all_windows.end(); ++iter) { + bool created_ok = result->CreateCookieStoreValue(iter->first, iter->second); + LOG_IF(WARNING, !created_ok) << "Could not create cookie store value:" + << result->LastError(); + } + + if (result->IsEmpty()) // This is an error! + result->PostError(keys::kNoCookieStoreFoundError); + + result->FlushAllPosts(); +} + +// Static wrapper for the real event handler implementation. +bool CookieChanged::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + CookieChanged event_handler; + return event_handler.EventHandlerImpl(input_args, converted_args); +} + +// Handles a cookies.onChanged event by verifying that the store ID is +// registered. If not, this function registers the store ID. +bool CookieChanged::EventHandlerImpl(const std::string& input_args, + std::string* converted_args) { + DCHECK(converted_args); + // We don't need to modify the arguments, we only need to verify that the + // store ID is registered. + *converted_args = input_args; + + // Get the cookie info from the input arguments. + scoped_ptr<Value> input_val(base::JSONReader::Read(input_args, true)); + DCHECK(input_val.get() != NULL); + if (input_val == NULL) { + LOG(ERROR) << "Invalid Arguments sent to CookieChangedEventHandler()"; + return false; + } + + ListValue* input_list = static_cast<ListValue*>(input_val.get()); + DCHECK(input_list && input_list->GetSize() == 1); + + DictionaryValue* changeInfo = NULL; + bool success = input_list->GetDictionary(0, &changeInfo); + DictionaryValue* cookie = NULL; + if (success && changeInfo && changeInfo->HasKey(keys::kCookieKey)) + success = changeInfo->GetDictionary(keys::kCookieKey, &cookie); + if (!success || cookie == NULL) { + NOTREACHED() << "Failed to get the cookie value from the list of args."; + return false; + } + std::string store_id; + if (cookie->HasKey(keys::kStoreIdKey)) + success = cookie->GetString(keys::kStoreIdKey, &store_id); + if (!success || store_id.size() == 0) { + NOTREACHED() << "Failed to get the store ID value from the cookie arg."; + return false; + } + + scoped_ptr<CookieApiResult> api_result(CreateApiResult()); + HWND store_window = api_result->GetWindowFromStoreId(store_id, true); + if (store_window == NULL) { + // Error was already logged by GetWindowFromStoreId. + return false; + } + HRESULT is_registered = api_result->CookieStoreIsRegistered(store_window); + if (FAILED(is_registered)) { + // Error was already logged by CookieStoreIsRegistered. + return false; + } + if (is_registered == S_FALSE) { + // The store ID has not yet been queried by the user; register it here + // before exposing it to the user. + is_registered = api_result->RegisterCookieStore(store_window); + } + if (is_registered != S_OK) { + // Any errors should have already been logged by RegisterCookieStore. + return false; + } + + return true; +} + +CookieInfo::CookieInfo() { + name = NULL; + value = NULL; + domain = NULL; + host_only = false; + path = NULL; + secure = false; + http_only = false; + session = false; + expiration_date = 0; + store_id = NULL; +} + +CookieInfo::~CookieInfo() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(name); + ::SysFreeString(value); + ::SysFreeString(domain); + ::SysFreeString(path); + ::SysFreeString(store_id); +} + +} // namespace cookie_api diff --git a/ceee/ie/broker/cookie_api_module.h b/ceee/ie/broker/cookie_api_module.h new file mode 100644 index 0000000..ceace34 --- /dev/null +++ b/ceee/ie/broker/cookie_api_module.h @@ -0,0 +1,149 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation. + +#ifndef CEEE_IE_BROKER_COOKIE_API_MODULE_H_ +#define CEEE_IE_BROKER_COOKIE_API_MODULE_H_ + +#include <list> +#include <map> +#include <set> +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" + +#include "toolband.h" //NOLINT + +namespace cookie_api { + +class CookieApiResult; +typedef ApiResultCreator<CookieApiResult> CookieApiResultCreator; + +// Registers all Cookie API Invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +// Helper class to handle the data cleanup. +class CookieInfo : public CeeeCookieInfo { + public: + CookieInfo(); + ~CookieInfo(); + private: + DISALLOW_COPY_AND_ASSIGN(CookieInfo); +}; + +class CookieApiResult : public ApiDispatcher::InvocationResult { + public: + typedef std::set<HWND> WindowSet; + typedef std::map<DWORD, WindowSet> ProcessWindowMap; + + explicit CookieApiResult(int request_id) + : ApiDispatcher::InvocationResult(request_id) { + } + + // Creates a value object with the information for a cookie and assigns the + // API result value with it. + // @return True on success. Returns false and also calls + // ApiDispatcher::InvocationResult::PostError on failure. + virtual bool CreateCookieValue(const CookieInfo& info); + + // Gets information for the cookie with the given URL and name, using the + // cookie store corresponding to the given window. + virtual HRESULT GetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info); + + // Constructs a cookie store value given the IE process ID and a set of all + // IEFrame windows for that process. As defined by the chrome.cookies + // API, a cookie store value consists of a string ID and a list of all tab + // IDs belonging to that cookie store. Assigns the API result value with the + // newly constructed cookie store object. + // @return True on success. Returns false and also calls + // ApiDispatcher::InvocationResult::PostError on failure. + virtual bool CreateCookieStoreValue(DWORD process_id, + const WindowSet& windows); + + // Finds an IEFrame window belonging to the process associated with the given + // cookie store ID. + // If allow_unregistered_store is true, the function succeeds even if the + // given store ID has not been registered. If it's false, an unregistered + // store ID will result in a failure and PostError. + // @return A window associated with the given cookie store, or NULL on error. + // Will also call ApiDispatcher::InvocationResult::PostError + // on failure to find a window. + virtual HWND GetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store); + + // Registers the cookie store for the process corresponding to the given + // window. + // @return S_OK on success, failure code on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual HRESULT RegisterCookieStore(HWND window); + + // Checks whether the given window's process is a registered cookie store. + // @return S_OK if the cookie store is registered, S_FALSE if not. Returns + // a failure code on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual HRESULT CookieStoreIsRegistered(HWND window); + + // Finds all IEFrame windows and puts them in a map of HWND sets keyed by + // process ID. + static void FindAllProcessesAndWindows(ProcessWindowMap* all_windows); + + private: + // Retrieves all tab IDs from the given window and adds them to the given tab + // ID list. + // @return false on error and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + bool AppendToTabIdList(HWND window, ListValue* tab_ids); +}; + +typedef IterativeApiResult<CookieApiResult> IterativeCookieApiResult; + +class GetCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class GetAllCookies : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class SetCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class RemoveCookie : public CookieApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; +class GetAllCookieStores : public ApiResultCreator<IterativeCookieApiResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Permanent event handler for cookies.onChanged. We define a class for the +// event in order to ease unit testing of the CookieApiResult used by the +// event handler implementation. +class CookieChanged { + public: + bool EventHandlerImpl(const std::string& input_args, + std::string* converted_args); + + // Static wrapper for the event handler implementation. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); + protected: + virtual ~CookieChanged() {} + + // Returns allocated memory; the caller is responsible for + // freeing it. + virtual CookieApiResult* CreateApiResult() { + return new CookieApiResult(CookieApiResult::kNoRequestId); + } +}; + +} // namespace cookie_api + +#endif // CEEE_IE_BROKER_COOKIE_API_MODULE_H_ diff --git a/ceee/ie/broker/cookie_api_module_unittest.cc b/ceee/ie/broker/cookie_api_module_unittest.cc new file mode 100644 index 0000000..00e28c8 --- /dev/null +++ b/ceee/ie/broker/cookie_api_module_unittest.cc @@ -0,0 +1,777 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Cookie API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/string_util.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace keys = extension_cookies_api_constants; + +namespace { + +using cookie_api::CookieApiResult; +using cookie_api::CookieChanged; +using cookie_api::CookieInfo; +using cookie_api::GetCookie; +using cookie_api::GetAllCookies; +using cookie_api::SetCookie; +using cookie_api::RemoveCookie; +using cookie_api::GetAllCookieStores; +using cookie_api::IterativeCookieApiResult; +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::Invoke; +using testing::InstanceCountMixin; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +using window_api::WindowApiResult; + +const int kRequestId = 21; +const int kThreadId = 1; +const int kNumWindows = 4; +const int kNumProcesses = 3; +const HWND kAllWindows[kNumWindows] = {(HWND)1, (HWND)17, (HWND)5, (HWND)8}; +const HWND kBadWindows[] = {(HWND)23, (HWND)2}; +const DWORD kWindowProcesses[kNumWindows] = {2, 3, 2, 7}; +const DWORD kAllProcesses[kNumProcesses] = {2, 3, 7}; +const HANDLE kProcessHandles[kNumProcesses] + = {HANDLE(0xFF0002), HANDLE(0xFF0003), HANDLE(0xFF0007)}; + +class MockCookieApiResult : public CookieApiResult { + public: + explicit MockCookieApiResult(int request_id) : CookieApiResult(request_id) {} + + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + MOCK_METHOD4(GetCookieInfo, HRESULT(const std::string&, const std::string&, + HWND, CookieInfo*)); + MOCK_METHOD2(CreateCookieStoreValue, bool(DWORD, const WindowSet&)); + MOCK_METHOD1(RegisterCookieStore, HRESULT(HWND)); + MOCK_METHOD1(CookieStoreIsRegistered, HRESULT(HWND)); + MOCK_METHOD2(GetWindowFromStoreId, HWND(const std::string&, bool)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + HRESULT CallGetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info) { + return CookieApiResult::GetCookieInfo(url, name, window, cookie_info); + } + + bool CallCreateCookieStoreValue(DWORD process_id, const WindowSet& windows) { + return CookieApiResult::CreateCookieStoreValue(process_id, windows); + } + + HWND CallGetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + return CookieApiResult::GetWindowFromStoreId( + store_id, allow_unregistered_store); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +// Mocks the iterative version of result. Unlike in the mock of the 'straight' +// version, here PostResult and Post Error are not mocked because they serve +// to accumulate result, which can be examined by standard means. +class MockIterativeCookieApiResult : public IterativeCookieApiResult { + public: + explicit MockIterativeCookieApiResult(int request_id) + : IterativeCookieApiResult(request_id) {} + + MOCK_METHOD0(CallRealPostResult, void()); + MOCK_METHOD1(CallRealPostError, void(const std::string&)); + MOCK_METHOD4(GetCookieInfo, HRESULT(const std::string&, const std::string&, + HWND, CookieInfo*)); + MOCK_METHOD2(CreateCookieStoreValue, bool(DWORD, const WindowSet&)); + MOCK_METHOD1(RegisterCookieStore, HRESULT(HWND)); + MOCK_METHOD1(CookieStoreIsRegistered, HRESULT(HWND)); + MOCK_METHOD2(GetWindowFromStoreId, HWND(const std::string&, bool)); + + virtual void PostError(const std::string& error) { + ++error_counter_; + IterativeCookieApiResult::PostError(error); + } + + virtual void PostResult() { + ++success_counter_; + IterativeCookieApiResult::PostResult(); + } + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + HRESULT CallGetCookieInfo(const std::string& url, + const std::string& name, + HWND window, CookieInfo* cookie_info) { + return CookieApiResult::GetCookieInfo(url, name, window, cookie_info); + } + + bool CallCreateCookieStoreValue(DWORD process_id, const WindowSet& windows) { + return CookieApiResult::CreateCookieStoreValue(process_id, windows); + } + + static HRESULT MockGetTabs(BSTR* tab_list) { + // The string is not retained since the calling object takes overship. + *tab_list = SysAllocString(L"[27, 1]"); + return S_OK; + } + + HWND CallGetWindowFromStoreId(const std::string& store_id, + bool allow_unregistered_store) { + return CookieApiResult::GetWindowFromStoreId( + store_id, allow_unregistered_store); + } + + static void ResetCounters() { + success_counter_ = 0; + error_counter_ = 0; + } + + static int success_counter() { + return success_counter_; + } + + static int error_counter() { + return error_counter_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + static int success_counter_; + static int error_counter_; +}; + +int MockIterativeCookieApiResult::success_counter_ = 0; +int MockIterativeCookieApiResult::error_counter_ = 0; + +class MockCookieChanged : public CookieChanged { + public: + void AllocateApiResult() { + api_result_.reset(new MockCookieApiResult(CookieApiResult::kNoRequestId)); + } + + virtual CookieApiResult* CreateApiResult() { + DCHECK(api_result_.get() != NULL); + return api_result_.release(); + } + + scoped_ptr<MockCookieApiResult> api_result_; +}; + +// Mock static functions defined in CookieApiResult. +MOCK_STATIC_CLASS_BEGIN(MockCookieApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockCookieApiResultStatics) + MOCK_STATIC_INIT2(CookieApiResult::FindAllProcessesAndWindows, + FindAllProcessesAndWindows); + MOCK_STATIC_INIT_END() + MOCK_STATIC1(void, , FindAllProcessesAndWindows, + CookieApiResult::ProcessWindowMap*); +MOCK_STATIC_CLASS_END(MockCookieApiResultStatics) + +// Mock static functions defined in WindowApiResult. +MOCK_STATIC_CLASS_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT2(WindowApiResult::TopIeWindow, + TopIeWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(HWND, , TopIeWindow); +MOCK_STATIC_CLASS_END(MockWindowApiResultStatics) + +class CookieApiTests: public testing::Test { + protected: + template <class T> void ExpectInvalidArguments( + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + T> >& invocation, + const ListValue& args) { + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq( + api_module_constants::kInvalidArgumentsError))).Times(1); + invocation.Execute(args, kRequestId); + } + + StrictMock<testing::MockUser32> user32_; +}; + +TEST_F(CookieApiTests, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, + RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(5)); + cookie_api::RegisterInvocations(&disp); +} + +TEST_F(CookieApiTests, CreateCookieValue) { + CookieInfo cookie_info; + cookie_info.name = ::SysAllocString(L"foo"); + cookie_info.value = ::SysAllocString(L"bar"); + cookie_info.domain = ::SysAllocString(L"google.com"); + cookie_info.host_only = FALSE; + cookie_info.path = ::SysAllocString(L"/testpath"); + cookie_info.secure = TRUE; + cookie_info.session = TRUE; + cookie_info.expiration_date = 0; + cookie_info.store_id = ::SysAllocString(L"test_store_id"); + + CookieApiResult result(CookieApiResult::kNoRequestId); + EXPECT_TRUE(result.CreateCookieValue(cookie_info)); + EXPECT_EQ(Value::TYPE_DICTIONARY, result.value()->GetType()); + const DictionaryValue* cookie = + reinterpret_cast<const DictionaryValue*>(result.value()); + + std::string string_value; + bool boolean_value; + EXPECT_TRUE(cookie->GetString(keys::kNameKey, &string_value)); + EXPECT_EQ("foo", string_value); + EXPECT_TRUE(cookie->GetString(keys::kValueKey, &string_value)); + EXPECT_EQ("bar", string_value); + EXPECT_TRUE(cookie->GetString(keys::kDomainKey, &string_value)); + EXPECT_EQ("google.com", string_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kHostOnlyKey, &boolean_value)); + EXPECT_FALSE(boolean_value); + EXPECT_TRUE(cookie->GetString(keys::kPathKey, &string_value)); + EXPECT_EQ("/testpath", string_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kSecureKey, &boolean_value)); + EXPECT_TRUE(boolean_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kHttpOnlyKey, &boolean_value)); + EXPECT_FALSE(boolean_value); + EXPECT_TRUE(cookie->GetBoolean(keys::kSessionKey, &boolean_value)); + EXPECT_TRUE(boolean_value); + EXPECT_TRUE(cookie->GetString(keys::kStoreIdKey, &string_value)); + EXPECT_EQ("test_store_id", string_value); + EXPECT_FALSE(cookie->HasKey(keys::kExpirationDateKey)); +} + +TEST_F(CookieApiTests, GetCookieInfo) { + testing::LogDisabler no_dchecks; + MockCookieApiResult result(CookieApiResult::kNoRequestId); + HWND test_frame_window = reinterpret_cast<HWND>(1); + HWND test_tab_window = reinterpret_cast<HWND>(2); + StrictMock<testing::MockWindowUtils> window_utils; + + // Test no tab windows found. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)) + .WillOnce(Return(false)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Test invalid tab window. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillOnce( + DoAll(SetArgumentPointee<3>(HWND(NULL)), Return(true))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillRepeatedly( + DoAll(SetArgumentPointee<3>(test_tab_window), Return(true))); + + // Test failed executor access. + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(test_tab_window, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Test executor. + testing::MockCookieExecutor* mock_cookie_executor; + CComPtr<ICeeeCookieExecutor> mock_cookie_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockCookieExecutor::CreateInitialized( + &mock_cookie_executor, &mock_cookie_executor_keeper)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(test_tab_window, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_cookie_executor_keeper.p), + AddRef(mock_cookie_executor_keeper.p))); + // Failing Executor. + // The executor classes are already strict from their base class impl. + EXPECT_CALL(*mock_cookie_executor, GetCookie(_, _, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_HRESULT_FAILED(result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); + // Success. + EXPECT_CALL(*mock_cookie_executor, GetCookie(_, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(result, PostError(_)).Times(0); + EXPECT_EQ(S_OK, result.CallGetCookieInfo("helloworld", "foo", + test_frame_window, NULL)); +} + +TEST_F(CookieApiTests, GetWindowFromStoreId) { + MockCookieApiResult result(CookieApiResult::kNoRequestId); + + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, + result.CallGetWindowFromStoreId("test_store_id", false)); + + std::set<HWND> empty_window_set; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(empty_window_set)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + std::set<HWND> test_windows(kAllWindows, kAllWindows + kNumWindows); + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillRepeatedly(SetArgumentPointee<1>(test_windows)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(DoAll(SetArgumentPointee<1>(1), + Return(kThreadId))); + // Test unregistered cookie store. + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillRepeatedly(Return(S_FALSE)); + EXPECT_EQ((HWND)NULL, result.CallGetWindowFromStoreId("1", false)); + + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", true)); + + // Test registered cookie store. + EXPECT_CALL(result, CookieStoreIsRegistered(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", false)); + EXPECT_EQ((HWND)1, result.CallGetWindowFromStoreId("1", true)); +} + +TEST_F(CookieApiTests, CreateCookieStoreValue) { + testing::LogDisabler no_dchecks; + MockCookieApiResult result(CookieApiResult::kNoRequestId); + CookieApiResult::WindowSet windows; + + // Test empty window set. + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test failed cookie store registration. + windows.insert(kAllWindows[0]); + EXPECT_CALL(result, RegisterCookieStore(kAllWindows[0])). + WillOnce(Return(E_FAIL)); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test failed executor access. + EXPECT_CALL(result, RegisterCookieStore(kAllWindows[0])). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test executor. + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper)); + EXPECT_CALL(result.mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + // Failing Executor. + // The executor classes are already strict from their base class impl. + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(result, PostError(_)).Times(1); + EXPECT_FALSE(result.CallCreateCookieStoreValue(2, windows)); + EXPECT_EQ(NULL, result.value()); + + // Test success. + BSTR tab_ids = SysAllocString(L"[27, 1]"); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_ids), Return(S_OK))); + EXPECT_CALL(result, PostResult()).Times(1); + EXPECT_TRUE(result.CallCreateCookieStoreValue(2, windows)); + + ASSERT_TRUE(result.value() != NULL && + result.value()->IsType(Value::TYPE_DICTIONARY)); + const DictionaryValue* cookie_store = + static_cast<const DictionaryValue*>(result.value()); + std::string id; + EXPECT_TRUE(cookie_store->GetString(keys::kIdKey, &id)); + EXPECT_EQ(std::string("2"), id); + ListValue* tab_list; + EXPECT_TRUE(cookie_store->GetList(keys::kTabIdsKey, &tab_list)); + EXPECT_EQ(1, tab_list->GetSize()); + int tab; + EXPECT_TRUE(tab_list->GetInteger(0, &tab)); + EXPECT_EQ(27, tab); + // The cookie_store takes ownership of this pointer, so there's no need to + // free it. + tab_ids = NULL; +} + +TEST_F(CookieApiTests, FindAllProcessesAndWindowsNoWindows) { + std::set<HWND> empty_window_set; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(empty_window_set)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).Times(0); + + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + EXPECT_EQ(0, all_windows.size()); +} + +TEST_F(CookieApiTests, FindAllProcessesAndWindowsMultipleProcesses) { + testing::LogDisabler no_dchecks; + + std::set<HWND> test_windows(kAllWindows, kAllWindows + kNumWindows); + test_windows.insert(kBadWindows[0]); + test_windows.insert(kBadWindows[1]); + StrictMock<testing::MockWindowUtils> window_utils; + + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)). + WillOnce(SetArgumentPointee<1>(test_windows)); + for (int i = 0; i < kNumWindows; ++i) { + EXPECT_CALL(user32_, GetWindowThreadProcessId(kAllWindows[i], _)). + WillOnce(DoAll(SetArgumentPointee<1>(kWindowProcesses[i]), + Return(kThreadId))); + } + // Test that threads and processes with ID 0 don't get added. + EXPECT_CALL(user32_, GetWindowThreadProcessId(kBadWindows[0], _)). + WillOnce(Return(0)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(kBadWindows[1], _)). + WillOnce(DoAll(SetArgumentPointee<1>(0), Return(kThreadId))); + + CookieApiResult::ProcessWindowMap all_windows; + CookieApiResult::FindAllProcessesAndWindows(&all_windows); + + EXPECT_EQ(kNumProcesses, all_windows.size()); + + CookieApiResult::WindowSet& window_set = all_windows[kAllProcesses[0]]; + EXPECT_EQ(2, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[0]) != window_set.end()); + EXPECT_TRUE(window_set.find(kAllWindows[2]) != window_set.end()); + + window_set = all_windows[kAllProcesses[1]]; + EXPECT_EQ(1, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[1]) != window_set.end()); + + window_set = all_windows[kAllProcesses[2]]; + EXPECT_EQ(1, window_set.size()); + EXPECT_TRUE(window_set.find(kAllWindows[3]) != window_set.end()); +} + +TEST_F(CookieApiTests, GetCookiesInvalidArgumentsResultInErrors) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetCookie> > invocation; + // First test that required arguments are enforced. + ExpectInvalidArguments(invocation, args); + DictionaryValue* details = new DictionaryValue(); + args.Append(details); + ExpectInvalidArguments(invocation, args); + // TODO(cindylau@chromium.org): Check for invalid URL error. + details->SetString(keys::kUrlKey, "helloworld"); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq(ExtensionErrorUtils::FormatErrorMessage( + keys::kInvalidUrlError, "helloworld")))). + Times(1); + invocation.Execute(args, kRequestId); + details->SetString(keys::kUrlKey, "http://www.foobar.com"); + ExpectInvalidArguments(invocation, args); + details->SetString(keys::kNameKey, "foo"); + details->SetInteger(keys::kStoreIdKey, 1); + ExpectInvalidArguments(invocation, args); + + // GetWindowFromStoreId fails. + std::string store_id("test_cookie_store"); + details->SetString(keys::kStoreIdKey, store_id); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetWindowFromStoreId(StrEq(store_id), false)). + WillOnce(Return((HWND)NULL)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(0); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, GetCookiesTopIeWindowIsDefaultCookieStore) { + // TODO(cindylau@chromium.org): The expected behavior here will + // change; we should use the current execution context's cookie + // store, not the top window. + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetCookie> > invocation; + + ListValue args; + DictionaryValue* details = new DictionaryValue(); + args.Append(details); + details->SetString(keys::kUrlKey, "http://www.foobar.com"); + details->SetString(keys::kNameKey, "foo"); + + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics window_statics; + EXPECT_CALL(window_statics, TopIeWindow()).WillOnce(Return(HWND(42))); + EXPECT_CALL(*invocation.invocation_result_, GetCookieInfo(_, _, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, GetAllCookieStores) { + testing::LogDisabler no_dchecks; + + MockCookieApiResultStatics result_statics; + CookieApiResult::ProcessWindowMap all_windows; + ListValue args; + + StrictMock<MockApiInvocation<IterativeCookieApiResult, + MockIterativeCookieApiResult, + GetAllCookieStores> > invocation; + + // First test the trivial case of no cookie stores. One call to success and + // no calls to error function. + EXPECT_CALL(result_statics, FindAllProcessesAndWindows(_)). + WillOnce(SetArgumentPointee<0>(all_windows)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + + // Now test cases with multiple windows. + for (int i = 0; i < kNumWindows; ++i) { + all_windows[kWindowProcesses[i]].insert(kAllWindows[i]); + } + EXPECT_CALL(result_statics, FindAllProcessesAndWindows(_)). + WillRepeatedly(SetArgumentPointee<0>(all_windows)); + + // Test error case: can't access a single cookie store. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + CreateCookieStoreValue(_, _)). + WillRepeatedly(Return(false)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(0); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(args, kRequestId); + + // Test error case: accessing dispatcher fails each time and so an error is + // reported. Count errors and make sure everything is reported as error. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(0); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), 0); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), all_windows.size()); + + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper)); + + // Now test the case of multiple open windows/processes. Each invocation is + // successful and a result is produced. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillRepeatedly(Invoke(MockIterativeCookieApiResult::MockGetTabs)); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), + all_windows.size()); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), 0); + + // Now test the case of multiple open windows/processes. One invocation + // fails, but everything else will be OK. + MockIterativeCookieApiResult::ResetCounters(); + invocation.AllocateNewResult(kRequestId); + + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper.p), + AddRef(mock_window_executor_keeper.p))); + EXPECT_CALL(invocation.invocation_result_->mock_api_dispatcher_, + GetExecutor(kAllWindows[0], _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillRepeatedly(Invoke(MockIterativeCookieApiResult::MockGetTabs)); + EXPECT_CALL(*invocation.invocation_result_, CreateCookieStoreValue(_, _)). + WillRepeatedly(Invoke(invocation.invocation_result_.get(), + &MockIterativeCookieApiResult::CallCreateCookieStoreValue)); + EXPECT_CALL(*invocation.invocation_result_, + RegisterCookieStore(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(0); + invocation.Execute(args, kRequestId); + EXPECT_EQ(MockIterativeCookieApiResult::success_counter(), + all_windows.size() - 1); + EXPECT_EQ(MockIterativeCookieApiResult::error_counter(), 1); +} + +TEST_F(CookieApiTests, GetAllCookiesNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + GetAllCookies> > invocation; + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, SetCookieNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + SetCookie> > invocation; + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, RemoveCookieNotImplemented) { + testing::LogDisabler no_dchecks; + ListValue args; + StrictMock<MockApiInvocation<CookieApiResult, + MockCookieApiResult, + RemoveCookie> > invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + PostError(StrEq("Not implemented."))).Times(1); + invocation.Execute(args, kRequestId); +} + +TEST_F(CookieApiTests, CookieChangedEventHandler) { + testing::LogDisabler no_dchecks; + MockCookieChanged cookie_changed; + std::string converted_args; + // Empty args. + std::string input_args = ""; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + // Invalid args. + input_args = "[false, {hello]"; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + input_args = "[3]"; + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + // Valid args. + input_args = "[{\"removed\": false, \"cookie\": {\"storeId\": \"1\"}}]"; + + // Invalid store ID. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(NULL))); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Cookie store access errors. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(E_FAIL)); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_FALSE)); + EXPECT_CALL(*cookie_changed.api_result_, + RegisterCookieStore(HWND(5))).WillOnce(Return(E_FAIL)); + EXPECT_EQ(false, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Registered cookie store. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_OK)); + EXPECT_EQ(true, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); + + // Unregistered cookie store. + cookie_changed.AllocateApiResult(); + EXPECT_CALL(*cookie_changed.api_result_, + GetWindowFromStoreId(StrEq("1"), true)) + .WillOnce(Return(HWND(5))); + EXPECT_CALL(*cookie_changed.api_result_, + CookieStoreIsRegistered(HWND(5))).WillOnce(Return(S_FALSE)); + EXPECT_CALL(*cookie_changed.api_result_, + RegisterCookieStore(HWND(5))).WillOnce(Return(S_OK)); + EXPECT_EQ(true, + cookie_changed.EventHandlerImpl(input_args, &converted_args)); +} + +} // namespace diff --git a/ceee/ie/broker/event_dispatching_docs.h b/ceee/ie/broker/event_dispatching_docs.h new file mode 100644 index 0000000..254821b --- /dev/null +++ b/ceee/ie/broker/event_dispatching_docs.h @@ -0,0 +1,262 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ + +/** @page EventDispatchingDoc Detailed documentation of CEEE Event Dispatching. + +@section EventDispatchingIntro Introduction + +<a href="http://code.google.com/chrome/extensions/index.html">Chrome extensions +</a> can register to be notified of specific events like +<a href="http://code.google.com/chrome/extensions/windows.html#event-onCreated"> +window creation</a> or +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onUpdated"> +tab update</a>. + +So CEEE needs to let Chrome know when these events occure in IE. To do so, we +unfortunately need to be hooked in a few different places. So in the same way +that the Chrome Extensions API implementation was described in @ref +ApiDispatcherDoc, this page describes all the notifications one by one and how +we implement their broadcasting. + +Note that we use the EventFunnel class to properly package the arguments to be +sent with the events, and then send them to the Broker via the +ICeeeBroker::FireEvent method. + +@section WindowsEvents Windows Events + +There are three +<a href="http://code.google.com/chrome/extensions/windows.html#events">Windows +events</a> that can be sent to Chrome extensions, +<a href="http://code.google.com/chrome/extensions/windows.html#event-onCreated"> +onCreated</a>, <a href= +"http://code.google.com/chrome/extensions/windows.html#event-onFocusChanged"> +onFocusChanged</a>, and +<a href="http://code.google.com/chrome/extensions/windows.html#event-onRemoved"> +onRemoved</a>. + +These notifications are for top level windows only, so we can get notifications +from the OS by using the WH_SHELL hook (as described on +<a href="http://msdn.microsoft.com/en-us/library/ms644991(VS.85).aspx"> +MSDN</a>). So we need to implement a hook function in the ceee_ie.dll so that +it can be injected in the system process and be called for the following +events: +<table> +<tr><td>HSHELL_WINDOWACTIVATED</td><td>Handle to the activated window.</td></tr> +<tr><td>HSHELL_WINDOWCREATED</td><td>Handle to the created window.</td></tr> +<tr><td>HSHELL_WINDOWDESTROYED</td><td>Handle to the destroyed window.</td></tr> +</table> + +Then we must redirect those via Chrome Frame (more details in the @ref +ChromeFrameDispatching section). In the case of Windows Events, we must relay +the notifications to the Broker process via the ICeeeBrokerNotification +(TBD) interface since they get handled by an injected ceee_ie.dll. + +@subsection onCreated + +The onCreated event needs to send (as arguments to the notification message) +the same information about the Window as the one returned by the windows.create +API (and as described in the @ref ApiDispatcherDoc and on the <a href= +"http://code.google.com/chrome/extensions/windows.html#type-Window"> Chrome +Extensions documentation</a>). + +@subsection onFocusChanged + +All that is needed here is to send the window identifier of the newly focused +window to the listener. + +@subsection onRemoved + +All that is needed here is to send the window identifier of the removed window +to the listener. + +@section TabsEvents Tabs Events + +There are seven +<a href="http://code.google.com/chrome/extensions/tabs.html#events">Tabs +events</a> that can be sent to Chrome extensions, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onAttached"> +onAttached</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onCreated"> +onCreated</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onDetached"> +onDetached</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onMoved"> +onMoved</a>, +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onRemoved"> +onRemoved</a>, <a href= +"http://code.google.com/chrome/extensions/tabs.html#event-onSelectionChanged" +>onSelectionChanged</a>, and +<a href="http://code.google.com/chrome/extensions/tabs.html#event-onUpdated"> +onUpdated</a>. + +Since IE can't move tabs between windows (yet), CEEE won't fire the +attached/detached notifications. The tab created event can be fired when an +instance of the CEEE BHO is created and associated to a tab. The move, remove +and selected tab events will most likely need to be caught from IE's +TabWindowManager somehow (TBD). And finally, the update tab event can be +fired when the instance of the BHO attached to the given tab receives a +notification that the tab content was updated (via a series of notification from +the <a href="http://msdn.microsoft.com/en-us/library/aa752085(VS.85).aspx"> +WebBrowser</a> or the <a href= +"http://msdn.microsoft.com/en-us/library/aa752574(VS.85).aspx">document</a>). + +@note As a side note, here's an email from Siggi about tricks for this: + +@note <em>2009/10/16 Sigurour Asgeirsson <siggi@google.com></em> + +@note I think we should be able to derive all the window events from tab +activity, if we e.g. have the BHO register each tab with its HWND/ID, then we +can derive the association to parent window from there, which means we have +created/destroyed on first registration and last unregistration for a frame +window. + +@note I'm pretty sure we can derive the focus event from events fired on the +top-level browser associated with a BHO as well. + +@subsection onAttached + +Not implemented on IE. + +@subsection onDetached + +Not implemented on IE. + +@subsection onMoved + +TBD: We need to find a way to get the tab move notification from the +TabWindowManager... somehow... (e.g., patch the move method, but we would +rather find a notification coming from there instead). + +We could also see if we can get win32 notifications of focus or window +visibility or things of the like. + +Once we do, the arguments that must be sent with this notification are the +identifier of the tab, and a dictionary of moveInfo that contains the +"windowId", the "fromIndex" and the "toIndex" fields, which are all integers. + +@subsection onRemoved + +TBD: We need to find a way to get the tab removed notification from the +TabWindowManager... somehow... + +We could also use the destruction of the BHO as a hint that the tab is being +removed. + +Once we do, the argument that must be sent with this notification is just the +identifier of the tab. + +@subsection onSelectionChanged + +TBD: We need to find a way to get the tab selection changed notification from +the TabWindowManager... somehow... + +We could also see if we can get win32 notifications of focus or window +visibility or things of the like. + +Once we do, the arguments that must be sent with this notification are the +identifier of the tab that just got selected, and a dictionary of selectInfo +that only contains the "windowId" field. + +@subsection onCreated + +When a new BHO instance is created and attached to a tab, it can send this +notification directly to the Chrome Frame it is attached to. We will most likely +change this direct call to Chrome Frame once we implement the asynchronous +callback to Chrome Extension for the tabs.create API implementation for IE. At +that point, we should use the same ICeeeBrokerNotification (TBD) interface +as we plan on using for the @link WindowsEvents Windows Events @endlink. + +The onCreated event needs to send to the extensions registered as listeners, +the same information about the Tab as the one returned by the tabs.create +API (and as described in the @ref ApiDispatcherDoc and on the <a href= +"http://code.google.com/chrome/extensions/tabs.html#type-Tab"> Chrome +Extensions documentation</a>). + +@subsection onUpdated + +When a new BHO instance receives a <a href= +"http://msdn.microsoft.com/en-us/library/aa768334(VS.85).aspx">NavigateComplete2 +</a> notification from the WebBrowser, it must fire the onUpdated notification +for the tab it is attached to. It must also fire it for the ready state change +notifications it would get from the document. There's an edge case for when +IE refreshes the page, which can be caught when the frame ready state drops from +complete to loading, so we must also handle this case to send an onUpdated +notification for the tab. + +It gets trickier when the page has embedded frames. These independent frames +will all have their own ready state, and changing the ready state (by +refreshing) a frame in the middle of the hierarchy, will change its own ready +state from complete, to loading, and then to interactive, and then the child +frames will go from uninitialized, to loading and then to interactive. The +innermost child will also go all the way to complete, and then its immediate +parent will get from interactive to complete, and then the next ancestor, all +the way to where the refresh started, but not further (so the outermost frame's +ready state will not change unless it is the one that triggered the refresh). + +From the Chrome Extension's point of view, there should be only two events sent, +the initial one that goes from complete to loading, and the final one to go +from loading to complete. Since this doesn't necessary happen on the topmost +frame, we must listen to all the frames ready state changes, but we must be +able to compute the state of the whole page, so that we only send one +notification that would go from complete to loading at the beginning of the +refresh, and then only one that will go from loading to complete, once the +page is completely refreshed. + +To do so, we need to propagate the state changes upward in the hierarchy and +parse the whole tree to confirm the page state, which is loading unless all +the frames are complete, in which case it will be complete. To minimize the +number of times we need to parse the hierarchy, only the top frame needs to +do the parsing. Also, frames don't need to propagate up their children's state +unless they are, themselves (the parent) in the complete state. If they are not, +then their own parent (or themselves if its the root) know that the overall +state isn't complete. + +When we fire the onUpdated notification we must provide the identifier of the +tab and dictionary that contains the status field, which can be either "loading" +or "complete", as well as an optional URL field that the tab is set to show +(it is only provided if it changed). + +@section BookmarksEvents Bookmarks Events + +TBD + +http://code.google.com/chrome/extensions/bookmarks.html#events + +@section ExtensionsEvents Extensions Events + +This is already available... more details TBD... + +http://code.google.com/chrome/extensions/extension.html#events + +@section ChromeFrameDispatching Dispatching to Chrome via the Chrome Frame + +To simplify our access to (and reception of notifications from) Chrome Frame, +we have a ChromeFrameHost class that implements all we need. It takes care of +notification registration, and have virtual methods for all of them, and it +also has a PostMessage method to allow calls to be dispatched to Chrome via +the automation layer and Chrome Frame. + +Events are such notifications that need to be sent, so the places where we +intercept IE notifications can use a ChromeFrameHost object, package the +event information in a JSON encoded string and pass it to the +ChromeFrameHost::PostMessage method. The JSON data must be a list with two +entries, the first one is the event name (e.g., "windows.onRemoved" or +"tabs.onUpdated") and the other one is the arguments for this event (e.g., +a single int representing the id of the window to remove or for the +tabs.onUpdated event, a list of two values, the first one being an int for the +tab identifier, and the other one is a dictionary with a mandatory field for the +"status", which value can be "loading" or "complete", and an optional one for +the "url" which is a string reprensenting the URL to which the tab is being +navigated to). + +The target of the message must be kAutomationBrowserEventRequestTarget (which is +"__priv_evtreq") and the origin, must, of course, be kAutomationOrigin (which is +"__priv_xtapi"). + +**/ + +#endif // CEEE_IE_BROKER_EVENT_DISPATCHING_DOCS_H_ diff --git a/ceee/ie/broker/executors_manager.cc b/ceee/ie/broker/executors_manager.cc new file mode 100644 index 0000000..5d87467 --- /dev/null +++ b/ceee/ie/broker/executors_manager.cc @@ -0,0 +1,453 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ExecutorsManager implementation. + +#include "ceee/ie/broker/executors_manager.h" + +#include "base/logging.h" +#include "ceee/ie/broker/broker_module_util.h" +#include "ceee/common/com_utils.h" + +namespace { + +// The timeout we set before accepting a failure when we wait for events. +const DWORD kTimeOut = 20000; + +// Utility class to ensure the setting of an event before we exit a method. +class AutoSetEvent { + public: + explicit AutoSetEvent(HANDLE event_handle) : event_handle_(event_handle) {} + ~AutoSetEvent() { ::SetEvent(event_handle_); } + private: + HANDLE event_handle_; +}; + +// A cached handle to our current process which we use when we call +// DuplicateHandle. GetCurrentProcess returns a pseudo HANDLE that doesn't +// need to be closed. +const HANDLE kProcessHandle = ::GetCurrentProcess(); + +} // namespace + + +const size_t ExecutorsManager::kTerminationHandleIndexOffset = 0; +const size_t ExecutorsManager::kUpdateHandleIndexOffset = 1; +const size_t ExecutorsManager::kLastHandleIndexOffset = + kUpdateHandleIndexOffset; +const size_t ExecutorsManager::kExtraHandles = kLastHandleIndexOffset + 1; + +ExecutorsManager::ExecutorsManager(bool no_thread) + : update_threads_list_gate_(::CreateEvent(NULL, FALSE, FALSE, NULL)), + // Termination is manual reset. When we're terminated... We're terminated! + termination_gate_(::CreateEvent(NULL, TRUE, FALSE, NULL)) { + DCHECK(update_threads_list_gate_ != NULL); + DCHECK(termination_gate_ != NULL); + + if (!no_thread) { + ThreadStartData thread_start_data; + thread_start_data.me = this; + // Again, manual reset, because when we are started... WE ARE STARTED!!! :-) + thread_start_data.thread_started_gate.Attach(::CreateEvent(NULL, TRUE, + FALSE, NULL)); + DCHECK(thread_start_data.thread_started_gate != NULL); + + // Since we provide the this pointer to the thread, we must make sure to + // keep ALL INITIALIZATION CODE ABOVE THIS LINE!!! + thread_.Attach(::CreateThread(NULL, 0, ThreadProc, &thread_start_data, + 0, 0)); + DCHECK(thread_ != NULL); + + // Make sure the thread is ready before continuing + DWORD result = WaitForSingleObject(thread_start_data.thread_started_gate, + kTimeOut); + DCHECK(result == WAIT_OBJECT_0); + } +} + +HRESULT ExecutorsManager::RegisterTabExecutor(ThreadId thread_id, + IUnknown* executor) { + // We will need to know outside of the lock if the map was empty or not. + // This way we can add a ref to the module for the existence of the map. + bool map_was_empty = false; + { + AutoLock lock(lock_); + map_was_empty = executors_.empty(); + if (!map_was_empty && executors_.find(thread_id) != executors_.end()) { + return S_OK; + } + CHandle thread_handle(::OpenThread(SYNCHRONIZE, FALSE, thread_id)); + if (thread_handle == NULL) { + DCHECK(false) << "Can't Open thread: " << thread_id; + return E_UNEXPECTED; + } + ExecutorInfo& new_executor_info = executors_[thread_id]; + new_executor_info.executor = executor; + new_executor_info.thread_handle = thread_handle; + } // End of lock. + + if (map_was_empty) { + // We go from empty to not empty, + // so lock the module to make sure we stay alive. + ceee_module_util::LockModule(); + } + return S_OK; +} + +HRESULT ExecutorsManager::RegisterWindowExecutor(ThreadId thread_id, + IUnknown* executor) { + // We need to fetch the event handle associated to this thread ID from + // our map in a thread safe way... + CHandle executor_registration_gate; + { + AutoLock lock(lock_); + if (executors_.find(thread_id) != executors_.end()) { + DCHECK(false) << "Unexpected registered thread_id: " << thread_id; + return E_UNEXPECTED; + } + + Tid2Event::iterator iter = pending_registrations_.find(thread_id); + if (iter == pending_registrations_.end() || executor == NULL) { + DCHECK(false) << "Invalid thread_id: " << thread_id << + ", or NULL executor."; + return E_INVALIDARG; + } + // Make sure we use a duplicate handle so that we don't get caught setting + // a dead handle when we exit, in case the other thread wakes up because + // of a (unlikely) double registration. + BOOL success = ::DuplicateHandle( + kProcessHandle, iter->second.m_h, kProcessHandle, + &executor_registration_gate.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } // End of lock. + + // We must make sure to wake up the thread(s) that might be waiting on us. + // But only when we are done. + AutoSetEvent auto_set_event(executor_registration_gate); + + // Try to get a handle to this thread right away so that we can do the rest + // atomically. We need it to wake us up when it dies. + CHandle thread_handle(::OpenThread(SYNCHRONIZE, FALSE, thread_id)); + if (thread_handle == NULL) { + DCHECK(false) << "Can't Open thread: " << thread_id; + return S_FALSE; + } + + // We will need to know outside of the lock if the map was empty or not. + // This way we can add a ref to the module for the existence of the map. + bool map_was_empty = false; + { + AutoLock lock(lock_); + map_was_empty = executors_.empty(); + // We should not get here if we already have an executor for that thread. + DCHECK(executors_.find(thread_id) == executors_.end()); + ExecutorInfo& new_executor_info = executors_[thread_id]; + new_executor_info.executor = executor; + new_executor_info.thread_handle = thread_handle; + } // End of lock. + + if (map_was_empty) { + // We go from empty to not empty, + // so lock the module to make sure we stay alive. + ceee_module_util::LockModule(); + } + + // Update the list of handles that our thread is waiting on. + BOOL success = ::SetEvent(update_threads_list_gate_); + DCHECK(success); + return S_OK; +} + +HRESULT ExecutorsManager::GetExecutor(ThreadId thread_id, HWND window, + REFIID riid, void** executor) { + DCHECK(executor != NULL); + // We may need to wait for either a currently pending + // or own newly created registration of a new executor. + CHandle executor_registration_gate; + + // We need to remember if we must create a new one or not. + // But we must create the executor creator outside of the lock. + bool create_executor = false; + { + AutoLock lock(lock_); + ExecutorsMap::iterator exec_iter = executors_.find(thread_id); + if (exec_iter != executors_.end()) { + // Found it... We're done... That was quick!!! :-) + DCHECK(exec_iter->second.executor != NULL); + return exec_iter->second.executor->QueryInterface(riid, executor); + } + + // Check if we need to wait for a pending registration. + Tid2Event::iterator event_iter = pending_registrations_.find(thread_id); + if (event_iter == pending_registrations_.end()) { + // No pending registration, so we will need to create a new executor. + create_executor = true; + + // Use the thread id as a cookie to only allow known threads to register. + // Also use it to map to a new event we will use to signal the end of this + // registration. We use a manual reset event so that more than one thread + // can wait for it, and once we're done... we're done... period! :-) + executor_registration_gate.Attach(::CreateEvent(NULL, TRUE, FALSE, NULL)); + DCHECK(executor_registration_gate != NULL); + CHandle& new_registration_handle = pending_registrations_[thread_id]; + // Make sure we use a duplicate handle so that we don't get caught waiting + // on a dead handle later, in case other threads wake up before we do and + // close the handle before we wake up. + BOOL success = ::DuplicateHandle( + kProcessHandle, executor_registration_gate, kProcessHandle, + &new_registration_handle.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } else { + // Same comment as above... + BOOL success = ::DuplicateHandle( + kProcessHandle, event_iter->second.m_h, kProcessHandle, + &executor_registration_gate.m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + } + } // End of lock. + + CComPtr<ICeeeExecutorCreator> executor_creator; + if (create_executor) { + // We need to create an executor creator so that the code setting up + // a Windows Hook in the other process, runs from a DLL that can be + // injected in that other process... WE are running in an executable. + HRESULT hr = GetExecutorCreator(&executor_creator); + DCHECK(SUCCEEDED(hr) && executor_creator != NULL) << + "CoCreating Executor Creator. " << com::LogHr(hr); + hr = executor_creator->CreateWindowExecutor(thread_id, + reinterpret_cast<CeeeWindowHandle>(window)); + if (FAILED(hr)) { + // This could happen if the thread we want to hook to died prematurely. + AutoLock lock(lock_); + pending_registrations_.erase(thread_id); + return hr; + } + } + + // Wait for the registration to complete. + DWORD result = WaitForSingleObject(executor_registration_gate, kTimeOut); + LOG_IF(INFO, result != WAIT_OBJECT_0) << "Registration problem? " << + "Wait Result: " << com::LogWe(result); + + // Let the executor creator know that we got the registration + // and it can tear down what was needed to trigger it. + if (executor_creator != NULL) { + HRESULT hr = executor_creator->Teardown(thread_id); + DCHECK(SUCCEEDED(hr)) << "Tearing down executor creator" << com::LogHr(hr); + } + + // Do our own cleanup and return a reference thread safely... + AutoLock lock(lock_); + pending_registrations_.erase(thread_id); + ExecutorsMap::iterator iter = executors_.find(thread_id); + if (iter == executors_.end()) { + DCHECK(false) << "New executor registration failed."; + return E_UNEXPECTED; + } + + DCHECK(iter->second.executor != NULL); + return iter->second.executor->QueryInterface(riid, executor); +} + +HRESULT ExecutorsManager::RemoveExecutor(ThreadId thread_id) { + // Make sure to Release the executor outside the lock. + CComPtr<IUnknown> dead_executor; + bool map_is_empty = false; + { + AutoLock lock(lock_); + ExecutorsMap::iterator iter = executors_.find(thread_id); + if (iter == executors_.end()) { + return S_FALSE; + } + + dead_executor.Attach(iter->second.executor.Detach()); + executors_.erase(iter); + map_is_empty = executors_.empty(); + } // End of lock. + + if (map_is_empty) { + // We go from not empty to empty, + // so unlock the module it can leave in peace. + ceee_module_util::UnlockModule(); + } + return S_OK; +} + +HRESULT ExecutorsManager::Terminate() { + if (thread_ != NULL) { + // Ask our thread to quit and wait for it to be done. + DWORD result = ::SignalObjectAndWait(termination_gate_, thread_, kTimeOut, + FALSE); + DCHECK(result == WAIT_OBJECT_0); + thread_.Close(); + } + if (!executors_.empty()) { + // TODO(mad@chromium.org): Can this happen??? + NOTREACHED(); + ceee_module_util::UnlockModule(); + } + + executors_.clear(); + update_threads_list_gate_.Close(); + termination_gate_.Close(); + + return S_OK; +} + +void ExecutorsManager::SetTabIdForHandle(long tab_id, HWND handle) { + AutoLock lock(lock_); + DCHECK(tab_id_map_.end() == tab_id_map_.find(tab_id)); + DCHECK(handle_map_.end() == handle_map_.find(handle)); + if (handle == reinterpret_cast<HWND>(INVALID_HANDLE_VALUE) || + tab_id == kInvalidChromeSessionId) { + NOTREACHED(); + return; + } + + tab_id_map_[tab_id] = handle; + handle_map_[handle] = tab_id; +} + +void ExecutorsManager::DeleteTabHandle(HWND handle) { + AutoLock lock(lock_); + HandleMap::iterator handle_it = handle_map_.find(handle); + if(handle_map_.end() != handle_it) { + DCHECK(false); + return; + } + + TabIdMap::iterator tab_id_it = tab_id_map_.find(handle_it->second); + if(tab_id_map_.end() != tab_id_it) { + DCHECK(false); + return; + } + +#ifdef DEBUG + tab_id_map_[handle_it->second] = reinterpret_cast<HWND>(INVALID_HANDLE_VALUE); + handle_map_[handle] = kInvalidChromeSessionId; +#else + tab_id_map_.erase(handle_it->second); + handle_map_.erase(handle); +#endif // DEBUG +} + +HWND ExecutorsManager::GetTabHandleFromId(int tab_id) { + AutoLock lock(lock_); + TabIdMap::const_iterator it = tab_id_map_.find(tab_id); + DCHECK(it != tab_id_map_.end()); + + if (it == tab_id_map_.end()) + return reinterpret_cast<HWND>(INVALID_HANDLE_VALUE); + + // Deleted? I hope not. + DCHECK(it->second != reinterpret_cast<HWND>(INVALID_HANDLE_VALUE)); + return it->second; +} + +int ExecutorsManager::GetTabIdFromHandle(HWND tab_handle) { + AutoLock lock(lock_); + HandleMap::const_iterator it = handle_map_.find(tab_handle); + DCHECK(it != handle_map_.end()); + if (it == handle_map_.end()) + return kInvalidChromeSessionId; + DCHECK(it->second != kInvalidChromeSessionId); // Deleted? I hope not. + return it->second; +} + +HRESULT ExecutorsManager::GetExecutorCreator( + ICeeeExecutorCreator** executor_creator) { + return ::CoCreateInstance(CLSID_CeeeExecutorCreator, NULL, + CLSCTX_INPROC_SERVER, IID_ICeeeExecutorCreator, + reinterpret_cast<void**>(executor_creator)); +} + +size_t ExecutorsManager::GetThreadHandles( + CHandle thread_handles[], ThreadId thread_ids[], size_t num_threads) { + AutoLock lock(lock_); + ExecutorsMap::iterator iter = executors_.begin(); + size_t index = 0; + for (; index < num_threads && iter != executors_.end(); ++index, ++iter) { + DCHECK(thread_handles[index].m_h == NULL); + // We need to duplicate the handle to make sure the caller will not wait + // on a closed handle. + BOOL success = ::DuplicateHandle( + kProcessHandle, iter->second.thread_handle, kProcessHandle, + &thread_handles[index].m_h, 0, FALSE, DUPLICATE_SAME_ACCESS); + DCHECK(success) << com::LogWe(); + thread_ids[index] = iter->first; + } + + return index; +} + +DWORD ExecutorsManager::WaitForSingleObject(HANDLE wait_handle, DWORD timeout) { + return ::WaitForSingleObject(wait_handle, timeout); +} + +DWORD ExecutorsManager::WaitForMultipleObjects(DWORD num_handles, + const HANDLE* wait_handles, BOOL wait_all, DWORD timeout) { + return ::WaitForMultipleObjects(num_handles, wait_handles, wait_all, timeout); +} + + +DWORD ExecutorsManager::ThreadProc(LPVOID parameter) { + // We must make sure to join the multi thread apartment so that the executors + // get released properly in the same apartment they were acquired from. + ::CoInitializeEx(NULL, COINIT_MULTITHREADED); + + ThreadStartData* thread_start_data = + reinterpret_cast<ThreadStartData*>(parameter); + DCHECK(thread_start_data != NULL); + ExecutorsManager* me = thread_start_data->me; + DCHECK(me != NULL); + + // Let our parent know that we are old enough now! + ::SetEvent(thread_start_data->thread_started_gate); + // Setting the event will destroy the thread start data living on the stack + // so make sure we don't use it anymore. + thread_start_data = NULL; + + while (true) { + CHandle smart_handles[MAXIMUM_WAIT_OBJECTS]; + HANDLE handles[MAXIMUM_WAIT_OBJECTS]; + ThreadId thread_ids[MAXIMUM_WAIT_OBJECTS]; + // Get as many handles as we can, leaving room for kExtraHandles. + size_t num_threads = me->GetThreadHandles( + smart_handles, thread_ids, MAXIMUM_WAIT_OBJECTS - kExtraHandles); + // Wait function needs an array of raw handles, not smart ones. + for (size_t index = 0; index < num_threads; ++index) + handles[index] = smart_handles[index]; + + // We also need to wait for our termination signal. + handles[num_threads + kTerminationHandleIndexOffset] = + me->termination_gate_; + // As well as a signal warning us to go fetch more thread handles. + handles[num_threads + kUpdateHandleIndexOffset] = + me->update_threads_list_gate_; + + size_t num_handles = num_threads + kExtraHandles; + DWORD result = me->WaitForMultipleObjects(num_handles, handles, FALSE, + INFINITE); + if (result == WAIT_OBJECT_0 + num_threads + + kUpdateHandleIndexOffset) { + // We got a new thread added, + // simply let the loop turn to add it to our watch list. + } else if (result >= WAIT_OBJECT_0 && + result < WAIT_OBJECT_0 + num_threads) { + // One of our threads have died, cleanup time. + me->RemoveExecutor(thread_ids[result - WAIT_OBJECT_0]); + } else if (result == WAIT_OBJECT_0 + num_threads + + kTerminationHandleIndexOffset) { + // we are being terminated, break the cycle. + break; + } else { + DCHECK(result == WAIT_FAILED); + LOG(ERROR) << "ExecutorsManager::ThreadProc " << com::LogWe(); + break; + } + } + // Merci... Bonsoir... + ::CoUninitialize(); + return 1; +} diff --git a/ceee/ie/broker/executors_manager.h b/ceee/ie/broker/executors_manager.h new file mode 100644 index 0000000..3b9ac3b --- /dev/null +++ b/ceee/ie/broker/executors_manager.h @@ -0,0 +1,210 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// ExecutorsManager implementation, an object to keep track of the +// CeeeExecutor objects that were instantiated in destination threads. + +#ifndef CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ +#define CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <map> + +#include "base/lock.h" +#include "base/singleton.h" + +#include "toolband.h" // NOLINT + +// This class is to be used as a single instance for the broker module to +// hold on a map of executor objects per thread that won't go away when +// the instance of the Broker object does. +// +// See the @ref ExecutorsManagerDoc page for more details. + +// Manages a map of destination threads to CeeeExecutor interfaces. +class ExecutorsManager { + public: + // Identifiers for destination threads where to run executors. + typedef DWORD ThreadId; + + // To avoid lint errors, even though we are only virtual for unittests. + virtual ~ExecutorsManager() {} + + // Adds a new executor to the map associated to the given thread_id. + // + // @param thread_id The thread for which we want to register a new executor. + // @param executor The executor we want to register for the given thread_id. + // @return S_OK iff we didn't already have an executor, and we had a pending + // request to add one for that exact same thread. + virtual HRESULT RegisterWindowExecutor(ThreadId thread_id, + IUnknown* executor); + // TODO(mad@chromium.org): Implement the proper manual/secure registration. + // + // @param thread_id The thread for which we want to register a new executor. + // @param executor The executor we want to register for the given thread_id. + // @return S_OK iff we didn't already have an executor, and we had a pending + // request to add one for that exact same thread. + virtual HRESULT RegisterTabExecutor(ThreadId thread_id, IUnknown* executor); + + // Gets the executor associated to the given thread_id. Gets if from the map + // if there was already one in there or create a new one otherwise. + // + // @param thread_id The thread for which we want the executor. + // @param window The window handle for which we want the executor. + // @param riid Which interface is to be returned in @p executor. + // @param executor Where to return the pointer to that executor. + // @return S_OK iff we found an existing or successfully created an executor. + virtual HRESULT GetExecutor(ThreadId thread_id, HWND window, REFIID riid, + void** executor); + + // Removes an executor from our map. + // + // @param thread_id The thread for which we want to remove the executor. + // @return S_OK if we removed the executor or S_FALSE if it wasn't there. + virtual HRESULT RemoveExecutor(ThreadId thread_id); + + // Terminates the usage of the map by freeing our resources. + virtual HRESULT Terminate(); + + // Return a tab handle associated with the id. + // + // @param tab_id The tab identifier. + // @return The corresponding HWND (or INVALID_HANDLE_VALUE if tab_id isn't + // found). + virtual HWND GetTabHandleFromId(int tab_id); + + // Return a tab id associated with the HWND. + // + // @param tab_handle The tab HWND. + // @return The corresponding tab id (or 0 if tab_handle isn't found). + virtual int GetTabIdFromHandle(HWND tab_handle); + + // Register the relation between a tab_id and a HWND. + virtual void SetTabIdForHandle(long tab_id, HWND tab_handle); + + // Unregister the HWND and its corresponding tab_id. + virtual void DeleteTabHandle(HWND handle); + + // Traits for Singleton<ExecutorsManager> so that we can pass an argument + // to the constructor. + struct SingletonTraits { + static ExecutorsManager* New() { + return new ExecutorsManager(false); // By default, we want a thread. + } + static void Delete(ExecutorsManager* x) { + delete x; + } + static const bool kRegisterAtExit = true; + }; + + protected: + // The data we pass to start our worker thread. + // THERE IS A COPY OF THIS CLASS IN THE UNITTEST WHICH YOU NEED TO UPDATE IF + // you change this one... + struct ThreadStartData { + ExecutorsManager* me; + CHandle thread_started_gate; + }; + + // A structures holding on the info about an executor and thread it runs in. + struct ExecutorInfo { + ExecutorInfo(IUnknown* new_executor = NULL, HANDLE handle = NULL) + : executor(new_executor), thread_handle(handle) { + } + ExecutorInfo(const ExecutorInfo& executor_info) + : executor(executor_info.executor), + thread_handle(executor_info.thread_handle) { + } + CComPtr<IUnknown> executor; + // mutable so that we can assign/Detach a const copy. + mutable CHandle thread_handle; + }; + + typedef std::map<ThreadId, ExecutorInfo> ExecutorsMap; + typedef std::map<ThreadId, CHandle> Tid2Event; + typedef std::map<int, HWND> TabIdMap; + typedef std::map<HWND, int> HandleMap; + + // The index of the termination event in the array of handles we wait for. + static const size_t kTerminationHandleIndexOffset; + + // The index of the update event in the array of handles we wait for. + static const size_t kUpdateHandleIndexOffset; + + // The index of the last event in the array of handles we wait for. + static const size_t kLastHandleIndexOffset; + + // The number of extra handles we used for the events described above. + static const size_t kExtraHandles; + + // Protected constructor to ensure single instance and initialize some + // members. Set no_thread for testing... + explicit ExecutorsManager(bool no_thread); + + // Creates an executor creator in a virtual method so we can override it in + // Our unit test code. + // + // @param executor_creator Where to return the executor creator. + virtual HRESULT GetExecutorCreator( + ICeeeExecutorCreator** executor_creator); + + // Returns a list of HANDLEs of threads for which we have an executor. + // + // @param thread_handles Where to return at most @p num_threads handles. + // @param thread_ids Where to return at most @p num_threads ThreadIds. + // @param num_threads How many handles can fit in @p thread_handles. + // @return How many handles have been added in @p thread_handles and the same + // ThreadIds have been added to @p thread_ids. + virtual size_t GetThreadHandles(CHandle thread_handles[], + ThreadId thread_ids[], size_t num_threads); + + // A few seams so that we don't have to mock the kernel functions. + virtual DWORD WaitForSingleObject(HANDLE wait_handle, DWORD timeout); + virtual DWORD WaitForMultipleObjects(DWORD num_handles, + const HANDLE* wait_handles, BOOL wait_all, DWORD timeout); + + // The thread procedure that we use to clean up dead threads from the map. + // + // @param thread_data A small structure containing this and an event to + // signal when the thread has finished initializing itself. + static DWORD WINAPI ThreadProc(LPVOID thread_data); + + // The map of executor and their thread handle keyed by thread identifiers. + // Thread protected by lock_. + ExecutorsMap executors_; + + // We remember the thread identifiers for which we are pending a registration + // so that we make sure that we only accept registration that we initiate. + // Also, for each pending registration we must wait on a different event + // per thread_id that we are waiting for the registration of. + // Thread protected by ExecutorsManager::lock_. + Tid2Event pending_registrations_; + + // The mapping between a tab_id and the HWND of the window holding the BHO. + // In DEBUG, this mapping will grow over time since we don't remove it on + // DeleteTabHandle. This is useful for debugging as we know if a mapping has + // been deleted and is invalidly used. + // Thread protected by ExecutorsManager::lock_. + TabIdMap tab_id_map_; + HandleMap handle_map_; + + // The handle to the thread running ThreadProc. + CHandle thread_; + + // Used to signal the thread to reload the list of thread handles. + CHandle update_threads_list_gate_; + + // Used to signal the thread to terminate. + CHandle termination_gate_; + + // To protect the access to the maps (ExecutorsManager::executors_ & + // ExecutorsManager::pending_registrations_ & tab_id_map_/handle_map_). + Lock lock_; + + DISALLOW_EVIL_CONSTRUCTORS(ExecutorsManager); +}; + +#endif // CEEE_IE_BROKER_EXECUTORS_MANAGER_H_ diff --git a/ceee/ie/broker/executors_manager_docs.h b/ceee/ie/broker/executors_manager_docs.h new file mode 100644 index 0000000..3c607a4 --- /dev/null +++ b/ceee/ie/broker/executors_manager_docs.h @@ -0,0 +1,208 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ // Mainly for lint +#define CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ + +/* @page ExecutorsManagerDoc Detailed documentation of the ExecutorsManager. + +@section ExecutorsManagerIntro Introduction + +The Executors Manager implemented by the ExecutorsManager class, +is used by the CeeeBroker object to instantiate and hold on to +Marshaled proxies of CeeeExecutor COM objects running in other processes. + +The complexity lies in the fact that the Broker could send concurrent requests +to access Executor proxies and the creation of new proxies can't be done +synchronously. + +So the Executors Manager must block on a request for a new Executor, and all +subsequent requests for the same executor until the asynchronous creation of +the Executor is complete. Once the Executor is completely registered in the +Executors Manager, it can be returned right away when requested (though a wait +may need to be done on a lock to access the map of Executors). + +Executors are created to execute code in a given thread (called the +destination thread from now on), so we map the Executor to their associated +destination thread identifier and reuse existing Executors for subsequent +requests to execute in the same destination thread. We also keep an open +handle to the destination thread so that we can wait to it in a separate +Executors Manager @ref ThreadProc "worker thread" and wake up when the +destination thread dies, so that we can clean our map. + +@section PublicMethods Public methods + +The Executors Manager has has 3 public entry points, @ref GetExecutor, +@ref AddExecutor and @ref RemoveExecutor. + +@subsection GetExecutor + +The @link ExecutorsManager::GetExecutor GetExecutor @endlink method returns a +reference to a CeeeExecutor COM object (simply called Executor from now on) +for a given destination thread. It first checks for an existing Executor in +the @link ExecutorsManager::executors_ Executors map@endlink, and if it finds +one, it simply returns it (though it must @link ExecutorsManager::lock_ lock +@endlink the access to the Executors map). + +If the @link ExecutorsManager::executors_ Executors map@endlink doesn't +already have an Executor for the requested +destination thread, we must first check if we already have a @link +ExecutorsManager::pending_registrations_ pending registration @endlink for that +same destination thread. + +As mentioned @ref ExecutorsManagerIntro "above", registration requests can be +pending, since the creation of new Executors can not be done synchronously. + +If there are no existing Executor and no pending registration to create one, +we can create a new pending registration (all of this, properly locked so that +we make sure to only start one pending registration for a given destination +thread). + +The creation of new Executors is done via the usage of the @link +CeeeExecutorCreator Executor Creator @endlink COM object that is exposed by +a DLL so that it can inject a @link CeeeExecutorCreator::hook_ hook +@endlink in the destination thread to create the Executor. Once the hook is +injected, we can only post to the hook (since sending a message creates +a user-synchronous operation that can't get out of the process, so we won't be +able to register the newly created Executor in the Broker). + +Once the Executor Creator posted a message to the hook to instantiate a new +Executor and get it registered in the Broker, the Executors Manager must wait +for the registration to complete. Since the Executors Manager is called by the +Broker, which is instantiated in a multi-threaded apartment, the registration +will happen in a free thread while the thread that made the call to GetExecutor +is blocked, waiting for the pending registration to complete. + +At that point, there may be more than one thread calling GetExecutor for the +same destination thread, so they must all wait on the registration complete +event. Also, since there can be more than one destination thread with pending +registrations for a new Executor, there must be multiple events used to signal +the completion of the registration, one for each destination thread that has a +pending request to create a new executor. So the Executors Manager keeps a +@link ExecutorsManager::pending_registrations_ map of destination thread +identifier to registration completion event@endlink. + +Once the registration of a new Executor is complete (via a call to @ref +RegisterTabExecutor), the event for the destination thread for which the +Executor is being registered is signaled so that any thread waiting for it can +wake up, get the new Executor from the map, and return it to their caller. + +@subsection RegisterExecutor + +The @link ExecutorsManager::RegisterExecutor RegisterExecutor @endlink method is +called by the Broker when it receives a request to register a new Executor for a +given destination thread. So the Executors Manager must first confirm that we +didn't already have an Executor for that destination thread, and also confirm +that we actually have a pending request for that destination thread. + +@note <b>TODO(mad@chromium.org)</b>: We need to change this so that we +allow the BHO and maybe other modules in the future to preemptively +register Executors without a pending request. + +Once this is confirmed, the new Executor is added to the map (again, in a +thread safe way, of course), and the event of the pending request to register +an executor for that destination thread is signaled. We also need to wake up +the @ref ThreadProc "worker thread", waiting on destination thread handles, +(using the @ref update_threads_list_gate_ event) so that the new one can be +added to the list to wait on. + +@subsection RemoveExecutor + +If for some reason, an executor must remove itself from the Executors Manager's +map, it can do so by calling the @link +ICeeeBrokerRegistrar::UnregisterExecutor UnregisterExecutor @endlink method +on the Broker ICeeeBrokerRegistrar interface which delegates to the +Executors Manager's @link ExecutorsManager::RemoveExecutor RemoveExecutor +@endlink method. + +This same method is also called from the Executors Manager's @ref ThreadProc +"worker thread" waiting for the death of the destination thread for which we +have an executor in the map. When the thread dies, we remove the associated +executor from the map. + +@subsection Terminate + +The @link ExecutorsManager::Terminate Terminate @endlink method is to be called +when we are done with the Executors Manager, and it can clean up and release +all its resources (including stopping the @ref ThreadProc "worker thread" used +to wait for the destination threads to die, by setting the @ref +termination_gate_ event). + +@section PrivateMethods Private methods + +We use a few private methods to help with the implementation of the public +methods described @ref PublicMethods "above". + +@subsection GetThreadHandles + +@link ExecutorsManager::GetThreadHandles GetThreadHandles @endlink parses +through our @link ExecutorsManager::executors_ Executors map @endlink to return +the list of handles that the @ref ThreadProc "worker thread" has to wait for. + +The caller must provide two pre-allocated arrays, one for thread handles and the +other one for thread identifiers. So we may not be able to return info for +all the threads if there isn't enough room. This is OK since the only caller is +the worker thread procedure (described @ref ThreadProc "below") and it can't +wait for more than a specific number of handles anyway. The arrays must be kept +in sync, so that index i in one of them refers to the same thread as index i in +the other. + +@subsection ThreadProc + +The @link ExecutorsManager::ThreadProc ThreadProc @endlink is the worker thread +procedure that is used to get the list of destination threads for +which we have an executor in the @link ExecutorsManager::executors_ Executors +map@endlink, and then wait on them to know when they die. + +We also need to wait on two events, @ref termination_gate_ for the termination +of the worker thread (set in the Terminate public method described @ref +Terminate "above") and @ref update_threads_list_gate_ warning us that there are +new threads that we must wait on (set in the RegisterExecutor public method +described @ref RegisterExecutor "above"). + +@section PrivateDataMembers Private data members + +@subsection executors_ + +@link ExecutorsManager::executors_ executors_ @endlink is a map from a +destination thread identifier to a couple made of an Executor interface and a +thread handle. We need to keep an opened thread handle so that we can wait on +it until it dies. + +@subsection pending_registrations_ + +@link ExecutorsManager::pending_registrations_ pending_registrations_ @endlink +is another map of destination thread identifier, but this one holds on to +synchronization events that we will set once the registration of an executor +for the given destination thread is complete. Since there may be more than one +thread waiting on this signal, and we use the signal only once, we create it as +a manual reset event. + +@subsection thread_ + +Of course, we need to keep a @link ExecutorsManager::thread_ handle @endlink on +our worker thread. + +@subsection update_threads_list_gate_ + +This @link ExecutorsManager::update_threads_list_gate_ event @endlink is used +to wake the worker thread so that it refreshes its list of thread handles that +it waits on. It is set from the RegisterExecutor public method described +@ref RegisterExecutor "above". + +@subsection termination_gate_ + +This other @link ExecutorsManager::termination_gate_ event @endlink is set from +the Terminate public method described @ref Terminate "above" to notify the +worker thread to terminate itself. + +@subsection lock_ + +Since the Executors Manager can be called from many different threads, it must +protect the access to its data (and atomize certain operations) with this @link +ExecutorsManager::lock_ lock @endlink. + +*/ + +#endif // CEEE_IE_BROKER_EXECUTORS_MANAGER_DOCS_H_ diff --git a/ceee/ie/broker/executors_manager_unittest.cc b/ceee/ie/broker/executors_manager_unittest.cc new file mode 100644 index 0000000..47c6275 --- /dev/null +++ b/ceee/ie/broker/executors_manager_unittest.cc @@ -0,0 +1,664 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for executors manager. + +#include <vector> + +#include "base/logging.h" +#include "ceee/ie/broker/executors_manager.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/nt_internals.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::DoAll; +using testing::InvokeWithoutArgs; +using testing::Return; +using testing::SetArgumentPointee; + +// We mock the IUnknown interface to make sure we properly AddRef and +// Release the executors in the manager's map. +// TODO(mad@chromium.org): replace this with the MockExecutorIUnknown +// in mock_broker_and_friends.h. +class MockExecutor : public IUnknown { + public: + MockExecutor() : ref_count_(0) {} + STDMETHOD_(ULONG, AddRef)() { return ++ref_count_; } + STDMETHOD_(ULONG, Release)() { + EXPECT_GT(ref_count_, 0UL); + return --ref_count_; + } + ULONG ref_count() const { return ref_count_; } + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, + HRESULT(REFIID, void**)); + private: + ULONG ref_count_; +}; + +// We also need to mock the creator or executors for 1) making sure it is +// called properly, and 2) for it to return our mock executor. +class MockExecutorCreator: public ICeeeExecutorCreator { + public: + // No need to mock AddRef, it is not called. + STDMETHOD_(ULONG, AddRef)() { return 1; } + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + STDMETHOD (QueryInterface)(REFIID, LPVOID*) { return S_OK; } + + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, CreateWindowExecutor, + HRESULT(long, CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Teardown, HRESULT(long)); +}; + +// We need to override some virtual functions of the executors manager and +// also provide public access to some protected methods and data. +class TestingExecutorsManager : public ExecutorsManager { + public: + TestingExecutorsManager() + : ExecutorsManager(true), // no thread. + current_executor_(NULL), + current_handle_(NULL), + current_thread_id_(kThreadId) { + } + + ~TestingExecutorsManager() { + nt_internals::PUBLIC_OBJECT_BASIC_INFORMATION pobi; + ULONG length = 0; + for (size_t index = 0; index < opened_handles_.size(); ++index) { + nt_internals::NtQueryObject(opened_handles_[index].first, + nt_internals::ObjectBasicInformation, &pobi, sizeof(pobi), &length); + EXPECT_EQ(length, sizeof(pobi)); + EXPECT_EQ(1UL, pobi.HandleCount); + if (1UL != pobi.HandleCount) { + printf("Leaked handle for: %s\n", opened_handles_[index].second); + } + EXPECT_TRUE(::CloseHandle(opened_handles_[index].first)); + } + } + + MOCK_METHOD2(WaitForSingleObject, DWORD(HANDLE, DWORD)); + MOCK_METHOD4(WaitForMultipleObjects, + DWORD(DWORD, const HANDLE*, BOOL, DWORD)); + + HANDLE GetNewHandle(const char* info) { + // Some tests depend on the handle to be manual reset. + opened_handles_.push_back(std::pair<HANDLE, const char*>( + ::CreateEvent(NULL, TRUE, FALSE, NULL), info)); + HANDLE returned_handle = NULL; + EXPECT_TRUE(::DuplicateHandle(::GetCurrentProcess(), + opened_handles_.back().first, ::GetCurrentProcess(), &returned_handle, + 0, FALSE, DUPLICATE_SAME_ACCESS)); + return returned_handle; + } + + // This is a seem in the base class allowing us ot use our own creator. + HRESULT GetExecutorCreator(ICeeeExecutorCreator** executor_creator) { + *executor_creator = &executor_creator_; + return S_OK; + } + // provide public access so we can test the method. + using ExecutorsManager::GetThreadHandles; + // Wrap the call to ThreadPRoc by first creating the necessary info. + DWORD CallThreadProc() { + // This struct is declared in the unnamed namespace of the implementation. + struct ThreadStartData { + ExecutorsManager* me; + CHandle thread_started_gate; + } thread_start_data; + thread_start_data.me = this; + thread_start_data.thread_started_gate.Attach(GetNewHandle("TSD")); + DCHECK(thread_start_data.thread_started_gate != NULL); + return ExecutorsManager::ThreadProc(&thread_start_data); + } + + // Access to protected handles. + HANDLE GetUpdateHandle() { + return update_threads_list_gate_.m_h; + } + HANDLE GetTerminateHandle() { + return termination_gate_.m_h; + } + + // Fake a pending registration, to properly test that case. + void AddPendingRegistration(HANDLE thread_handle, + ThreadId thread_id = kThreadId) { + pending_registrations_[thread_id].Attach(thread_handle); + } + void RemovePendingRegistration(ThreadId thread_id = kThreadId) { + pending_registrations_.erase(thread_id); + } + + // This method is to be Invoked by the Mock of WaitForXXXObject[s]. + void RegisterExecutorOnWait() { + executors_[current_thread_id_] = ExecutorInfo(current_executor_, + current_handle_); + } + + // Public access to the executors_ map. + void FakeRegisterExecutor(HANDLE handle, + IUnknown* executor = NULL, + ThreadId thread_id = kThreadId) { + executors_[thread_id] = ExecutorInfo(executor, handle); + } + size_t GetNumExecutors() { + return executors_.size(); + } + bool IsExecutorRegistered(ThreadId thread_id = kThreadId) { + return (executors_.find(thread_id) != executors_.end()); + } + + IUnknown* current_executor_; + HANDLE current_handle_; + ThreadId current_thread_id_; + MockExecutorCreator executor_creator_; + static const ThreadId kThreadId; + static const HWND kWindowHwnd; + static const CeeeWindowHandle kWindowHandle; + + // Publicize these protected static const values. + using ExecutorsManager::kTerminationHandleIndexOffset; + using ExecutorsManager::kUpdateHandleIndexOffset; + + std::vector<std::pair<HANDLE, const char*>> opened_handles_; +}; + +const TestingExecutorsManager::ThreadId TestingExecutorsManager::kThreadId = 42; +const HWND TestingExecutorsManager::kWindowHwnd = reinterpret_cast<HWND>(93); +const CeeeWindowHandle TestingExecutorsManager::kWindowHandle + = reinterpret_cast<CeeeWindowHandle>(kWindowHwnd); + +class ExecutorsManagerTests: public testing::Test { + public: + ExecutorsManagerTests() : initial_handle_count_(0) { + } + // This is not reliable enough, but we keep it here so that we can enable + // it from time to time to make sure everything is OK. + /* + virtual void SetUp() { + // This is called from the threadproc and changes the handles count if we + // don't call it first. + ASSERT_HRESULT_SUCCEEDED(::CoInitializeEx(NULL, COINIT_MULTITHREADED)); + + // Acquire the number of handles in the process. + ::GetProcessHandleCount(::GetCurrentProcess(), &initial_handle_count_); + printf("Initial Count: %d\n", initial_handle_count_); + } + + virtual void TearDown() { + // Make sure the number of handles in the process didn't change. + DWORD new_handle_count = 0; + ::GetProcessHandleCount(::GetCurrentProcess(), &new_handle_count); + EXPECT_EQ(initial_handle_count_, new_handle_count); + printf("Final Count: %d\n", new_handle_count); + + ::CoUninitialize(); + } + */ + private: + DWORD initial_handle_count_; +}; + +TEST_F(ExecutorsManagerTests, RegisterExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Invalid arguments. + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, NULL)); + + MockExecutor executor; + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingRegistration1")); + EXPECT_EQ(E_INVALIDARG, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, NULL)); + + // Dead thread... + testing::MockKernel32 kernel32; + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return((HANDLE)NULL)); + EXPECT_EQ(S_FALSE, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + // Failed, already registered. + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("FakeExec1"), &executor); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + EXPECT_EQ(E_UNEXPECTED, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(0, executor.ref_count()); + + // Success!!! + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce( + Return(executors_manager.GetNewHandle("OpenThread"))); + EXPECT_EQ(S_OK, executors_manager.RegisterWindowExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + executors_manager.RemovePendingRegistration(); + EXPECT_EQ(S_OK, executors_manager.Terminate()); + EXPECT_EQ(0, executor.ref_count()); +} + +TEST_F(ExecutorsManagerTests, RegisterTabExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Already registered. + MockExecutor executor; + HANDLE new_handle = executors_manager.GetNewHandle("FakeExec1"); + executors_manager.FakeRegisterExecutor(new_handle, &executor); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + EXPECT_EQ(S_OK, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(0, executor.ref_count()); + + // Dead thread... + testing::MockKernel32 kernel32; + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return((HANDLE)NULL)); + EXPECT_EQ(E_UNEXPECTED, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(0, executor.ref_count()); + + // Success!!! + new_handle = executors_manager.GetNewHandle("OpenThread"); + EXPECT_CALL(kernel32, OpenThread(SYNCHRONIZE, FALSE, + TestingExecutorsManager::kThreadId)).WillOnce(Return(new_handle)); + EXPECT_EQ(S_OK, executors_manager.RegisterTabExecutor( + TestingExecutorsManager::kThreadId, &executor)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + EXPECT_EQ(S_OK, executors_manager.Terminate()); + EXPECT_EQ(0, executor.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); +} + +TEST_F(ExecutorsManagerTests, GetExecutor) { + testing::LogDisabler no_dchecks; + TestingExecutorsManager executors_manager; + + // Already in the map. + MockExecutor executor; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("FakeExec1"), &executor); + EXPECT_GT(executor.ref_count(), 0UL); + ULONG previous_ref_count = executor.ref_count(); + + IUnknown* result_executor = NULL; + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + // Since our mocked QI doesn't addref, ref count shouldn't have changed. + EXPECT_EQ(previous_ref_count, executor.ref_count()); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + + // Fail Executor creation. + result_executor = NULL; + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_EQ(E_FAIL, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Fail registration. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(Return(WAIT_OBJECT_0)); + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, + Teardown(TestingExecutorsManager::kThreadId)).WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_EQ(E_UNEXPECTED, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Pending registration fail. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(Return(WAIT_OBJECT_0)); + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingReg1")); + + EXPECT_EQ(E_UNEXPECTED, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(NULL, result_executor); + + // Success Creating new. + executors_manager.current_executor_ = &executor; + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(WAIT_OBJECT_0))); + EXPECT_CALL(executors_manager.executor_creator_, + CreateWindowExecutor(TestingExecutorsManager::kThreadId, + TestingExecutorsManager::kWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, + Teardown(TestingExecutorsManager::kThreadId)).WillOnce(Return(S_OK)); + EXPECT_CALL(executors_manager.executor_creator_, Release()). + WillOnce(Return(1)); + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + EXPECT_GT(executor.ref_count(), 0UL); + + // Cleanup. + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + + // Success with pending registration. + EXPECT_CALL(executors_manager, WaitForSingleObject(_, _)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(WAIT_OBJECT_0))); + executors_manager.AddPendingRegistration( + executors_manager.GetNewHandle("PendingReg2")); + EXPECT_CALL(executor, QueryInterface(_, _)).WillOnce( + DoAll(SetArgumentPointee<1>(&executor), Return(S_OK))); + EXPECT_EQ(S_OK, executors_manager.GetExecutor( + TestingExecutorsManager::kThreadId, TestingExecutorsManager::kWindowHwnd, + IID_IUnknown, reinterpret_cast<void**>(&result_executor))); + EXPECT_EQ(&executor, result_executor); + EXPECT_GT(executor.ref_count(), 0UL); + + // Make sure we properly cleanup. + EXPECT_EQ(S_OK, executors_manager.Terminate()); + // Since our mocked QI doesn't addref, we should be back to 0 now. + EXPECT_EQ(0, executor.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); +} + +TEST_F(ExecutorsManagerTests, RemoveExecutor) { + TestingExecutorsManager executors_manager; + + // Nothing to remove... + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor(1)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + + // Success. + MockExecutor executor1; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake0"), &executor1); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(S_FALSE, executors_manager.RemoveExecutor( + TestingExecutorsManager::kThreadId)); + + // Multiple values, removed one at a time... + MockExecutor executor2; + MockExecutor executor3; + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake1"), &executor1, 1); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake2"), &executor2, 2); + EXPECT_EQ(2, executors_manager.GetNumExecutors()); + EXPECT_GT(executor2.ref_count(), 0UL); + executors_manager.FakeRegisterExecutor( + executors_manager.GetNewHandle("Fake3"), &executor3, 3); + EXPECT_EQ(3, executors_manager.GetNumExecutors()); + EXPECT_GT(executor3.ref_count(), 0UL); + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(2)); + EXPECT_EQ(2, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(2)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(1)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(3)); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor2.ref_count()); + EXPECT_GT(executor3.ref_count(), 0UL); + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(3)); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(3)); + EXPECT_TRUE(executors_manager.IsExecutorRegistered(1)); + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor3.ref_count()); + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(1)); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + EXPECT_FALSE(executors_manager.IsExecutorRegistered(1)); + EXPECT_EQ(0, executor1.ref_count()); +} + +// Since the returned handle is a duplicate, making sure we have the +// appropriate handle is a little tricky. +void VerifyIsSameHandle(HANDLE handle1, HANDLE handle2) { + // First make sure neither is set. + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle2, 0)); + + // Now check that setting one also sets the other. + ::SetEvent(handle1); + EXPECT_EQ(WAIT_OBJECT_0, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_OBJECT_0, ::WaitForSingleObject(handle2, 0)); + + // Manual reset. + ::ResetEvent(handle1); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle1, 0)); + EXPECT_EQ(WAIT_TIMEOUT, ::WaitForSingleObject(handle2, 0)); +} + +TEST_F(ExecutorsManagerTests, GetThreadHandles) { + TestingExecutorsManager executors_manager; + + // We need to work with invalid, and one other valid value for thread info. + static const TestingExecutorsManager::ThreadId kInvalidThreadId = 0xF0F0F0F0; + static const TestingExecutorsManager::ThreadId kThreadIdA = 0xAAAAAAAA; + static const TestingExecutorsManager::ThreadId kThreadIdB = 0xBBBBBBBB; + + // As for the thread proc code, we work with arrays of handles and thread ids + // on the stack. + CHandle thread_handles[MAXIMUM_WAIT_OBJECTS]; + TestingExecutorsManager::ThreadId thread_ids[MAXIMUM_WAIT_OBJECTS]; + + // Make sure the arrays are not touched. + thread_ids[0] = kInvalidThreadId; + size_t num_threads = executors_manager.GetThreadHandles( + thread_handles, thread_ids, MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(0, num_threads); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[0]); + EXPECT_EQ(kInvalidThreadId, thread_ids[0]); + + // Make sure only the first index will get affected. + thread_ids[1] = kInvalidThreadId; + HANDLE thread_handle_a = executors_manager.GetNewHandle("A"); + executors_manager.FakeRegisterExecutor(thread_handle_a, NULL, kThreadIdA); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(1, num_threads); + VerifyIsSameHandle(thread_handle_a, thread_handles[0]); + EXPECT_EQ(kThreadIdA, thread_ids[0]); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[1]); + EXPECT_EQ(kInvalidThreadId, thread_ids[1]); + // Need to close active handles since GetThreadHandles expect NULL m_h. + thread_handles[0].Close(); + + // Now expect 2 values and make sure the third one isn't affected. + thread_ids[0] = kInvalidThreadId; + thread_ids[2] = kInvalidThreadId; // We asserted above that index 1 is OK. + HANDLE thread_handle_b = executors_manager.GetNewHandle("B"); + executors_manager.FakeRegisterExecutor(thread_handle_b, NULL, kThreadIdB); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(2, num_threads); + // We can't be sure of the order in which the map returns the elements. + if (thread_ids[0] == kThreadIdA) { + VerifyIsSameHandle(thread_handle_a, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[1]); + VerifyIsSameHandle(thread_handle_b, thread_handles[1]); + } else { + VerifyIsSameHandle(thread_handle_b, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[0]); + VerifyIsSameHandle(thread_handle_a, thread_handles[1]); + EXPECT_EQ(kThreadIdA, thread_ids[1]); + } + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[2]); + EXPECT_EQ(kInvalidThreadId, thread_ids[2]); + thread_handles[0].Close(); + thread_handles[1].Close(); + + // Now remove threads and make sure they won't be returned again. + thread_ids[0] = kInvalidThreadId; + thread_ids[1] = kInvalidThreadId; // Asserted index 2 invalid already. + + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(kThreadIdA)); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(1, num_threads); + VerifyIsSameHandle(thread_handle_b, thread_handles[0]); + EXPECT_EQ(kThreadIdB, thread_ids[0]); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[1]); + EXPECT_EQ(kInvalidThreadId, thread_ids[1]); + thread_handles[0].Close(); + + // Now remove the last one... + thread_ids[0] = kInvalidThreadId; + EXPECT_EQ(S_OK, executors_manager.RemoveExecutor(kThreadIdB)); + num_threads = executors_manager.GetThreadHandles(thread_handles, thread_ids, + MAXIMUM_WAIT_OBJECTS); + EXPECT_EQ(0, num_threads); + EXPECT_EQ(static_cast<HANDLE>(NULL), thread_handles[0]); + EXPECT_EQ(kInvalidThreadId, thread_ids[0]); +} + +TEST_F(ExecutorsManagerTests, ThreadProc) { + testing::LogDisabler no_logs; + TestingExecutorsManager executors_manager; + + // Register an object and make sure we wake up to update our list. + MockExecutor executor1; + executors_manager.current_executor_ = &executor1; + executors_manager.current_handle_ = executors_manager.GetNewHandle("1"); + EXPECT_CALL(executors_manager, WaitForMultipleObjects(2, _, FALSE, INFINITE)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), + Return(TestingExecutorsManager::kUpdateHandleIndexOffset))); + EXPECT_CALL(executors_manager, WaitForMultipleObjects(3, _, FALSE, INFINITE)). + WillOnce(Return( + TestingExecutorsManager::kTerminationHandleIndexOffset + 1)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); + + EXPECT_TRUE(executors_manager.IsExecutorRegistered()); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + EXPECT_GT(executor1.ref_count(), 0UL); + + // Add another destination thread. + static const TestingExecutorsManager::ThreadId kOtherThreadId = 14; + MockExecutor executor2; + executors_manager.current_executor_ = &executor2; + executors_manager.current_handle_ = executors_manager.GetNewHandle("2"); + executors_manager.current_thread_id_ = kOtherThreadId; + // Number of handles and return index offset needs to be increased by 1 + // because we already have a destination thread in our map. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(3, _, FALSE, INFINITE)). + WillOnce(DoAll(InvokeWithoutArgs(&executors_manager, + &TestingExecutorsManager::RegisterExecutorOnWait), Return( + TestingExecutorsManager::kUpdateHandleIndexOffset + 1))).WillOnce( + Return(TestingExecutorsManager::kTerminationHandleIndexOffset + 1)); + // Now that we have another destination thread in the map, + // we wait for 4 handles. We return 1 to remove one of them, and wait will + // be called with three handles again, the mock above will be called for the + // second time with 3 handles and will return the proper termination offset. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(4, _, FALSE, INFINITE)). + WillOnce(Return(1)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); + EXPECT_EQ(1, executors_manager.GetNumExecutors()); + // We can't tell which one is at position 1 in the array returned by + // GetThreadHandles since they are fetched from an unsorted map. + EXPECT_TRUE(executors_manager.IsExecutorRegistered(kOtherThreadId) || + executors_manager.IsExecutorRegistered( + TestingExecutorsManager::kThreadId)); + if (executors_manager.IsExecutorRegistered(kOtherThreadId)) { + // We kept the other executor in the map. + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_GT(executor2.ref_count(), 0UL); + } else { + EXPECT_GT(executor1.ref_count(), 0UL); + EXPECT_EQ(0, executor2.ref_count()); + } + + // Cleanup. + executors_manager.Terminate(); + EXPECT_EQ(0, executor1.ref_count()); + EXPECT_EQ(0, executor2.ref_count()); + EXPECT_EQ(0, executors_manager.GetNumExecutors()); + + // Test the thread failure path. + EXPECT_CALL(executors_manager, WaitForMultipleObjects(2, _, FALSE, INFINITE)). + WillOnce(Return(WAIT_FAILED)); + EXPECT_EQ(1, executors_manager.CallThreadProc()); +} + +} // namespace diff --git a/ceee/ie/broker/infobar_api_module.cc b/ceee/ie/broker/infobar_api_module.cc new file mode 100644 index 0000000..f857b4a --- /dev/null +++ b/ceee/ie/broker/infobar_api_module.cc @@ -0,0 +1,99 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Infobar API implementation. + +#include "ceee/ie/broker/infobar_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> // Must be included AFTER base. + +#include "base/string_number_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_infobar_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" + +namespace ext = extension_infobar_module_constants; + +namespace infobar_api { + +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); } while (false) + REGISTER_INFOBAR_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION +} + +void ShowInfoBar::Execute(const ListValue& args, int request_id) { + scoped_ptr<InfobarApiResult> result(CreateApiResult(request_id)); + + // Get the first parameter (object details). + DictionaryValue* dict; + if (!args.GetDictionary(0, &dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // The dictionary should have both tabId and path properties. + int tab_id; + std::string path; + if (!dict->GetInteger(ext::kTabId, &tab_id) || + !dict->GetString(ext::kHtmlPath, &path)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id)).c_str()); + return; + } + + CComPtr<ICeeeInfobarExecutor> executor; + GetDispatcher()->GetExecutor(tab_window, IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to show infobar."; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + CeeeWindowHandle window_handle; + HRESULT hr = executor->ShowInfobar(CComBSTR(path.data()), &window_handle); + if (FAILED(hr)) { + // No DCHECK, this may happen if the window/thread dies on the way. + LOG(ERROR) << "Can't show infobar. " << com::LogHr(hr) << " path=" << path; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // Store the window information returned by ShowInfobar(). + result->CreateWindowValue(dispatcher->GetWindowHandleFromId(window_handle), + false); + + // Now wait until the document is complete before responding. + dispatcher->RegisterEphemeralEventHandler(kOnDocumentCompleteEventName, + ShowInfoBar::ContinueExecution, result.release()); +} + +HRESULT ShowInfoBar::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<InfobarApiResult> result( + static_cast<InfobarApiResult*>(user_data)); + result->PostResult(); + return S_OK; +} + +} // namespace infobar_api diff --git a/ceee/ie/broker/infobar_api_module.h b/ceee/ie/broker/infobar_api_module.h new file mode 100644 index 0000000..53b3f65 --- /dev/null +++ b/ceee/ie/broker/infobar_api_module.h @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Infobar API implementation. + +#ifndef CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ +#define CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +namespace infobar_api { + +class InfobarApiResult; +typedef ApiResultCreator<InfobarApiResult> InfobarApiResultCreator; + +// Registers all Infobar API invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class InfobarApiResult : public common_api::CommonApiResult { + public: + explicit InfobarApiResult(int request_id) + : common_api::CommonApiResult(request_id) {} +}; + +typedef IterativeApiResult<InfobarApiResult> IterativeInfobarApiResult; + +class ShowInfoBar : public InfobarApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // We need to wait for the infobar browser ready event to complete the + // result response. + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_BROKER_INFOBAR_API_MODULE_H_ diff --git a/ceee/ie/broker/infobar_api_module_unittest.cc b/ceee/ie/broker/infobar_api_module_unittest.cc new file mode 100644 index 0000000..d9bce10 --- /dev/null +++ b/ceee/ie/broker/infobar_api_module_unittest.cc @@ -0,0 +1,144 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "ceee/ie/broker/infobar_api_module.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_infobar_module_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext = extension_infobar_module_constants; + +namespace { + +using infobar_api::InfobarApiResult; +using infobar_api::RegisterInvocations; +using infobar_api::ShowInfoBar; + +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +const int kGoodTabWindowId = 99; +const int kGoodFrameWindowId = 88; +const int kRequestId = 43; +const char kHtmlPath[] = "/infobar/test.html"; + +const HWND kGoodTabWindow = (HWND)(kGoodTabWindowId + 1); +const HWND kGoodFrameWindow = (HWND)(kGoodFrameWindowId + 1); + +TEST(InfobarApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(1)); + RegisterInvocations(&disp); +} + +class MockInfobarApiResult : public InfobarApiResult { + public: + explicit MockInfobarApiResult(int request_id) + : InfobarApiResult(request_id) {} + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +class InfobarApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockInfobarExecutor::CreateInitialized( + &mock_infobar_executor_, &mock_infobar_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_infobar_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + void AlwaysMockGetInfobarExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_infobar_executor_keeper_.p), + AddRef(mock_infobar_executor_keeper_.p))); + } + + void MockGetInfobarExecutorOnce(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(mock_infobar_executor_keeper_.p), + AddRef(mock_infobar_executor_keeper_.p))); + } + + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockInfobarExecutor* mock_infobar_executor_; + + private: + // To control the life span of the tab executor. + CComPtr<ICeeeInfobarExecutor> mock_infobar_executor_keeper_; +}; + +TEST_F(InfobarApiTests, ShowInfoBarNoErrors) { + // TODO(vadimb@google.com): Make the implementation work. +#if 0 + testing::LogDisabler no_dchecks; + + DictionaryValue dict; + dict.Set(ext::kTabId, Value::CreateIntegerValue(kGoodTabWindowId)); + dict.Set(ext::kHtmlPath, Value::CreateStringValue(std::string(kHtmlPath))); + ListValue good_args; + ASSERT_TRUE(good_args.Set(0, dict.DeepCopy())); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + + StrictMock<MockApiInvocation<InfobarApiResult, MockInfobarApiResult, + ShowInfoBar> > invocation; + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kGoodFrameWindowId)). + WillRepeatedly(Return(kGoodFrameWindow)); + MockGetInfobarExecutorOnce(&invocation.mock_api_dispatcher_, kGoodTabWindow); + CComBSTR html_path(kHtmlPath); + CeeeWindowHandle window_handle = + reinterpret_cast<CeeeWindowHandle>(kGoodFrameWindow); + EXPECT_CALL(*mock_infobar_executor_, ShowInfobar(StrEq(html_path.m_str), _)). + WillOnce(DoAll(SetArgumentPointee<1>(window_handle), Return(S_OK))); + + invocation.AllocateNewResult(kRequestId); + + invocation.Execute(good_args, kRequestId); +#endif +} + +} // namespace diff --git a/ceee/ie/broker/resource.h b/ceee/ie/broker/resource.h new file mode 100644 index 0000000..60c6977 --- /dev/null +++ b/ceee/ie/broker/resource.h @@ -0,0 +1,31 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Resource constants for user broker executable. + +#ifndef CEEE_IE_BROKER_RESOURCE_H_ +#define CEEE_IE_BROKER_RESOURCE_H_ + + +// {{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by broker_module.rc +// +#define IDS_PROJNAME 100 +#define IDR_BROKER_MODULE 101 +#define IDR_BROKER 102 + + +// Next default values for new objects +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 201 +#define _APS_NEXT_COMMAND_VALUE 32768 +#define _APS_NEXT_CONTROL_VALUE 201 +#define _APS_NEXT_SYMED_VALUE 103 +#endif +#endif + + +#endif // CEEE_IE_BROKER_RESOURCE_H_ diff --git a/ceee/ie/broker/tab_api_module.cc b/ceee/ie/broker/tab_api_module.cc new file mode 100644 index 0000000..c75e2c1 --- /dev/null +++ b/ceee/ie/broker/tab_api_module.cc @@ -0,0 +1,1185 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation. +// +// Tab IDs are the window handle of the "TabWindowClass" window class +// of the whole tab. +// +// To find the chrome.window.* "window ID" we can just get the top-level parent +// window of the tab window. +// +// TODO(joi@chromium.org) Figure out what to do in IE6 (which has no tabs). + +#include "ceee/ie/broker/tab_api_module.h" + +#include <atlbase.h> +#include <atlcom.h> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/values.h" +#include "base/utf_string_conversions.h" +#include "base/win/windows_version.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/common/api_registration.h" +#include "ceee/ie/common/constants.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "chrome/common/url_constants.h" +#include "googleurl/src/gurl.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; +namespace keys = extension_automation_constants; + +namespace tab_api { + +namespace { + +// bb3147348 +// Convert the tab_id parameter (always the first one) and verify that the +// event received has the right number of parameters. +// NumParam is the number of parameters we expect from events_funnel. +// AddTabParam if true, we add a Tab object to the converted_args. +template<int NumParam, bool AddTabParam> +bool ConvertTabIdEventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + *converted_args = input_args; + + // Get the tab ID from the input arguments. + scoped_ptr<ListValue> input_list; + if (!api_module_util::GetListFromJsonString(input_args, &input_list)) { + NOTREACHED() << "Invalid Arguments sent to event."; + return false; + } + + if (input_list == NULL || input_list->GetSize() != NumParam) { + NOTREACHED() << "Invalid Number of Arguments sent to event."; + return false; + } + + int tab_handle = -1; + bool success = input_list->GetInteger(0, &tab_handle); + DCHECK(success) << "Failed getting the tab_id value from the list of args."; + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + input_list->Set(0, Value::CreateIntegerValue(tab_id)); + + if (AddTabParam) { + TabApiResult result(TabApiResult::kNoRequestId); + // Don't DCHECK here since we have cases where the tab died beforehand. + if (!result.CreateTabValue(tab_id, -1)) { + LOG(ERROR) << "Failed to create a value for tab: " << std::hex << tab_id; + return false; + } + + input_list->Append(result.value()->DeepCopy()); + } + + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; +} + +bool CeeeUnmapTabEventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + int tab_handle = reinterpret_cast<int>(INVALID_HANDLE_VALUE); + + scoped_ptr<ListValue> input_list; + if (!api_module_util::GetListAndIntegerValue(input_args, &input_list, + &tab_handle) || + tab_handle == kInvalidChromeSessionId) { + NOTREACHED() << "An invalid argument was passed to UnmapTab"; + return false; + } +#ifdef DEBUG + int tab_id = kInvalidChromeSessionId; + input_list->GetInteger(1, &tab_id); + DCHECK(tab_id == dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle))); +#endif // DEBUG + + HWND tab_window = reinterpret_cast<HWND>(tab_handle); + dispatcher->DeleteTabHandle(tab_window); + return false; +} + +} + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); }\ + while (false) + REGISTER_TAB_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + // Registers our private events. + dispatcher->RegisterPermanentEventHandler( + ceee_event_names::kCeeeOnTabUnmapped, CeeeUnmapTabEventHandler); + + // And now register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabCreated, + CreateTab::EventHandler); + + // For OnTabUpdate, we receive 2 from events_funnel, and add a Tab Parameter. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabUpdated, + ConvertTabIdEventHandler<2, true>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabAttached, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabDetached, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabMoved, + ConvertTabIdEventHandler<2, false>); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnTabRemoved, + ConvertTabIdEventHandler<1, false>); + dispatcher->RegisterPermanentEventHandler( + ext_event_names::kOnTabSelectionChanged, + ConvertTabIdEventHandler<2, false>); +} + +bool TabApiResult::CreateTabValue(int tab_id, long index) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + + if (window_utils::WindowHasNoThread(tab_window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + + if (!IsTabWindowClass(tab_window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get tab info."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + TabInfo tab_info; + HRESULT hr = executor->GetTabInfo(&tab_info); + if (FAILED(hr)) { + LOG(WARNING) << "Executor failed to get tab info." << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + + scoped_ptr<DictionaryValue> result(new DictionaryValue()); + result->SetInteger(ext::kIdKey, tab_id); + + // The window ID is just the window handle of the frame window, which is the + // top-level ancestor of this window. + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + if (frame_window == tab_window || + !window_utils::IsWindowClass(frame_window, + windows::kIeFrameWindowClass)) { + // If we couldn't get a valid parent frame window, then it must be because + // the frame window (and the tab then) has been closed by now or it lives + // under the hidden IE window. + DCHECK(!::IsWindow(tab_window) || window_utils::IsWindowClass(frame_window, + windows::kHiddenIeFrameWindowClass)); + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return false; + } + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + result->SetInteger(ext::kWindowIdKey, frame_window_id); + + // Only the currently selected tab has the VS_VISIBLE style. + result->SetBoolean(ext::kSelectedKey, TRUE == ::IsWindowVisible(tab_window)); + + result->SetString(ext::kUrlKey, com::ToString(tab_info.url)); + result->SetString(ext::kTitleKey, com::ToString(tab_info.title)); + + std::string status = ext::kStatusValueComplete; + if (tab_info.status == kCeeeTabStatusLoading) + status = ext::kStatusValueLoading; + else + DCHECK(tab_info.status == kCeeeTabStatusComplete) << "New Status???"; + + result->SetString(ext::kStatusKey, status); + + if (tab_info.fav_icon_url != NULL) { + result->SetString(ext::kFavIconUrlKey, + com::ToString(tab_info.fav_icon_url)); + } + + // When enumerating all tabs, we already have the index + // so we can save an IPC call. + if (index == -1) { + // We need another executor to get the index from the frame window thread. + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get tab index."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + hr = executor->GetTabIndex(reinterpret_cast<CeeeWindowHandle>(tab_window), + &index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get tab info for tab: " << std::hex << tab_id << + ". " << com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + } + result->SetInteger(ext::kIndexKey, static_cast<int>(index)); + + result->SetBoolean(ext::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + + if (value_ == NULL) { + value_.reset(result.release()); + } else { + DCHECK(value_->IsType(Value::TYPE_LIST)); + ListValue* list = reinterpret_cast<ListValue*>(value_.get()); + list->Append(result.release()); + } + return true; +} + +bool TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + const DictionaryValue& input_dict, + const Value* saved_window_value, + HWND* tab_window, + ApiDispatcher* dispatcher) { + int tab_id = 0; + bool success = input_dict.GetInteger(ext::kIdKey, &tab_id); + DCHECK(success && tab_id != 0) << "The input_dict MUST have a tab ID!!!"; + DCHECK(dispatcher != NULL); + HWND input_tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (tab_window != NULL) + *tab_window = input_tab_window; + + if (saved_window_value == NULL) + return true; + + DCHECK(saved_window_value->IsType(Value::TYPE_INTEGER)); + int saved_window_id = 0; + success = saved_window_value->GetAsInteger(&saved_window_id); + DCHECK(success && saved_window_id != 0); + + HWND frame_window = NULL; + int frame_window_id = 0; + if (!input_dict.GetInteger(ext::kWindowIdKey, &frame_window_id)) { + // If the parent window is not specified, it is easy to fetch it ourselves. + frame_window = window_utils::GetTopLevelParent(input_tab_window); + frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + DCHECK_NE(0, frame_window_id); + } else { + frame_window = dispatcher->GetWindowHandleFromId(frame_window_id); + DCHECK_EQ(window_utils::GetTopLevelParent(input_tab_window), frame_window); + } + + return frame_window_id == saved_window_id; +} + +bool GetIntegerFromValue( + const Value& value, const char* key_name, int* out_value) { + switch (value.GetType()) { + case Value::TYPE_INTEGER: { + return value.GetAsInteger(out_value); + } + case Value::TYPE_DICTIONARY: { + const DictionaryValue* dict = static_cast<const DictionaryValue*>(&value); + if (dict->HasKey(key_name)) + return dict->GetInteger(key_name, out_value); + *out_value = 0; + return true; + } + case Value::TYPE_LIST: { + const ListValue* args_list = static_cast<const ListValue*>(&value); + Value* anonymous_value = NULL; + if (!args_list->Get(0, &anonymous_value)) { + // If given an empty list value, we return 0 so that the frame window is + // fetched. + *out_value = 0; + return true; + } + DCHECK(anonymous_value != NULL); + return GetIntegerFromValue(*anonymous_value, key_name, out_value); + } + case Value::TYPE_NULL: { + // If given an empty list value, we return 0 so that the frame window is + // fetched. + *out_value = 0; + return true; + } + default: { + return false; + } + } +} + +HWND TabApiResult::GetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified) { + int window_id = 0; + if (!GetIntegerFromValue(args, ext::kWindowIdKey, &window_id)) { + NOTREACHED() << "Invalid Arguments."; + return NULL; + } + + HWND frame_window = NULL; + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + if (window_id != 0) + frame_window = dispatcher->GetWindowHandleFromId(window_id); + + if (!frame_window) { + // TODO(mad@chromium.org): We currently don't have access to the + // actual 'current' window from the point of view of the extension + // API caller. Use one of the top windows for now. bb2255140 + window_utils::FindDescendentWindow(NULL, windows::kIeFrameWindowClass, + true, &frame_window); + if (specified != NULL) + *specified = false; + } else { + if (specified != NULL) + *specified = true; + } + + if (!frame_window) { + return NULL; + } + + if (!window_utils::IsWindowClass(frame_window, windows::kIeFrameWindowClass)) + return NULL; + + return frame_window; +} + +void GetTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = kInvalidChromeSessionId; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + // -1 when we don't know the index. + if (result->CreateTabValue(tab_id, -1)) { + // CreateTabValue called PostError if it returned false. + result->PostResult(); + } +} + +void GetSelectedTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + + bool specified = false; + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(args, + &specified); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + // The selected tab is the only visible "TabWindowClass" window + // that is a child of the frame window. Enumerate child windows to find it, + // and fill in the value_ when we do. + HWND selected_tab = NULL; + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + if (!window_utils::FindDescendentWindow( + frame_window, windows::kIeTabWindowClass, true, &selected_tab)) { + if (specified) { + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + // We remember the frame window if it was specified so that we only + // react asynchronously to new tabs created in the same frame window. + result->SetValue(ext::kWindowIdKey, + Value::CreateIntegerValue(frame_window_id)); + } + DCHECK(dispatcher != NULL); + dispatcher->RegisterEphemeralEventHandler( + ext_event_names::kOnTabCreated, + GetSelectedTab::ContinueExecution, + // We don't want to destroy the result in the scoped_ptr when we pass + // it as user_data to GetSelectedTab::ContinueExecution(). + result.release()); + } else { + int tab_id = dispatcher->GetTabIdFromHandle(selected_tab); + DCHECK(tab_id != kInvalidChromeSessionId); + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); + } +} + +HRESULT GetSelectedTab::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(dispatcher != NULL); + DCHECK(user_data != NULL); + + // Any tab is good for us, so relaunch the search for a selected tab + // by using the frame window of the newly created tab. + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + scoped_ptr<ListValue> args_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &args_list); + if (input_dict == NULL) { + DCHECK(false) << "Event arguments are not a list with a dictionary in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + HWND tab_window = NULL; + if (!TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow(*input_dict, + result->GetValue(ext::kWindowIdKey), &tab_window, dispatcher)) { + // These are not the droids you are looking for. :-) + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + // We must reset the value and start from scratch in CreateTabValue. + // TODO(mad@chromium.org): We might be able to save a few steps if + // we support adding to existing value... Maybe... + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + result->set_value(NULL); + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); + return S_OK; +} + +bool GetAllTabsInWindowResult::Execute(BSTR tab_handles) { + // This is a list of tab_handles as it comes from the executor, not Chrome. + DCHECK(tab_handles); + scoped_ptr<ListValue> tabs_list; + if (!api_module_util::GetListFromJsonString(CW2A(tab_handles).m_psz, + &tabs_list)) { + NOTREACHED() << "Invalid tabs list BSTR: " << tab_handles; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + size_t num_values = tabs_list->GetSize(); + if (num_values % 2 != 0) { + // Values should come in pairs, one for the id and another one for the + // index. + NOTREACHED() << "Invalid tabs list BSTR: " << tab_handles; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + + // This will get populated by the calls to CreateTabValue in the loop below. + value_.reset(new ListValue()); + num_values /= 2; + for (size_t index = 0; index < num_values; ++index) { + int tab_handle = 0; + tabs_list->GetInteger(index * 2, &tab_handle); + int tab_index = -1; + tabs_list->GetInteger(index * 2 + 1, &tab_index); + int tab_id = dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle)); + DCHECK(tab_id != kInvalidChromeSessionId); + if (!CreateTabValue(tab_id, tab_index)) { + return false; + } + } + return true; +} + +void GetAllTabsInWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<GetAllTabsInWindowResult> result(CreateApiResult(request_id)); + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(args, NULL); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to get list of tabs."; + result->PostError("Internal Error while getting all tabs in window."); + return; + } + + long num_tabs = 0; + CComBSTR tab_handles; + HRESULT hr = executor->GetTabs(&tab_handles); + if (FAILED(hr)) { + DCHECK(tab_handles == NULL); + LOG(ERROR) << "Failed to get list of tabs for window: " << std::hex << + frame_window << ". " << com::LogHr(hr); + result->PostError("Internal Error while getting all tabs in window."); + return; + } + + // Execute posted an error if it returns false. + if (result->Execute(tab_handles)) + result->PostResult(); +} + +void UpdateTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = 0; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + if (window_utils::WindowHasNoThread(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + DictionaryValue* update_props = NULL; + if (!args.GetDictionary(1, &update_props)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + if (update_props->HasKey(ext::kUrlKey)) { + std::wstring url; + if (!update_props->GetString(ext::kUrlKey, &url)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to navigate tab."; + result->PostError("Internal error trying to update tab."); + return; + } + HRESULT hr = executor->Navigate(CComBSTR(url.c_str()), 0, + CComBSTR(L"_top")); + // Don't DCHECK here, see the comment at the bottom of + // CeeeExecutor::Navigate(). + if (FAILED(hr)) { + LOG(ERROR) << "Failed to navigate tab: " << std::hex << tab_id << + " to " << url << ". " << com::LogHr(hr); + result->PostError("Internal error trying to update tab."); + return; + } + } + + if (update_props->HasKey(ext::kSelectedKey)) { + bool selected = false; + if (!update_props->GetBoolean(ext::kSelectedKey, &selected)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // We only take action if the user wants to select the tab; this function + // does not actually let you deselect a tab. + if (selected) { + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(window_utils::GetTopLevelParent(tab_window), + IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get a frame executor to select tab."; + result->PostError("Internal error trying to select tab."); + return; + } + HRESULT hr = frame_executor->SelectTab( + reinterpret_cast<CeeeWindowHandle>(tab_window)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to select tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal error trying to select tab."); + return; + } + } + } + // TODO(mad@chromium.org): Check if we need to wait for the + // tabs.onUpdated event to make sure that the update was fully + // completed (e.g., Navigate above is async). + if (result->CreateTabValue(tab_id, -1)) + result->PostResult(); +} + +void RemoveTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(window_utils::GetTopLevelParent(tab_window), + IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get a frame executor to select tab."; + result->PostError("Internal error trying to select tab."); + return; + } + HRESULT hr = frame_executor->RemoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window)); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to remove tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal error trying to remove tab."); + return; + } + + // Now we must wait for the tab removal to be completely done before + // posting the response back to Chrome Frame. + // And we remember the tab identifier so that we can recognize the event. + result->SetValue(ext::kTabIdKey, Value::CreateIntegerValue(tab_id)); + dispatcher->RegisterEphemeralEventHandler(ext_event_names::kOnTabRemoved, + RemoveTab::ContinueExecution, + result.release()); +} + +HRESULT RemoveTab::ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + + scoped_ptr<ListValue> args_list; + int tab_id = 0; + if (!api_module_util::GetListAndIntegerValue(input_args, &args_list, + &tab_id)) { + NOTREACHED() << "Event arguments are not a list with an integer in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + const Value* saved_tab_value = result->GetValue(ext::kTabIdKey); + DCHECK(saved_tab_value != NULL && + saved_tab_value->IsType(Value::TYPE_INTEGER)); + int saved_tab_id = 0; + bool success = saved_tab_value->GetAsInteger(&saved_tab_id); + DCHECK(success && saved_tab_id != 0); + if (saved_tab_id == tab_id) { + // The tabs.remove callback doesn't have any arguments. + result->set_value(NULL); + result->PostResult(); + return S_OK; + } else { + // release doesn't destroy result, we need to keep it for next try. + result.release(); + return S_FALSE; // S_FALSE keeps us in the queue. + } +} + +void CreateTab::Execute(const ListValue& args, int request_id) { + // TODO(joi@chromium.org) Handle setting remaining tab properties + // ('title' and 'favIconUrl') if/when CE adds them (this is per a + // TODO for rafaelw@chromium.org in the extensions code). + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + DictionaryValue* input_dict = NULL; + if (!args.GetDictionary(0, &input_dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + bool specified = false; + HWND frame_window = result->GetSpecifiedOrCurrentFrameWindow(*input_dict, + &specified); + if (!frame_window) { + result->PostError(ext::kNoCurrentWindowError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + // In case the frame window wasn't specified, we must remember it for later + // use when we react to events below. + if (specified) { + int frame_window_id = dispatcher->GetWindowIdFromHandle(frame_window); + result->SetValue( + ext::kWindowIdKey, Value::CreateIntegerValue(frame_window_id)); + } + + std::string url_string(chrome::kAboutBlankURL); // default if no URL provided + if (input_dict->HasKey(ext::kUrlKey)) { + if (!input_dict->GetString(ext::kUrlKey, &url_string)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + GURL url(url_string); + if (!url.is_valid()) { + // TODO(joi@chromium.org) See if we can support absolute paths in IE (see + // extension_tabs_module.cc, AbsolutePath function and its uses) + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kInvalidUrlError, url_string)); + return; + } + // Remember the URL, we will use it to recognize the event below. + result->SetValue(ext::kUrlKey, Value::CreateStringValue(url_string)); + } + + bool selected = true; + if (input_dict->HasKey(ext::kSelectedKey)) { + if (!input_dict->GetBoolean(ext::kSelectedKey, &selected)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + } + + if (input_dict->HasKey(ext::kIndexKey)) { + int index = -1; + if (!input_dict->GetInteger(ext::kIndexKey, &index)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + result->SetValue(ext::kIndexKey, Value::CreateIntegerValue(index)); + } + + // We will have some work pending, even after we completed the tab creation, + // because the tab creation itself is asynchronous and we must wait for it + // to complete before we can post the complete result. + + // UNFORTUNATELY, this scheme doesn't work in protected mode for some reason. + // So bb2284073 & bb2492252 might still occur there. + std::wstring url_wstring = UTF8ToWide(url_string); + if (base::win::GetVersion() < base::win::VERSION_VISTA) { + CComPtr<IWebBrowser2> browser; + HRESULT hr = ie_util::GetWebBrowserForTopLevelIeHwnd( + frame_window, NULL, &browser); + DCHECK(SUCCEEDED(hr)) << "Can't get the browser for window: " << + frame_window; + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + long flags = selected ? navOpenInNewTab : navOpenInBackgroundTab; + hr = browser->Navigate(CComBSTR(url_wstring.c_str()), + &CComVariant(flags), + &CComVariant(L"_blank"), + &CComVariant(), // Post data + &CComVariant()); // Headers + DCHECK(SUCCEEDED(hr)) << "Failed to create tab. " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError("Internal error while trying to create tab."); + return; + } + } else { + // To create a new tab, we find an existing tab in the desired window (there + // is always at least one), and use it to navigate to a new tab. + HWND existing_tab = NULL; + bool success = window_utils::FindDescendentWindow( + frame_window, windows::kIeTabWindowClass, false, &existing_tab); + DCHECK(success && existing_tab != NULL) << + "Can't find an existing tab for" << frame_window; + + CComPtr<ICeeeTabExecutor> executor; + dispatcher->GetExecutor(existing_tab, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to create a tab."; + result->PostError("Internal error while trying to create tab."); + return; + } + + long flags = selected ? navOpenInNewTab : navOpenInBackgroundTab; + HRESULT hr = executor->Navigate(CComBSTR(url_wstring.c_str()), + flags, CComBSTR(L"_blank")); + // We can DCHECK here because navigating to a new tab shouldn't fail as + // described in the comment at the bottom of CeeeExecutor::Navigate(). + DCHECK(SUCCEEDED(hr)) << "Failed to create tab. " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError("Internal error while trying to create tab."); + return; + } + } + + // And now we must wait for the new tab to be created before we can respond. + dispatcher->RegisterEphemeralEventHandler( + ext_event_names::kOnTabCreated, + CreateTab::ContinueExecution, + // We don't want to destroy the result in the scoped_ptr when we pass + // it as user_data to CreateTab::ContinueExecution(). + result.release()); +} + +HRESULT CreateTab::ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(user_data != NULL); + DCHECK(dispatcher != NULL); + + scoped_ptr<TabApiResult> result(static_cast<TabApiResult*>(user_data)); + // Check if it has been created with the same info we were created for. + scoped_ptr<ListValue> args_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &args_list); + if (input_dict == NULL) { + DCHECK(false) << "Event arguments are not a list with a dictionary in it."; + result->PostError(api_module_constants::kInvalidArgumentsError); + return E_INVALIDARG; + } + + HWND tab_window = NULL; + if (!TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow(*input_dict, + result->GetValue(ext::kWindowIdKey), &tab_window, dispatcher)) { + // These are not the droids you are looking for. :-) + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + std::string event_url; + bool success = input_dict->GetString(ext::kUrlKey, &event_url); + DCHECK(success) << "The event MUST send a URL!!!"; + // if we didn't specify a URL, we should have navigated to about blank. + std::string requested_url(chrome::kAboutBlankURL); + // Ignore failures here, we fall back to the default about blank. + const Value* url_value = result->GetValue(ext::kUrlKey); + DCHECK(url_value != NULL && url_value->IsType(Value::TYPE_STRING)); + if (url_value != NULL && url_value->IsType(Value::TYPE_STRING)) { + bool success = url_value->GetAsString(&requested_url); + DCHECK(success) << "url_value->GetAsString() Failed!"; + } + + if (GURL(event_url) != GURL(requested_url)) { + result.release(); // The ApiDispatcher will keep it alive. + return S_FALSE; + } + + // We can't rely on selected, since it may have changed if + // another tab creation was made before we got to broadcast the completion + // of this one, so we will assume this one is ours. + + // Now move the tab to desired index if specified, we couldn't do it until we + // had a tab_id. + long destination_index = -1; + const Value* index_value = result->GetValue(ext::kIndexKey); + if (index_value != NULL) { + DCHECK(index_value->IsType(Value::TYPE_INTEGER)); + int destination_index_int = -1; + bool success = index_value->GetAsInteger(&destination_index_int); + DCHECK(success) << "index_value->GetAsInteger()"; + + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(frame_window, __uuidof(ICeeeWindowExecutor), + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal error while trying to move created tab."); + return E_UNEXPECTED; + } + + destination_index = static_cast<long>(destination_index_int); + HRESULT hr = frame_executor->MoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window), destination_index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to move tab: " << std::hex << tab_window << ". " << + com::LogHr(hr); + result->PostError("Internal error while trying to move created tab."); + return E_UNEXPECTED; + } + } + + // We must reset current state before calling CreateTabValue. + result->set_value(NULL); + // TODO(mad@chromium.org): Do we need to go through CreateTabValue? + // Maybe we already have enough info available to create the + // response??? + int tab_id = dispatcher->GetTabIdFromHandle(tab_window); + DCHECK(tab_id != kInvalidChromeSessionId); + if (result->CreateTabValue(tab_id, destination_index)) + result->PostResult(); + return S_OK; +} + +bool CreateTab::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + *converted_args = input_args; + + scoped_ptr<ListValue> input_list; + DictionaryValue* input_dict = + api_module_util::GetListAndDictionaryValue(input_args, &input_list); + if (input_dict == NULL) { + DCHECK(false) << "Input arguments are not a list with a dictionary in it."; + return false; + } + + // Check if we got the index, this would mean we already have all we need. + int int_value = -1; + if (input_dict->GetInteger(ext::kIndexKey, &int_value)) { + // We should also have all other non-optional values + DCHECK(input_dict->GetInteger(ext::kWindowIdKey, &int_value)); + bool bool_value = false; + DCHECK(input_dict->GetBoolean(ext::kSelectedKey, &bool_value)); + return false; + } + + // Get the complete tab info from the tab_handle coming from IE + int tab_handle = reinterpret_cast<int>(INVALID_HANDLE_VALUE); + bool success = input_dict->GetInteger(ext::kIdKey, &tab_handle); + DCHECK(success) << "Couldn't get the tab ID key from the input args."; + int tab_id = dispatcher->GetTabIdFromHandle( + reinterpret_cast<HWND>(tab_handle)); + DCHECK(tab_id != kInvalidChromeSessionId); + + TabApiResult result(TabApiResult::kNoRequestId); + if (result.CreateTabValue(tab_id, -1)) { + input_list->Set(0, result.value()->DeepCopy()); + base::JSONWriter::Write(input_list.get(), false, converted_args); + return true; + } else { + // Don't DCHECK, this can happen if we close the window while tabs are + // being created. + // TODO(mad@chromium.org): Find a way to DCHECK that the window is + // actually closing. + return false; + } +} + +void MoveTab::Execute(const ListValue& args, int request_id) { + scoped_ptr<TabApiResult> result(CreateApiResult(request_id)); + int tab_id = 0; + if (!args.GetInteger(0, &tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(tab_id); + if (!result->IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(tab_id))); + return; + } + + DictionaryValue* update_props = NULL; + if (!args.GetDictionary(1, &update_props)) { + NOTREACHED() << "Can't get update properties from dictionary argument"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + if (update_props->HasKey(ext::kWindowIdKey)) { + // TODO(joi@chromium.org) Move to shared constants file + result->PostError("Moving tabs between windows is not supported."); + return; + } + + int new_index = -1; + if (!update_props->GetInteger(ext::kIndexKey, &new_index)) { + NOTREACHED() << "Can't get tab index from update properties."; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + HWND frame_window = window_utils::GetTopLevelParent(tab_window); + + CComPtr<ICeeeWindowExecutor> frame_executor; + dispatcher->GetExecutor(frame_window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&frame_executor)); + if (frame_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal Error while trying to move tab."); + return; + } + + HRESULT hr = frame_executor->MoveTab( + reinterpret_cast<CeeeWindowHandle>(tab_window), new_index); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to move tab: " << std::hex << tab_id << ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to move tab."); + return; + } + if (result->CreateTabValue(tab_id, new_index)) + result->PostResult(); +} + +ApiDispatcher::InvocationResult* TabsInsertCode::ExecuteImpl( + const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr) { + scoped_ptr<ApiDispatcher::InvocationResult> result( + CreateApiResult(request_id)); + // TODO(ericdingle@chromium.org): This needs to support when NULL is + // sent in as the first parameter. + if (!args.GetInteger(0, tab_id)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return NULL; + } + + DictionaryValue* dict; + if (!args.GetDictionary(1, &dict)) { + result->PostError(api_module_constants::kInvalidArgumentsError); + return NULL; + } + + // The dictionary should have either a code property or a file property, + // but not both. + std::string code; + std::string file; + if (dict->HasKey(ext::kCodeKey) && dict->HasKey(ext::kFileKey)) { + result->PostError(ext::kMoreThanOneValuesError); + return NULL; + } else if (dict->HasKey(ext::kCodeKey)) { + dict->GetString(ext::kCodeKey, &code); + } else if (dict->HasKey(ext::kFileKey)) { + dict->GetString(ext::kFileKey, &file); + } else { + result->PostError(ext::kNoCodeOrFileToExecuteError); + return NULL; + } + + // All frames is optional. If not specified, the default value is false. + bool all_frames; + if (!dict->GetBoolean(ext::kAllFramesKey, &all_frames)) + all_frames = false; + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND tab_window = dispatcher->GetTabHandleFromId(*tab_id); + if (!TabApiResult::IsTabWindowClass(tab_window)) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kTabNotFoundError, base::IntToString(*tab_id))); + return NULL; + } + + CComPtr<ICeeeTabExecutor> tab_executor; + dispatcher->GetExecutor(tab_window, IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&tab_executor)); + if (tab_executor == NULL) { + LOG(WARNING) << "Failed to get an executor for the frame."; + result->PostError("Internal Error while trying to insert code in tab."); + return NULL; + } + + *hr = tab_executor->InsertCode(CComBSTR(code.c_str()), + CComBSTR(file.c_str()), + all_frames, + type); + return result.release(); +} + +void TabsExecuteScript::Execute(const ListValue& args, int request_id) { + int tab_id; + HRESULT hr = S_OK; + scoped_ptr<ApiDispatcher::InvocationResult> result( + TabsInsertCode::ExecuteImpl( + args, request_id, kCeeeTabCodeTypeJs, &tab_id, &hr)); + if (result.get() == NULL) + return; + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to execute script in tab: " << + std::hex << + tab_id << + ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to execute script in tab."); + } else { + result->PostResult(); + } +} + +void TabsInsertCSS::Execute(const ListValue& args, int request_id) { + int tab_id; + HRESULT hr = S_OK; + scoped_ptr<ApiDispatcher::InvocationResult> result( + TabsInsertCode::ExecuteImpl( + args, request_id, kCeeeTabCodeTypeCss, &tab_id, &hr)); + if (result.get() == NULL) + return; + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to insert CSS in tab: " << + std::hex << + tab_id << + ". " << + com::LogHr(hr); + result->PostError("Internal Error while trying to insert CSS in tab."); + } else { + result->PostResult(); + } +} + +TabInfo::TabInfo() { + url = NULL; + title = NULL; + status = kCeeeTabStatusLoading; + fav_icon_url = NULL; +} + +TabInfo::~TabInfo() { + Clear(); +} + +void TabInfo::Clear() { + // SysFreeString accepts NULL pointers. + ::SysFreeString(url); + url = NULL; + ::SysFreeString(title); + title = NULL; + ::SysFreeString(fav_icon_url); + fav_icon_url = NULL; + + status = kCeeeTabStatusLoading; +} + +} // namespace tab_api diff --git a/ceee/ie/broker/tab_api_module.h b/ceee/ie/broker/tab_api_module.h new file mode 100644 index 0000000..50c0f69 --- /dev/null +++ b/ceee/ie/broker/tab_api_module.h @@ -0,0 +1,148 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation. + +#ifndef CEEE_IE_BROKER_TAB_API_MODULE_H_ +#define CEEE_IE_BROKER_TAB_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +#include "toolband.h" //NOLINT + +namespace tab_api { + +class TabApiResult; +typedef ApiResultCreator<TabApiResult> TabApiResultCreator; + +// Registers all Tab API Invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class TabApiResult : public common_api::CommonApiResult { + public: + explicit TabApiResult(int request_id) + : common_api::CommonApiResult(request_id) { + } + + // Retrieves the frame window to use from the arguments provided, or the + // current frame window if none was specified in arguments. + // Will set @p specified to true if the window was specified and @p specified + // isn't NULL. It can be NULL if caller doesn't need to know. + virtual HWND GetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified); + + // Creates a value object with the information for a tab, as specified + // by the API definition. + // @param tab_id The ID of the tab we want the create a value for. + // @param index The index of the tab if already known, -1 otherwise. + // @return true for success, false for failure and will also call + // ApiDispatcher::InvocationResult::PostError on failures. + virtual bool CreateTabValue(int tab_id, long index); + + // Check if saved_dict has a specified frame window and if so compares it + // to the frame window that is contained in the given input_dict or the + // grand parent of the tab window found in the input_dict and returned in + // tab_window. + static bool IsTabFromSameOrUnspecifiedFrameWindow( + const DictionaryValue& input_dict, + const Value* saved_window_value, + HWND* tab_window, + ApiDispatcher* dispatcher); +}; + +class GetTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetSelectedTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +class GetAllTabsInWindowResult : public TabApiResult { + public: + explicit GetAllTabsInWindowResult(int request_id) : TabApiResult(request_id) { + } + + // Populates the result with all tabs in the given JSON encoded list. + virtual bool Execute(BSTR tab_handles); +}; + +class GetAllTabsInWindow + : public ApiResultCreator<GetAllTabsInWindowResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class UpdateTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class RemoveTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); +}; + +class CreateTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // We need to wait for the new tab to complete the result response. + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); + // And we also have a PermanentEventHandler too... + // To properly convert and complete the event arguments. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class MoveTab : public TabApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class TabsInsertCode : public ApiResultCreator<> { + protected: + // Will return NULL after calling PostError on the result upon failure. + ApiDispatcher::InvocationResult* ExecuteImpl(const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr); +}; + +class TabsExecuteScript : public TabsInsertCode { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class TabsInsertCSS : public TabsInsertCode { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Helper class to handle the data cleanup. +class TabInfo : public CeeeTabInfo { + public: + TabInfo(); + ~TabInfo(); + // Useful for reuse in unit tests. + void Clear(); +}; + +} // namespace tab_api + +#endif // CEEE_IE_BROKER_TAB_API_MODULE_H_ diff --git a/ceee/ie/broker/tab_api_module_unittest.cc b/ceee/ie/broker/tab_api_module_unittest.cc new file mode 100644 index 0000000..8f1f4df --- /dev/null +++ b/ceee/ie/broker/tab_api_module_unittest.cc @@ -0,0 +1,1452 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tab API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/json/json_writer.h" +#include "base/json/json_reader.h" +#include "base/win/windows_version.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +namespace { + +using tab_api::CreateTab; +using tab_api::GetAllTabsInWindow; +using tab_api::GetAllTabsInWindowResult; +using tab_api::GetSelectedTab; +using tab_api::GetTab; +using tab_api::MoveTab; +using tab_api::RegisterInvocations; +using tab_api::RemoveTab; +using tab_api::TabApiResult; +using tab_api::TabsInsertCode; +using tab_api::UpdateTab; + +using testing::_; +using testing::AddRef; +using testing::AtLeast; +using testing::CopyInterfaceToArgument; +using testing::CopyStringToArgument; +using testing::Gt; +using testing::InstanceCountMixin; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::Sequence; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::VariantPointerEq; + +const int kGoodTabWindowId = 99; +const int kGoodFrameWindowId = 88; +const int kBadWindowId = 666; + +const CeeeWindowHandle kGoodTabWindowHandle = kGoodTabWindowId + 1; +const CeeeWindowHandle kGoodFrameWindowHandle = kGoodFrameWindowId + 1; +const CeeeWindowHandle kBadWindowHandle = kBadWindowId + 1; + +const HWND kGoodTabWindow = reinterpret_cast<HWND>(kGoodTabWindowHandle); +const HWND kGoodFrameWindow = reinterpret_cast<HWND>(kGoodFrameWindowHandle); +const HWND kBadWindow = reinterpret_cast<HWND>(kBadWindowHandle); + +const int kTabIndex = 26; +const wchar_t kClassName[] = L"TabWindowClass"; +const int kRequestId = 43; + +TEST(TabApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(7)); + RegisterInvocations(&disp); +} + +class MockTabApiResult : public TabApiResult { + public: + explicit MockTabApiResult(int request_id) : TabApiResult(request_id) {} + MOCK_METHOD2(GetSpecifiedOrCurrentFrameWindow, HWND(const Value&, bool*)); + MOCK_METHOD2(CreateTabValue, bool(int, long)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + // Need to overload to bypass the mock. + HWND CallGetSpecifiedOrCurrentFrameWindow(const Value& args, + bool* specified) { + return TabApiResult::GetSpecifiedOrCurrentFrameWindow(args, specified); + } + bool CallCreateTabValue(int tab_id, long index) { + return TabApiResult::CreateTabValue(tab_id, index); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +// TODO(mad@chromium.org) TODO(hansl@google.com): Unify tests between +// {window,tab}_module_api. Create a base class for executor mocker +// tests, since we now have two very similar classes. Consider adding +// the cookie and infobar API tests too. +class TabApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockTabExecutor::CreateInitialized( + &mock_tab_executor_, &mock_tab_executor_keeper_)); + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor_, &mock_window_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_window_executor_keeper_.Release(); + mock_tab_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + void AlwaysMockGetTabExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void MockGetTabExecutorOnce(MockApiDispatcher* api_dispatcher, HWND window) { + // We can't use CopyInterfaceToArgument here because GetExecutor takes a + // void** argument instead of an interface. + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void MockGetWindowExecutorOnce(MockApiDispatcher* api_dispatcher, + HWND window) { + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillOnce(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + void AlwaysMockGetWindowExecutor(MockApiDispatcher* api_dispatcher, + HWND window) { + EXPECT_CALL(*api_dispatcher, GetExecutor(window, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockTabExecutor* mock_tab_executor_; + testing::MockWindowExecutor* mock_window_executor_; + + private: + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); + }; + // We should never get to the postman, we mock all the calls getting there. + // So we simply instantiate it strict and it will register itself as the + // one and only singleton to use all the time. + CComObjectStackEx< StrictMock< MockChromePostman > > postman_; + // To control the life span of the tab executor. + CComPtr<ICeeeTabExecutor> mock_tab_executor_keeper_; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; +}; + +TEST_F(TabApiTests, CreateTabValueErrorHandling) { + testing::LogDisabler no_dchecks; + + // Window with no thread. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillOnce(Return(true)); + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kGoodFrameWindow)). + WillRepeatedly(Return(kGoodFrameWindowId)); + + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Not an IE Frame. + invocation_result.set_value(NULL); + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillRepeatedly(Return(false)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(false)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // No Executor for the tab window. + invocation_result.set_value(NULL); + EXPECT_CALL(window_utils, GetTopLevelParent(_)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(user32_, IsWindowVisible(_)).WillRepeatedly(Return(FALSE)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Failing executor for the tab info. + invocation_result.set_value(NULL); + MockGetTabExecutorOnce(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // No Executor for the frame window. + // We only mock once at a time so that the executor for the tab doesn't get + // confused by the one for the frame window, or vice versa. + invocation_result.set_value(NULL); + MockGetTabExecutorOnce(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); + + // Failing executor for the tab index. + invocation_result.set_value(NULL); + AlwaysMockGetWindowExecutor(&invocation_result.mock_api_dispatcher_, + kGoodFrameWindow); + AlwaysMockGetTabExecutor(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + EXPECT_CALL(*mock_window_executor_, GetTabIndex(kGoodTabWindowHandle, + NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateTabValue(kGoodTabWindowId, -1)); +} + +TEST_F(TabApiTests, CreateTabValue) { + // Setup all we need from other for success. + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, WindowHasNoThread(kGoodTabWindow)). + WillRepeatedly(Return(false)); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, GetTopLevelParent(_)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // Try a few different combination or parameters. + struct { + bool window_selected; + const char* status; + const char* fav_icon_url; + const char* title; + const char* url; + int index; + } tests[] = { + { false, ext::kStatusValueLoading, NULL, "Mine", "http://icietla", 1 }, + { false, ext::kStatusValueComplete, "Here", "Yours", "https://secure", 32 }, + { true, ext::kStatusValueLoading, "There", "There's", "Just a string", -1 }, + { true, ext::kStatusValueComplete, NULL, "Unknown", "Boo!!!", -1 }, + }; + + for (int i = 0; i < arraysize(tests); ++i) { + EXPECT_CALL(user32_, IsWindowVisible(_)).WillOnce( + Return(tests[i].window_selected ? TRUE : FALSE)); + CeeeTabInfo tab_info; + // The allocation within the tab_info will be freed by the caller of + // GetTabInfo which we mock below... + tab_info.url = ::SysAllocString(CA2W(tests[i].url).m_psz); + tab_info.title = ::SysAllocString(CA2W(tests[i].title).m_psz); + + if (strcmp(tests[i].status, ext::kStatusValueLoading) == 0) + tab_info.status = kCeeeTabStatusLoading; + else + tab_info.status = kCeeeTabStatusComplete; + + if (tests[i].fav_icon_url) { + tab_info.fav_icon_url = + ::SysAllocString(CA2W(tests[i].fav_icon_url).m_psz); + } else { + tab_info.fav_icon_url = NULL; + } + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + AlwaysMockGetTabExecutor(&invocation_result.mock_api_dispatcher_, + kGoodTabWindow); + AlwaysMockGetWindowExecutor(&invocation_result.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_tab_executor_, GetTabInfo(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_info), Return(S_OK))); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kGoodFrameWindow)). + WillOnce(Return(kGoodFrameWindowId)); + + if (tests[i].index == -1) { + EXPECT_CALL(*mock_window_executor_, GetTabIndex(kGoodTabWindowHandle, + NotNull())).WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), + Return(S_OK))); + } + if (i % 2 == 0) + invocation_result.set_value(new ListValue); + EXPECT_TRUE(invocation_result.CallCreateTabValue( + kGoodTabWindowId, tests[i].index)); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + const Value* result = NULL; + if (i % 2 == 0) { + EXPECT_TRUE(invocation_result.value()->IsType(Value::TYPE_LIST)); + const ListValue* list = static_cast<const ListValue*>( + invocation_result.value()); + Value * non_const_result = NULL; + EXPECT_TRUE(list->Get(0, &non_const_result)); + result = non_const_result; + } else { + result = invocation_result.value(); + } + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + const DictionaryValue* dict = static_cast<const DictionaryValue*>(result); + + int tab_id; + EXPECT_TRUE(dict->GetInteger(ext::kIdKey, &tab_id)); + EXPECT_EQ(kGoodTabWindowId, tab_id); + + int window_id; + EXPECT_TRUE(dict->GetInteger(ext::kWindowIdKey, &window_id)); + EXPECT_EQ(kGoodFrameWindowId, window_id); + + bool selected = false; + EXPECT_TRUE(dict->GetBoolean(ext::kSelectedKey, &selected)); + EXPECT_EQ(tests[i].window_selected, selected); + + std::string url; + EXPECT_TRUE(dict->GetString(ext::kUrlKey, &url)); + EXPECT_STREQ(tests[i].url, url.c_str()); + + std::string title; + EXPECT_TRUE(dict->GetString(ext::kTitleKey, &title)); + EXPECT_STREQ(tests[i].title, title.c_str()); + + std::string status; + EXPECT_TRUE(dict->GetString(ext::kStatusKey, &status)); + EXPECT_STREQ(tests[i].status, status.c_str()); + + std::string fav_icon_url; + if (tests[i].fav_icon_url == NULL) { + EXPECT_FALSE(dict->GetString(ext::kFavIconUrlKey, &fav_icon_url)); + } else { + EXPECT_TRUE(dict->GetString(ext::kFavIconUrlKey, &fav_icon_url)); + EXPECT_STREQ(tests[i].fav_icon_url, fav_icon_url.c_str()); + } + + int index = -1; + EXPECT_TRUE(dict->GetInteger(ext::kIndexKey, &index)); + if (tests[i].index == -1) + EXPECT_EQ(kTabIndex, index); + else + EXPECT_EQ(tests[i].index, index); + } +} + +TEST(TabApi, IsTabWindowClassWithNull) { + testing::LogDisabler no_dchecks; + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(NULL)); +} + +TEST(TabApi, IsTabWindowClassWithNonWindow) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillRepeatedly(Return(FALSE)); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, IsTabWindowClassWrongClassName) { + const wchar_t kBadClassName[] = L"BadWindowClass"; + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kBadClassName), + Return(arraysize(kBadClassName)))); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, IsTabWindowClassStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + TabApiResult invocation_result(TabApiResult::kNoRequestId); + EXPECT_TRUE(invocation_result.IsTabWindowClass(reinterpret_cast<HWND>(1))); +} + +TEST(TabApi, GetSpecifiedOrCurrentFrameWindow) { + testing::LogDisabler no_dchecks; + + StrictMock<testing::MockWindowUtils> window_utils; + scoped_ptr<Value> bad_args(Value::CreateRealValue(4.7)); + DictionaryValue bad_dict_args; // no window ID key. + DictionaryValue good_dict_args; + int window1_id = 77; + HWND window1 = reinterpret_cast<HWND>(window1_id); + good_dict_args.SetInteger(ext::kWindowIdKey, window1_id); + + StrictMock<MockTabApiResult> invocation_result(TabApiResult::kNoRequestId); + + // First, fail because no method finds us a window. + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)) + .WillOnce(Return(false)); + HWND null_window = NULL; + EXPECT_EQ(null_window, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), NULL)); + + // Then, fail because we find a window that's not a window. + HWND window2 = reinterpret_cast<HWND>(88); + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, _)).WillRepeatedly( + DoAll(SetArgumentPointee<3>(window2), Return(true))); + EXPECT_CALL(window_utils, IsWindowClass(window2, _)).WillOnce(Return(false)); + EXPECT_EQ(null_window, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), NULL)); + + // From now on, all windows are valid. + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + + // So succeed for once - we're using null args so we get the + // window from the FindDescendentWindow. + bool specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + ListValue(), &specified)); + EXPECT_FALSE(specified); + + ListValue good_list_with_null; + ASSERT_TRUE(good_list_with_null.Set(0, Value::CreateNullValue())); + specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_null, &specified)); + EXPECT_FALSE(specified); + + ListValue good_list_with_null_dict; + ASSERT_TRUE(good_list_with_null_dict.Set(0, new DictionaryValue())); + specified = true; + EXPECT_EQ(window2, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_null_dict, &specified)); + EXPECT_FALSE(specified); + + // From now on, we can expect those to always return consistent values. + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(window1_id)).WillRepeatedly(Return(window1)); + + // Get window from good args. + scoped_ptr<Value> good_int_args(Value::CreateIntegerValue(kGoodTabWindowId)); + ListValue good_list_with_int; + ASSERT_TRUE(good_list_with_int.Set(0, Value::CreateIntegerValue(window1_id))); + ListValue good_list_with_dict; + ASSERT_TRUE(good_list_with_dict.Set(0, good_dict_args.DeepCopy())); + + EXPECT_EQ(kGoodTabWindow, invocation_result. + CallGetSpecifiedOrCurrentFrameWindow(*good_int_args.get(), &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_dict_args, &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_int, &specified)); + EXPECT_TRUE(specified); + specified = false; + EXPECT_EQ(window1, invocation_result.CallGetSpecifiedOrCurrentFrameWindow( + good_list_with_dict, &specified)); + EXPECT_TRUE(specified); +} + +TEST_F(TabApiTests, GetTab) { + testing::LogDisabler no_dchecks; + + ListValue good_args; + ASSERT_TRUE(good_args.Set(0, Value::CreateIntegerValue(kGoodTabWindowId))); + ListValue bad_window; + ASSERT_TRUE(bad_window.Set(0, Value::CreateIntegerValue(kBadWindowId))); + + // Mocking IsTabWindowClass. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, IsWindowClass(kBadWindow, _)). + WillRepeatedly(Return(false)); + + StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, GetTab> > + invocation; + invocation.AllocateNewResult(kRequestId); + + // Start with success. + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // No more successful calls. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kBadWindowId)).WillOnce(Return(kBadWindow)); + invocation.Execute(bad_window, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(TabApiTests, GetSelectedTab) { + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, GetSelectedTab> > + invocation; + // No frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce( + Return(static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Success + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(true), + Return(kGoodFrameWindow))); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow( + kGoodFrameWindow, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<3>(kGoodTabWindow), Return(true))); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + invocation.Execute(ListValue(), kRequestId); + + // TODO(mad@chromium.org): Try the async case. +} + +class MockGetAllTabsInWindowResult : public GetAllTabsInWindowResult { + public: + explicit MockGetAllTabsInWindowResult(int request_id) + : GetAllTabsInWindowResult(request_id) {} + MOCK_METHOD2(CreateTabValue, bool(int, long)); + MOCK_METHOD2(GetSpecifiedOrCurrentFrameWindow, HWND(const Value&, bool*)); + MOCK_METHOD1(Execute, bool(BSTR)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + bool CallExecute(BSTR tabs) { + return GetAllTabsInWindowResult::Execute(tabs); + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +TEST_F(TabApiTests, GetAllTabsInWindowResult) { + testing::LogDisabler no_dchecks; + StrictMock<MockGetAllTabsInWindowResult> invocation_result( + TabApiResult::kNoRequestId); + + // Start with a few failure cases. + // Not a proper JSON string. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"Bla bla bla"); + invocation_result.set_value(NULL); + + // Proper JSON string, not being a list. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"{}"); + invocation_result.set_value(NULL); + + // Wrong number of elements in the list. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + invocation_result.CallExecute(L"[23]"); + invocation_result.set_value(NULL); + + // Empty is valid yet doesn't get CreateTabValue called. + EXPECT_TRUE(invocation_result.CallExecute(L"[]")); + invocation_result.set_value(NULL); + + // Successes. + int index = 24; + EXPECT_CALL(invocation_result, CreateTabValue(kGoodTabWindowId, index)). + WillOnce(Return(true)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + { + std::wostringstream args; + args << L"[" << kGoodTabWindowHandle << L"," << index << L"]"; + EXPECT_TRUE(invocation_result.CallExecute((BSTR)(args.str().c_str()))); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + invocation_result.set_value(NULL); + } + + { + std::wostringstream args; + Sequence seq; + struct id_map_struct { + int id; + HWND handle; + int value; + } const test_id_map[] = { + { 1, reinterpret_cast<HWND>(2), 3 }, + { 4, reinterpret_cast<HWND>(5), 6 }, + { 7, reinterpret_cast<HWND>(8), 9 }, + { 0 } + }; + + // Build the JSON list from the above test map. We also want to expect the + // correct underlying calls. + args << L"["; + for (int i = 0; test_id_map[i].id != 0; ++i) { + const id_map_struct& item = test_id_map[i]; + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetTabIdFromHandle(item.handle)).WillOnce(Return(item.id)); + EXPECT_CALL(invocation_result, CreateTabValue(item.id, item.value)). + InSequence(seq).WillOnce(Return(true)); + if (i) + args << L","; + args << reinterpret_cast<int>(item.handle) << L"," << item.value; + } + args << L"]"; + EXPECT_TRUE(invocation_result.CallExecute((BSTR)(args.str().c_str()))); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + } +} + +TEST_F(TabApiTests, GetAllTabsInWindow) { + testing::LogDisabler no_dchecks; + StrictMock<MockApiInvocation<GetAllTabsInWindowResult, + MockGetAllTabsInWindowResult, GetAllTabsInWindow>> invocation; + + // No frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce( + Return(static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // No Executor. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // Failing Executor. + // The executor classes are already strict from their base class impl. + testing::MockWindowExecutor* mock_window_executor; + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor, &mock_window_executor_keeper_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<2>( + mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Successes. + invocation.AllocateNewResult(kRequestId); + BSTR tab_ids = ::SysAllocString(L""); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + EXPECT_CALL(*mock_window_executor, GetTabs(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(tab_ids), Return(S_OK))); + EXPECT_CALL(*invocation.invocation_result_, Execute(tab_ids)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); + // Execute freed tab_ids... + tab_ids = NULL; +} + +// TODO(mad@chromium.org): Test the event handling. +class MockUpdateTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + UpdateTab> > { +}; + +TEST_F(TabApiTests, UpdateTab) { + ListValue bad_window; + bad_window.Append(Value::CreateIntegerValue(kBadWindowId)); + + ListValue wrong_second_arg; + wrong_second_arg.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_second_arg.Append(Value::CreateStringValue(L"Angus")); + + ListValue wrong_url_type; + DictionaryValue* wrong_url_type_dictionary = new DictionaryValue(); + wrong_url_type_dictionary->SetInteger(ext::kUrlKey, 1); + wrong_url_type.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_url_type.Append(wrong_url_type_dictionary); + + ListValue wrong_selected_type; + DictionaryValue* wrong_selected_type_dictionary = new DictionaryValue(); + wrong_selected_type_dictionary->SetString(ext::kSelectedKey, L"yes"); + wrong_selected_type.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + wrong_selected_type.Append(wrong_selected_type_dictionary); + + ListValue good_url; + DictionaryValue* good_url_dictionary = new DictionaryValue(); + good_url_dictionary->SetString(ext::kUrlKey, "http://google.com"); + good_url.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + good_url.Append(good_url_dictionary); + + ListValue selected_true; + DictionaryValue* selected_true_dictionary = new DictionaryValue(); + selected_true_dictionary->SetBoolean(ext::kSelectedKey, true); + selected_true.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + selected_true.Append(selected_true_dictionary); + + testing::LogDisabler no_dchecks; + + MockUpdateTab invocation; + ListValue empty_list; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(empty_list, kRequestId); + + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kBadWindowId)).WillRepeatedly(Return(kBadWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + + // Not an IeServerClass. + invocation.AllocateNewResult(kRequestId); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(kBadWindow, _)). + WillOnce(Return(false)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(bad_window, kRequestId); + + // Window has no thread. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, WindowHasNoThread(kBadWindow)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(bad_window, kRequestId); + + EXPECT_CALL(window_utils, WindowHasNoThread(_)).WillRepeatedly(Return(false)); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_second_arg, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_url_type, kRequestId); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(wrong_selected_type, kRequestId); + + // Can't get an executor for Navigate. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_url, kRequestId); + + // Failing Executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_url, kRequestId); + + // Can't get an executor for SelectTab. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(selected_true, kRequestId); + + // Failing Executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, SelectTab(kGoodTabWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(selected_true, kRequestId); + + // Successful Navigate. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(good_url, kRequestId); + + // Successful SelectTab. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, SelectTab(kGoodTabWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(selected_true, kRequestId); +} + +class MockRemoveTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + RemoveTab> > { +}; + +TEST_F(TabApiTests, RemoveTabExecute) { + testing::LogDisabler no_dchecks; + + MockRemoveTab invocation; + ListValue list_args; + + // Bad arguments. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_args, kRequestId); + + // Not a IeServerClass. + invocation.AllocateNewResult(kRequestId); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(false)); + + list_args.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Fail to get the executor. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, NotNull())). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Failing executor. + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, RemoveTab(kGoodTabWindowHandle)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(list_args, kRequestId); + + // Success. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, RemoveTab(kGoodTabWindowHandle)). + WillOnce(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnTabRemoved), + RemoveTab::ContinueExecution, + invocation.invocation_result_.get())); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + // This will cause the invocation result to be released at the end of the + // test since the success case purposely does not delete it. + scoped_ptr<ApiDispatcher::InvocationResult> result( + invocation.invocation_result_.get()); + invocation.Execute(list_args, kRequestId); +} + +TEST_F(TabApiTests, RemoveTabContinueExecution) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockRemoveTab invocation; + + ListValue list_value; + list_value.Append(Value::CreateIntegerValue(kGoodTabWindowId)); + std::string input_args; + base::JSONWriter::Write(&list_value, false, &input_args); + + // Bad arguments. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + HRESULT hr = invocation.CallContinueExecution(""); + EXPECT_HRESULT_FAILED(hr); + + // Mismatched tab IDs. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kTabIdKey, Value::CreateIntegerValue(1234)); + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kTabIdKey, Value::CreateIntegerValue(kGoodTabWindowId)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +// Mock the CoCreateInstance call that is used to create a new IE window. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetWebBrowserForTopLevelIeHwnd, + GetWebBrowserForTopLevelIeHwnd); + MOCK_STATIC_INIT_END() + MOCK_STATIC3(HRESULT, , GetWebBrowserForTopLevelIeHwnd, + HWND, IWebBrowser2*, IWebBrowser2**); +MOCK_STATIC_CLASS_END(MockIeUtil) + +// Mock static calls to TabApiResult. +MOCK_STATIC_CLASS_BEGIN(MockStaticTabApiResult) + MOCK_STATIC_INIT_BEGIN(MockStaticTabApiResult) + MOCK_STATIC_INIT2(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow, + IsTabFromSameOrUnspecifiedFrameWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC4(bool, , IsTabFromSameOrUnspecifiedFrameWindow, + const DictionaryValue&, const Value*, HWND*, ApiDispatcher*); +MOCK_STATIC_CLASS_END(MockStaticTabApiResult) + +// TODO(mad@chromium.org): Test the asynchronicity and the event handling. +class MockCreateTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + CreateTab> > { +}; + +TEST_F(TabApiTests, CreateTabExecute) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockCreateTab invocation; + + ListValue list_value; + DictionaryValue* list_dict = new DictionaryValue; + // list_dict will be freed by ListValue, yet we need to keep it around + // to set values into it. + list_value.Append(list_dict); + + // No Frame window. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)).WillOnce(Return( + static_cast<HWND>(NULL))); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Can't get Executor/IWebBrowser2. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillRepeatedly(Return(kGoodFrameWindow)); + StrictMock<MockIeUtil> mock_ie_util; + StrictMock<testing::MockWindowUtils> mock_window_utils; + bool pre_vista = base::win::GetVersion() < base::win::VERSION_VISTA; + if (pre_vista) { + EXPECT_CALL(mock_ie_util, GetWebBrowserForTopLevelIeHwnd( + kGoodFrameWindow, _, NotNull())).WillOnce(Return(E_FAIL)); + } else { + EXPECT_CALL(mock_window_utils, FindDescendentWindow( + kGoodFrameWindow, _, _, NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<3>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + } + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Navigate Fails. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance( + &browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + if (pre_vista) { + EXPECT_CALL(mock_ie_util, GetWebBrowserForTopLevelIeHwnd( + kGoodFrameWindow, _, NotNull())).WillRepeatedly(DoAll( + CopyInterfaceToArgument<2>(browser_keeper.p), Return(S_OK))); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + } else { + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(E_FAIL)); + } + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + invocation.Execute(list_value, kRequestId); + + // Success! + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, + GetSpecifiedOrCurrentFrameWindow(_, _)). + WillOnce(Return(kGoodFrameWindow)); + if (pre_vista) { + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(S_OK)); + } else { + EXPECT_CALL(*mock_tab_executor_, Navigate(_, _, _)). + WillOnce(Return(S_OK)); + } + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnTabCreated), + CreateTab::ContinueExecution, + invocation.invocation_result_.get())); + + // This will cause the invocation result to be released at the end of the + // test since the success case purposely does not delete it. + scoped_ptr<ApiDispatcher::InvocationResult> result( + invocation.invocation_result_.get()); + invocation.Execute(list_value, kRequestId); +} + +TEST_F(TabApiTests, CreateTabContinueExecution) { + testing::LogDisabler no_dchecks; + + // Failure cases. + MockCreateTab invocation; + const char kUrl[] = "http://url/"; + const int kIndex = 0; + + ListValue list_value; + DictionaryValue* dict_value = new DictionaryValue; + list_value.Append(dict_value); + dict_value->SetInteger(ext::kWindowIdKey, kGoodTabWindowId); + dict_value->SetString(ext::kUrlKey, kUrl); + dict_value->SetInteger(ext::kIndexKey, kIndex); + + std::string input_args; + base::JSONWriter::Write(&list_value, false, &input_args); + + // No input dictionary. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)); + HRESULT hr = invocation.CallContinueExecution(""); + EXPECT_HRESULT_FAILED(hr); + + // IsTabFromSameOrUnspecifiedFrameWindow returns false. + invocation.AllocateNewResult(kRequestId); + StrictMock<MockStaticTabApiResult> tab_api_result; + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(Return(false)); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Mistmatched URLs. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kUrlKey, Value::CreateStringValue("http://other/")); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(Return(true)); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success with no index. + invocation.AllocateNewResult(kRequestId); + invocation.invocation_result_->SetValue( + ext::kUrlKey, Value::CreateStringValue(kUrl)); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, -1)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success with an index. + invocation.AllocateNewResult(kRequestId); + ApiDispatcher::InvocationResult* result = invocation.invocation_result_.get(); + result->SetValue(ext::kUrlKey, Value::CreateStringValue(kUrl)); + result->SetValue(ext::kIndexKey, Value::CreateIntegerValue(kIndex)); + + EXPECT_CALL(tab_api_result, + IsTabFromSameOrUnspecifiedFrameWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<2>(kGoodTabWindow), Return(true))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabIdFromHandle(kGoodTabWindow)).WillOnce(Return(kGoodTabWindowId)); + + StrictMock<testing::MockWindowUtils> window_utils; + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillOnce(Return(kGoodTabWindow)); + + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kIndex)). + WillOnce(Return(S_OK)); + + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, kIndex)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()); + + hr = invocation.CallContinueExecution(input_args); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +class MockMoveTab : public StrictMock<MockApiInvocation<TabApiResult, + MockTabApiResult, + MoveTab> > { +}; + +TEST_F(TabApiTests, MoveTab) { + testing::LogDisabler no_dchecks; + + // Empty list is not valid. + StrictMock<testing::MockWindowUtils> window_utils; + MockMoveTab invocation; + ListValue good_args; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // First entry should be an Integer. + invocation.AllocateNewResult(kRequestId); + DictionaryValue* good_args_dict = new DictionaryValue(); + good_args.Append(good_args_dict); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Good arg format, but not an IE Frame. + invocation.AllocateNewResult(kRequestId); + EXPECT_TRUE(good_args.Insert(0, Value::CreateIntegerValue(kGoodTabWindowId))); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillOnce(Return(false)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Wrong second list value. + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_TRUE(good_args.Insert(1, Value::CreateNullValue())); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Unsupported window id key. + invocation.AllocateNewResult(kRequestId); + good_args.Remove(1, NULL); + good_args_dict->SetInteger(ext::kWindowIdKey, kBadWindowId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Wrong format for index key + invocation.AllocateNewResult(kRequestId); + good_args_dict->Remove(ext::kWindowIdKey, NULL); + good_args_dict->SetBoolean(ext::kIndexKey, false); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Can't get an executor + invocation.AllocateNewResult(kRequestId); + good_args_dict->SetInteger(ext::kIndexKey, kTabIndex); + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodFrameWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(good_args, kRequestId); + + // Get the Executor to fail + invocation.AllocateNewResult(kRequestId); + AlwaysMockGetWindowExecutor(&invocation.mock_api_dispatcher_, + kGoodFrameWindow); + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kTabIndex)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); + + // Success + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*mock_window_executor_, + MoveTab(reinterpret_cast<CeeeWindowHandle>(kGoodTabWindow), kTabIndex)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*invocation.invocation_result_, + CreateTabValue(kGoodTabWindowId, kTabIndex)). + WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.Execute(good_args, kRequestId); +} + +class MockTabsInsertCode + : public StrictMock<MockApiInvocation<TabApiResult, MockTabApiResult, + TabsInsertCode> > { + public: + // Overloaded to change the type of the return value for easier testing. + StrictMock<MockTabApiResult>* CallExecuteImpl(const ListValue& args, + int request_id, + CeeeTabCodeType type, + int* tab_id, + HRESULT* hr) { + return static_cast<StrictMock<MockTabApiResult>*>(ExecuteImpl( + args, request_id, type, tab_id, hr)); + } + // Declare this so that this is not an abstract class. + virtual void Execute(const ListValue& args, int request_id) {} +}; + +TEST_F(TabApiTests, TabsInsertCode) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + int tab_id; + HRESULT hr; + + // Empty list is not valid. + MockTabsInsertCode invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + ListValue(), kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // First parameter should be an Integer. + ListValue good_args; + DictionaryValue* good_args_dict = new DictionaryValue(); + good_args.Append(good_args_dict); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but no values in dictionary. + EXPECT_TRUE(good_args.Insert(0, Value::CreateIntegerValue(kGoodTabWindowId))); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but both code and file values in dictionary. + good_args_dict->SetString(ext::kCodeKey, "alert(5);"); + good_args_dict->SetString(ext::kFileKey, "test.js"); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Good arg format, but not an IE Frame. + good_args_dict->Remove(ext::kFileKey, NULL); + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillOnce(Return(false)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Can't get an executor. + EXPECT_CALL(window_utils, IsWindowClass(kGoodTabWindow, _)). + WillRepeatedly(Return(true)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kGoodTabWindow, _, _)). + WillOnce(SetArgumentPointee<2>(static_cast<void*>(NULL))); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + EXPECT_EQ(NULL, invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + + // Executor failure, all_frames defaulted to false. + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, + InsertCode(_, _, false, kCeeeTabCodeTypeCss)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + scoped_ptr<StrictMock<MockTabApiResult>> result(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + EXPECT_EQ(kGoodTabWindowId, tab_id); + EXPECT_EQ(E_FAIL, hr); + + // Success, all_frames defaulted to false. + AlwaysMockGetTabExecutor(&invocation.mock_api_dispatcher_, kGoodTabWindow); + EXPECT_CALL(*mock_tab_executor_, + InsertCode(_, _, false, kCeeeTabCodeTypeCss)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + result.reset(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeCss, &tab_id, &hr)); + EXPECT_EQ(tab_id, kGoodTabWindowId); + EXPECT_HRESULT_SUCCEEDED(hr); + + // Success, set all_frames to true. + good_args_dict->SetBoolean(ext::kAllFramesKey, true); + EXPECT_CALL(*mock_tab_executor_, InsertCode(_, _, true, + kCeeeTabCodeTypeJs)).WillOnce(Return(S_OK)); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetTabHandleFromId(kGoodTabWindowId)).WillOnce(Return(kGoodTabWindow)); + invocation.AllocateNewResult(kRequestId); + result.reset(invocation.CallExecuteImpl( + good_args, kRequestId, kCeeeTabCodeTypeJs, &tab_id, &hr)); + EXPECT_EQ(tab_id, kGoodTabWindowId); + EXPECT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(TabApiTests, IsTabFromSameOrUnspecifiedFrameWindow) { + // We need a mock for this now. + StrictMock<MockApiDispatcher> mock_api_dispatcher; + + // We expect these calls repeatedly + EXPECT_CALL(mock_api_dispatcher, GetTabHandleFromId(kGoodTabWindowId)). + WillRepeatedly(Return(kGoodTabWindow)); + EXPECT_CALL(mock_api_dispatcher, GetWindowIdFromHandle(kGoodFrameWindow)). + WillRepeatedly(Return(kGoodFrameWindowId)); + EXPECT_CALL(mock_api_dispatcher, GetWindowHandleFromId(kGoodFrameWindowId)). + WillRepeatedly(Return(kGoodFrameWindow)); + + // We always need a kIdKey value in the input_dict. + DictionaryValue input_dict; + input_dict.SetInteger(ext::kIdKey, kGoodTabWindowId); + // Start with no saved dict, so any input value is good. + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, NULL, NULL, &mock_api_dispatcher)); + // Also test that we are properly returned the input value. + HWND tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, NULL, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // Now check with the same value found as a grand parent. + FundamentalValue saved_window(kGoodFrameWindowId); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, GetTopLevelParent(kGoodTabWindow)). + WillRepeatedly(Return(kGoodFrameWindow)); + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // Now check with the same value provided in the input_dict. + input_dict.SetInteger(ext::kWindowIdKey, kGoodFrameWindowId); + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_TRUE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + // And now check the cases where they differ. + FundamentalValue other_saved_window(kGoodFrameWindowId + 1); + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); + + input_dict.Remove(ext::kWindowIdKey, NULL); + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, NULL, &mock_api_dispatcher)); + tab_window = NULL; + EXPECT_FALSE(TabApiResult::IsTabFromSameOrUnspecifiedFrameWindow( + input_dict, &other_saved_window, &tab_window, &mock_api_dispatcher)); + EXPECT_EQ(kGoodTabWindow, tab_window); +} + +} // namespace diff --git a/ceee/ie/broker/webnavigation_api_module.cc b/ceee/ie/broker/webnavigation_api_module.cc new file mode 100644 index 0000000..e35bc51 --- /dev/null +++ b/ceee/ie/broker/webnavigation_api_module.cc @@ -0,0 +1,40 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebNavigation implementation that needs to reside on the broker +// side. + +#include "ceee/ie/broker/webnavigation_api_module.h" + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_util.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" + +namespace ext = extension_webnavigation_api_constants; + +namespace webnavigation_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { + // Register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeNavigate, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeRetarget, + api_module_util::ConvertTabIdInDictionary<ext::kSourceTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnCommitted, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnCompleted, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnDOMContentLoaded, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); + dispatcher->RegisterPermanentEventHandler( + ext::kOnErrorOccurred, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); +} + +} // namespace webnavigation_api diff --git a/ceee/ie/broker/webnavigation_api_module.h b/ceee/ie/broker/webnavigation_api_module.h new file mode 100644 index 0000000..5f7c24e --- /dev/null +++ b/ceee/ie/broker/webnavigation_api_module.h @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebNavigation implementation that needs to reside on the broker +// side. + +#ifndef CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ +#define CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ + +class ApiDispatcher; + +namespace webnavigation_api { + +// Registers permanent event handlers to convert tab window handles in event +// arguments into actual tab IDs. +void RegisterInvocations(ApiDispatcher* dispatcher); + +} // namespace webnavigation_api + +#endif // CEEE_IE_BROKER_WEBNAVIGATION_API_MODULE_H_ diff --git a/ceee/ie/broker/webrequest_api_module.cc b/ceee/ie/broker/webrequest_api_module.cc new file mode 100644 index 0000000..77c1177 --- /dev/null +++ b/ceee/ie/broker/webrequest_api_module.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebRequest implementation that needs to reside on the broker +// side. + +#include "ceee/ie/broker/webrequest_api_module.h" + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/api_module_util.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" + +namespace ext = extension_webrequest_api_constants; + +namespace webrequest_api { + +void RegisterInvocations(ApiDispatcher* dispatcher) { + // Register the permanent event handler. + dispatcher->RegisterPermanentEventHandler( + ext::kOnBeforeRequest, + api_module_util::ConvertTabIdInDictionary<ext::kTabIdKey>); +} + +} // namespace webrequest_api diff --git a/ceee/ie/broker/webrequest_api_module.h b/ceee/ie/broker/webrequest_api_module.h new file mode 100644 index 0000000..9ace96c --- /dev/null +++ b/ceee/ie/broker/webrequest_api_module.h @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// The part of WebRequest implementation that needs to reside on the broker +// side. + +#ifndef CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ +#define CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ + +class ApiDispatcher; + +namespace webrequest_api { + +// Registers permanent event handlers to convert tab window handles in event +// arguments into actual tab IDs. +void RegisterInvocations(ApiDispatcher* dispatcher); + +} // namespace webrequest_api + +#endif // CEEE_IE_BROKER_WEBREQUEST_API_MODULE_H_ diff --git a/ceee/ie/broker/window_api_module.cc b/ceee/ie/broker/window_api_module.cc new file mode 100644 index 0000000..2115163 --- /dev/null +++ b/ceee/ie/broker/window_api_module.cc @@ -0,0 +1,639 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation. +// +// Window IDs are the HWND of the top-level frame window of IE. + +#include "ceee/ie/broker/window_api_module.h" + +#include <atlbase.h> +#include <iepmapi.h> +#include <sddl.h> +#include <set> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/broker/api_module_constants.h" +#include "ceee/ie/broker/api_module_util.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/api_registration.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/url_constants.h" +#include "chrome/common/extensions/extension_error_utils.h" +#include "googleurl/src/gurl.h" + +namespace ext = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +namespace window_api { + +// We sometimes need to wait for window creation and removal to be completed +// even though we receive an event about them, these events sometimes arrive +// before the creation/removal is really totally completed. +const int kMaxDelayMs = 5000; // 5 seconds may be needed on slow machines. +const int kDelayMs = 50; + +void RegisterInvocations(ApiDispatcher* dispatcher) { +#define REGISTER_API_FUNCTION(func) do { dispatcher->RegisterInvocation(\ + func##Function::function_name(), NewApiInvocation< func >); } while (false) + REGISTER_WINDOW_API_FUNCTIONS(); +#undef REGISTER_API_FUNCTION + dispatcher->RegisterInvocation(CreateWindowFunction::function_name(), + NewApiInvocation< CreateWindowFunc >); + // And now register the permanent event handlers. + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnWindowRemoved, + RemoveWindow::EventHandler); + dispatcher->RegisterPermanentEventHandler(ext_event_names::kOnWindowCreated, + CreateWindowFunc::EventHandler); +} + +bool WindowApiResult::UpdateWindowRect(HWND window, + const DictionaryValue* window_props) { + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + int window_id = dispatcher->GetWindowIdFromHandle(window); + if (window_utils::WindowHasNoThread(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + if (!IsIeFrameClass(window)) { + PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return false; + } + + // Unspecified entries are set to -1 to let the executor know not to change + // them. + long left = -1; + long top = -1; + long width = -1; + long height = -1; + + if (window_props) { + int dim; + if (window_props->HasKey(ext::kLeftKey)) { + if (!window_props->GetInteger(ext::kLeftKey, &dim)) { + PostError("bad request"); + return false; + } + left = dim; + } + + if (window_props->HasKey(ext::kTopKey)) { + if (!window_props->GetInteger(ext::kTopKey, &dim)) { + PostError("bad request"); + return false; + } + top = dim; + } + + if (window_props->HasKey(ext::kWidthKey)) { + if (!window_props->GetInteger(ext::kWidthKey, &dim) || dim < 0) { + PostError("bad request"); + return false; + } + width = dim; + } + + if (window_props->HasKey(ext::kHeightKey)) { + if (!window_props->GetInteger(ext::kHeightKey, &dim) || dim < 0) { + PostError("bad request"); + return false; + } + height = dim; + } + } + + common_api::WindowInfo window_info; + if (left != -1 || top != -1 || width != -1 || height != -1) { + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to update window."; + PostError(api_module_constants::kInternalErrorError); + return false; + } + + HRESULT hr = executor->UpdateWindow(left, top, width, height, &window_info); + if (FAILED(hr)) { + LOG(ERROR) << "Couldn't update window: " << std::hex << window << ". " << + com::LogHr(hr); + PostError(api_module_constants::kInternalErrorError); + return false; + } + SetResultFromWindowInfo(window, window_info, false); + return true; + } else { + return CreateWindowValue(window, false); + } +} + +void GetWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + if (!args.GetInteger(0, &window_id)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + // CreateWindowValue validates the HWND. + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (result->CreateWindowValue(window, false)) + result->PostResult(); +} + +void GetCurrentWindow::Execute(const ListValue& args, int request_id) { + // TODO(mad@chromium.org): We currently don't have access to the + // actual 'current' window from the point of view of the extension + // API caller. Use the top window for now. bb2255140 + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + if (result->CreateWindowValue(result->TopIeWindow(), false)) + result->PostResult(); +} + +void GetLastFocusedWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + if (result->CreateWindowValue(result->TopIeWindow(), false)) + result->PostResult(); +} + +bool CreateWindowFunc::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + // We don't need to modify anything in the arguments, we just want to delay. + *converted_args = input_args; + + scoped_ptr<ListValue> args_list; + int window_id = 0; + if (!api_module_util::GetListAndDictIntValue( + input_args, ext::kIdKey, &args_list, &window_id)) { + NOTREACHED() << "Event arguments wasn't a dictionary with an ID in it. " << + input_args; + return false; + } + + // The hook may call us before the window is completely created, so we + // must delay the execution until the first tab is completely created. + // TODO(mad@chromium.org): Find a way to do this without blocking + // all other ApiDispatching. + HWND window = dispatcher->GetWindowHandleFromId(window_id); + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && + ::IsWindow(window) && + !window_utils::FindDescendentWindow( + window, windows::kIeTabWindowClass, false, NULL)) { + ::SleepEx(kDelayMs, TRUE); // TRUE = Alertable. + waited_ms += kDelayMs; + } + // We don't DCHECK if the window died, but we must still return false + // if it did, so that we don't broadcast the event back to Chrome. + DCHECK(waited_ms < kMaxDelayMs || !::IsWindow(window)); + return waited_ms < kMaxDelayMs && ::IsWindow(window); +} + +void CreateWindowFunc::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + scoped_ptr<DictionaryValue> input_dict; + // The input dictionary is optional; if not provided, the args may be + // either an empty list or a list with a single null value element. + if (args.GetSize() > 0) { + Value* first_arg = NULL; + if (!args.Get(0, &first_arg) || first_arg == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + if (first_arg->GetType() != Value::TYPE_NULL) { + DictionaryValue* args_dict = NULL; + if (!args.GetDictionary(0, &args_dict) || args_dict == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + // Remember the arguments so that we can use them later. + input_dict.reset(static_cast<DictionaryValue*>(args_dict->DeepCopy())); + } + } + + // Look for optional url. + scoped_ptr<GURL> spec(new GURL(chrome::kAboutBlankURL)); + std::string url_input; + if (input_dict.get() != NULL) { + if (input_dict->HasKey(ext::kUrlKey)) { + if (!input_dict->GetString(ext::kUrlKey, &url_input)) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + spec.reset(new GURL(url_input)); + if (!spec->is_valid()) { + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kInvalidUrlError, url_input).c_str()); + return; + } + } + } + + // There are many ways to create new IE windows, but they don't all behave + // the same depending on the IE version, OS or even settings. The most + // reliable way we found was to CoCreate an instance of IE, but navigating + // this instance doesn't behave exactly the same on all platforms. + // + // The main problem is with protected mode. If an instance of IE is launched + // with protected mode on, and we navigate to a URL that doesn't need + // protection (or vice versa), then the navigation will need to occur in + // another browser, not the one that was launched in the inappropriate + // protected mode. + // + // On IE7 if you CoCreate an instance of IE from a process running at an + // elevated integrity level as our Broker is, you get an unprotected mode + // IE, and if you navigate to a URL that needs protected mode, it will either + // create a new IE with the proper protected mode, or re-use an existing one + // if one is already opened. + // + // On IE8 the current process' integrity level is not taken into account + // when CoCreating a new IE, it relies on the CLSID which is the regular one + // (CLSID_InternetExplorer) for protected mode and a new one used for running + // at medium integrity level (CLSID_InternetExplorerMedium). Also, if you + // would then navigate to a URL that has a different protected mode than the + // one used for the CoCreate, then, a new window will always be created, an + // existing one will never be used. + // + // The other alternatives we looked at was to + // 1) Explicitly take the IWebBrowser2 interface of an existing IE. + // But this can cause the navigate to create a new tab instead + // of a new window (even if we specified navOpenInNewWindow) if + // the tab settings specify that popups should be opened in a + // new tab as opposed to a new window. + // 2) Use the IELaunchURL API (available in iepmapi.h). + // Except from the fact that it always creates a set of new + // IE processes in their own individual session this would + // behave exactly as we would like... But the price is too high. + // And it isn't available on XP... + // 3) Still use CoCreateInstance but don't rely on the knowledge of the + // current OS or IE version but rely on notifications sent to the + // DWebBrowser2 events thingy. Experimenting with these was not trivial + // and in some case lead to a non-deterministic way of identifying if + // a new window had been created or not. + + // We need to know whether we will navigate to a URL that needs protected + // mode enabled or not. On earlier versions of the OS (e.g., pre-Vista), + // this call simply fails and thus act as if we didn't need protected mode. + std::wstring url = UTF8ToWide(spec->spec()); + HRESULT protected_mode_url_hr = ::IEIsProtectedModeURL(url.c_str()); + + // We default to CLSID_InternetExplorer and CLSCTX_ALL but we may need to + // switch to CLSID_InternetExplorerMedium or add CLSCTX_ENABLE_CLOAKING later. + DWORD class_context = CLSCTX_ALL; + CLSID ie_clsid = CLSID_InternetExplorer; + bool lowered_integrity_level = false; + bool impersonating = false; + if (protected_mode_url_hr != S_OK) { // S_FALSE is returned for no. + // When we don't need protected mode, we need to explicitly + // request that IE8 starts the medium integrity version. + if (ie_util::GetIeVersion() == ie_util::IEVERSION_IE8) + ie_clsid = CLSID_InternetExplorerMedium; + } else if (ie_util::GetIeVersion() == ie_util::IEVERSION_IE7) { + // IE7 in protected mode needs to be started at lower integrity level + // than the broker process. So we must enable cloaking and temporary bring + // down the integrity level of this thread. + class_context |= CLSCTX_ENABLE_CLOAKING; + // We would have liked to use the CAccessToken class from ATL but it doesn't + // support the integrity level impersonation, just the owner, primary group + // and DACL. So we do it ourselves. + if (!::ImpersonateSelf(SecurityImpersonation)) { + DCHECK(false) << com::LogWe(); + return; + } + // Remember that we successfully impersonated, so we can RevertToSelf. + impersonating = true; + // This call fails on XP, so we don't look at the error, we just log it and + // remember our success so we can revert it later. Specifying NULL for the + // thread pointer means that we want to affect the current thread. + HRESULT hr_lowered_integrity_level = + process_utils_win::SetThreadIntegrityLevel(NULL, SDDL_ML_LOW); + lowered_integrity_level = SUCCEEDED(hr_lowered_integrity_level); + DLOG_IF(WARNING, !lowered_integrity_level) << + "SetThreadIntegrityLevelLow" << com::LogHr(hr_lowered_integrity_level); + } + + // Now we can create a new web browser and be sure it will be the one that is + // kept (as well as its window) once we navigate. + CComPtr<IWebBrowser2> web_browser; + HRESULT hr = web_browser.CoCreateInstance(ie_clsid, NULL, class_context); + DCHECK(SUCCEEDED(hr)) << "Can't CoCreate IE! " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // And now we can bring back the integrity level to where it was. + if (lowered_integrity_level) { + HRESULT hr_integrity_level = + process_utils_win::ResetThreadIntegrityLevel(NULL); + DCHECK(SUCCEEDED(hr_integrity_level)) << "Failed to bring back thread " << + "integrity level! " << com::LogHr(hr_integrity_level); + LOG_IF(WARNING, FAILED(hr)) << "ResetThreadIntegrityLevel(NULL) " << + com::LogHr(hr_integrity_level); + } + + // And stop impersonating. + if (impersonating) { + BOOL success = ::RevertToSelf(); + DCHECK(success) << "Failed to stop impersonating! " << com::LogWe(); + LOG_IF(WARNING, !success) << "RevertToSelf() " << com::LogWe(); + } + + // We need the HWND to create the window value to fill the info needed + // by the callback, and also, to potentially resize and position it. + HWND web_browserhwnd = NULL; + hr = web_browser->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&web_browserhwnd)); + DCHECK(SUCCEEDED(hr)) << "Can't get HWND!" << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + if (input_dict.get() == NULL) { + // No arguments, so no need to popupize, resize or reposition. + if (!result->CreateWindowValue(web_browserhwnd, false)) { + // CreateWindowValue will have posted the error if any. + return; + } + } else { + // Popupize if needed. + std::string window_type; + if (input_dict->GetString(ext::kWindowTypeKey, &window_type) && + window_type == ext::kWindowTypeValuePopup) { + HRESULT hr = web_browser->put_AddressBar(VARIANT_FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed to hide address bar. " << com::LogHr(hr); + hr = web_browser->put_StatusBar(VARIANT_FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed to hide status bar. " << com::LogHr(hr); + hr = web_browser->put_ToolBar(FALSE); + DCHECK(SUCCEEDED(hr)) << "Failed put_ToolBar. " << com::LogHr(hr); + } + // Reposition and Resize if needed. + if (!result->UpdateWindowRect(web_browserhwnd, input_dict.get())) { + // UpdateWindowRect will have posted the error if any. + return; + } + } + + // Now we can Navigate to the requested url. + hr = web_browser->Navigate(CComBSTR(url.c_str()), + &CComVariant(), // unused flags + &CComVariant(L"_top"), // Target frame + &CComVariant(), // Unused POST DATA + &CComVariant()); // Unused Headers + DCHECK(SUCCEEDED(hr)) << "Can't Navigate IE to " << url << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + // A CoCreated IE is not visible until we ask it to be. + hr = web_browser->put_Visible(VARIANT_TRUE); + DCHECK(SUCCEEDED(hr)) << "put_Visible: " << com::LogHr(hr); + if (FAILED(hr)) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + result->PostResult(); +} + +void UpdateWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + DictionaryValue* update_props; + if (!args.GetInteger(0, &window_id) || + !args.GetDictionary(1, &update_props)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (!result->IsIeFrameClass(window)) { + LOG(WARNING) << "Extension trying to access non-IE or dying window: " << + std::hex << window_id; + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id)).c_str()); + return; + } + + if (result->UpdateWindowRect(window, update_props)) + result->PostResult(); +} + +void RemoveWindow::Execute(const ListValue& args, int request_id) { + scoped_ptr<WindowApiResult> result(CreateApiResult(request_id)); + int window_id = 0; + if (!args.GetInteger(0, &window_id)) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + return; + } + + ApiDispatcher* dispatcher = GetDispatcher(); + DCHECK(dispatcher != NULL); + HWND window = dispatcher->GetWindowHandleFromId(window_id); + if (!result->IsIeFrameClass(window)) { + LOG(WARNING) << "Extension trying to access non-IE or dying window: " << + std::hex << window_id; + result->PostError(ExtensionErrorUtils::FormatErrorMessage( + ext::kWindowNotFoundError, base::IntToString(window_id))); + return; + } + + CComPtr<ICeeeWindowExecutor> executor; + dispatcher->GetExecutor(window, IID_ICeeeWindowExecutor, + reinterpret_cast<void**>(&executor)); + if (executor == NULL) { + LOG(WARNING) << "Failed to get an executor to remove window."; + result->PostError(api_module_constants::kInternalErrorError); + return; + } + + HRESULT hr = executor->RemoveWindow(); + if (FAILED(hr)) { + LOG(ERROR) << "Executor failed to remove window: " << std::hex << + window_id << ". " << com::LogHr(hr); + result->PostError("Internal Error trying to close window."); + return; + } + + // S_FALSE is returned when there are no tabs to close, e.g., the initial + // tab has not finished loading by the time we are asked to close the window. + if (hr == S_FALSE) { + LOG(WARNING) << "Failed to get window manager to close the window, " + "trying WM_CLOSE instead." << com::LogHr(hr); + // We fall back to this to be slightly more robust, but this approach has + // the drawback that it shows a pop-up dialog with a message like "do you + // want to close N tabs" if there is more than one tab. And we may need + // to try a few times because we have seen cases where SendMessage didn't + // return 0 because the window couldn't process the message for some reason. + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && ::SendMessage(window, WM_CLOSE, 0, 0)) { + ::SleepEx(kDelayMs, TRUE); // Alertable. + waited_ms += kDelayMs; + } + + DCHECK(waited_ms < kMaxDelayMs); + if (waited_ms >= kMaxDelayMs) { + result->PostError(api_module_constants::kInternalErrorError); + return; + } + } + + // Now we must wait for the window removal to be completely done before + // posting the response back to Chrome Frame. + // And we remember the window identifier so that we can recognize the event. + result->SetValue(ext::kWindowIdKey, Value::CreateIntegerValue(window_id)); + DCHECK(dispatcher != NULL); + dispatcher->RegisterEphemeralEventHandler(ext_event_names::kOnWindowRemoved, + RemoveWindow::ContinueExecution, + result.release()); +} + +HRESULT RemoveWindow::ContinueExecution( + const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher) { + DCHECK(dispatcher != NULL); + DCHECK(user_data != NULL); + + scoped_ptr<WindowApiResult> result(static_cast<WindowApiResult*>(user_data)); + + scoped_ptr<ListValue> args_list; + int window_id = 0; + if (!api_module_util::GetListAndIntegerValue(input_args, &args_list, + &window_id)) { + NOTREACHED() << "Event arguments are not a list with an integer in it."; + result->PostError(api_module_constants::kInternalErrorError); + return E_INVALIDARG; + } + + const Value* saved_window_value = result->GetValue(ext::kWindowIdKey); + DCHECK(saved_window_value != NULL && + saved_window_value->IsType(Value::TYPE_INTEGER)); + int saved_window_id = 0; + bool success = saved_window_value->GetAsInteger(&saved_window_id); + DCHECK(success && saved_window_id != 0); + if (saved_window_id == window_id) { + // The windows.remove callback doesn't have any arguments. + result->set_value(NULL); + result->PostResult(); + return S_OK; + } else { + // release doesn't destroy result, we need to keep it for next try. + result.release(); + return S_FALSE; // S_FALSE keeps us in the queue. + } +} + +bool RemoveWindow::EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher) { + DCHECK(converted_args); + // We don't need to modify anything in the arguments, we just want to delay. + *converted_args = input_args; + + scoped_ptr<Value> input_args_val(base::JSONReader::Read(input_args, true)); + DCHECK(input_args_val.get() != NULL && + input_args_val->IsType(Value::TYPE_LIST)); + if (input_args_val.get() == NULL || !input_args_val->IsType(Value::TYPE_LIST)) + return false; + + int window_id = 0; + bool success = static_cast<ListValue*>(input_args_val.get())->GetInteger( + 0, &window_id); + DCHECK(success) << "Couldn't get an int window Id from input args."; + if (!success) + return false; + // The hook may call us before the window has completely disappeared so we + // must delay the execution until the window is completely gone. + // TODO(mad@chromium.org): Find a way to do this without blocking + // all other ApiDispatching. + HWND window = dispatcher->GetWindowHandleFromId(window_id); + int waited_ms = 0; + while (waited_ms < kMaxDelayMs && ::IsWindow(window)) { + ::SleepEx(kDelayMs, TRUE); // Alertable. + waited_ms += kDelayMs; + } + DCHECK(waited_ms < kMaxDelayMs); + if (waited_ms < kMaxDelayMs) { + return true; + } else { + return false; + } +} + +void GetAllWindows::Execute(const ListValue& args, int request_id) { + scoped_ptr<IterativeWindowApiResult> result(CreateApiResult(request_id)); + bool populate_tabs = false; + // The input dictionary is optional; if not provided, the args may be + // either an empty list or a list with a single null value element. + if (args.GetSize() > 0) { + Value* first_arg = NULL; + if (!args.Get(0, &first_arg) || first_arg == NULL) { + NOTREACHED() << "bad request"; + result->PostError(api_module_constants::kInvalidArgumentsError); + } else if (first_arg->GetType() != Value::TYPE_NULL) { + DictionaryValue* dict = NULL; + if (!args.GetDictionary(0, &dict) || + (dict->HasKey(ext::kPopulateKey) && + !dict->GetBoolean(ext::kPopulateKey, &populate_tabs))) { + NOTREACHED() << "bad message"; + result->PostError(api_module_constants::kInvalidArgumentsError); + } + } + + if (result->AllFailed()) { + result->FlushAllPosts(); + return; + } + } + + std::set<HWND> ie_frame_windows; + window_utils::FindTopLevelWindows(windows::kIeFrameWindowClass, + &ie_frame_windows); + if (ie_frame_windows.empty()) { + result->FlushAllPosts(); + return; + } + + std::set<HWND>::const_iterator iter = ie_frame_windows.begin(); + for (; iter != ie_frame_windows.end(); ++iter) { + if (result->CreateWindowValue(*iter, populate_tabs)) + result->PostResult(); + } + + DCHECK(!result->IsEmpty()); + if (result->IsEmpty()) // This is an error! + result->PostError(api_module_constants::kInternalErrorError); + + result->FlushAllPosts(); +} + +} // namespace window_api diff --git a/ceee/ie/broker/window_api_module.h b/ceee/ie/broker/window_api_module.h new file mode 100644 index 0000000..6aed95f --- /dev/null +++ b/ceee/ie/broker/window_api_module.h @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation. + +#ifndef CEEE_IE_BROKER_WINDOW_API_MODULE_H_ +#define CEEE_IE_BROKER_WINDOW_API_MODULE_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/broker/common_api_module.h" + +#include "toolband.h" // NOLINT + +namespace window_api { + +class WindowApiResult; +typedef ApiResultCreator<WindowApiResult> WindowApiResultCreator; + +// Registers all Window API invocations with the given dispatcher. +void RegisterInvocations(ApiDispatcher* dispatcher); + +class WindowApiResult : public common_api::CommonApiResult { + public: + explicit WindowApiResult(int request_id) + : common_api::CommonApiResult(request_id) {} + + // Updates the position of the given window based on the arguments given and + // sets the result value appropriately. Calls PostError() if there is an + // error and returns false. + // @param window The window to update. + // @param window_rect The arguments for the window update, a DictionaryValue + // containing the left, top, width, height to update the window with. + virtual bool UpdateWindowRect(HWND window, + const DictionaryValue* window_rect); +}; + +typedef IterativeApiResult<WindowApiResult> IterativeWindowApiResult; + +class GetWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetCurrentWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class GetLastFocusedWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +// Unfortunately winuser.h uses a #define for CreateWindow to use either +// the ASCII or Wide char version, so it replaces the Constructor declaration +// with CreateWindowW() and fails compilation if we use CreateWindow as a +// class name. So we must have another class name and use a special case to +// specify the name of the function we replace in Chrome in RegisterInvocations. +class CreateWindowFunc : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + // public so that the RegisterInvocations can see it. + // Simply reacts to an OnWindowCreated event and waits for the window to + // be completely created before letting the ApiDispatcher broadcast the event. + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class UpdateWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +class RemoveWindow : public WindowApiResultCreator { + public: + virtual void Execute(const ListValue& args, int request_id); + static HRESULT ContinueExecution(const std::string& input_args, + ApiDispatcher::InvocationResult* user_data, + ApiDispatcher* dispatcher); + static bool EventHandler(const std::string& input_args, + std::string* converted_args, + ApiDispatcher* dispatcher); +}; + +class GetAllWindows : public ApiResultCreator<IterativeWindowApiResult> { + public: + virtual void Execute(const ListValue& args, int request_id); +}; + +} // namespace window_api + +#endif // CEEE_IE_BROKER_WINDOW_API_MODULE_H_ diff --git a/ceee/ie/broker/window_api_module_unittest.cc b/ceee/ie/broker/window_api_module_unittest.cc new file mode 100644 index 0000000..04f67ef --- /dev/null +++ b/ceee/ie/broker/window_api_module_unittest.cc @@ -0,0 +1,937 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window API implementation unit tests. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include <set> + +#include "base/scoped_ptr.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::AddRef; +using testing::AnyNumber; +using testing::AtLeast; +using testing::CopyStringToArgument; +using testing::DoAll; +using testing::Gt; +using testing::InstanceCountMixin; +using testing::Invoke; +using testing::MockApiDispatcher; +using testing::MockApiInvocation; +using testing::NotNull; +using testing::Return; +using testing::Sequence; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +using window_api::WindowApiResult; +using window_api::IterativeWindowApiResult; + +namespace keys = extension_tabs_module_constants; +namespace ext_event_names = extension_event_names; + +const wchar_t kClassName[] = L"IEFrame"; +const int kRequestId = 12; +const int kWindowId = 23; +const HWND kWindowHwnd = (HWND)34; +const int kRandomWindowId = 45; +const HWND kRandomWindowHwnd = (HWND)56; + +TEST(WindowApi, RegisterInvocations) { + StrictMock<MockApiDispatcher> disp; + EXPECT_CALL(disp, RegisterInvocation(NotNull(), NotNull())).Times(AtLeast(7)); + window_api::RegisterInvocations(&disp); +} + +TEST(WindowApi, IsIeFrameClassWithNull) { + testing::LogDisabler no_dchecks; + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(NULL)); +} + +TEST(WindowApi, IsIeFrameClassWithNonWindow) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillRepeatedly(Return(FALSE)); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, IsIeFrameClassWrongClassName) { + const wchar_t kClassName[] = L"IEFrames"; // note 's', making it invalid. + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_FALSE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, IsIeFrameClassStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockUser32> user32; + EXPECT_CALL(user32, IsWindow(NotNull())).WillOnce(Return(TRUE)); + EXPECT_CALL(user32, GetClassName(NotNull(), NotNull(), Gt(0))).WillOnce( + DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + EXPECT_TRUE(invocation_result.IsIeFrameClass(reinterpret_cast<HWND>(1))); +} + +TEST(WindowApi, TopIeWindowNeverFound) { + testing::LogDisabler no_dchecks; + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, NotNull())). + WillOnce(Return(false)); + EXPECT_EQ(NULL, WindowApiResult::TopIeWindow()); +} + +TEST(WindowApi, TopIeWindowStraightline) { + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindDescendentWindow(_, _, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<3>(kRandomWindowHwnd), Return(true))); + EXPECT_EQ(kRandomWindowHwnd, WindowApiResult::TopIeWindow()); +} + +class MockWindowApiResult + : public WindowApiResult, + public InstanceCountMixin<MockWindowApiResult> { + public: + explicit MockWindowApiResult(int request_id) + : WindowApiResult(request_id) { + } + MOCK_METHOD1(CreateTabList, Value*(BSTR)); + MOCK_METHOD3(SetResultFromWindowInfo, void(HWND, const CeeeWindowInfo&, + bool)); + MOCK_METHOD2(UpdateWindowRect, bool(HWND, const DictionaryValue*)); + MOCK_METHOD2(CreateWindowValue, bool(HWND, bool)); + MOCK_METHOD0(PostResult, void()); + MOCK_METHOD1(PostError, void(const std::string&)); + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + void CallSetResultFromWindowInfo(HWND window, const CeeeWindowInfo& info, + bool pop) { + WindowApiResult::SetResultFromWindowInfo(window, info, pop); + } + bool CallCreateWindowValue(HWND window, bool pop) { + return WindowApiResult::CreateWindowValue(window, pop); + } + Value* CallCreateTabList(BSTR tabs) { + return WindowApiResult::CreateTabList(tabs); + } + bool CallUpdateWindowRect(HWND window, const DictionaryValue* dict_value) { + return WindowApiResult::UpdateWindowRect(window, dict_value); + } + StrictMock<MockApiDispatcher> mock_api_dispatcher_; +}; + +class MockIterativeWindowApiResult + : public IterativeWindowApiResult, + public InstanceCountMixin<MockIterativeWindowApiResult> { + public: + explicit MockIterativeWindowApiResult(int request_id) + : IterativeWindowApiResult(request_id) {} + + MOCK_METHOD0(CallRealPostResult, void()); + MOCK_METHOD1(CallRealPostError, void(const std::string&)); + MOCK_METHOD1(CreateTabList, Value*(BSTR)); + MOCK_METHOD3(SetResultFromWindowInfo, void(HWND, const CeeeWindowInfo&, + bool)); + MOCK_METHOD2(UpdateWindowRect, bool(HWND, const DictionaryValue*)); + MOCK_METHOD2(CreateWindowValue, bool(HWND, bool)); + + virtual void PostError(const std::string& error) { + ++error_counter_; + IterativeWindowApiResult::PostError(error); + } + + virtual void PostResult() { + ++success_counter_; + IterativeWindowApiResult::PostResult(); + } + + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + static void ResetCounters() { + success_counter_ = 0; + error_counter_ = 0; + } + + static int success_counter() { + return success_counter_; + } + + static int error_counter() { + return error_counter_; + } + + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + static int success_counter_; + static int error_counter_; +}; + +int MockIterativeWindowApiResult::success_counter_ = 0; +int MockIterativeWindowApiResult::error_counter_ = 0; + +// Mock static functions defined in WindowApiResult. +MOCK_STATIC_CLASS_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT_BEGIN(MockWindowApiResultStatics) + MOCK_STATIC_INIT2(WindowApiResult::TopIeWindow, + TopIeWindow); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(HWND, , TopIeWindow); +MOCK_STATIC_CLASS_END(MockWindowApiResultStatics) + +class WindowApiTests: public testing::Test { + public: + virtual void SetUp() { + EXPECT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &mock_window_executor_, &mock_window_executor_keeper_)); + EXPECT_HRESULT_SUCCEEDED(testing::MockTabExecutor::CreateInitialized( + &mock_tab_executor_, &mock_tab_executor_keeper_)); + } + + virtual void TearDown() { + // Everything should have been relinquished. + mock_window_executor_keeper_.Release(); + mock_tab_executor_keeper_.Release(); + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + protected: + void MockGetWindowExecutor(MockApiDispatcher* executor_owner) { + EXPECT_CALL(*executor_owner, GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_window_executor_keeper_.p), + AddRef(mock_window_executor_keeper_.p))); + } + + void MockGetTabExecutor(MockApiDispatcher* executor_owner) { + EXPECT_CALL(*executor_owner, GetExecutor(_, _, NotNull())). + WillRepeatedly(DoAll( + SetArgumentPointee<2>(mock_tab_executor_keeper_.p), + AddRef(mock_tab_executor_keeper_.p))); + } + + void WindowAlwaysHasThread() { + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).WillRepeatedly( + Return(1)); // We only need to return a non-0 value. + } + + void MockIsIeFrameClass() { + // This is to mock the static call to IsIeFrameClass. + EXPECT_CALL(user32_, IsWindow(NotNull())).WillRepeatedly(Return(TRUE)); + EXPECT_CALL(user32_, GetClassName(NotNull(), NotNull(), Gt(0))). + WillRepeatedly(DoAll(CopyStringToArgument<1>(kClassName), + Return(arraysize(kClassName)))); + } + StrictMock<testing::MockUser32> user32_; + // The executor classes are already strict from their base class impl. + testing::MockWindowExecutor* mock_window_executor_; + testing::MockTabExecutor* mock_tab_executor_; + // To control the life span of the executors. + CComPtr<ICeeeWindowExecutor> mock_window_executor_keeper_; + CComPtr<ICeeeTabExecutor> mock_tab_executor_keeper_; + + private: + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(PostMessage, void(BSTR, BSTR)); + }; + // We should never get to the postman, we mock all the calls getting there. + // So we simply instantiate it strict and it will register itself as the + // one and only singleton to use all the time. + CComObjectStackEx<StrictMock<MockChromePostman>> postman_; +}; + +TEST_F(WindowApiTests, CreateTabList) { + // We only check that we can properly handle an empty list since we can't + // easily mock the tab_api::GetAllTabsInWindow declared on the stack. + // We validate it more completely in the tabs api unittest anyway. + WindowApiResult invocation_result(WindowApiResult::kNoRequestId); + scoped_ptr<Value> returned_value(invocation_result.CreateTabList(L"[]")); + EXPECT_NE(static_cast<Value*>(NULL), returned_value.get()); + EXPECT_TRUE(returned_value->IsType(Value::TYPE_LIST)); + EXPECT_EQ(0, static_cast<ListValue*>(returned_value.get())->GetSize()); + + // Also test the failure path. + testing::LogDisabler no_dchecks; + returned_value.reset(invocation_result.CreateTabList(L"")); + EXPECT_EQ(NULL, returned_value.get()); +} + +// Mock IeIsInPrivateBrowsing. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIEIsInPrivateBrowsing, + GetIEIsInPrivateBrowsing); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetIEIsInPrivateBrowsing); +MOCK_STATIC_CLASS_END(MockIeUtil) + +TEST_F(WindowApiTests, SetResultFromWindowInfo) { + testing::LogDisabler no_dchecks; + + // Standard test without tabs list populate nor an internal list. + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + CeeeWindowInfo window_info; + window_info.rect.left = 1; + window_info.rect.right = 2; + window_info.rect.top = 3; + window_info.rect.bottom = 4; + window_info.tab_list = NULL; + + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + + StrictMock<MockIeUtil> mock_ie_util; + EXPECT_CALL(mock_ie_util, GetIEIsInPrivateBrowsing()).WillRepeatedly( + Return(false)); + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, + false); + const Value * result = invocation_result.value(); + EXPECT_NE(static_cast<Value*>(NULL), result); + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + DictionaryValue dict; + dict.SetInteger(keys::kIdKey, kWindowId); + dict.SetBoolean(keys::kFocusedKey, window_info.focused != FALSE); + + dict.SetInteger(keys::kLeftKey, window_info.rect.left); + dict.SetInteger(keys::kTopKey, window_info.rect.top); + dict.SetInteger(keys::kWidthKey, window_info.rect.right - + window_info.rect.left); + dict.SetInteger(keys::kHeightKey, window_info.rect.bottom - + window_info.rect.top); + + dict.SetBoolean(keys::kIncognitoKey, false); + + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we are not handling popups or app windows in IE yet. + dict.SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + EXPECT_TRUE(dict.Equals(result)); + + invocation_result.set_value(NULL); + + // Now make sure we call CreateTablist, yet still succeed if it returns NULL. + int tab_list = 42; + EXPECT_CALL(invocation_result, CreateTabList((BSTR)tab_list)). + WillOnce(Return(static_cast<Value*>(NULL))); + window_info.tab_list = (BSTR)tab_list; + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, true); + EXPECT_NE(static_cast<Value*>(NULL), invocation_result.value()); + + invocation_result.set_value(NULL); + + // And now a successful run with CreateTablist. + EXPECT_CALL(invocation_result, CreateTabList((BSTR)tab_list)). + WillOnce(Return(Value::CreateIntegerValue(tab_list))); + + invocation_result.CallSetResultFromWindowInfo(kWindowHwnd, window_info, true); + result = invocation_result.value(); + EXPECT_NE(static_cast<Value*>(NULL), result); + EXPECT_TRUE(result->IsType(Value::TYPE_DICTIONARY)); + dict.Set(keys::kTabsKey, Value::CreateIntegerValue(tab_list)); + EXPECT_TRUE(dict.Equals(result)); +} + +TEST_F(WindowApiTests, CreateWindowValue) { + testing::LogDisabler no_dchecks; + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(Return(0)); + + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + + // Fail because the window is not associated to a thread. + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now fail because the window is not an IE Frame. + WindowAlwaysHasThread(); + invocation_result.set_value(NULL); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now fail because we can't get an executor. + MockIsIeFrameClass(); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now get the executor to fail. + MockGetWindowExecutor(&invocation_result.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, GetWindow(FALSE, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now go all the way. + EXPECT_CALL(*mock_window_executor_, GetWindow(FALSE, NotNull())). + WillOnce(Return(S_OK)); + EXPECT_CALL(invocation_result, SetResultFromWindowInfo(_, _, _)).Times(1); + EXPECT_TRUE(invocation_result.CallCreateWindowValue(kWindowHwnd, false)); +} + +TEST_F(WindowApiTests, UpdateWindowRect) { + testing::LogDisabler no_dchecks; + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + + StrictMock<MockWindowApiResult> invocation_result( + WindowApiResult::kNoRequestId); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kWindowHwnd)).WillRepeatedly(Return(kWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kWindowId)).WillRepeatedly(Return(kWindowHwnd)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowIdFromHandle(kRandomWindowHwnd)). + WillRepeatedly(Return(kRandomWindowId)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillRepeatedly(Return(kRandomWindowHwnd)); + + // Window has no thread. + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)).WillRepeatedly( + Return(0)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Window is not an IEFrame. + WindowAlwaysHasThread(); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now try an empty dictionary (which should not cause an error). + MockIsIeFrameClass(); + EXPECT_CALL(invocation_result, CreateWindowValue(_, false)). + WillOnce(Return(true)); + EXPECT_TRUE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, NULL)); + + // Now try a dictionary with invalid dictionary values. + DictionaryValue bad_values; + bad_values.Set(keys::kLeftKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kLeftKey, Value::CreateIntegerValue(43)); + bad_values.Set(keys::kTopKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kTopKey, Value::CreateIntegerValue(44)); + bad_values.Set(keys::kWidthKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + bad_values.Set(keys::kWidthKey, Value::CreateIntegerValue(45)); + bad_values.Set(keys::kHeightKey, Value::CreateNullValue()); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + &bad_values)); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now make sure the values get properly propagated. + // But start by failing the GetExecutor. + scoped_ptr<DictionaryValue> good_values(static_cast<DictionaryValue*>( + bad_values.DeepCopy())); + good_values->Set(keys::kHeightKey, Value::CreateIntegerValue(46)); + EXPECT_CALL(invocation_result.mock_api_dispatcher_, + GetExecutor(kRandomWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now get the executor to fail. + MockGetWindowExecutor(&invocation_result.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, UpdateWindow(43, 44, 45, 46, _)). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(invocation_result, PostError(_)).Times(1); + EXPECT_FALSE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); + EXPECT_EQ(NULL, invocation_result.value()); + + // Now go all the way, + EXPECT_CALL(invocation_result, SetResultFromWindowInfo(_, _, _)).Times(1); + EXPECT_CALL(*mock_window_executor_, UpdateWindow(43, 44, 45, 46, _)). + WillOnce(Return(S_OK)); + EXPECT_TRUE(invocation_result.CallUpdateWindowRect(kRandomWindowHwnd, + good_values.get())); +} + +template <class BaseClass> +class MockWindowInvocation + : public MockApiInvocation<WindowApiResult, MockWindowApiResult, + BaseClass> { +}; + +template <class BaseClass> +class MockIterativeWindowInvocation + : public MockApiInvocation<IterativeWindowApiResult, + MockIterativeWindowApiResult, + BaseClass> { +}; + +TEST_F(WindowApiTests, GetWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::GetWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + + // Bad args failure. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(WindowApiTests, GetWindowStraightline) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::GetWindow>> invocation; + MockIsIeFrameClass(); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + ListValue args_list; + args_list.Append(Value::CreateIntegerValue(kRandomWindowId)); + invocation.Execute(args_list, kRequestId); +} + +TEST_F(WindowApiTests, GetCurrentWindowStraightline) { + testing::LogDisabler no_dchecks; + + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(TRUE)); + + StrictMock<MockWindowInvocation<window_api::GetCurrentWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics result_statics; + EXPECT_CALL(result_statics, TopIeWindow()).WillOnce(Return( + kRandomWindowHwnd)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +TEST_F(WindowApiTests, GetLastFocusedWindowStraightline) { + testing::LogDisabler no_dchecks; // don't care about NULL pointers. + + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(TRUE)); + + StrictMock<MockWindowInvocation<window_api::GetLastFocusedWindow>> invocation; + invocation.AllocateNewResult(kRequestId); + MockWindowApiResultStatics result_statics; + EXPECT_CALL(result_statics, TopIeWindow()).WillOnce(Return( + kRandomWindowHwnd)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kRandomWindowHwnd, _)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + invocation.Execute(ListValue(), kRequestId); +} + +// Mock the CoCreateInstance call that is used to create a new IE window. +MOCK_STATIC_CLASS_BEGIN(MockIeWindowCreation) + MOCK_STATIC_INIT_BEGIN(MockIeWindowCreation) + MOCK_STATIC_INIT(CoCreateInstance); + MOCK_STATIC_INIT_END() + + MOCK_STATIC5(HRESULT, CALLBACK, CoCreateInstance, REFCLSID, LPUNKNOWN, + DWORD, REFIID, LPVOID*); +MOCK_STATIC_CLASS_END(MockIeWindowCreation) + +TEST_F(WindowApiTests, CreateWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::CreateWindowFunc>> invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + ListValue wrong_type; + wrong_type.Append(Value::CreateBooleanValue(false)); + invocation.Execute(wrong_type, kRequestId); + + invocation.AllocateNewResult(kRequestId); + ListValue args_list; + DictionaryValue* dict_value = new DictionaryValue; + args_list.Append(dict_value); + // Wrong data type... + dict_value->SetInteger(keys::kUrlKey, rand()); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + // InvalidUrl + dict_value->SetString(keys::kUrlKey, "ht@tp://www.google.com/"); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + // Valid dictionary... + dict_value->SetString(keys::kUrlKey, "http://ossum.the.magnificent.com/"); + dict_value->SetInteger(keys::kLeftKey, 21); // Just to force the rect access. + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, _)).Times(AnyNumber()); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + MockIeWindowCreation mock_ie_create; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillRepeatedly(DoAll(SetArgumentPointee<4>(browser_keeper.p), + AddRef(browser_keeper.p), Return(S_OK))); + + EXPECT_CALL(*browser, get_HWND(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(0), Return(S_OK))); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(false)); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); + + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*browser, put_Visible(VARIANT_TRUE)).WillOnce(Return(E_FAIL)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(args_list, kRequestId); +} + +TEST_F(WindowApiTests, CreateWindowStraightline) { + testing::LogDisabler no_dchecks; + + ListValue args1; + DictionaryValue* dict1 = new DictionaryValue; + args1.Append(dict1); + dict1->SetString(keys::kUrlKey, "http://ossum.the.magnificent.com/"); + dict1->SetInteger(keys::kLeftKey, 21); // Just to force the rect access. + + ListValue args2; + DictionaryValue* dict2 = new DictionaryValue; + args2.Append(dict2); + dict2->SetString(keys::kUrlKey, "http://ossum.the.omnipotent.com/"); + dict2->SetString(keys::kWindowTypeKey, keys::kWindowTypeValuePopup); + + ListValue args3; + args3.Append(Value::CreateNullValue()); + + ListValue empty_args; + + const wchar_t* about_blank = L"about:blank"; + struct { + const ListValue* args; + const wchar_t* expected_url; + const bool popup; + } test_data[] = { + { &args1, L"http://ossum.the.magnificent.com/", false }, + { &args2, L"http://ossum.the.omnipotent.com/", true }, + { &args3, about_blank, false }, + { &empty_args, about_blank, false }, + }; + + typedef StrictMock<MockWindowInvocation<window_api::CreateWindowFunc>> + MockCreateWindowFunc; + + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser; + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser); + DCHECK(browser != NULL); + CComPtr<IWebBrowser2> browser_keeper = browser; + MockIeWindowCreation mock_ie_create; + EXPECT_CALL(mock_ie_create, CoCreateInstance(_, _, _, _, _)). + WillRepeatedly(DoAll(SetArgumentPointee<4>(browser_keeper.p), + AddRef(browser_keeper.p), Return(S_OK))); + EXPECT_CALL(*browser, get_HWND(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(0), Return(S_OK))); + EXPECT_CALL(*browser, Navigate(_, _, _, _, _)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*browser, put_Visible(VARIANT_TRUE)).WillRepeatedly(Return(S_OK)); + + for (int i = 0; i < arraysize(test_data); ++i) { + MockCreateWindowFunc invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + Value* first_arg = NULL; + if (!test_data[i].args->Get(0, &first_arg) || + first_arg->GetType() == Value::TYPE_NULL) { + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue(_, _)). + WillOnce(Return(true)); + invocation.Execute(*test_data[i].args, kRequestId); + } else { + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect(_, _)). + WillOnce(Return(true)); + if (test_data[i].popup) { + EXPECT_CALL(*browser, put_AddressBar(VARIANT_FALSE)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*browser, put_StatusBar(VARIANT_FALSE)). + WillOnce(Return(S_OK)); + EXPECT_CALL(*browser, put_ToolBar(FALSE)).WillOnce(Return(S_OK)); + } + invocation.Execute(*test_data[i].args, kRequestId); + } + } +} + +TEST_F(WindowApiTests, UpdateWindowErrorHandling) { + testing::LogDisabler no_dchecks; + StrictMock<MockWindowInvocation<window_api::UpdateWindow>> + invocation; + invocation.AllocateNewResult(kRequestId); + + ListValue root; + root.Append(Value::CreateIntegerValue(kRandomWindowId)); + // Too few values in list. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(root, kRequestId); + + invocation.AllocateNewResult(kRequestId); + + Value* wrong_type = Value::CreateStringValue(L"The Answer"); + root.Append(wrong_type); // root takes ownership. + // Right number of values, wrong type of second value. + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(root, kRequestId); + + invocation.AllocateNewResult(kRequestId); + + root.Remove(*wrong_type); + root.Append(new DictionaryValue()); // root takes ownership. + // Right values, but not IE Frame. + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, IsWindowClass(_, _)).WillOnce(Return(FALSE)); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(root, kRequestId); +} + +TEST_F(WindowApiTests, UpdateWindowStraightline) { + testing::LogDisabler no_dchecks; + + ListValue root; + root.Append(Value::CreateIntegerValue(kRandomWindowId)); + DictionaryValue* args = new DictionaryValue(); // root takes ownership. + root.Append(args); + + MockIsIeFrameClass(); + + StrictMock<MockWindowInvocation<window_api::UpdateWindow>> + invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, UpdateWindowRect( + kRandomWindowHwnd, args)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(root, kRequestId); +} + +class MockRemoveWindow + : public StrictMock<MockWindowInvocation<window_api::RemoveWindow>> { +}; + +TEST_F(WindowApiTests, RemoveWindowErrorHandling) { + testing::LogDisabler no_dchecks; + + // Wrong argument type. + MockRemoveWindow invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + invocation.Execute(ListValue(), kRequestId); + + // Not a valid window. + EXPECT_CALL(user32_, IsWindow(_)).WillRepeatedly(Return(FALSE)); + ListValue good_args; + good_args.Append(Value::CreateIntegerValue(kRandomWindowId)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); + + // No executor. + MockIsIeFrameClass(); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetExecutor(kRandomWindowHwnd, _, NotNull())). + WillOnce(SetArgumentPointee<2>((HANDLE)NULL)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); + + // Executor fails. + MockGetWindowExecutor(&invocation.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, RemoveWindow()). + WillOnce(Return(E_FAIL)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostError(_)).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + invocation.Execute(good_args, kRequestId); +} + +TEST_F(WindowApiTests, RemoveWindowStraightline) { + testing::LogDisabler no_dchecks; + + MockIsIeFrameClass(); + MockRemoveWindow invocation; + ListValue good_args; + good_args.Append(Value::CreateIntegerValue(kRandomWindowId)); + MockGetWindowExecutor(&invocation.mock_api_dispatcher_); + EXPECT_CALL(*mock_window_executor_, RemoveWindow()). + WillOnce(Return(S_OK)); + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, PostResult()).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, RegisterEphemeralEventHandler( + StrEq(ext_event_names::kOnWindowRemoved), + window_api::RemoveWindow::ContinueExecution, + invocation.invocation_result_.get())).Times(1); + EXPECT_CALL(invocation.mock_api_dispatcher_, + GetWindowHandleFromId(kRandomWindowId)). + WillOnce(Return(kRandomWindowHwnd)); + ApiDispatcher::InvocationResult* result = invocation.invocation_result_.get(); + invocation.Execute(good_args, kRequestId); + + std::ostringstream args; + args << "[" << kRandomWindowId << "]"; + invocation.ContinueExecution(args.str(), result, invocation.GetDispatcher()); +} + +TEST_F(WindowApiTests, GetAllWindowsErrorHandling) { + testing::LogDisabler no_dchecks; + ListValue bad_args; + DictionaryValue* bad_dict = new DictionaryValue(); + bad_dict->SetInteger(keys::kPopulateKey, 42); + bad_args.Append(bad_dict); + StrictMock<MockIterativeWindowInvocation<window_api::GetAllWindows> > + invocation; + invocation.AllocateNewResult(kRequestId); + // Using a strict mock ensures that FindTopLevelWindows won't get called. + MockIterativeWindowApiResult::ResetCounters(); + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(*invocation.invocation_result_, CallRealPostError(_)).Times(1); + invocation.Execute(bad_args, kRequestId); + EXPECT_EQ(MockIterativeWindowApiResult::error_counter(), 1); +} + +TEST_F(WindowApiTests, GetAllWindowsStraightline) { + testing::LogDisabler no_dchecks; + + ListValue empty_args; + ListValue null_args; + null_args.Append(Value::CreateNullValue()); + ListValue true_args; + DictionaryValue* dict_value = new DictionaryValue; + // The list will take ownership + ASSERT_TRUE(true_args.Set(0, dict_value)); + dict_value->SetBoolean(keys::kPopulateKey, true); + struct { + const ListValue* value; + bool populate; + } values[] = { + { &empty_args, false }, + { &null_args, false }, + { &true_args, true } + }; + + std::set<HWND> ie_hwnds; + static const HWND kWindow1 = reinterpret_cast<HWND>(1); + ie_hwnds.insert(kWindow1); + static const HWND kWindow2 = reinterpret_cast<HWND>(2); + ie_hwnds.insert(kWindow2); + + StrictMock<testing::MockWindowUtils> window_utils; + EXPECT_CALL(window_utils, FindTopLevelWindows(_, NotNull())).WillRepeatedly( + SetArgumentPointee<1>(ie_hwnds)); + + EXPECT_CALL(user32_, GetWindowThreadProcessId(_, _)). + WillRepeatedly(Return(1)); + + for (int i = 0; i < arraysize(values); ++i) { + StrictMock<MockIterativeWindowInvocation<window_api::GetAllWindows> > + invocation; + invocation.AllocateNewResult(kRequestId); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kWindow1, values[i].populate)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, CreateWindowValue( + kWindow2, values[i].populate)).WillOnce(Return(true)); + EXPECT_CALL(*invocation.invocation_result_, CallRealPostResult()).Times(1); + MockIterativeWindowApiResult::ResetCounters(); + invocation.Execute(*values[i].value, kRequestId); + EXPECT_EQ(MockIterativeWindowApiResult::error_counter(), 0); + EXPECT_EQ(MockIterativeWindowApiResult::success_counter(), ie_hwnds.size()); + } +} + +} // namespace diff --git a/ceee/ie/broker/window_events_funnel.cc b/ceee/ie/broker/window_events_funnel.cc new file mode 100644 index 0000000..4300dcc --- /dev/null +++ b/ceee/ie/broker/window_events_funnel.cc @@ -0,0 +1,154 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events to be sent to Chrome. + +#include "ceee/ie/broker/window_events_funnel.h" + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/common/ie_util.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { +// The Shell Hook uses a registered message to communicate with registered +// windows. +const UINT kShellHookMessage = ::RegisterWindowMessage(L"SHELLHOOK"); + +// The HWND, Class and WindowProc for the Registered Shell Hook Window. +HWND shell_hook_window = NULL; +const wchar_t kShellHookWindowClassName[] = L"CeeeShellHookWindow"; +LRESULT CALLBACK WindowEventsFunnelWindowProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam) { + if (message == kShellHookMessage) { + switch (wparam) { + case HSHELL_WINDOWCREATED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnCreated(lparam); + } + break; + } + case HSHELL_WINDOWDESTROYED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnRemoved(lparam); + } + break; + } + case HSHELL_WINDOWACTIVATED: { + if (window_utils::IsWindowClass(reinterpret_cast<HWND>(lparam), + windows::kIeFrameWindowClass)) { + WindowEventsFunnel window_events_funnel; + window_events_funnel.OnFocusChanged(lparam); + } + break; + } + } + } + return ::DefWindowProc(hwnd, message, wparam, lparam); +} +} + +void WindowEventsFunnel::Initialize() { + if (shell_hook_window) + return; + + WNDCLASSEX shell_window_hook_class = {0}; + shell_window_hook_class.cbSize = sizeof(WNDCLASSEX); + shell_window_hook_class.lpfnWndProc = WindowEventsFunnelWindowProc; + shell_window_hook_class.hInstance = GetModuleHandle(NULL); + shell_window_hook_class.lpszClassName = kShellHookWindowClassName; + + ATOM class_registration = ::RegisterClassEx(&shell_window_hook_class); + DCHECK(class_registration != NULL) << + "Couldn't register Shell Hook Window class!" << com::LogWe(); + if (!class_registration) + return; + + shell_hook_window = ::CreateWindow( + reinterpret_cast<wchar_t*>(class_registration), + L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); + DCHECK(shell_hook_window != NULL) << "Couldn't Create Shell Hook Window!" << + com::LogWe(); + if (shell_hook_window) { + BOOL success = ::RegisterShellHookWindow(shell_hook_window); + DCHECK(success) << "Couldn't register shell hook window!" << com::LogWe(); + } +} + +void WindowEventsFunnel::Terminate() { + if (shell_hook_window) { + BOOL success = ::DeregisterShellHookWindow(shell_hook_window); + DCHECK(success) << "Couldn't unregister shell hook window!" << com::LogWe(); + success = ::DestroyWindow(shell_hook_window); + shell_hook_window = NULL; + DCHECK(success) << "Couldn't destroy shell hook window!" << com::LogWe(); + } +} + +HRESULT WindowEventsFunnel::SendEvent(const char* event_name, + const Value& event_args) { + // Event arguments are always stored in a list. + std::string event_args_str; + if (event_args.IsType(Value::TYPE_LIST)) { + base::JSONWriter::Write(&event_args, false, &event_args_str); + } else { + ListValue list; + list.Append(event_args.DeepCopy()); + base::JSONWriter::Write(&list, false, &event_args_str); + } + + DCHECK(ChromePostman::GetInstance() != NULL); + ChromePostman::GetInstance()->FireEvent(CComBSTR(event_name), + CComBSTR(event_args_str.c_str())); + return S_OK; +} + +HRESULT WindowEventsFunnel::OnCreated(int window_id) { + HWND window = reinterpret_cast<HWND>(window_id); + RECT window_rect; + if (!::GetWindowRect(window, &window_rect)) { + DWORD we = ::GetLastError(); + DCHECK(false) << "GetWindowRect failed " << com::LogWe(we); + return HRESULT_FROM_WIN32(we); + } + + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + dict->SetInteger(keys::kIdKey, window_id); + dict->SetBoolean(keys::kFocusedKey, + (window == window_utils::GetTopLevelParent(::GetForegroundWindow()))); + dict->SetInteger(keys::kLeftKey, window_rect.left); + dict->SetInteger(keys::kTopKey, window_rect.top); + dict->SetInteger(keys::kWidthKey, window_rect.right - window_rect.left); + dict->SetInteger(keys::kHeightKey, window_rect.bottom - window_rect.top); + dict->SetBoolean(keys::kIncognitoKey, ie_util::GetIEIsInPrivateBrowsing()); + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we don't yet have a way to tell if the window is a popup or not. + dict->SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + + return SendEvent(ext_event_names::kOnWindowCreated, *dict.get()); +} + +HRESULT WindowEventsFunnel::OnFocusChanged(int window_id) { + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + return SendEvent(ext_event_names::kOnWindowFocusedChanged, *args.get()); +} + +HRESULT WindowEventsFunnel::OnRemoved(int window_id) { + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + return SendEvent(ext_event_names::kOnWindowRemoved, *args.get()); +} diff --git a/ceee/ie/broker/window_events_funnel.h b/ceee/ie/broker/window_events_funnel.h new file mode 100644 index 0000000..0278959 --- /dev/null +++ b/ceee/ie/broker/window_events_funnel.h @@ -0,0 +1,52 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Window Events. + +#ifndef CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ +#define CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ + +#include <wtypes.h> // For HRESULT and others... + +#include "base/basictypes.h" + + +// Fwd. +class Value; + + +// Implements a set of methods to send window related events to the Chrome. +class WindowEventsFunnel { + public: + WindowEventsFunnel() {} + virtual ~WindowEventsFunnel() {} + + // Un/Register our window to receive Shell Hook Notification Messages. + static void Initialize(); + static void Terminate(); + + // Sends the windows.onCreated event to Chrome. + // @param window_id The identifier of the window that was created. + virtual HRESULT OnCreated(int window_id); + + // Sends the windows.onFocusChanged event to Chrome. + // @param window_id The identifier of the window that received the focus. + virtual HRESULT OnFocusChanged(int window_id); + + // Sends the windows.onRemoved event to Chrome. + // @param window_id The identifier of the window that was removed. + virtual HRESULT OnRemoved(int window_id); + + protected: + // Send the given event to Chrome. + // @param event_name The name of the event. + // @param event_args The arguments to be sent with the event. + // protected virtual for testability... + virtual HRESULT SendEvent(const char* event_name, + const Value& event_args); + private: + DISALLOW_COPY_AND_ASSIGN(WindowEventsFunnel); +}; + +#endif // CEEE_IE_BROKER_WINDOW_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/broker/window_events_funnel_unittest.cc b/ceee/ie/broker/window_events_funnel_unittest.cc new file mode 100644 index 0000000..d116bbd --- /dev/null +++ b/ceee/ie/broker/window_events_funnel_unittest.cc @@ -0,0 +1,156 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WindowEventsFunnel. + +// MockWin32 can't be included after ChromeFrameHost because of an include +// incompatibility with atlwin.h. +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include "base/json/json_writer.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/broker/chrome_postman.h" +#include "ceee/ie/broker/window_events_funnel.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { + +using testing::_; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWindowEventsFunnel : public WindowEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); + HRESULT CallSendEvent(const char* name, const Value& value) { + return WindowEventsFunnel::SendEvent(name, value); + } +}; + +TEST(WindowEventsFunnelTest, SendEvent) { + TestWindowEventsFunnel events_funnel; + + static const char* kEventName = "MADness"; + DictionaryValue event_args; + event_args.SetInteger("Answer to the Ultimate Question of Life," + "the Universe, and Everything", 42); + event_args.SetString("AYBABTU", "All your base are belong to us"); + event_args.SetReal("www.unrealtournament.com", 3.0); + + ListValue args_list; + args_list.Append(event_args.DeepCopy()); + + std::string event_args_str; + base::JSONWriter::Write(&args_list, false, &event_args_str); + + class MockChromePostman : public ChromePostman { + public: + MOCK_METHOD2(FireEvent, void(BSTR, BSTR)); + }; + // Simply instantiating the postman will register it as the + // one and only singleton to use all the time. + CComObjectStackEx<testing::StrictMock<MockChromePostman>> postman; + EXPECT_CALL(postman, FireEvent( + StrEq(CComBSTR(kEventName).m_str), + StrEq(CComBSTR(event_args_str.c_str()).m_str))).Times(1); + EXPECT_HRESULT_SUCCEEDED(events_funnel.CallSendEvent(kEventName, event_args)); +} + +// Mock IeIsInPrivateBrowsing. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIEIsInPrivateBrowsing, + GetIEIsInPrivateBrowsing); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetIEIsInPrivateBrowsing); +MOCK_STATIC_CLASS_END(MockIeUtil) + +TEST(WindowEventsFunnelTest, OnWindowCreated) { + testing::LogDisabler no_dchecks; + TestWindowEventsFunnel window_events_funnel; + + int window_id = 42; + HWND window = reinterpret_cast<HWND>(window_id); + + testing::MockUser32 user32; + EXPECT_CALL(user32, GetWindowRect(window, NotNull())) + .WillOnce(Return(FALSE)); + EXPECT_CALL(window_events_funnel, SendEvent(_, _)).Times(0); + // We return the HRESULT conversion of GetLastError. + ::SetLastError(ERROR_INVALID_ACCESS); + EXPECT_HRESULT_FAILED(window_events_funnel.OnCreated(window_id)); + ::SetLastError(ERROR_SUCCESS); + + RECT window_rect = {1, 2, 3, 4}; + EXPECT_CALL(user32, GetWindowRect(window, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + EXPECT_CALL(user32, GetForegroundWindow()).WillOnce(Return((HWND)NULL)); + testing::MockWindowUtils mock_windows_utils; + EXPECT_CALL(mock_windows_utils, GetTopLevelParent((HWND)NULL)) + .WillOnce(Return(window)); + + StrictMock<MockIeUtil> mock_ie_util; + EXPECT_CALL(mock_ie_util, GetIEIsInPrivateBrowsing()).WillRepeatedly( + Return(false)); + + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + dict->SetInteger(keys::kIdKey, window_id); + dict->SetBoolean(keys::kFocusedKey, true); + dict->SetInteger(keys::kLeftKey, window_rect.left); + dict->SetInteger(keys::kTopKey, window_rect.top); + dict->SetInteger(keys::kWidthKey, window_rect.right - window_rect.left); + dict->SetInteger(keys::kHeightKey, window_rect.bottom - window_rect.top); + dict->SetBoolean(keys::kIncognitoKey, false); + // TODO(mad@chromium.org): for now, always setting to "normal" since + // we are not handling popups or app windows in IE yet. + dict->SetString(keys::kWindowTypeKey, keys::kWindowTypeValueNormal); + + EXPECT_CALL(window_events_funnel, SendEvent(StrEq( + ext_event_names::kOnWindowCreated), ValuesEqual(dict.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnCreated(window_id)); +} + +TEST(WindowEventsFunnelTest, OnFocusChanged) { + TestWindowEventsFunnel window_events_funnel; + int window_id = 42; + + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + EXPECT_CALL(window_events_funnel, SendEvent(StrEq( + ext_event_names::kOnWindowFocusedChanged), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnFocusChanged(window_id)); +} + +TEST(WindowEventsFunnelTest, OnWindowRemoved) { + TestWindowEventsFunnel window_events_funnel; + int window_id = 42; + + scoped_ptr<Value> args(Value::CreateIntegerValue(window_id)); + EXPECT_CALL(window_events_funnel, SendEvent( + StrEq(ext_event_names::kOnWindowRemoved), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(window_events_funnel.OnRemoved(window_id)); +} + +} // namespace diff --git a/ceee/ie/common/api_registration.h b/ceee/ie/common/api_registration.h new file mode 100644 index 0000000..26dad37 --- /dev/null +++ b/ceee/ie/common/api_registration.h @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Lists the APIs to be registered, and in which modules to register them. +// This is to give us a single place to specify the list of APIs to handle +// (in the broker) and to redirect (in each Chrome Frame host - which is not +// necessarily in the broker and thus wouldn't have a ProductionApiDispatcher +// object available to query for registered functions). + +#ifndef CEEE_IE_COMMON_API_REGISTRATION_H_ +#define CEEE_IE_COMMON_API_REGISTRATION_H_ + +#include "chrome/browser/extensions/execute_code_in_tab_function.h" +#include "chrome/browser/extensions/extension_cookies_api.h" +#include "chrome/browser/extensions/extension_infobar_module.h" +#include "chrome/browser/extensions/extension_tabs_module.h" + +#ifdef REGISTER_API_FUNCTION +#error Must change the name of the REGISTER_API_FUNCTION macro in this file +#endif // REGISTER_API_FUNCTION + +// You must define a REGISTER_API_FUNCTION macro in the compilation unit where +// you include this file, before invoking any of the REGISTER_XYZ_API_FUNCTIONS +// macros. +// +// In the compilation unit that registers function groups with the production +// API dispatcher, define PRODUCTION_API_DISPATCHER and your own definition of +// each of these macros. This may seem a bit roundabout, it's just done this +// way to ensure that if you add function groups to this file and forget to +// update the production API dispatcher you'll get a compilation error. + +#ifndef PRODUCTION_API_DISPATCHER + +// Registers the chrome.tab.* functions we handle. +#define REGISTER_TAB_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetTab); \ + REGISTER_API_FUNCTION(GetSelectedTab); \ + REGISTER_API_FUNCTION(GetAllTabsInWindow); \ + REGISTER_API_FUNCTION(CreateTab); \ + REGISTER_API_FUNCTION(UpdateTab); \ + REGISTER_API_FUNCTION(MoveTab); \ + REGISTER_API_FUNCTION(TabsExecuteScript); \ + REGISTER_API_FUNCTION(TabsInsertCSS); \ + REGISTER_API_FUNCTION(RemoveTab) + +// Registers the chrome.window.* functions we handle. +// We don't have CreateWindow in here because of a compilation limitation. +// See the comment in the windows_api::CreateWindowFunc class for more details. +#define REGISTER_WINDOW_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetWindow); \ + REGISTER_API_FUNCTION(GetCurrentWindow); \ + REGISTER_API_FUNCTION(GetLastFocusedWindow); \ + REGISTER_API_FUNCTION(UpdateWindow); \ + REGISTER_API_FUNCTION(RemoveWindow); \ + REGISTER_API_FUNCTION(GetAllWindows) + +// Registers the chrome.cookies.* functions we handle. +#define REGISTER_COOKIE_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(GetCookie); \ + REGISTER_API_FUNCTION(GetAllCookies); \ + REGISTER_API_FUNCTION(SetCookie); \ + REGISTER_API_FUNCTION(RemoveCookie); \ + REGISTER_API_FUNCTION(GetAllCookieStores) + +// Registers the chrome.experimental.infobars.* functions we handle. +#define REGISTER_INFOBAR_API_FUNCTIONS() \ + REGISTER_API_FUNCTION(ShowInfoBar) + +// Although we don't need to handle any chrome.experimental.webNavigation.* +// functions, we use this macro to register permanent event handlers when +// PRODUCTION_API_DISPATCHER is defined. +#define REGISTER_WEBNAVIGATION_API_FUNCTIONS() + +// Although we don't need to handle any chrome.experimental.webRequest.* +// functions, we use this macro to register permanent event handlers when +// PRODUCTION_API_DISPATCHER is defined. +#define REGISTER_WEBREQUEST_API_FUNCTIONS() + +// Add new tab function groups before this line. +#endif // PRODUCTION_API_DISPATCHER + +// Call this to register all functions. If you don't define +// PRODUCTION_API_DISPATCHER it will simply cause REGISTER_FUNCTION to be called +// for all functions. Otherwise it will cause your custom implementation of +// REGISTER_XYZ_API_FUNCTIONS to be called for each of the function groups +// above. +#define REGISTER_ALL_API_FUNCTIONS() \ + REGISTER_TAB_API_FUNCTIONS(); \ + REGISTER_WINDOW_API_FUNCTIONS(); \ + REGISTER_COOKIE_API_FUNCTIONS(); \ + REGISTER_INFOBAR_API_FUNCTIONS(); \ + REGISTER_WEBNAVIGATION_API_FUNCTIONS(); \ + REGISTER_WEBREQUEST_API_FUNCTIONS() + +#endif // CEEE_IE_COMMON_API_REGISTRATION_H_ diff --git a/ceee/ie/common/ceee_module_util.cc b/ceee/ie/common/ceee_module_util.cc new file mode 100644 index 0000000..0b9335e --- /dev/null +++ b/ceee/ie/common/ceee_module_util.cc @@ -0,0 +1,279 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CEEE module-wide utilities. + +#include "ceee/ie/common/ceee_module_util.h" + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "base/win/registry.h" +#include "ceee/common/process_utils_win.h" +#include "chrome/installer/util/google_update_constants.h" + +namespace { + +const wchar_t* kRegistryPath = L"SOFTWARE\\Google\\CEEE"; +const wchar_t* kRegistryValueToolbandIsHidden = L"toolband_is_hidden"; +const wchar_t* kRegistryValueToolbandPlaced = L"toolband_placed"; +const wchar_t* kRegistryValueCrxInstalledPath = L"crx_installed_path"; +const wchar_t* kRegistryValueCrxInstalledTime = L"crx_installed_time"; + +// Global state needed by the BHO and the +// toolband, to indicate whether ShowDW calls should affect +// registry tracking of the user's visibility preference. A +// non-zero value indicates that the calls should be ignored. +LONG g_ignore_show_dw_changes = 0; + +bool GetCeeeRegistryBoolean(const wchar_t* key, bool default_value) { + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + LOG_IF(ERROR, !hkcu.Valid()) << "Could not open reg key: " << kRegistryPath; + + DWORD dword_value_representation = 0; + DWORD size = sizeof(dword_value_representation); + DWORD type = REG_DWORD; + + if (!hkcu.Valid() || + !hkcu.ReadValue(key, &dword_value_representation, &size, &type)) { + return default_value; + } + + return dword_value_representation != 0; +} + +void SetCeeeRegistryBoolean(const wchar_t* key, bool assign_value) { + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_WRITE); + LOG_IF(ERROR, !hkcu.Valid()) << "Could not open reg key: " << kRegistryPath; + + DWORD dword_value_representation = assign_value ? 1 : 0; + bool write_result = hkcu.WriteValue(key, &dword_value_representation, + sizeof(dword_value_representation), + REG_DWORD); + + LOG_IF(ERROR, !write_result) << "Failed to write a registry key: " << key; +} + +} // anonymous namespace + +namespace ceee_module_util { + + +// The name of the profile we want ChromeFrame to use (for Internet Explorer). +const wchar_t kChromeProfileName[] = L"iexplore"; + +// The name of the profile we want ChromeFrame to use (for Internet Explorer) +// in case when explorer is 'Run as Administrator'. +const wchar_t kChromeProfileNameForAdmin[] = L"iexplore_admin"; + +const wchar_t kChromeProfileNameForFirefox[] = L"ceee_ff"; +const wchar_t kInternetExplorerModuleName[] = L"iexplore.exe"; +const wchar_t kCeeeBrokerModuleName[] = L"ceee_broker.exe"; + +std::wstring GetExtensionPath() { + const wchar_t* kRegistryValue = L"crx_path"; + + // We first check HKEY_CURRENT_USER, then HKEY_LOCAL_MACHINE. This is to + // let individual users override the machine-wide setting. The machine-wide + // setting is needed as our installer runs as system. + // + // On 64-bit Windows, there are separate versions of the registry for 32-bit + // applications and 64-bit applications. When you manually set things in the + // registry, you may have set them using the 32-bit regedit program + // (generally at c:\windows\SysWOW64\regedit.exe) or the 64-bit version, + // confusingly named regedt32.exe (at c:\windows\system32\regedt32.exe). + // + // You need to make sure you set the version of the registry corresponding to + // the way this executable is compiled. + // + // If the registry values are not set, then attempt to locate the extension + // by convention: that is, in a folder called + // "[32-bit program files]\Google\CEEE\Extensions", look for the + // first valid directory and use that. Eventually, when we support more + // than one extension, we can load/install all directories and/or crx files + // found here. + std::wstring crx_path; + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, kRegistryPath, KEY_READ); + + base::win::RegKey* keys[] = { &hkcu, &hklm }; + for (int i = 0; i < arraysize(keys); ++i) { + if (keys[i]->Valid() && keys[i]->ReadValue(kRegistryValue, &crx_path)) + break; + } + + if (crx_path.size() == 0u) { + FilePath file_path; + PathService::Get(base::DIR_PROGRAM_FILES, &file_path); + + file_path = file_path.Append(L"Google").Append(L"CEEE"). + Append(L"Extensions"); + if (!file_path.empty()) { + // First check for a .crx file (we prefer the .crx) + file_util::FileEnumerator e( + file_path, false, file_util::FileEnumerator::FILES); + for (FilePath ext = e.Next(); !ext.empty(); ext = e.Next()) { + if (ext.Extension() == L".crx") { + crx_path = ext.value(); + break; + } + } + + if (crx_path.size() == 0u) { + // Use the first directory instead, in the hope that it is an + // exploded extension directory. + file_util::FileEnumerator e( + file_path, false, file_util::FileEnumerator::DIRECTORIES); + FilePath directory = e.Next(); + if (!directory.empty()) { + crx_path = directory.value(); + } + } + } + } + + // Don't DCHECK here - it's an expected case that no .crx file is provided + // for installation along with CEEE. + LOG_IF(WARNING, crx_path.empty()) << "No CRX found to install."; + return crx_path; +} + +FilePath GetInstalledExtensionPath() { + std::wstring crx_path; + + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + // Default value is good enough for us. + hkcu.ReadValue(kRegistryValueCrxInstalledPath, &crx_path); + + return FilePath(crx_path); +} + +base::Time GetInstalledExtensionTime() { + int64 crx_time = 0; + DWORD size = sizeof(crx_time); + + base::win::RegKey hkcu(HKEY_CURRENT_USER, kRegistryPath, KEY_READ); + // Default value is good enough for us. + hkcu.ReadValue(kRegistryValueCrxInstalledTime, &crx_time, &size, NULL); + + return base::Time::FromInternalValue(crx_time); +} + +bool NeedToInstallExtension() { + std::wstring path = GetExtensionPath(); + if (path.empty()) { + // No extension to install, so no need to install the extension. + return false; + } + + const FilePath crx_path(path); + if (IsCrxOrEmpty(crx_path.value())) { + if (crx_path == GetInstalledExtensionPath()) { + base::Time installed_time; + base::PlatformFileInfo extension_info; + const bool success = file_util::GetFileInfo(crx_path, &extension_info); + // If the call above didn't succeed, assume we need to install. + return !success || + extension_info.last_modified > GetInstalledExtensionTime(); + } + + return true; + } + + return false; +} + +void SetInstalledExtensionPath(const FilePath& path) { + base::PlatformFileInfo extension_info; + const bool success = file_util::GetFileInfo(path, &extension_info); + const int64 crx_time = success ? + extension_info.last_modified.ToInternalValue() : + base::Time::Now().ToInternalValue(); + + base::win::RegKey key(HKEY_CURRENT_USER, kRegistryPath, KEY_WRITE); + bool write_result = key.WriteValue(kRegistryValueCrxInstalledTime, + &crx_time, + sizeof(crx_time), + REG_QWORD); + DCHECK(write_result); + write_result = key.WriteValue(kRegistryValueCrxInstalledPath, + path.value().c_str()); + DCHECK(write_result); +} + +bool IsCrxOrEmpty(const std::wstring& path) { + return (path.empty() || + (path.substr(std::max(path.size() - 4, 0u)) == L".crx")); +} + +void SetOptionToolbandIsHidden(bool is_hidden) { + SetCeeeRegistryBoolean(kRegistryValueToolbandIsHidden, is_hidden); +} + +bool GetOptionToolbandIsHidden() { + return GetCeeeRegistryBoolean(kRegistryValueToolbandIsHidden, false); +} + +void SetOptionToolbandForceReposition(bool reposition_next_time) { + SetCeeeRegistryBoolean(kRegistryValueToolbandPlaced, !reposition_next_time); +} + +bool GetOptionToolbandForceReposition() { + return !GetCeeeRegistryBoolean(kRegistryValueToolbandPlaced, false); +} + +void SetIgnoreShowDWChanges(bool ignore) { + if (ignore) { + ::InterlockedIncrement(&g_ignore_show_dw_changes); + } else { + ::InterlockedDecrement(&g_ignore_show_dw_changes); + } +} + +bool GetIgnoreShowDWChanges() { + return ::InterlockedExchangeAdd(&g_ignore_show_dw_changes, 0) != 0; +} + +const wchar_t* GetBrokerProfileNameForIe() { + bool running_as_admin = false; + HRESULT hr = + process_utils_win::IsCurrentProcessUacElevated(&running_as_admin); + DCHECK(SUCCEEDED(hr)); + + // Profile name for 'runas' mode has to be different because this mode spawns + // its own copy of ceee_broker. To avoid scrambling user data, we run it in + // different profile. In the unlikely event of even failing to retrieve info, + // run as normal user. + return (FAILED(hr) || !running_as_admin) ? kChromeProfileName + : kChromeProfileNameForAdmin; +} + +// TODO(vitalybuka@google.com) : remove this code and use BrowserDistribution. +// BrowserDistribution requires modification to know about CEEE (bb3136374). +std::wstring GetCromeFrameClientStateKey() { + static const wchar_t kChromeFrameGuid[] = + L"{8BA986DA-5100-405E-AA35-86F34A02ACBF}"; + std::wstring key(google_update::kRegPathClientState); + key.append(L"\\"); + key.append(kChromeFrameGuid); + return key; +} + +// TODO(vitalybuka@google.com) : remove this code and use +// GoogleUpdateSettings::GetCollectStatsConsent() code. +// BrowserDistribution requires modification to know about CEEE (bb3136374). +bool GetCollectStatsConsent() { + std::wstring reg_path = GetCromeFrameClientStateKey(); + base::win::RegKey key(HKEY_CURRENT_USER, reg_path.c_str(), KEY_READ); + DWORD value; + if (!key.ReadValueDW(google_update::kRegUsageStatsField, &value)) { + base::win::RegKey hklm_key(HKEY_LOCAL_MACHINE, reg_path.c_str(), KEY_READ); + if (!hklm_key.ReadValueDW(google_update::kRegUsageStatsField, &value)) + return false; + } + return (1 == value); +} + +} // namespace ceee_module_util diff --git a/ceee/ie/common/ceee_module_util.h b/ceee/ie/common/ceee_module_util.h new file mode 100644 index 0000000..e7b2f30 --- /dev/null +++ b/ceee/ie/common/ceee_module_util.h @@ -0,0 +1,101 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CEEE module-wide utilities. + +#ifndef CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ +#define CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ + +#include <wtypes.h> +#include <string> + +#include "base/file_path.h" +#include "base/time.h" + +namespace ceee_module_util { + +void AddRefModuleWorkerThread(); +void ReleaseModuleWorkerThread(); + +// Fires an event to the broker, so that the call can be made with an +// instance of a broker proxy that was CoCreated in the worker thread. +void FireEventToBroker(const std::string& event_name, + const std::string& event_args); + +void Lock(); +void Unlock(); + +LONG LockModule(); +LONG UnlockModule(); + +class AutoLock { + public: + AutoLock() { + Lock(); + } + ~AutoLock() { + Unlock(); + } +}; + +// Gets the path of a .crx that should be installed at the same time as +// CEEE is installed. May be empty, meaning no .crx files need to be +// installed. +// +// If the returned path does not have a .crx extension, it should be +// assumed to be an exploded extension directory rather than a .crx +std::wstring GetExtensionPath(); + +// Return the path and time of the last installation that occurred. +FilePath GetInstalledExtensionPath(); +base::Time GetInstalledExtensionTime(); + +// Return true if the extension is outdated compared to the registry +bool NeedToInstallExtension(); + +// Install an extension and sets the right registry values +void SetInstalledExtensionPath(const FilePath& path); + +// Returns true if the given path ends with .crx or is empty (as an +// unset "what to load/install" preference means we should expect +// an extension to already be installed). +bool IsCrxOrEmpty(const std::wstring& path); + +// Stores/reads a registry entry that tracks whether the user has made the +// toolband visible or hidden. +void SetOptionToolbandIsHidden(bool isHidden); +bool GetOptionToolbandIsHidden(); + +// Stores/reads a registry entry that tracks whether intial (after setup) +// positioning of the toolband has been completed. +void SetOptionToolbandForceReposition(bool reposition_next_time); +bool GetOptionToolbandForceReposition(); + +// Indicates whether ShowDW calls should affect registry tracking of the +// user's visibility preference. +void SetIgnoreShowDWChanges(bool ignore); +bool GetIgnoreShowDWChanges(); + +// Chooses between kChromeProfileName and kChromeProfileNameForAdmin depending +// on process properties (run as admin or not). +const wchar_t* GetBrokerProfileNameForIe(); + +// Returns true if Chrome Frame is allowed to send usage stats. +bool GetCollectStatsConsent(); + +// Returns Google Update ClientState registry key for ChromeFrame. +std::wstring GetCromeFrameClientStateKey(); + +// The name of the profile that is used by the Firefox CEEE. +extern const wchar_t kChromeProfileNameForFirefox[]; + +// The name of the Internet Explorer executable. +extern const wchar_t kInternetExplorerModuleName[]; + +// The name of the CEEE Broker executable. +extern const wchar_t kCeeeBrokerModuleName[]; + +} // namespace + +#endif // CEEE_IE_COMMON_TOOLBAND_MODULE_UTIL_H__ diff --git a/ceee/ie/common/ceee_module_util_unittest.cc b/ceee/ie/common/ceee_module_util_unittest.cc new file mode 100644 index 0000000..67c0dd1 --- /dev/null +++ b/ceee/ie/common/ceee_module_util_unittest.cc @@ -0,0 +1,287 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for the CEEE module-wide utilities. + +#include "ceee/ie/common/ceee_module_util.h" + +#include <wtypes.h> +#include <string> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "base/win/registry.h" +#include "base/string_util.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/mock_win32.h" +#include "ceee/testing/utils/test_utils.h" +#include "chrome/installer/util/google_update_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::DoAll; +using testing::NotNull; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::Return; + +const wchar_t* kReplacementRoot = + L"Software\\Google\\InstallUtilUnittest"; +const wchar_t* kHKCUReplacement = + L"Software\\Google\\InstallUtilUnittest\\HKCU"; +const wchar_t* kHKLMReplacement = + L"Software\\Google\\InstallUtilUnittest\\HKLM"; + +MOCK_STATIC_CLASS_BEGIN(MockProcessWinUtils) + MOCK_STATIC_INIT_BEGIN(MockProcessWinUtils) + MOCK_STATIC_INIT2(process_utils_win::IsCurrentProcessUacElevated, + IsCurrentProcessUacElevated); + MOCK_STATIC_INIT_END() + + MOCK_STATIC1(HRESULT, , IsCurrentProcessUacElevated, bool*); +MOCK_STATIC_CLASS_END(MockProcessWinUtils) + + +class CeeeModuleUtilTest : public testing::Test { + protected: + static const DWORD kFalse = 0; + static const DWORD kTrue = 1; + static const DWORD kInvalid = 5; + + virtual void SetUp() { + // Wipe the keys we redirect to. + // This gives us a stable run, even in the presence of previous + // crashes or failures. + LSTATUS err = SHDeleteKey(HKEY_CURRENT_USER, kReplacementRoot); + EXPECT_TRUE(err == ERROR_SUCCESS || err == ERROR_FILE_NOT_FOUND); + + // Create the keys we're redirecting HKCU and HKLM to. + ASSERT_TRUE(hkcu_.Create(HKEY_CURRENT_USER, kHKCUReplacement, KEY_READ)); + ASSERT_TRUE(hklm_.Create(HKEY_CURRENT_USER, kHKLMReplacement, KEY_READ)); + + // And do the switcharoo. + ASSERT_EQ(ERROR_SUCCESS, + ::RegOverridePredefKey(HKEY_CURRENT_USER, hkcu_.Handle())); + ASSERT_EQ(ERROR_SUCCESS, + ::RegOverridePredefKey(HKEY_LOCAL_MACHINE, hklm_.Handle())); + } + + virtual void TearDown() { + // Undo the redirection. + EXPECT_EQ(ERROR_SUCCESS, ::RegOverridePredefKey(HKEY_CURRENT_USER, NULL)); + EXPECT_EQ(ERROR_SUCCESS, ::RegOverridePredefKey(HKEY_LOCAL_MACHINE, NULL)); + + // Close our handles and delete the temp keys we redirected to. + hkcu_.Close(); + hklm_.Close(); + EXPECT_EQ(ERROR_SUCCESS, SHDeleteKey(HKEY_CURRENT_USER, kReplacementRoot)); + } + + base::win::RegKey hkcu_; + base::win::RegKey hklm_; +}; + +// Mock PathService::Get that is used to look up files. +MOCK_STATIC_CLASS_BEGIN(MockPathService) + MOCK_STATIC_INIT_BEGIN(MockPathService) + bool (*func_ptr)(int, FilePath*) = PathService::Get; + MOCK_STATIC_INIT2(func_ptr, Get); + MOCK_STATIC_INIT_END() + + MOCK_STATIC2(bool, , Get, int, FilePath*); +MOCK_STATIC_CLASS_END(MockPathService); + +// Empty registry case (without crx) +TEST_F(CeeeModuleUtilTest, ExtensionPathTestNoRegistry) { + namespace cmu = ceee_module_util; + + StrictMock<MockPathService> mock_path; + FilePath temp_path; + ASSERT_TRUE(file_util::CreateNewTempDirectory(L"CeeeModuleUtilTest", + &temp_path)); + + EXPECT_CALL(mock_path, Get(base::DIR_PROGRAM_FILES, NotNull())). + WillOnce(DoAll( + SetArgumentPointee<1>(temp_path), + Return(true))); + EXPECT_EQ(std::wstring(), cmu::GetExtensionPath()); + + FilePath full_path = temp_path.Append(L"Google"). + Append(L"CEEE").Append(L"Extensions"); + FilePath crx_path = full_path.Append(L"testing.crx"); + ASSERT_TRUE(file_util::CreateDirectory(full_path)); + FilePath temp_file_path; + ASSERT_TRUE(file_util::CreateTemporaryFileInDir(full_path, &temp_file_path)); + ASSERT_TRUE(file_util::Move(temp_file_path, crx_path)); + EXPECT_CALL(mock_path, Get(base::DIR_PROGRAM_FILES, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(temp_path), Return(true))); + EXPECT_EQ(crx_path.value(), cmu::GetExtensionPath()); + + // Clean up. + file_util::Delete(temp_path, true); +} + +TEST_F(CeeeModuleUtilTest, ExtensionPathTest) { + namespace cmu = ceee_module_util; + + // The FilePath::Get method shouldn't be called if we take the value + // from the registry. + StrictMock<MockPathService> mock_path; + + // Creates a registry key + base::win::RegKey hkcu(HKEY_CURRENT_USER, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + + // Create a random string so we can compare the result with it. + // Ideally, this would be a really random string, not some arbitrary constant. + std::wstring path = L"asdfqwerasdfqwer"; + const wchar_t* kRegistryValueName = L"crx_path"; + + hklm.WriteValue(kRegistryValueName, path.c_str()); + hklm.Close(); + EXPECT_EQ(path, cmu::GetExtensionPath()); + + // Change the random string and verify we get the new one if we set it + // to HKCU + path += L"_HKCU"; + hkcu.WriteValue(kRegistryValueName, path.c_str()); + hkcu.Close(); + EXPECT_EQ(path, cmu::GetExtensionPath()); +} + +TEST_F(CeeeModuleUtilTest, NeedToInstallExtension) { + namespace cmu = ceee_module_util; + + // Create our registry key and a temporary file + base::win::RegKey hkcu(HKEY_CURRENT_USER, L"SOFTWARE\\Google\\CEEE", + KEY_WRITE); + FilePath temp_path; + ASSERT_TRUE(file_util::CreateNewTempDirectory(L"CeeeModuleUtilTest", + &temp_path)); + FilePath crx_path = temp_path.Append(L"temp.crx"); + FilePath temp_file_path; + ASSERT_TRUE(file_util::CreateTemporaryFileInDir(temp_path, &temp_file_path)); + ASSERT_TRUE(file_util::Move(temp_file_path, crx_path)); + + const wchar_t* kRegistryCrxPathValueName = L"crx_path"; + const wchar_t* kRegistryValueCrxInstalledPath = L"crx_installed_path"; + const wchar_t* kRegistryValueCrxInstalledTime = L"crx_installed_time"; + + hkcu.WriteValue(kRegistryCrxPathValueName, crx_path.value().c_str()); + + // Those should return empty values + // We use EXPECT_TRUE instead of EXPECT_EQ because there's no operator<< + // declared for FilePath and base::Time so we cannot output the + // expected/received values. + EXPECT_TRUE(FilePath() == cmu::GetInstalledExtensionPath()); + EXPECT_TRUE(base::Time() == cmu::GetInstalledExtensionTime()); + + // Now we should need to install, since the keys aren't there yet. + EXPECT_TRUE(cmu::NeedToInstallExtension()); + + // And if we update the install info, we shouldn't need to anymore. + cmu::SetInstalledExtensionPath(crx_path); + EXPECT_FALSE(cmu::NeedToInstallExtension()); + + // We get the installed path and time and verify them against our values. + base::PlatformFileInfo crx_info; + ASSERT_TRUE(file_util::GetFileInfo(crx_path, &crx_info)); + EXPECT_TRUE(crx_path == cmu::GetInstalledExtensionPath()); + EXPECT_TRUE(crx_info.last_modified == cmu::GetInstalledExtensionTime()); + + // Finally, if we update the file time, we should be able to install again. + // But we must make sure to cross the 10ms boundary which is the granularity + // of the FILETIME, so sleep for 20ms to be on the safe side. + ::Sleep(20); + ASSERT_TRUE(file_util::SetLastModifiedTime(crx_path, base::Time::Now())); + EXPECT_TRUE(cmu::NeedToInstallExtension()); +} + +TEST(CeeeModuleUtil, GetBrokerProfileNameForIe) { + MockProcessWinUtils mock_query_admin; + + EXPECT_CALL(mock_query_admin, IsCurrentProcessUacElevated(_)). + WillOnce(DoAll(SetArgumentPointee<0>(false), Return(S_OK))); + const wchar_t* profile_user = + ceee_module_util::GetBrokerProfileNameForIe(); + + EXPECT_CALL(mock_query_admin, IsCurrentProcessUacElevated(_)). + WillOnce(DoAll(SetArgumentPointee<0>(true), Return(S_OK))); + const wchar_t* profile_admin = + ceee_module_util::GetBrokerProfileNameForIe(); + + ASSERT_STRNE(profile_user, profile_admin); +} + +TEST(CeeeModuleUtil, GetCromeFrameClientStateKey) { + EXPECT_EQ(L"Software\\Google\\Update\\ClientState\\" + L"{8BA986DA-5100-405E-AA35-86F34A02ACBF}", + ceee_module_util::GetCromeFrameClientStateKey()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentFromHkcu) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hkcu(HKEY_CURRENT_USER, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kTrue)); + EXPECT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kInvalid)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.DeleteValue(google_update::kRegUsageStatsField)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentFromHklm) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kTrue)); + EXPECT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kInvalid)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hklm.DeleteValue(google_update::kRegUsageStatsField)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); +} + +TEST_F(CeeeModuleUtilTest, GetCollectStatsConsentHkcuBeforeHklm) { + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + base::win::RegKey hkcu(HKEY_CURRENT_USER, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + base::win::RegKey hklm(HKEY_LOCAL_MACHINE, + ceee_module_util::GetCromeFrameClientStateKey().c_str(), KEY_WRITE); + + ASSERT_TRUE(hklm.WriteValue(google_update::kRegUsageStatsField, kTrue)); + ASSERT_TRUE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.WriteValue(google_update::kRegUsageStatsField, kFalse)); + EXPECT_FALSE(ceee_module_util::GetCollectStatsConsent()); + + ASSERT_TRUE(hkcu.DeleteValue(google_update::kRegUsageStatsField)); + ASSERT_TRUE(hklm.DeleteValue(google_update::kRegUsageStatsField)); +} + +} // namespace diff --git a/ceee/ie/common/chrome_frame_host.cc b/ceee/ie/common/chrome_frame_host.cc new file mode 100644 index 0000000..907b1f9 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host.cc @@ -0,0 +1,373 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ChromeFrameHost implementation. +#include "ceee/ie/common/chrome_frame_host.h" + +#include <algorithm> +#include <vector> + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/process_utils_win.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/common/chrome_switches.h" + +#include "toolband.h" // NOLINT + +namespace ext = extension_automation_constants; + + +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_bstr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BSTR } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_idispatch_variantptr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BYREF | VT_VARIANT } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_bstrarray_= + { CC_STDCALL, VT_EMPTY, 1, { VT_ARRAY | VT_BSTR } }; +_ATL_FUNC_INFO ChromeFrameHost::handler_type_void_= + { CC_STDCALL, VT_EMPTY, 0, { } }; + +// {AFA3E2CF-2C8E-4546-8CD0-A2D93759A4DE} +extern const GUID IID_IChromeFrameHost = + { 0xafa3e2cf, 0x2c8e, 0x4546, + { 0x8c, 0xd0, 0xa2, 0xd9, 0x37, 0x59, 0xa4, 0xde } }; + +ChromeFrameHost::ChromeFrameHost() + : document_loaded_(false), origin_(ext::kAutomationOrigin) { + LOG(INFO) << "Create ChromeFrameHost(" << this << ")"; +} + +ChromeFrameHost::~ChromeFrameHost() { + LOG(INFO) << "Destroy ChromeFrameHost(" << this << ")"; +} + +HRESULT ChromeFrameHost::FinalConstruct() { + return S_OK; +} + +void ChromeFrameHost::FinalRelease() { +} + +STDMETHODIMP ChromeFrameHost::GetWantsPrivileged(boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::GetChromeProfileName(BSTR* profile_name) { + return chrome_profile_name_.CopyTo(profile_name); +} + +STDMETHODIMP ChromeFrameHost::GetExtensionApisToAutomate( + BSTR* functions_enabled) { + DCHECK(functions_enabled != NULL); + HRESULT hr = S_FALSE; + if (event_sink_ != NULL) { + hr = event_sink_->OnCfGetExtensionApisToAutomate(functions_enabled); +#ifndef NDEBUG + if (*functions_enabled != NULL) { + // Only one chrome frame host is allowed to return a list of functions + // to enable automation on, so make sure we are the one and only. + std::wstring event_name(L"google-ceee-apiautomation!"); + + DCHECK(chrome_profile_name_ != NULL); + if (chrome_profile_name_ != NULL) + event_name += chrome_profile_name_; + + std::replace(event_name.begin(), event_name.end(), '\\', '!'); + std::transform( + event_name.begin(), event_name.end(), event_name.begin(), tolower); + automating_extension_api_.Set( + ::CreateEvent(NULL, TRUE, TRUE, event_name.c_str())); + DWORD we = ::GetLastError(); + DCHECK(automating_extension_api_ != NULL && + we != ERROR_ALREADY_EXISTS && + we != ERROR_ACCESS_DENIED); + } +#endif // NDEBUG + } + return hr; +} + +HRESULT ChromeFrameHost::Initialize() { + return S_OK; +} + +STDMETHODIMP ChromeFrameHost::TearDown() { + if (IsWindow()) { + // TearDown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + + chrome_frame_.Release(); +#ifndef NDEBUG + automating_extension_api_.Close(); +#endif + return S_OK; +} + +STDMETHODIMP_(void) ChromeFrameHost::SetEventSink( + IChromeFrameHostEvents* event_sink) { + event_sink_ = event_sink; +} + +HRESULT ChromeFrameHost::InstallExtension(BSTR crx_path) { + if (chrome_frame_) { + return chrome_frame_->installExtension(crx_path); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::LoadExtension(BSTR extension_dir) { + if (chrome_frame_) { + return chrome_frame_->installExtension(extension_dir); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::GetEnabledExtensions() { + if (chrome_frame_) { + return chrome_frame_->getEnabledExtensions(); + } else { + NOTREACHED(); + return E_UNEXPECTED; + } +} + +HRESULT ChromeFrameHost::GetSessionId(int *session_id) { + if (chrome_frame_) { + CComQIPtr<IChromeFrameInternal> chrome_frame_internal_(chrome_frame_); + if (chrome_frame_internal_) + return chrome_frame_internal_->getSessionId(session_id); + else + return kInvalidChromeSessionId; + } + NOTREACHED(); + return E_UNEXPECTED; +} + +void ChromeFrameHost::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +HRESULT ChromeFrameHost::SetChildSite(IUnknown* child) { + if (child == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + CComPtr<IObjectWithSite> child_site; + hr = child->QueryInterface(&child_site); + if (SUCCEEDED(hr)) + hr = child_site->SetSite(GetUnknown()); + + return hr; +} + +LRESULT ChromeFrameHost::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + return 0; +} + +HRESULT ChromeFrameHost::SetUrl(BSTR url) { + HRESULT hr = chrome_frame_->put_src(url); + DCHECK(SUCCEEDED(hr)) << "Failed to navigate Chrome Frame: " << + com::LogHr(hr); + return hr; +} + +STDMETHODIMP ChromeFrameHost::StartChromeFrame() { + DCHECK(!IsWindow()); + + // Create a message window to host our control. + if (NULL == Create(HWND_MESSAGE)) + return E_FAIL; + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CreateActiveXHost(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create ActiveX host window: " << com::LogHr(hr); + return hr; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = CreateChromeFrame(&chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed create Chrome Frame: " << com::LogHr(hr); + return hr; + } + + // And attach it to our window. This causes the host to subclass + // our window and attach itself to it. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host" << com::LogHr(hr); + return hr; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink: " << com::LogHr(hr); + return hr; + } + + return hr; +} + +HRESULT ChromeFrameHost::CreateActiveXHost(IAxWinHostWindow** host) { + return CAxHostWindow::CreateInstance(host); +} + +HRESULT ChromeFrameHost::CreateChromeFrame(IChromeFrame** chrome_frame) { + CComPtr<IChromeFrame> new_cf; + HRESULT hr = new_cf.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (SUCCEEDED(hr)) + hr = new_cf.CopyTo(chrome_frame); + + return hr; +} + +STDMETHODIMP ChromeFrameHost::PostMessage(BSTR message, BSTR target) { + if (!document_loaded_) { + PostedMessage posted_message = { message, target }; + posted_messages_.push_back(posted_message); + return S_FALSE; + } + + HRESULT hr = chrome_frame_->postPrivateMessage(message, origin_, target); + + return hr; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfLoad(IDispatch* event) { + DLOG(INFO) << "OnCfLoad"; + if (document_loaded_) { + // If we were already loaded, our list should be empty. + DCHECK(posted_messages_.empty()); + return; + } + document_loaded_ = true; + + // Flush all posted messages. + PostedMessageList::iterator it(posted_messages_.begin()); + for (; it != posted_messages_.end(); ++it) { + HRESULT hr = chrome_frame_->postPrivateMessage(it->message, origin_, + it->target); + DCHECK(SUCCEEDED(hr)) << "postPrivateMessage failed with: " << + com::LogHr(hr); + } + posted_messages_.clear(); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfLoadError(IDispatch* event) { + DLOG(ERROR) << "OnCfLoadError"; + DCHECK(false) << "OnCfLoadError"; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfExtensionReady(BSTR path, + int response) { + DLOG(INFO) << "OnCfExtensionReady: " << path << ", " << response; + // Early exit if there's no sink. + if (event_sink_ == NULL) + return; + event_sink_->OnCfExtensionReady(path, response); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* extension_directories) { + DLOG(INFO) << "OnCfGetEnabledExtensionsComplete"; + if (event_sink_) + event_sink_->OnCfGetEnabledExtensionsComplete(extension_directories); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfChannelError() { + DCHECK(false) << "OnCfChannelError means that Chrome has Crashed!"; + if (event_sink_) + event_sink_->OnCfChannelError(); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfMessage(IDispatch* event) { + DLOG(INFO) << "OnCfMessage"; +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfReadyStateChanged(LONG state) { + DLOG(INFO) << "OnCfReadyStateChanged(" << state << ")"; + if (event_sink_) + event_sink_->OnCfReadyStateChanged(state); +} + +STDMETHODIMP_(void) ChromeFrameHost::OnCfPrivateMessage(IDispatch* event, + BSTR target) { + // Early exit if there's no sink. + if (event_sink_ == NULL) + return; + + // Make sure that the message has a "data" member and get it. This should + // be a JSON-encoded command to execute. + CComDispatchDriver event_dispatch(event); + + CComVariant origin; + HRESULT hr = event_dispatch.GetPropertyByName(L"origin", &origin); + DCHECK(SUCCEEDED(hr) && origin.vt == VT_BSTR); + if (FAILED(hr) || origin.vt != VT_BSTR) { + NOTREACHED() << "No origin on event"; + return; + } + + CComVariant data; + hr = event_dispatch.GetPropertyByName(L"data", &data); + DCHECK(SUCCEEDED(hr) && data.vt == VT_BSTR); + if (FAILED(hr) || data.vt != VT_BSTR) { + NOTREACHED() << "No data on event"; + return; + } + + // Forward to the sink. + event_sink_->OnCfPrivateMessage(V_BSTR(&data), V_BSTR(&origin), target); +} + +STDMETHODIMP_(void) ChromeFrameHost::SetChromeProfileName( + const wchar_t* chrome_profile_name) { + chrome_profile_name_ = chrome_profile_name; + DLOG(INFO) << "Assigned profile name " << chrome_profile_name_; +} diff --git a/ceee/ie/common/chrome_frame_host.h b/ceee/ie/common/chrome_frame_host.h new file mode 100644 index 0000000..ef43499 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host.h @@ -0,0 +1,237 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE ChromeFrameHost implementation. +#ifndef CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ +#define CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ + +#include <atlbase.h> +#include <atlwin.h> +#include <atlcrack.h> +#include <list> + +#include "base/basictypes.h" +#include "base/scoped_handle.h" +#include "ceee/common/initializing_coclass.h" +#include "chrome_tab.h" // NOLINT + +// fwd. +struct IAxWinHostWindow; + +class IChromeFrameHostEvents: public IUnknown { + public: + virtual HRESULT OnCfReadyStateChanged(LONG state) = 0; + virtual HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target) = 0; + virtual HRESULT OnCfExtensionReady(BSTR path, int response) = 0; + virtual HRESULT OnCfGetEnabledExtensionsComplete(SAFEARRAY* base_dirs) = 0; + virtual HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled) = 0; + virtual HRESULT OnCfChannelError() = 0; +}; + +// This is the interface the chrome frame host presents to its consumers. +extern const GUID IID_IChromeFrameHost; +class IChromeFrameHost: public IUnknown { + public: + // Set the name of the profile we want Chrome Frame to use. + // @param chrome_profile_name The name of the profile to use. + STDMETHOD_(void, SetChromeProfileName)( + const wchar_t* chrome_profile_name) = 0; + + // Set the URL we want to navigate Chrome Frame to once it is ready. + // @param url The URL to navigate to. + STDMETHOD(SetUrl)(BSTR url) = 0; + + // Creates and initializes our Chrome Frame instance. + STDMETHOD(StartChromeFrame)() = 0; + + // Posts a message through Chrome Frame, or enqueues it if + // Chrome Frame is not yet ready and @p queueable is true. + // @param message The message to post to Chrome Frame. + // @param target Where we want the message to be posted within Chrome. + STDMETHOD(PostMessage)(BSTR message, BSTR target) = 0; + + // Tears down an initialized ChromeFrameHost. + STDMETHOD(TearDown)() = 0; + + // Sets the event sink for this ChromeFrameHost. + STDMETHOD_(void, SetEventSink)(IChromeFrameHostEvents* event_sink) = 0; + + // Installs the given extension. Results come back via + // IChromeFrameHostEvents::OnCfExtensionReady. + STDMETHOD(InstallExtension)(BSTR crx_path) = 0; + + // Loads the given exploded extension directory. Results come back via + // IChromeFrameHostEvents::OnCfExtensionReady. + STDMETHOD(LoadExtension)(BSTR extension_dir) = 0; + + // Initiates a request for installed extensions. Results come back via + // IChromeFrameHostEvents::OnCfGetEnabledExtensionsComplete. + STDMETHOD(GetEnabledExtensions)() = 0; + + // Retrieves the session_id used by Chrome for the CF tab. Will return S_FALSE + // if the session id is not yet available. + // The session_id is the id used for the Tab javascript object. + STDMETHOD(GetSessionId)(int* session_id) = 0; +}; + +class ATL_NO_VTABLE ChromeFrameHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ChromeFrameHost>, + public IServiceProviderImpl<ChromeFrameHost>, + public IChromeFrameHost, + public IChromeFramePrivileged, + public IDispEventSimpleImpl<0, + ChromeFrameHost, + &DIID_DIChromeFrameEvents>, + public CWindowImpl<ChromeFrameHost> { + public: + typedef IDispEventSimpleImpl<0, + ChromeFrameHost, + &DIID_DIChromeFrameEvents> ChromeFrameEvents; + ChromeFrameHost(); + ~ChromeFrameHost(); + + BEGIN_COM_MAP(ChromeFrameHost) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY_IID(IID_IChromeFrameHost, IChromeFrameHost) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(ChromeFrameHost) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + END_SERVICE_MAP() + + BEGIN_SINK_MAP(ChromeFrameHost) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONLOAD, + OnCfLoad, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONLOADERROR, + OnCfLoadError, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, CF_EVENT_DISPID_ONMESSAGE, + OnCfMessage, &handler_type_idispatch_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONPRIVATEMESSAGE, + OnCfPrivateMessage, &handler_type_idispatch_bstr_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + OnCfGetEnabledExtensionsComplete, &handler_type_bstrarray_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONCHANNELERROR, + OnCfChannelError, &handler_type_void_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + HRESULT Initialize(); + + HRESULT FinalConstruct(); + void FinalRelease(); + + BEGIN_MSG_MAP(ChromeFrameHost) + MSG_WM_CREATE(OnCreate) + END_MSG_MAP() + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean* wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR* args); + STDMETHOD(GetChromeProfileName)(BSTR* args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR* functions_enabled); + // @} + + // @name ChromeFrame event handlers + // @{ + STDMETHOD_(void, OnCfLoad)(IDispatch* event); + STDMETHOD_(void, OnCfLoadError)(IDispatch* event); + STDMETHOD_(void, OnCfMessage)(IDispatch* event); + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfPrivateMessage)(IDispatch *event, BSTR target); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfGetEnabledExtensionsComplete)( + SAFEARRAY* extension_directories); + STDMETHOD_(void, OnCfChannelError)(void); + // @} + + // @name IChromeFrameHost implementation. + // @{ + STDMETHOD_(void, SetChromeProfileName)(const wchar_t* chrome_profile_name); + STDMETHOD(SetUrl)(BSTR url); + STDMETHOD(StartChromeFrame)(); + STDMETHOD(PostMessage)(BSTR message, BSTR target); + STDMETHOD(TearDown)(); + STDMETHOD_(void, SetEventSink)(IChromeFrameHostEvents* event_sink); + STDMETHOD(InstallExtension)(BSTR crx_path); + STDMETHOD(LoadExtension)(BSTR extension_dir); + STDMETHOD(GetEnabledExtensions)(); + STDMETHOD(GetSessionId)(int* session_id); + // @} + + protected: + virtual HRESULT CreateActiveXHost(IAxWinHostWindow** host); + virtual HRESULT CreateChromeFrame(IChromeFrame** chrome_frame); + + // Our window maintains a refcount on us for the duration of its lifetime. + // The self-reference is managed with those two methods. + virtual void OnFinalMessage(HWND window); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + private: + struct PostedMessage { + CComBSTR message; + CComBSTR target; + }; + typedef std::list<PostedMessage> PostedMessageList; + + // Set us as site for child. + HRESULT SetChildSite(IUnknown* child); + + // Our Chrome Frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // The Chrome profile we ask to connect with. + CComBSTR chrome_profile_name_; + + // Messages posted before Chrome Frame has loaded have to be enqueued, + // to ensure that the toolband extension has propertly loaded and + // initialized before we attempt to post messages at it. + // This list stores such messages until Chrome Frame reports the + // document loaded. + PostedMessageList posted_messages_; + + // True iff Chrome Frame has reported a document loaded event. + // TODO(mad@chromium.org): Use a three states variable to take the + // error case into account. + bool document_loaded_; + +#ifndef NDEBUG + // We use a cross process event to make sure there is only one chrome frame + // host that returns ExtensionApisToAutomate... But only needed for a DCHECK. + ScopedHandle automating_extension_api_; +#endif + + // A cached BSTR for the posted messages origin (which is kAutomationOrigin). + CComBSTR origin_; + + // Our event sink. + CComPtr<IChromeFrameHostEvents> event_sink_; + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_; + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_idispatch_bstr_; + static _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_bstrarray_; + static _ATL_FUNC_INFO handler_type_void_; + + DISALLOW_COPY_AND_ASSIGN(ChromeFrameHost); +}; + +#endif // CEEE_IE_COMMON_CHROME_FRAME_HOST_H_ diff --git a/ceee/ie/common/chrome_frame_host_unittest.cc b/ceee/ie/common/chrome_frame_host_unittest.cc new file mode 100644 index 0000000..f7cf592 --- /dev/null +++ b/ceee/ie/common/chrome_frame_host_unittest.cc @@ -0,0 +1,607 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for chrome frame host. +#include "ceee/ie/common/chrome_frame_host.h" + +#include "base/string_util.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::FireEvent; + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Eq; +using testing::Field; +using testing::Invoke; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::MockDispatchEx; + +class MockChromeFrameHost : public ChromeFrameHost { + public: + // Mock the creator functions to control them. + MOCK_METHOD1(CreateChromeFrame, HRESULT(IChromeFrame** chrome_frame)); + // And the event handlers to test event subscription. + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfLoad, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfLoadError, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfMessage, void(IDispatch* event)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfReadyStateChanged, void(LONG)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, + OnCfPrivateMessage, + void(IDispatch* event, BSTR origin)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, + OnCfExtensionReady, + void(BSTR path, int response)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnCfGetEnabledExtensionsComplete, + void(SAFEARRAY* extensions)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, OnCfChannelError, void(void)); +}; + +class TestChromeFrameHost + : public StrictMock<MockChromeFrameHost>, + public InitializingCoClass<TestChromeFrameHost> { + public: + using InitializingCoClass<TestChromeFrameHost>::CreateInitialized; + + TestChromeFrameHost() : active_x_host_creation_error_(S_OK) { + ++instance_count_; + } + ~TestChromeFrameHost() { + --instance_count_; + } + + void set_active_x_host_creation_error(HRESULT hr) { + active_x_host_creation_error_ = hr; + } + + HRESULT Initialize(TestChromeFrameHost** self) { + *self = this; + return S_OK; + } + + + HRESULT CreateActiveXHost(IAxWinHostWindow** host) { + if (FAILED(active_x_host_creation_error_)) + return active_x_host_creation_error_; + + return ChromeFrameHost::CreateActiveXHost(host); + } + + // For some of the mocks, we want to test the original behavior. + void CallOnCfLoad(IDispatch* event) { + ChromeFrameHost::OnCfLoad(event); + } + void CallOnCfReadyStateChanged(LONG state) { + ChromeFrameHost::OnCfReadyStateChanged(state); + } + void CallOnCfPrivateMessage(IDispatch* event, BSTR origin) { + ChromeFrameHost::OnCfPrivateMessage(event, origin); + } + void CallOnCfExtensionReady(BSTR path, int response) { + ChromeFrameHost::OnCfExtensionReady(path, response); + } + void CallOnCfGetEnabledExtensionsComplete(SAFEARRAY* enabled_extensions) { + ChromeFrameHost::OnCfGetEnabledExtensionsComplete(enabled_extensions); + } + void CallOnCfChannelError() { + ChromeFrameHost::OnCfChannelError(); + } + + public: + HRESULT active_x_host_creation_error_; + + static size_t instance_count_; +}; + +size_t TestChromeFrameHost::instance_count_ = 0; + +class IChromeFrameImpl : public IDispatchImpl<IChromeFrame, + &IID_IChromeFrame, + &LIBID_ChromeTabLib> { + public: + // @name IChromeFrame implementation + // @{ + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_src, HRESULT(BSTR *src)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_src, HRESULT(BSTR src)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, postMessage, HRESULT( + BSTR message, VARIANT target)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onload, HRESULT( + VARIANT *onload_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onload, HRESULT( + VARIANT onload_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onloaderror, HRESULT( + VARIANT *onerror_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onloaderror, HRESULT( + VARIANT onerror_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_onmessage, HRESULT( + VARIANT *onmessage_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_onmessage, HRESULT( + VARIANT onmessage_handler)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_readyState, HRESULT( + LONG *ready_state)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, addEventListener, HRESULT( + BSTR event_type, IDispatch *listener, VARIANT use_capture)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, removeEventListener, HRESULT( + BSTR event_type, IDispatch *listener, VARIANT use_capture)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_version, HRESULT(BSTR *version)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, postPrivateMessage, HRESULT( + BSTR message, BSTR origin, BSTR target)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_useChromeNetwork, HRESULT( + VARIANT_BOOL *pVal)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_useChromeNetwork, HRESULT( + VARIANT_BOOL newVal)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, installExtension, HRESULT( + BSTR crx_path)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, loadExtension, HRESULT( + BSTR path)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, getEnabledExtensions, HRESULT()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, registerBhoIfNeeded, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, getSessionId, HRESULT(int*)); + // @} +}; + +class MockChromeFrame + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass< StrictMock<MockChromeFrame> >, + public IObjectWithSiteImpl<MockChromeFrame>, + public StrictMock<IChromeFrameImpl>, + public IConnectionPointContainerImpl<MockChromeFrame>, + public IConnectionPointImpl<MockChromeFrame, &DIID_DIChromeFrameEvents> { + public: + BEGIN_COM_MAP(MockChromeFrame) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IChromeFrame) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(MockChromeFrame) + CONNECTION_POINT_ENTRY(DIID_DIChromeFrameEvents) + END_CONNECTION_POINT_MAP() + + MockChromeFrame() : no_events_(false) { + ++instance_count_; + } + ~MockChromeFrame() { + --instance_count_; + } + + void set_no_events(bool no_events) { + no_events_ = no_events; + } + + HRESULT Initialize(MockChromeFrame** self) { + *self = this; + return S_OK; + } + + // Override from IConnectionPointContainerImpl + STDMETHOD(FindConnectionPoint)(REFIID iid, IConnectionPoint** cp) { + typedef IConnectionPointContainerImpl<MockChromeFrame> CPC; + + if (iid == DIID_DIChromeFrameEvents && no_events_) + return CONNECT_E_NOCONNECTION; + + return CPC::FindConnectionPoint(iid, cp); + } + + typedef IConnectionPointImpl<MockChromeFrame, &DIID_DIChromeFrameEvents> CP; + void FireEvent1(DISPID id, IDispatch* event) { + return FireEvent(static_cast<CP*>(this), id, 1, &CComVariant(event)); + } + + void FireCfLoad(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONLOAD, event); + } + + void FireCfLoadError(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONLOADERROR, event); + } + + void FireCfMessage(IDispatch* event) { + FireEvent1(CF_EVENT_DISPID_ONMESSAGE, event); + } + + void FireCfReadyStateChanged(LONG ready_state) { + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONREADYSTATECHANGED, + 1, + &CComVariant(ready_state)); + } + + void FireCfPrivateMessage(IDispatch* event, BSTR origin) { + CComVariant args[] = { origin, event }; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONPRIVATEMESSAGE, + arraysize(args), + args); + } + + void FireCfExtensionReady(BSTR path, int response) { + CComVariant args[] = { response , path}; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONEXTENSIONREADY, + arraysize(args), + args); + } + + void FireCfGetEnabledExtensionsComplete(SAFEARRAY* array) { + VARIANT args[] = { { VT_ARRAY | VT_BSTR } }; + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + arraysize(args), + args); + } + + void FireCfChannelError() { + return FireEvent(static_cast<CP*>(this), + CF_EVENT_DISPID_ONCHANNELERROR, + 0, + NULL); + } + + public: + // Quench our event sink. + bool no_events_; + + static size_t instance_count_; +}; + +size_t MockChromeFrame::instance_count_ = 0; + +class IChromeFrameHostEventsMockImpl : public IChromeFrameHostEvents { + public: + MOCK_METHOD3(OnCfPrivateMessage, HRESULT(BSTR, BSTR, BSTR)); + MOCK_METHOD2(OnCfExtensionReady, HRESULT(BSTR, int)); + MOCK_METHOD1(OnCfGetEnabledExtensionsComplete, + HRESULT(SAFEARRAY* extensions)); + MOCK_METHOD1(OnCfGetExtensionApisToAutomate, + HRESULT(BSTR* enabled_functions)); + MOCK_METHOD1(OnCfReadyStateChanged, HRESULT(LONG)); + MOCK_METHOD0(OnCfChannelError, HRESULT(void)); +}; + +class MockChromeFrameHostEvents + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockChromeFrameHostEvents>, + public StrictMock<IChromeFrameHostEventsMockImpl> { + public: + BEGIN_COM_MAP(MockChromeFrameHostEvents) + COM_INTERFACE_ENTRY(IUnknown) + END_COM_MAP() + + HRESULT Initialize(MockChromeFrameHostEvents** self) { + *self = this; + return S_OK; + } +}; + +class ChromeFrameHostTest: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + TestChromeFrameHost::CreateInitialized(&host_, &host_keeper_)); + } + + virtual void TearDown() { + if (host_) + host_->TearDown(); + host_ = NULL; + host_keeper_.Release(); + + chrome_frame_ = NULL; + chrome_frame_keeper_ = NULL; + + ASSERT_EQ(0, TestChromeFrameHost::instance_count_); + ASSERT_EQ(0, MockChromeFrame::instance_count_); + } + + void ExpectCreateChromeFrame(HRESULT hr) { + if (SUCCEEDED(hr)) { + ASSERT_HRESULT_SUCCEEDED( + MockChromeFrame::CreateInitialized(&chrome_frame_, + &chrome_frame_keeper_)); + EXPECT_CALL(*host_, CreateChromeFrame(_)). + WillRepeatedly( + DoAll( + CopyInterfaceToArgument<0>(chrome_frame_keeper_), + Return(S_OK))); + } else { + EXPECT_CALL(*host_, CreateChromeFrame(_)). + WillRepeatedly(Return(hr)); + } + } + + public: + TestChromeFrameHost* host_; + CComPtr<IUnknown> host_keeper_; + + MockChromeFrame* chrome_frame_; + CComPtr<IChromeFrame> chrome_frame_keeper_; + + // Quench logging for all tests. + testing::LogDisabler no_dchecks_; +}; + +TEST_F(ChromeFrameHostTest, StartChromeFrameSuccess) { + ExpectCreateChromeFrame(S_OK); + + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnActiveXHostCreationFailure) { + ExpectCreateChromeFrame(S_OK); + + host_->set_active_x_host_creation_error(E_OUTOFMEMORY); + ASSERT_EQ(E_OUTOFMEMORY, host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnAdviseFailure) { + ExpectCreateChromeFrame(S_OK); + + chrome_frame_->set_no_events(true); + ASSERT_HRESULT_FAILED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, StartChromeFrameFailsOnCreationFailure) { + ExpectCreateChromeFrame(E_OUTOFMEMORY); + + ASSERT_HRESULT_FAILED(host_->StartChromeFrame()); +} + +TEST_F(ChromeFrameHostTest, ChromeFramePrivilegedInServiceProviderChain) { + ExpectCreateChromeFrame(S_OK); + + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Get the service provider on our mock Chrome frame. + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + chrome_frame_->GetSite(IID_IServiceProvider, + reinterpret_cast<void**>(&sp))); + + CComPtr<IChromeFramePrivileged> priv; + ASSERT_HRESULT_SUCCEEDED(sp->QueryService(SID_ChromeFramePrivileged, + IID_IChromeFramePrivileged, + reinterpret_cast<void**>(&priv))); + ASSERT_TRUE(priv != NULL); + + boolean wants_priv = FALSE; + ASSERT_HRESULT_SUCCEEDED(priv->GetWantsPrivileged(&wants_priv)); + ASSERT_TRUE(wants_priv); + + CComBSTR profile_name; + ASSERT_HRESULT_SUCCEEDED(priv->GetChromeProfileName(&profile_name)); + ASSERT_TRUE(profile_name == NULL); + + static const wchar_t* kProfileName = L"iexplore"; + host_->SetChromeProfileName(kProfileName); + ASSERT_HRESULT_SUCCEEDED(priv->GetChromeProfileName(&profile_name)); + ASSERT_STREQ(kProfileName, profile_name); +} + +TEST_F(ChromeFrameHostTest, PostMessage) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Make sure we properly queue before the document gets loaded. + CComBSTR queue_it_1("queue_it_1"); + CComBSTR queue_it_2("queue_it_2"); + CComBSTR target_1("target_1"); + CComBSTR target_2("target_2"); + + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_1, target_1)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_2, target_2)); + + // Only the queued messages should be posted. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_1.m_str), + _, StrEq(target_1.m_str))).WillOnce(Return(S_OK)); + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_2.m_str), + _, StrEq(target_2.m_str))).WillOnce(Return(S_OK)); + + MockDispatchEx* event; + CComDispatchDriver event_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&event, + &event_keeper)); + host_->CallOnCfLoad(event); + + // Nothing left to be posted once we have loaded. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + host_->CallOnCfLoad(event); + + // Messages should go directly to Chrome Frame now, whether they are + // queueable or not. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_1.m_str), + _, StrEq(target_1.m_str))).WillOnce(Return(S_OK)); + EXPECT_CALL(*chrome_frame_, postPrivateMessage(StrEq(queue_it_2.m_str), + _, StrEq(target_2.m_str))).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_1, target_1)); + EXPECT_HRESULT_SUCCEEDED(host_->PostMessage(queue_it_2, target_2)); + + // Nothing left to be posted once we have loaded. + EXPECT_CALL(*chrome_frame_, postPrivateMessage(_, _, _)).Times(0); + host_->CallOnCfLoad(event); +} + +TEST_F(ChromeFrameHostTest, OnCfReadyStateChanged) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Should call event sink on all states, but only once + // event sink is set. + host_->CallOnCfReadyStateChanged(READYSTATE_UNINITIALIZED); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADING); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADED); + host_->CallOnCfReadyStateChanged(READYSTATE_INTERACTIVE); + host_->CallOnCfReadyStateChanged(READYSTATE_COMPLETE); + + MockChromeFrameHostEvents* event_sink; + CComPtr<IUnknown> event_sink_sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockChromeFrameHostEvents>::CreateInitialized( + &event_sink, &event_sink_sp)); + + host_->SetEventSink(event_sink); + EXPECT_CALL(*event_sink, OnCfReadyStateChanged(_)).Times(5); + host_->CallOnCfReadyStateChanged(READYSTATE_UNINITIALIZED); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADING); + host_->CallOnCfReadyStateChanged(READYSTATE_LOADED); + host_->CallOnCfReadyStateChanged(READYSTATE_INTERACTIVE); + host_->CallOnCfReadyStateChanged(READYSTATE_COMPLETE); +} + +TEST_F(ChromeFrameHostTest, ChromeFrameEventsCaptured) { + ExpectCreateChromeFrame(S_OK); + ASSERT_HRESULT_SUCCEEDED(host_->StartChromeFrame()); + + // Create a handy-dandy dispatch mock object. + MockDispatchEx* event; + CComDispatchDriver event_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&event, + &event_keeper)); + + EXPECT_CALL(*host_, OnCfLoad(event)).Times(1); + chrome_frame_->FireCfLoad(event); + + EXPECT_CALL(*host_, OnCfLoadError(event)).Times(1); + chrome_frame_->FireCfLoadError(event); + + EXPECT_CALL(*host_, OnCfMessage(event)).Times(1); + chrome_frame_->FireCfMessage(event); + + EXPECT_CALL(*host_, OnCfReadyStateChanged(READYSTATE_LOADING)).Times(1); + chrome_frame_->FireCfReadyStateChanged(READYSTATE_LOADING); + + EXPECT_CALL(*host_, OnCfReadyStateChanged(READYSTATE_COMPLETE)).Times(1); + chrome_frame_->FireCfReadyStateChanged(READYSTATE_COMPLETE); + + static const wchar_t* kOrigin = L"From Russia with Love"; + EXPECT_CALL(*host_, OnCfPrivateMessage(event, StrEq(kOrigin))).Times(1); + chrome_frame_->FireCfPrivateMessage(event, CComBSTR(kOrigin)); + + static const wchar_t* kPath = L"they all lead to Rome"; + EXPECT_CALL(*host_, OnCfExtensionReady(StrEq(kPath), 42)).Times(1); + chrome_frame_->FireCfExtensionReady(CComBSTR(kPath), 42); + + EXPECT_CALL(*host_, OnCfGetEnabledExtensionsComplete(_)).Times(1); + chrome_frame_->FireCfGetEnabledExtensionsComplete(NULL); + + EXPECT_CALL(*host_, OnCfChannelError()).Times(1); + chrome_frame_->FireCfChannelError(); +} + +// Used to compare two arrays of strings, as when GetIDsOfNames is called +MATCHER_P(SingleEntryLPOLESTRArraysEqual, single_entry, "") { + return std::wstring(single_entry) == arg[0]; +} + +TEST_F(ChromeFrameHostTest, EventSync) { + // Make sure all calls are safe without an event sink. + BSTR functions_enabled = NULL; + CComQIPtr<IChromeFramePrivileged> host_privileged(host_); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + EXPECT_EQ(NULL, functions_enabled); + + host_->CallOnCfExtensionReady(L"", 0); + host_->CallOnCfGetEnabledExtensionsComplete(NULL); + host_->CallOnCfPrivateMessage(NULL, NULL); + host_->CallOnCfPrivateMessage(NULL, NULL); + host_->CallOnCfChannelError(); + + // Now make sure the calls are properly propagated to the event sync. + MockChromeFrameHostEvents* event_sink; + CComPtr<IUnknown> event_sink_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockChromeFrameHostEvents>::CreateInitialized( + &event_sink, &event_sink_keeper)); + // We cheat a bit here because there is no IID to QI for + // IChromeFrameHostEvents. + host_->SetEventSink(event_sink); + + static const BSTR kCheatCode = L"Cheat code"; + EXPECT_CALL(*event_sink, OnCfGetExtensionApisToAutomate(NotNull())).WillOnce( + DoAll(SetArgumentPointee<0>(kCheatCode), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + DCHECK_EQ(kCheatCode, functions_enabled); + + // Other calls should be OK, as long as the string isn't set to a non-NULL + // value. + EXPECT_CALL(*event_sink, OnCfGetExtensionApisToAutomate(NotNull())).WillOnce( + DoAll(SetArgumentPointee<0>(static_cast<LPOLESTR>(0)), Return(S_FALSE))); + EXPECT_HRESULT_SUCCEEDED(host_privileged->GetExtensionApisToAutomate( + &functions_enabled)); + + EXPECT_CALL(*event_sink, OnCfExtensionReady(StrEq(L""), 0)).Times(1); + host_->CallOnCfExtensionReady(L"", 0); + + EXPECT_CALL(*event_sink, OnCfGetEnabledExtensionsComplete(NULL)).Times(1); + host_->CallOnCfGetEnabledExtensionsComplete(NULL); + + EXPECT_CALL(*event_sink, OnCfChannelError()).Times(1); + host_->CallOnCfChannelError(); + + MockDispatchEx* mock_dispatch = NULL; + CComDispatchDriver dispatch_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_dispatch, + &dispatch_keeper)); + static LPCOLESTR kOrigin = L"Origin"; + VARIANT origin; + origin.vt = VT_BSTR; + // This will be freed by the calling API. If we free it here, we're going to + // end up with a double-free. + origin.bstrVal = SysAllocString(kOrigin); + DISPID origin_dispid = 42; + EXPECT_CALL(*mock_dispatch, GetIDsOfNames(_, + SingleEntryLPOLESTRArraysEqual(L"origin"), 1, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<4>(origin_dispid), Return(S_OK))); + EXPECT_CALL(*mock_dispatch, Invoke(origin_dispid, _, _, DISPATCH_PROPERTYGET, + Field(&DISPPARAMS::cArgs, Eq(0)), _, _, _)).WillOnce(DoAll( + SetArgumentPointee<5>(origin), Return(S_OK))); + + static LPCOLESTR kData = L"Data"; + VARIANT data; + data.vt = VT_BSTR; + // This will be freed by the calling API. If we free it here, we're going to + // end up with a double-free. + data.bstrVal = SysAllocString(kData); + DISPID data_dispid = 24; + EXPECT_CALL(*mock_dispatch, GetIDsOfNames(_, + SingleEntryLPOLESTRArraysEqual(L"data"), 1, _, NotNull())). + WillOnce(DoAll(SetArgumentPointee<4>(data_dispid), Return(S_OK))); + EXPECT_CALL(*mock_dispatch, Invoke(data_dispid, _, _, DISPATCH_PROPERTYGET, + Field(&DISPPARAMS::cArgs, Eq(0)), _, _, _)).WillOnce(DoAll( + SetArgumentPointee<5>(data), Return(S_OK))); + + static LPOLESTR kTarget = L"Target"; + EXPECT_CALL(*event_sink, OnCfPrivateMessage( + StrEq(kData), StrEq(kOrigin), StrEq(kTarget))).Times(1); + + host_->CallOnCfPrivateMessage(dispatch_keeper, kTarget); + + // Clean the VARIANTs. + ZeroMemory(&origin, sizeof(origin)); + ZeroMemory(&data, sizeof(data)); +} + +} // namespace diff --git a/ceee/ie/common/common.gyp b/ceee/ie/common/common.gyp new file mode 100644 index 0000000..759c901 --- /dev/null +++ b/ceee/ie/common/common.gyp @@ -0,0 +1,124 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'ie_common_settings', + 'type': 'none', + 'direct_dependent_settings': { + 'defines': [ + # TODO(joi@chromium.org) Put into an include somewhere. + '_WIN32_WINDOWS=0x0410', + '_WIN32_IE=0x0600', + '_ATL_CSTRING_EXPLICIT_CONSTRUCTORS', + '_ATL_STATIC_REGISTRY', + '_WTL_NO_CSTRING', + ], + 'include_dirs': [ + '../../../third_party/wtl/include', + ], + }, + }, + { + 'target_name': 'ie_guids', + 'type': 'static_library', + 'dependencies': [ + 'ie_common_settings', + '../plugin/toolband/toolband.gyp:toolband_idl', + '../plugin/toolband/toolband.gyp:chrome_tab_idl', + ], + 'sources': [ + 'ie_guids.cc', + ], + 'include_dirs': [ + '../../..', + ], + }, + { + 'target_name': 'ie_common', + 'type': 'static_library', + 'dependencies': [ + 'ie_common_settings', + '../../../base/base.gyp:base', + '../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../build/temp_gyp/googleurl.gyp:googleurl', + '../../../net/net.gyp:net_base', + '../../../ceee/common/common.gyp:initializing_coclass', + '../../../ceee/common/common.gyp:ceee_common', + '../../../ceee/testing/utils/test_utils.gyp:test_utils', + '../plugin/toolband/toolband.gyp:chrome_tab_idl', + '../plugin/toolband/toolband.gyp:toolband_idl', + ], + 'sources': [ + 'api_registration.h', + 'chrome_frame_host.cc', + 'chrome_frame_host.h', + 'constants.cc', + 'constants.h', + 'crash_reporter.cc', + 'crash_reporter.h', + 'extension_manifest.cc', + 'extension_manifest.h', + 'ie_tab_interfaces.cc', + 'ie_tab_interfaces.h', + 'ie_util.cc', + 'ie_util.h', + 'mock_ie_tab_interfaces.h', + 'precompile.cc', + 'precompile.h', + 'ceee_module_util.cc', + 'ceee_module_util.h', + + # TODO(joi@chromium.org) Refactor to use chrome/common library. + '../../../chrome/browser/automation/extension_automation_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_bookmarks_module_constants.cc', + '../../../chrome/browser/extensions/extension_event_names.cc', + '../../../chrome/browser/extensions/' + 'extension_page_actions_module_constants.cc', + '../../../chrome/browser/extensions/extension_cookies_api_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_infobar_module_constants.cc', + '../../../chrome/browser/extensions/extension_tabs_module_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_webnavigation_api_constants.cc', + '../../../chrome/browser/extensions/' + 'extension_webrequest_api_constants.cc', + '../../../chrome/common/chrome_switches.cc', + '../../../chrome/common/chrome_switches.h', + '../../../chrome/common/url_constants.cc', + '../../../chrome/common/url_constants.h', + '../../../chrome/common/extensions/extension_constants.cc', + '../../../chrome/common/extensions/extension_constants.h', + '../../../chrome/common/extensions/extension_error_utils.cc', + '../../../chrome/common/extensions/extension_error_utils.h', + '../../../chrome/common/extensions/url_pattern.cc', + '../../../chrome/common/extensions/url_pattern.h', + '../../../chrome/common/extensions/user_script.cc', + '../../../chrome/common/extensions/user_script.h', + ], + 'direct_dependent_settings': { + 'include_dirs': [ + # Because we use some of the chrome files above directly, we need + # to specify thess include paths which they depend on. + '../../../skia/config/win', + '../../../third_party/skia/include/config', + ], + }, + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': 'precompile.cc', + 'msvs_precompiled_header': 'precompile.h', + }, + }, + }, + ] +} diff --git a/ceee/ie/common/constants.cc b/ceee/ie/common/constants.cc new file mode 100644 index 0000000..64a1d52 --- /dev/null +++ b/ceee/ie/common/constants.cc @@ -0,0 +1,11 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Shared constants between the different modules of CEEE. + +#include "ceee/ie/common/constants.h" + +namespace ceee_event_names { + const char kCeeeOnTabUnmapped[] = "ceee.OnTabUnmapped"; +} diff --git a/ceee/ie/common/constants.h b/ceee/ie/common/constants.h new file mode 100644 index 0000000..600a589 --- /dev/null +++ b/ceee/ie/common/constants.h @@ -0,0 +1,13 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_COMMON_CONSTANTS_H_ +#define CEEE_IE_COMMON_CONSTANTS_H_ + +namespace ceee_event_names { + // Private messages used by the ApiDispatcher and the funnels. + extern const char kCeeeOnTabUnmapped[]; +} + +#endif // CEEE_IE_COMMON_CONSTANTS_H_ diff --git a/ceee/ie/common/crash_reporter.cc b/ceee/ie/common/crash_reporter.cc new file mode 100644 index 0000000..4ed9b5c --- /dev/null +++ b/ceee/ie/common/crash_reporter.cc @@ -0,0 +1,75 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Definition of IE crash reporter. + +#include "ceee/ie/common/crash_reporter.h" + +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" + +const wchar_t kGoogleUpdatePipeName[] = + L"\\\\.\\pipe\\GoogleCrashServices\\S-1-5-18"; + +CrashReporter::CrashReporter(const wchar_t* component_name) + : exception_handler_(NULL) { + // Initialize the custom data that will be used to identify the client + // when reporting a crash. + // TODO(jeffbailey@google.com): Inherit Chrome's version number. + // (bb3143594). + google_breakpad::CustomInfoEntry ver_entry(L"ver", L"Ver.Goes.Here"); + google_breakpad::CustomInfoEntry prod_entry(L"prod", L"CEEE_IE"); + google_breakpad::CustomInfoEntry plat_entry(L"plat", L"Win32"); + google_breakpad::CustomInfoEntry type_entry(L"ptype", component_name); + google_breakpad::CustomInfoEntry entries[] = { + ver_entry, prod_entry, plat_entry, type_entry }; + + const int num_entries = arraysize(entries); + client_info_entries_.reset( + new google_breakpad::CustomInfoEntry[num_entries]); + + for (int i = 0; i < num_entries; ++i) { + client_info_entries_[i] = entries[i]; + } + + client_info_.entries = client_info_entries_.get(); + client_info_.count = num_entries; +} + +CrashReporter::~CrashReporter() { + DCHECK(exception_handler_ == NULL); +} + +void CrashReporter::InitializeCrashReporting(bool full_dump) { + DCHECK(exception_handler_ == NULL); + + if (!ceee_module_util::GetCollectStatsConsent()) + return; + + wchar_t temp_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(temp_path), temp_path); + if (len == 0) { + LOG(ERROR) << "Failed to instantiate Breakpad exception handler. " << + "Could not get a temp path."; + return; + } + + // Install an exception handler instance here, which should be the lowest + // level in the process. We give it the appropriate pipe name so it can + // talk to the reporting service (e.g. Omaha) to do dump generation and + // send information back to the server. + MINIDUMP_TYPE dump_type = full_dump ? MiniDumpWithFullMemory : MiniDumpNormal; + exception_handler_ = new google_breakpad::ExceptionHandler( + temp_path, NULL, NULL, NULL, + google_breakpad::ExceptionHandler::HANDLER_ALL, dump_type, + kGoogleUpdatePipeName, &client_info_); + + LOG_IF(ERROR, exception_handler_ == NULL) << + "Failed to instantiate Breakpad exception handler."; +} + +void CrashReporter::ShutdownCrashReporting() { + delete exception_handler_; + exception_handler_ = NULL; +} diff --git a/ceee/ie/common/crash_reporter.h b/ceee/ie/common/crash_reporter.h new file mode 100644 index 0000000..1f34370 --- /dev/null +++ b/ceee/ie/common/crash_reporter.h @@ -0,0 +1,41 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of common IE crash reporter. + +#ifndef CEEE_IE_COMMON_CRASH_REPORTER_H_ +#define CEEE_IE_COMMON_CRASH_REPORTER_H_ + +#include "base/scoped_ptr.h" +#include "client/windows/handler/exception_handler.h" + +// A wrapper around Breakpad's ExceptionHandler class for crash reporting using +// Omaha's crash reporting service. +class CrashReporter { + public: + explicit CrashReporter(const wchar_t* component_name); + virtual ~CrashReporter(); + + // Initialize the ExceptionHandler to begin catching and reporting unhandled + // exceptions. + // + // @param full_dump Whether or not to do a full memory dump. + void InitializeCrashReporting(bool full_dump); + + // Halt crash reporting, stopping the ExceptionHandler from catching any + // further unhandled exceptions. + void ShutdownCrashReporting(); + + // TODO(stevet@google.com): Provide a way to trigger a dump send, + // which can be used for debugging. + private: + google_breakpad::CustomClientInfo client_info_; + scoped_array<google_breakpad::CustomInfoEntry> client_info_entries_; + + // Valid after a call to InitializeCrashReporting and invalidated after a call + // to ShutdownCrashReporting. + google_breakpad::ExceptionHandler* exception_handler_; +}; + +#endif // CEEE_IE_COMMON_CRASH_REPORTER_H_ diff --git a/ceee/ie/common/crash_reporter_unittest.cc b/ceee/ie/common/crash_reporter_unittest.cc new file mode 100644 index 0000000..541f581 --- /dev/null +++ b/ceee/ie/common/crash_reporter_unittest.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for the Crash Reporter. + +#include "ceee/ie/common/crash_reporter.h" + +#include "base/logging.h" +#include "base/win/pe_image.h" +#include "ceee/ie/common/ceee_module_util.h" + +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using testing::_; +using testing::Return; +using testing::StrictMock; + +// A helper for just extracting the default exception filter. +static LPTOP_LEVEL_EXCEPTION_FILTER GetUnhandledExceptionFilter() { + LPTOP_LEVEL_EXCEPTION_FILTER old_exh = SetUnhandledExceptionFilter(NULL); + EXPECT_EQ(NULL, SetUnhandledExceptionFilter(old_exh)); + return old_exh; +} + +// A helper for just extracting the default invalid parameter handler. +static _invalid_parameter_handler GetInvalidParameterHandler() { + _invalid_parameter_handler old_iph = _set_invalid_parameter_handler(NULL); + EXPECT_EQ(NULL, _set_invalid_parameter_handler(old_iph)); + return old_iph; +} + +// A helper for just extracting the default purecall handler. +static _purecall_handler GetPureCallHandler() { + _purecall_handler old_pch = _set_purecall_handler(NULL); + EXPECT_EQ(NULL, _set_purecall_handler(old_pch)); + return old_pch; +} + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtil) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtil) + MOCK_STATIC_INIT2(ceee_module_util::GetCollectStatsConsent, + GetCollectStatsConsent); + MOCK_STATIC_INIT_END() + MOCK_STATIC0(bool, , GetCollectStatsConsent); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtil) + +// Test that basic exception handling from Breakpad works, by ensuring that +// calling CrashReporter functions correctly replaces default handlers. +TEST(CrashReporterTest, ExceptionHandling) { + base::win::PEImage my_image(reinterpret_cast<HMODULE>(&__ImageBase)); + + // Take the default handlers out and do a quick sanity check. It is needed + // later to ensure it gets replaced by breakpad. + LPTOP_LEVEL_EXCEPTION_FILTER orig_exh = GetUnhandledExceptionFilter(); + EXPECT_TRUE(orig_exh != NULL); + + _invalid_parameter_handler orig_iph = GetInvalidParameterHandler(); + EXPECT_EQ(NULL, orig_iph); + + _purecall_handler orig_pch = GetPureCallHandler(); + EXPECT_EQ(NULL, orig_pch); + + // Initialize and ensure that a new handler has replaced the original + // handler, and that the new handler is within this image/from breakpad. + CrashReporter crash_reporter(L"unittest"); + + StrictMock<MockCeeeModuleUtil> mock; + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(false)); + + crash_reporter.InitializeCrashReporting(false); + EXPECT_EQ(orig_exh, GetUnhandledExceptionFilter()); + crash_reporter.ShutdownCrashReporting(); + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(true)); + crash_reporter.InitializeCrashReporting(false); + + LPTOP_LEVEL_EXCEPTION_FILTER new_exh = GetUnhandledExceptionFilter(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_exh) != NULL); + EXPECT_TRUE(orig_exh != new_exh); + + _invalid_parameter_handler new_iph = GetInvalidParameterHandler(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_iph) != NULL); + EXPECT_TRUE(orig_iph != new_iph); + + _purecall_handler new_pch = GetPureCallHandler(); + EXPECT_TRUE(my_image.GetImageSectionFromAddr((PVOID)new_pch) != NULL); + EXPECT_TRUE(orig_pch != new_pch); + + // Shut down, and ensure that the original exception handler is replaced + // by breakpad. + crash_reporter.ShutdownCrashReporting(); + + LPTOP_LEVEL_EXCEPTION_FILTER final_exh = GetUnhandledExceptionFilter(); + EXPECT_EQ(orig_exh, final_exh); + + _invalid_parameter_handler final_iph = GetInvalidParameterHandler(); + EXPECT_EQ(orig_iph, final_iph); + + _purecall_handler final_pch = GetPureCallHandler(); + EXPECT_EQ(orig_pch, final_pch); +} + +} // namespace diff --git a/ceee/ie/common/extension_manifest.cc b/ceee/ie/common/extension_manifest.cc new file mode 100644 index 0000000..22d5f4a --- /dev/null +++ b/ceee/ie/common/extension_manifest.cc @@ -0,0 +1,306 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to access Chrome Extension manifest data. + +#include "ceee/ie/common/extension_manifest.h" + +#include "base/base64.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/third_party/nss/blapi.h" +#include "base/third_party/nss/sha256.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/url_constants.h" +#include "net/base/net_util.h" + +namespace ext_keys = extension_manifest_keys; +namespace ext_values = extension_manifest_values; + +const char ExtensionManifest::kManifestFilename[] = "manifest.json"; + +// First 16 bytes of SHA256 hashed public key. +const size_t ExtensionManifest::kIdSize = 16; + +HRESULT ExtensionManifest::ReadManifestFile(const FilePath& extension_path, + bool require_key) { + // TODO(mad@chromium.org): Unbranch (taken from constructor of + // Extension class). + DCHECK(extension_path.IsAbsolute()); +#if defined(OS_WIN) + // Normalize any drive letter to upper-case. We do this for consistency with + // net_utils::FilePathToFileURL(), which does the same thing, to make string + // comparisons simpler. + std::wstring path_str = extension_path.value(); + if (path_str.size() >= 2 && path_str[0] >= L'a' && path_str[0] <= L'z' && + path_str[1] == ':') + path_str[0] += ('A' - 'a'); + + path_ = FilePath(path_str); +#else + path_ = path; +#endif + + // This piece comes from ExtensionsServiceBackend::LoadExtension() + FilePath manifest_path = + extension_path.AppendASCII(ExtensionManifest::kManifestFilename); + std::string json_string; + if (!file_util::ReadFileToString(manifest_path, &json_string)) { + LOG(ERROR) << "Invalid extension path or manifest file: " << + extension_path.value(); + return E_FAIL; + } + + scoped_ptr<Value> value(base::JSONReader::Read(json_string, true)); + if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) { + LOG(ERROR) << "Invalid manifest file"; + return E_FAIL; + } + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + + // And the rest is highly inspired from Extension::InitFromValue() + + // Initialize public key and id. + if (dict->HasKey(ext_keys::kPublicKey)) { + if (!dict->GetString(ext_keys::kPublicKey, &public_key_) || + FAILED(CalculateIdFromPublicKey())) { + public_key_.clear(); + extension_id_.clear(); + LOG(ERROR) << "Invalid public key format"; + return E_FAIL; + } + } else if (require_key) { + LOG(ERROR) << "Required key not found in manifest file"; + return E_FAIL; + } + + // Initialize the URL. + extension_url_ = GURL(std::string(chrome::kExtensionScheme) + + chrome::kStandardSchemeSeparator + extension_id_ + "/"); + + // Initialize content scripts (optional). + // MUST BE DONE AFTER setting up extension_id_, extension_url_ and path_ + if (dict->HasKey(ext_keys::kContentScripts)) { + ListValue* list_value; + if (!dict->GetList(ext_keys::kContentScripts, &list_value)) { + LOG(ERROR) << "Invalid content script JSON value"; + return E_FAIL; + } + + for (size_t i = 0; i < list_value->GetSize(); ++i) { + DictionaryValue* content_script; + if (!list_value->GetDictionary(i, &content_script)) { + LOG(ERROR) << "Invalid content script JSON value"; + return E_FAIL; + } + + UserScript script; + HRESULT hr = LoadUserScriptHelper(content_script, &script); + if (FAILED(hr)) + return hr; + script.set_extension_id(extension_id_); + content_scripts_.push_back(script); + } + } + + // Initialize toolstrips (optional). + toolstrip_file_names_.clear(); + if (dict->HasKey(ext_keys::kToolstrips)) { + ListValue* list_value; + if (!dict->GetList(ext_keys::kToolstrips, &list_value)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + for (size_t i = 0; i < list_value->GetSize(); ++i) { + std::string toolstrip_file_name; + if (!list_value->GetString(i, &toolstrip_file_name)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + toolstrip_file_names_.push_back(toolstrip_file_name); + } + } + // Add code here to read other manifest properties. + return S_OK; +} + +// TODO(mad@chromium.org): Unbranch (taken from Extension class). + +// static +GURL ExtensionManifest::GetResourceUrl(const GURL& extension_url, + const std::string& relative_path) { + DCHECK(extension_url.SchemeIs(chrome::kExtensionScheme)); + DCHECK_EQ("/", extension_url.path()); + + GURL ret_val = GURL(extension_url.spec() + relative_path); + DCHECK(StartsWithASCII(ret_val.spec(), extension_url.spec(), false)); + + return ret_val; +} + +// TODO(mad@chromium.org): Reuse code in common\extensions\extension.cc +HRESULT ExtensionManifest::CalculateIdFromPublicKey() { + std::string public_key_bytes; + if (!base::Base64Decode(public_key_, &public_key_bytes)) + return E_FAIL; + + if (public_key_bytes.length() == 0) + return E_FAIL; + + // SHA256 needs to work with an array of bytes, which we get from a string. + const uint8* ubuf = + reinterpret_cast<const unsigned char*>(public_key_bytes.data()); + SHA256Context ctx; + SHA256_Begin(&ctx); + SHA256_Update(&ctx, ubuf, public_key_bytes.length()); + // We must hash this value to a fixed size array. + uint8 hash[kIdSize]; + SHA256_End(&ctx, hash, NULL, sizeof(hash)); + // To stay in sync with the code in Chrome, we start by converting the bytes + // to a string representing the concatenation of the Hex values of all bytes. + extension_id_ = base::HexEncode(hash, sizeof(hash)); + for (size_t i = 0; i < extension_id_.size(); ++i) { + // And then, for each nibble represented by a single Hex digit, we use + // the value to offset from the letter 'a' to construct the key in the + // limited alphabet used for Chrome extension Ids ['a', 'q']. + int val = -1; + if (base::HexStringToInt(extension_id_.substr(i, 1), &val)) + extension_id_[i] = val + 'a'; + else + extension_id_[i] = 'a'; + } + return S_OK; +} + + +// TODO(mad@chromium.org): Unbranch (taken from the Extension class). +HRESULT ExtensionManifest::LoadUserScriptHelper( + const DictionaryValue* content_script, UserScript* result) { + // run_at + if (content_script->HasKey(ext_keys::kRunAt)) { + std::string run_location; + if (!content_script->GetString(ext_keys::kRunAt, &run_location)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + + if (run_location == ext_values::kRunAtDocumentStart) { + result->set_run_location(UserScript::DOCUMENT_START); + } else if (run_location == ext_values::kRunAtDocumentEnd) { + result->set_run_location(UserScript::DOCUMENT_END); + } else if (run_location == ext_values::kRunAtDocumentIdle) { + result->set_run_location(UserScript::DOCUMENT_IDLE); + } else { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + } + + // all frames + if (content_script->HasKey(ext_keys::kAllFrames)) { + bool all_frames = false; + if (!content_script->GetBoolean(ext_keys::kAllFrames, &all_frames)) { + LOG(ERROR) << "Invalid toolstrip JSON value"; + return E_FAIL; + } + result->set_match_all_frames(all_frames); + } + + // matches + ListValue* matches = NULL; + if (!content_script->GetList(ext_keys::kMatches, &matches)) { + LOG(ERROR) << "Invalid manifest without a matches value"; + return E_FAIL; + } + + if (matches->GetSize() == 0) { + LOG(ERROR) << "Invalid manifest without a matches value"; + return E_FAIL; + } + for (size_t i = 0; i < matches->GetSize(); ++i) { + std::string match_str; + if (!matches->GetString(i, &match_str)) { + LOG(ERROR) << "Invalid matches JSON value"; + return E_FAIL; + } + + URLPattern pattern(UserScript::kValidUserScriptSchemes); + if (pattern.Parse(match_str) != URLPattern::PARSE_SUCCESS) { + LOG(ERROR) << "Invalid matches value"; + return E_FAIL; + } + + result->add_url_pattern(pattern); + } + + // js and css keys + ListValue* js = NULL; + if (content_script->HasKey(ext_keys::kJs) && + !content_script->GetList(ext_keys::kJs, &js)) { + LOG(ERROR) << "Invalid content script JS JSON value"; + return E_FAIL; + } + + ListValue* css = NULL; + if (content_script->HasKey(ext_keys::kCss) && + !content_script->GetList(ext_keys::kCss, &css)) { + LOG(ERROR) << "Invalid content script CSS JSON value"; + return E_FAIL; + } + + // The manifest needs to have at least one js or css user script definition. + if (((js ? js->GetSize() : 0) + (css ? css->GetSize() : 0)) == 0) { + LOG(ERROR) << "Invalid manifest without any content script"; + return E_FAIL; + } + + if (js) { + for (size_t script_index = 0; script_index < js->GetSize(); + ++script_index) { + Value* value; + std::wstring relative; + if (!js->Get(script_index, &value) || !value->GetAsString(&relative)) { + LOG(ERROR) << "Invalid content script JS JSON value"; + return E_FAIL; + } + // TODO(mad@chromium.org): Make GetResourceUrl accept wstring + // too and check with georged@chromium.org who has the same todo + // in chrome\common\extensions\extension.cc + GURL url = GetResourceUrl(WideToUTF8(relative)); + result->js_scripts().push_back( + UserScript::File(path(), FilePath(relative), url)); + // TODO(mad@chromium.org): Verify that the path refers to an + // existing file. + } + } + + if (css) { + for (size_t script_index = 0; script_index < css->GetSize(); + ++script_index) { + Value* value; + std::wstring relative; + if (!css->Get(script_index, &value) || !value->GetAsString(&relative)) { + LOG(ERROR) << "Invalid content script CSS JSON value"; + return E_FAIL; + } + // TODO(mad@chromium.org): Make GetResourceUrl accept wstring + // too and check with georged@chromium.org who has the same todo + // in chrome\common\extensions\extension.cc + GURL url = GetResourceUrl(WideToUTF8(relative)); + result->css_scripts().push_back( + UserScript::File(path(), FilePath(relative), url)); + // TODO(mad@chromium.org): Verify that the path refers to an + // existing file. + } + } + + return S_OK; +} diff --git a/ceee/ie/common/extension_manifest.h b/ceee/ie/common/extension_manifest.h new file mode 100644 index 0000000..11fe165 --- /dev/null +++ b/ceee/ie/common/extension_manifest.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to access Chrome Extension manifest data. + +#ifndef CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ +#define CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ + +#include <wtypes.h> + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/file_path.h" +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + + +class DictionaryValue; + + +// A helper class to read data from a Chrome Extension manifest file. +// TODO(mad@chromium.org): Find a way to reuse code from chrome +// extension classes. +class ExtensionManifest { + public: + ExtensionManifest() {} + ~ExtensionManifest() {} + + // The name of the manifest inside an extension. + static const char kManifestFilename[]; + + // The number of bytes in a legal id. + static const size_t kIdSize; + + // Clears the current content and reset it with the content of the manifest + // file found under the provided Chrome extension folder path. + HRESULT ReadManifestFile(const FilePath& extension_path, bool require_id); + + // Returns the extension ID as read from manifest without any transformation. + const std::string& extension_id() const { + return extension_id_; + } + + // Returns the public key as read from manifest without any transformation. + const std::string& public_key() const { + return public_key_; + } + + // Returns the list of toolstrip file names. + const std::vector<std::string>& GetToolstripFileNames() const { + return toolstrip_file_names_; + } + + const FilePath& path() const { return path_; } + const GURL& extension_url() const { return extension_url_; } + const UserScriptList& content_scripts() const { return content_scripts_; } + + // Returns an absolute url to a resource inside of an extension. The + // |extension_url| argument should be the url() from an Extension object. The + // |relative_path| can be untrusted user input. The returned URL will either + // be invalid() or a child of |extension_url|. + // NOTE: Static so that it can be used from multiple threads. + static GURL GetResourceUrl(const GURL& extension_url, + const std::string& relative_path); + GURL GetResourceUrl(const std::string& relative_path) { + return GetResourceUrl(extension_url(), relative_path); + } + + private: + // Transforms the value public_key_ to set the value of extension_id_ + // using the same algorithm as Chrome. + HRESULT CalculateIdFromPublicKey(); + + // Helper method that loads a UserScript object from a dictionary in the + // content_script list of the manifest. + HRESULT LoadUserScriptHelper(const DictionaryValue* content_script, + UserScript* result); + + // The absolute path to the directory the extension is stored in. + FilePath path_; + + // The base extension url for the extension. + GURL extension_url_; + + // Paths to the content scripts the extension contains. + UserScriptList content_scripts_; + + std::vector<std::string> toolstrip_file_names_; + std::string extension_id_; + std::string public_key_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionManifest); +}; + +#endif // CEEE_IE_COMMON_EXTENSION_MANIFEST_H_ diff --git a/ceee/ie/common/extension_manifest_unittest.cc b/ceee/ie/common/extension_manifest_unittest.cc new file mode 100644 index 0000000..9a3d990 --- /dev/null +++ b/ceee/ie/common/extension_manifest_unittest.cc @@ -0,0 +1,319 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ExtensionManifest. + +#include <atlconv.h> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/extensions/user_script.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ext_keys = extension_manifest_keys; +namespace ext_values = extension_manifest_values; + +namespace { + +// Static constants +static const char* kToolstripName1 = "MyToolstrip.html"; +static const char* kToolstripName2 = "YourToolstrip.html"; +static const char* kPublicKeyName = "0123456789ABCDEF0123456789ABCDEF"; +static const char* kUrlPattern1 = "http://madlymad.com/*"; +static const char* kUrlPattern2 = "https://superdave.com/*"; +static const char* kCssPath = "CssPath.css"; +static const wchar_t* kCssWPath = L"CssPath.css"; +static const char* kJsPath1 = "JsPath1.js"; +static const wchar_t* kJsWPath1 = L"JsPath1.js"; +static const char* kJsPath2 = "JsPath2.js"; +static const wchar_t* kJsWPath2 = L"JsPath2.js"; +// This Id value has been computed with a valid version of the code. +// If the algorithm changes, we must update this value. +static const char* kComputedId = "fppgjfcenabdcibneonnejnkdafgjcch"; + +static const char* kExtensionUrl = + "chrome-extension://fppgjfcenabdcibneonnejnkdafgjcch/"; + +// Test fixture to handle the stuff common to all tests. +class ExtensionManifestTest : public testing::Test { + protected: + ExtensionManifestTest() + // We need to remember whether we created the file or not so that we + // can delete it in TearDown because some tests don't create a file. + : created_file_(false) { + } + + // We always use the same temporary file name, so we can make it static. + static void SetUpTestCase() { + EXPECT_TRUE(file_util::GetTempDir(&file_dir_)); + file_path_ = file_dir_.AppendASCII(ExtensionManifest::kManifestFilename); + } + + // This is the common code to write the Json values to the manifest file. + void WriteJsonToFile(const Value& value) { + std::string json_string; + base::JSONWriter::Write(&value, false, &json_string); + + FILE* temp_file = file_util::OpenFile(file_path_, "w"); + EXPECT_TRUE(temp_file != NULL); + created_file_ = true; + + fwrite(json_string.c_str(), json_string.size(), 1, temp_file); + file_util::CloseFile(temp_file); + temp_file = NULL; + } + + // We must delete the file on each tests that created one. + virtual void TearDown() { + if (created_file_) { + EXPECT_TRUE(file_util::Delete(file_path_, false)); + } + } + + protected: + static FilePath file_dir_; + // We keep both the file path and the dir only path to avoid reconstructing it + // all the time. + static FilePath file_path_; + + private: + bool created_file_; +}; + +FilePath ExtensionManifestTest::file_dir_; +FilePath ExtensionManifestTest::file_path_; + + +TEST_F(ExtensionManifestTest, InvalidFileName) { + testing::LogDisabler no_dchecks; + + // This test assumes that there is no manifest file in the current path. + base::PlatformFileInfo dummy; + EXPECT_FALSE(file_util::GetFileInfo(FilePath(L"manifest.json"), &dummy)); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(FilePath(), false)); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile( + FilePath(L"LalaLandBoobooGaloo"), false)); +} + +TEST_F(ExtensionManifestTest, EmptyFile) { + testing::LogDisabler no_dchecks; + + // Value's constructor is protected, so we must go dynamic. + scoped_ptr<Value> value(Value::CreateNullValue()); + WriteJsonToFile(*value); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, InvalidJsonFile) { + testing::LogDisabler no_dchecks; + + ListValue dummy_list; + dummy_list.Append(Value::CreateIntegerValue(42)); + + WriteJsonToFile(dummy_list); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, InvalidPublicKey) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.Set(ext_keys::kPublicKey, + Value::CreateStringValue("Babebibobu")); + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_TRUE(manifest.public_key().empty()); +} + +TEST_F(ExtensionManifestTest, InvalidUserScript) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + ListValue* scripts = new ListValue(); + values.Set(ext_keys::kContentScripts, scripts); + DictionaryValue* script_dict = new DictionaryValue; + scripts->Append(script_dict); + + // Empty scripts are not allowed. + WriteJsonToFile(values); + ExtensionManifest manifest; + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + ListValue* matches1 = new ListValue(); + script_dict->Set(ext_keys::kMatches, matches1); + + // Matches must have at least one value. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + matches1->Append(Value::CreateStringValue(kUrlPattern1)); + + // Having a match isn't enough without at least one CSS or JS file. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + ListValue* css = new ListValue(); + script_dict->Set(ext_keys::kCss, css); + + // CSS list must have at least one item. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); + + script_dict->Remove(ext_keys::kCss, NULL); + ListValue* js = new ListValue(); + script_dict->Set(ext_keys::kJs, js); + + // Same thing for JS. + WriteJsonToFile(values); + EXPECT_HRESULT_FAILED(manifest.ReadManifestFile(file_dir_, false)); +} + +TEST_F(ExtensionManifestTest, EmptyValidJsonFile) { + testing::LogDisabler no_dchecks; + + WriteJsonToFile(DictionaryValue()); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_TRUE(manifest.GetToolstripFileNames().empty()); + EXPECT_STREQ(manifest.path().value().c_str(), file_dir_.value().c_str()); +} + +TEST_F(ExtensionManifestTest, ValidJsonFileWithOneValue) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.SetString("name", "My Name"); + + // We must dynamically allocate since DictionaryValue will free this memory. + ListValue * toolstrips = new ListValue(); + toolstrips->Append(Value::CreateStringValue(kToolstripName1)); + values.Set(ext_keys::kToolstrips, toolstrips); + toolstrips = NULL; + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, false)); + EXPECT_FALSE(manifest.GetToolstripFileNames().empty()); + EXPECT_STREQ(manifest.GetToolstripFileNames()[0].c_str(), kToolstripName1); + EXPECT_TRUE(manifest.public_key().empty()); + EXPECT_TRUE(manifest.extension_id().empty()); +} + +TEST_F(ExtensionManifestTest, ValidJsonFileWithManyValues) { + testing::LogDisabler no_dchecks; + + DictionaryValue values; + values.SetString("name", "My Name"); + values.SetString("job", "Your Job"); + values.SetString(ext_keys::kPublicKey, kPublicKeyName); + + ListValue* toolstrips = new ListValue(); + toolstrips->Append(Value::CreateStringValue(kToolstripName1)); + toolstrips->Append(Value::CreateStringValue(kToolstripName2)); + values.Set(ext_keys::kToolstrips, toolstrips); + toolstrips = NULL; + + ListValue* scripts = new ListValue(); + values.Set(ext_keys::kContentScripts, scripts); + DictionaryValue* script_dict1 = new DictionaryValue; + scripts->Append(script_dict1); + script_dict1->SetString(ext_keys::kRunAt, + ext_values::kRunAtDocumentStart); + + ListValue* matches1 = new ListValue(); + script_dict1->Set(ext_keys::kMatches, matches1); + matches1->Append(Value::CreateStringValue(kUrlPattern1)); + + ListValue* css = new ListValue(); + script_dict1->Set(ext_keys::kCss, css); + css->Append(Value::CreateStringValue(kCssPath)); + + DictionaryValue* script_dict2 = new DictionaryValue; + scripts->Append(script_dict2); + + ListValue* matches2 = new ListValue(); + script_dict2->Set(ext_keys::kMatches, matches2); + matches2->Append(Value::CreateStringValue(kUrlPattern1)); + matches2->Append(Value::CreateStringValue(kUrlPattern2)); + + ListValue* js = new ListValue(); + script_dict2->Set(ext_keys::kJs, js); + js->Append(Value::CreateStringValue(kJsPath1)); + js->Append(Value::CreateStringValue(kJsPath2)); + + WriteJsonToFile(values); + + ExtensionManifest manifest; + EXPECT_HRESULT_SUCCEEDED(manifest.ReadManifestFile(file_dir_, true)); + EXPECT_FALSE(manifest.GetToolstripFileNames().empty()); + // Prevent asserts blocking the tests if the test above failed. + if (manifest.GetToolstripFileNames().size() > 1) { + EXPECT_STREQ(manifest.GetToolstripFileNames()[0].c_str(), kToolstripName1); + EXPECT_STREQ(manifest.GetToolstripFileNames()[1].c_str(), kToolstripName2); + } + EXPECT_STREQ(manifest.public_key().c_str(), kPublicKeyName); + EXPECT_STREQ(manifest.extension_id().c_str(), kComputedId); + EXPECT_STREQ(manifest.path().value().c_str(), file_dir_.value().c_str()); + EXPECT_EQ(manifest.extension_url(), GURL(kExtensionUrl)); + + URLPattern url_pattern1(UserScript::kValidUserScriptSchemes); + url_pattern1.Parse(kUrlPattern1); + URLPattern url_pattern2(UserScript::kValidUserScriptSchemes); + url_pattern2.Parse(kUrlPattern2); + + const UserScriptList& script_list = manifest.content_scripts(); + EXPECT_EQ(script_list.size(), 2); + if (script_list.size() == 2) { + const UserScript& script1 = script_list[0]; + EXPECT_EQ(script1.run_location(), + UserScript::DOCUMENT_START); + const std::vector<URLPattern>& url_patterns1 = script1.url_patterns(); + EXPECT_EQ(url_patterns1.size(), 1); + if (url_patterns1.size() == 1) + EXPECT_EQ(url_patterns1[0].GetAsString(), url_pattern1.GetAsString()); + const UserScript::FileList& css_scripts = script1.css_scripts(); + EXPECT_EQ(css_scripts.size(), 1); + if (css_scripts.size() == 1) { + EXPECT_STREQ(css_scripts[0].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(css_scripts[0].relative_path().value().c_str(), kCssWPath); + } + const UserScript& script2 = script_list[1]; + const std::vector<URLPattern>& url_patterns2 = script2.url_patterns(); + EXPECT_EQ(url_patterns2.size(), 2); + if (url_patterns2.size() == 2) { + EXPECT_EQ(url_patterns2[0].GetAsString(), url_pattern1.GetAsString()); + EXPECT_EQ(url_patterns2[1].GetAsString(), url_pattern2.GetAsString()); + } + const UserScript::FileList& js_scripts = script2.js_scripts(); + EXPECT_EQ(js_scripts.size(), 2); + if (js_scripts.size() == 2) { + EXPECT_STREQ(js_scripts[0].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(js_scripts[0].relative_path().value().c_str(), kJsWPath1); + EXPECT_STREQ(js_scripts[1].extension_root().value().c_str(), + file_dir_.value().c_str()); + EXPECT_STREQ(js_scripts[1].relative_path().value().c_str(), kJsWPath2); + } + } +} + +} // namespace diff --git a/ceee/ie/common/ie_guids.cc b/ceee/ie/common/ie_guids.cc new file mode 100644 index 0000000..0babc29 --- /dev/null +++ b/ceee/ie/common/ie_guids.cc @@ -0,0 +1,26 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Pull in all the GUIDs we rely on. +#include "broker_lib.h" // NOLINT +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +#include <initguid.h> // NOLINT + +// Dispex guids - I don't know that they come in any library. +#include <dispex.h> + +// Pull in the tab interface guids also. +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "third_party/activscp/activdbg.h" + +extern "C" { +#include "broker_lib_i.c" // NOLINT +#include "toolband_i.c" // NOLINT + +const GUID IID_IProcessDebugManager = __uuidof(IProcessDebugManager); +const GUID IID_IDebugApplication = __uuidof(IDebugApplication); +const GUID IID_IDebugDocumentHelper = __uuidof(IDebugDocumentHelper); +} // extern "C" diff --git a/ceee/ie/common/ie_tab_interfaces.cc b/ceee/ie/common/ie_tab_interfaces.cc new file mode 100644 index 0000000..eeadd38e --- /dev/null +++ b/ceee/ie/common/ie_tab_interfaces.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ITabWindow and ITabWindowManager interfaces and approaches +// related to them. + +#include "ceee/ie/common/ie_tab_interfaces.h" + +#include <shlguid.h> // SID_SWebBrowserApp + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" + +namespace ie_tab_interfaces { + +HRESULT TabWindowManagerFromFrame(HWND ie_frame, REFIID riid, void** manager) { + DCHECK(ie_frame); + DCHECK(manager && !*manager); + + if (!window_utils::IsWindowThread(ie_frame)) { + LOG(ERROR) << "Can't get tab window manager from frame in other process " + "or thread that created the frame window."; + return E_INVALIDARG; + } + + IDropTarget* drop_target = reinterpret_cast<IDropTarget*>( + ::GetPropW(ie_frame, L"OleDropTargetInterface")); + if (!drop_target) { + NOTREACHED() << "No drop target"; + return E_UNEXPECTED; + } + + CComQIPtr<IServiceProvider> frame_service_provider(drop_target); + if (!frame_service_provider) { + NOTREACHED(); + return E_NOINTERFACE; + } + + return frame_service_provider->QueryService(SID_STabWindowManager, riid, + manager); +} + +} // namespace ie_tab_interfaces diff --git a/ceee/ie/common/ie_tab_interfaces.h b/ceee/ie/common/ie_tab_interfaces.h new file mode 100644 index 0000000..2340692 --- /dev/null +++ b/ceee/ie/common/ie_tab_interfaces.h @@ -0,0 +1,314 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ITabWindow and ITabWindowManager interfaces and approaches +// related to them. + +#ifndef CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ +#define CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ + +#include <atlbase.h> +#include <exdisp.h> // IWebBrowser2 +#include <guiddef.h> // DEFINE_GUID +#include <mshtml.h> // IHTMLDocument2 + +// Service ID to get the tab window manager. +// {122F0301-9AB9-4CBE-B5F6-CEADCF6AA9B7} +DEFINE_GUID(SID_STabWindowManager, + 0x122F0301L, + 0x9AB9, + 0x4CBE, + 0xB5, 0xF6, 0xCE, 0xAD, 0xCF, 0x6A, 0xA9, 0xB7); + +// Adapted from documentation available at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindowmanager.htm +// +// Available in IE7, IE8 and IE9. Retrieve by QueryService for +// SID_STabWindowManager on the IServiceProvider that is the +// IDropTarget you can retrieve from the browser frame window. +// +// The ITabWindowManager interface is NOT the same in IE7, IE8 and +// IE9, and has different IIDs in each. + +interface __declspec(uuid("CAE57FE7-5E06-4804-A285-A985E76708CD")) + ITabWindowManagerIe7 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, long*) PURE; + STDMETHOD(SelectTab)(long tab) PURE; + STDMETHOD(CloseAllTabs)() PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown** active_tab) PURE; + STDMETHOD(GetCount)(long* count) PURE; + // Actually ITabWindow** + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + // Window class of window should be "TabWindowClass" + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actually IBrowserFrame** + STDMETHOD(GetBrowserFrame)(IUnknown** browser_frame) PURE; + STDMETHOD(AddBlankTab)(unsigned long ul, long* l) PURE; + STDMETHOD(AddTabGroup)(LPCITEMIDLIST* pidl, long l, unsigned long ul) PURE; + STDMETHOD(GetCurrentTabGroup)(LPCITEMIDLIST** pidl, long* l1, long* l2) PURE; + STDMETHOD(OpenHomePages)(int flags) PURE; + // @param moving_id ID (as in ITabWindow::GetID) of the tab being moved. + // @param dest_id ID of the tab currently in the desired destination position. + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int) PURE; +}; + +interface __declspec(uuid("9706DA66-D17C-48a5-B42D-39963D174DC0")) + ITabWindowManagerIe8 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, long*) PURE; + STDMETHOD(_AddTabByPosition)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(SelectTab)(long) PURE; + STDMETHOD(CloseAllTabs)() PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown** active_tab) PURE; + STDMETHOD(GetCount)(long* count) PURE; + STDMETHOD(_GetFilteredCount)(void * UNKNOWN_ARGUMENTS) PURE; + // Actually ITabWindow** + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + STDMETHOD(_FilteredIndexFromID)(void * UNKNOWN_ARGUMENTS) PURE; + // Window class of window should be "TabWindowClass" + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actually IBrowserFrame** + STDMETHOD(GetBrowserFrame)(IUnknown** browser_frame) PURE; + STDMETHOD(AddBlankTab)(unsigned long, long*) PURE; + STDMETHOD(_AddBlankTabEx)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(AddTabGroup)(LPCITEMIDLIST* pidl, long, unsigned long) PURE; + STDMETHOD(GetCurrentTabGroup)(LPCITEMIDLIST** pidl, long*, long*) PURE; + STDMETHOD(OpenHomePages)(int flags) PURE; + // @param moving_id ID (as in ITabWindow::GetID) of the tab being moved. + // @param dest_id ID of the tab currently in the desired destination position. + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int) PURE; +}; + +// In IE9 IID and definition of the interface has changed. +interface __declspec(uuid("8059E123-28D5-4C75-A298-664B3720ACAE")) + ITabWindowManagerIe9 : IUnknown { + STDMETHOD(AddTab)(LPCITEMIDLIST pidl, UINT, ULONG, IUnknown*, long*) PURE; + STDMETHOD(AddTabByPosition)(LPCITEMIDLIST pidl, UINT, ULONG, long, + IUnknown*, DWORD*, long*) PURE; + STDMETHOD(SelectTab)(long) PURE; + STDMETHOD(CloseAllTabs)(void) PURE; + // Actually ITabWindow** + STDMETHOD(GetActiveTab)(IUnknown**) PURE; + STDMETHOD(GetCount)(long*) PURE; + STDMETHOD(GetFilteredCount)(long*, long) PURE; + STDMETHOD(GetItem)(long index, IUnknown** tab_window) PURE; + STDMETHOD(IndexFromID)(long id, long* index) PURE; + STDMETHOD(FilteredIndexFromID)(long, long, long*) PURE; + STDMETHOD(IndexFromHWND)(HWND window, long* index) PURE; + // Actual IBrowserFrame ** + STDMETHOD(GetBrowserFrame)(IUnknown**) PURE; + STDMETHOD(AddBlankTab)(ULONG, long*) PURE; + STDMETHOD(AddBlankTabEx)(ULONG, DWORD*, long*) PURE; + STDMETHOD(AddTabGroup)(DWORD, long, ULONG) PURE; + STDMETHOD(GetCurrentTabGroup)(DWORD, long*, long*) PURE; + STDMETHOD(OpenHomePages)(int) PURE; + STDMETHOD(RepositionTab)(long, long, int) PURE; + // Actually IClosedTabManager** + STDMETHOD(GetUndoTabManager)(IUnknown**) PURE; + STDMETHOD(GetRestoreTabManager)(IUnknown**) PURE; + // Actually, ITabWindowEvents* (a new interface) + STDMETHOD(AddTabWindowEventHandler)(IUnknown*) PURE; + // Actually, ITabWindowEvents* (a new interface) + STDMETHOD(UnregisterTabWindowEventHandler)(IUnknown*) PURE; + STDMETHOD(CloseTabGroup)(long) PURE; + STDMETHOD(CreateGroupMapping)(long*) PURE; + STDMETHOD(DestroyGroupMapping)(long) PURE; + STDMETHOD(SetDecorationPreference)(DWORD, DWORD*) PURE; + STDMETHOD(FindTabAdjacentToGroup)(long, long, + DWORD, IUnknown**, long*) PURE; + STDMETHOD(GetNewGroupID)(long*) PURE; + STDMETHOD(CloseOldTabIfFailed)(void) PURE; + STDMETHOD(CloseAllTabsExcept)(long) PURE; + STDMETHOD(CloseAllTabsExceptActive)(void) PURE; +}; + +// Adapted from documentation available at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindow.htm +// +// Also available differently in IE7 and IE8. +interface __declspec(uuid("9BAB3405-EE3F-4040-8836-25AA9C2D408E")) + ITabWindowIe7 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe7** tab_manager) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +interface __declspec(uuid("FF18630E-5B18-4A07-8A75-9FD3CE5A2D14")) + ITabWindowIe8 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + // REMOVED from IE7 version: STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +// New version that was introduced between versions 8.0.6001.18928 and +// 8.0.7600.16385. +interface __declspec(uuid("F704B7E0-4760-46ff-BBDB-7439E0A2A814")) + ITabWindowIe8_1 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(REFGUID cmd_group, DWORD cmd_id, DWORD exec_opt, + VARIANT* in_args, VARIANT* out_args) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(OnBrowserCreated)(int, int, int, int, int, void*) PURE; + STDMETHOD(OnNewWindow)(ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)() PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + // REMOVED from IE7 version: STDMETHOD(SetIcon)(HICON, int) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetIconWeakReference)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(void * UNKNOWN_ARGUMENTS) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetBrowserToolbarWindow)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(HBITMAP bitmap) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; +}; + +// Modified interface which appeared in IE9 beta. +interface __declspec(uuid("3927961B-9DB0-4174-B67A-39F34585A692")) + ITabWindowIe9 : IUnknown { + STDMETHOD(GetID)(long* id) PURE; + STDMETHOD(Close)() PURE; + STDMETHOD(AsyncExec)(GUID *, ULONG, ULONG, VARIANT *) PURE; + STDMETHOD(GetTabWindowManager)(ITabWindowManagerIe8** tab_manager) PURE; + STDMETHOD(SetBrowserWindowParent)(HWND) PURE; + STDMETHOD(OnBrowserCreated)(HWND, HWND, HWND, IDispatch*, + DWORD*, long) PURE; + STDMETHOD(OnNewWindow)(ULONG, ULONG, IDispatch**) PURE; + STDMETHOD(OnBrowserClosed)(void) PURE; + // Actually enum tagTAB_ATTENTION_STATE + STDMETHOD(OnRequestAttention)(int i) PURE; + STDMETHOD(FrameTranslateAccelerator)(MSG* msg, ULONG) PURE; + STDMETHOD(SetTitle)(LPCWSTR title, int title_length) PURE; + STDMETHOD(SetStatusBarState)(int, long) PURE; + STDMETHOD(SetITBarHolderWindow)(HWND) PURE; + STDMETHOD(EnsureITBar)(int, int) PURE; + STDMETHOD(GetTitle)(LPWSTR title, ULONG, int) PURE; + STDMETHOD(GetIcon)(HICON* icon, int*, int) PURE; + STDMETHOD(GetIconWeakReference)(HICON**) PURE; + STDMETHOD(GetLocationPidl)(LPCITEMIDLIST* pidl) PURE; + STDMETHOD(GetLocationUri)(IUnknown**) PURE; + STDMETHOD(GetNavigationState)(ULONG* state) PURE; + // First param is enum tagNAVIGATION_BAND_PROGRESS_STATE* + STDMETHOD(GetProgress)(int*, long*, long*) PURE; + STDMETHOD(GetFlags)(ULONG* flags) PURE; + STDMETHOD(GetBrowser)(IDispatch** browser) PURE; + STDMETHOD(GetParentComponentHandle)(HWND* window) PURE; + // Actually enum tagSEARCH_BAND_SEARCH_STATE* + STDMETHOD(GetSearchState)(int* state) PURE; + // Actually enum tagTAB_ATTENTION_STATE* + STDMETHOD(GetAttentionState)(int* state) PURE; + STDMETHOD(ResampleImageAsync)() PURE; + STDMETHOD(OnTabImageResampled)(IStream *) PURE; + STDMETHOD(GetStatusBarState)(int* bar, long* state) PURE; + // Resolved first time here, but existed in earlier versions. + STDMETHOD(GetThumbnailWindow)(HWND**) PURE; + STDMETHOD(GetBrowserWindow)(HWND**) PURE; + STDMETHOD(TransferRecoveryDataForSiteMode)(void) PURE; + STDMETHOD(GetTabGroup)(long*) PURE; + STDMETHOD(GetTabGroupDecoration)(DWORD*) PURE; + STDMETHOD(JoinTabGroup)(long, long) PURE; + STDMETHOD(LeaveTabGroup)(void) PURE; + STDMETHOD(IsParticipatingInTabGroup)(int*) PURE; + STDMETHOD(IsWaitingForGroupRecovery)(int*) PURE; + STDMETHOD(SetWaitingForGroupRecovery)(long, int) PURE; + STDMETHOD(BrowserTabIsHung)(void) PURE; + STDMETHOD(FrameTabWillNotHang)(ULONG, ULONG) PURE; + STDMETHOD(BrowserTabRespondsNow)(ULONG, int, int) PURE; + STDMETHOD(BrowserTabRespondsNow_SetHungAsync)(ULONG, ULONG) PURE; + STDMETHOD(BrowserTabIsPresumedResponsive)(void) PURE; + STDMETHOD(RecoverHungTab)(void) PURE; + STDMETHOD(Duplicate)(void) PURE; + STDMETHOD(ResetBrowserLCIEProxy)(IDispatch*) PURE; + STDMETHOD(SetPendingUrl)(LPCWSTR) PURE; +}; + +namespace ie_tab_interfaces { + +// Retrieves the requested tab manager interface for the specified IEFrame +// window. +// +// @param ie_frame The top-level frame window you wish to manage. +// @param riid The identifier of the requested interface. +// @param manager Returns the IE7 tab window manager on success. +HRESULT TabWindowManagerFromFrame(HWND ie_frame, REFIID riid, void** manager); + +} // namespace ie_tab_interfaces + +#endif // CEEE_IE_COMMON_IE_TAB_INTERFACES_H_ diff --git a/ceee/ie/common/ie_util.cc b/ceee/ie/common/ie_util.cc new file mode 100644 index 0000000..5484898 --- /dev/null +++ b/ceee/ie/common/ie_util.cc @@ -0,0 +1,134 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility functions to interact with IE. + +#include "ceee/ie/common/ie_util.h" + +#include <atlcomcli.h> +#include <exdisp.h> // IWebBrowser2 + +#include "base/logging.h" +#include "base/win/registry.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" + +namespace { + +const wchar_t kIeVersionKey[] = L"SOFTWARE\\Microsoft\\Internet Explorer"; +const wchar_t kIeVersionValue[] = L"Version"; + +HRESULT GetShellWindowsEnum(IEnumVARIANT** enum_windows) { + CComPtr<IShellWindows> shell_windows; + HRESULT hr = shell_windows.CoCreateInstance(CLSID_ShellWindows); + DCHECK(SUCCEEDED(hr)) << "Could not CoCreate ShellWindows. " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + CComPtr<IUnknown> enum_punk; + hr = shell_windows->_NewEnum(&enum_punk); + DCHECK(SUCCEEDED(hr)) << "Could not get Enum IUnknown??? " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + return enum_punk->QueryInterface(IID_IEnumVARIANT, + reinterpret_cast<void**>(enum_windows)); +} + +bool GetIeVersionString(std::wstring* version) { + DCHECK(version != NULL); + if (version == NULL) + return false; + base::win::RegKey key(HKEY_LOCAL_MACHINE, kIeVersionKey, KEY_READ); + DCHECK(key.ValueExists(kIeVersionValue)); + return key.ReadValue(kIeVersionValue, version); +} + +} // namespace + +namespace ie_util { + +HRESULT GetWebBrowserForTopLevelIeHwnd( + HWND window, IWebBrowser2* not_him, IWebBrowser2** browser) { + DCHECK(browser != NULL); + CComPtr<IEnumVARIANT> enum_windows; + HRESULT hr = GetShellWindowsEnum(&enum_windows); + DCHECK(SUCCEEDED(hr)) << "Could not get ShellWindows enum. " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + hr = enum_windows->Reset(); + DCHECK(SUCCEEDED(hr)) << "Can't Reset??? " << com::LogHr(hr); + CComVariant shell_window; + ULONG fetched = 0; + while (SUCCEEDED(enum_windows->Next(1, &shell_window, &fetched)) && + fetched == 1) { + DCHECK(shell_window.vt == VT_DISPATCH); + CComQIPtr<IWebBrowser2> this_browser(shell_window.pdispVal); + if (this_browser != NULL) { + HWND this_window = NULL; + hr = this_browser->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&this_window)); + // This can happen if the browser gets deconnected as we loop. + if (SUCCEEDED(hr) && this_window == window && not_him != this_browser) { + return this_browser.CopyTo(browser); + } + } + shell_window.Clear(); + } + return E_FAIL; +} + +IeVersion GetIeVersion() { + std::wstring ie_version_str; + if (GetIeVersionString(&ie_version_str)) { + int ie_version_num = wcstol( + ie_version_str.substr(0, ie_version_str.find(L'.')).c_str(), NULL, 10); + if (ie_version_num < 6) + return IEVERSION_PRE_IE6; + else if (ie_version_num == 6) + return IEVERSION_IE6; + else if (ie_version_num == 7) + return IEVERSION_IE7; + else if (ie_version_num == 8) + return IEVERSION_IE8; + else if (ie_version_num == 9) + return IEVERSION_IE9; + } + DCHECK(false) << "Failed to get a known IE version!!!"; + return IEVERSION_UNKNOWN; +} + +bool GetIEIsInPrivateBrowsing() { + // TODO(skare@google.com): unify with version in chrome_frame/utils.cc + + // InPrivate flag will not change over process lifetime, so cache it. See: + // http://blogs.msdn.com/ieinternals/archive/2009/06/30/IE8-Privacy-APIs-for-Addons.aspx + static bool inprivate_status_cached = false; + static bool is_inprivate = false; + if (inprivate_status_cached) + return is_inprivate; + + // InPrivate is only supported with IE8+. + if (GetIeVersion() < IEVERSION_IE8) { + inprivate_status_cached = true; + return false; + } + + typedef BOOL (WINAPI* IEIsInPrivateBrowsingPtr)(); + HMODULE ieframe_dll = GetModuleHandle(L"ieframe.dll"); + if (ieframe_dll) { + IEIsInPrivateBrowsingPtr IsInPrivate = + reinterpret_cast<IEIsInPrivateBrowsingPtr>(GetProcAddress( + ieframe_dll, "IEIsInPrivateBrowsing")); + if (IsInPrivate) { + is_inprivate = !!IsInPrivate(); + } + } + + inprivate_status_cached = true; + return is_inprivate; +} + +} // namespace ie_util diff --git a/ceee/ie/common/ie_util.h b/ceee/ie/common/ie_util.h new file mode 100644 index 0000000..5d2a19c --- /dev/null +++ b/ceee/ie/common/ie_util.h @@ -0,0 +1,45 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility functions to interact with IE. + +#ifndef CEEE_IE_COMMON_IE_UTIL_H_ +#define CEEE_IE_COMMON_IE_UTIL_H_ + +#include <wtypes.h> + +#include <set> + +struct IWebBrowser2; + +namespace ie_util { + +// Returns the IWebBrowser2 interface associated to a given top level IE HWND. +// but not the one passed into not_him (which could be NULL), since there are +// some states where there are two web browser assigned to a single HWND. +HRESULT GetWebBrowserForTopLevelIeHwnd( + HWND window, IWebBrowser2* not_him, IWebBrowser2** browser); + +// NOTE: Keep these in order so callers can do things like +// "if (GetIeVersion() > IEVERSION_7) ...". It's OK to change the values, +// though. +enum IeVersion { + IEVERSION_UNKNOWN = -1, + IEVERSION_PRE_IE6 = 0, // Not supported + IEVERSION_IE6 = 1, + IEVERSION_IE7 = 2, + IEVERSION_IE8 = 3, + IEVERSION_IE9 = 4, +}; + +// Returns the IE version. +// Returns true/false for success/failure. +IeVersion GetIeVersion(); + +// Returns true if IE is in InPrivate mode. +bool GetIEIsInPrivateBrowsing(); + +} // namespace ie_util + +#endif // CEEE_IE_COMMON_IE_UTIL_H_ diff --git a/ceee/ie/common/mock_ceee_module_util.h b/ceee/ie/common/mock_ceee_module_util.h new file mode 100644 index 0000000..af70657 --- /dev/null +++ b/ceee/ie/common/mock_ceee_module_util.h @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ceee_module_util mocks. + +#ifndef CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ +#define CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/testing/utils/mock_static.h" + +namespace testing { + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT2(ceee_module_util::SetOptionToolbandIsHidden, + SetOptionToolbandIsHidden); + MOCK_STATIC_INIT2(ceee_module_util::GetOptionToolbandIsHidden, + GetOptionToolbandIsHidden); + MOCK_STATIC_INIT2(ceee_module_util::SetOptionToolbandForceReposition, + SetOptionToolbandForceReposition); + MOCK_STATIC_INIT2(ceee_module_util::GetOptionToolbandForceReposition, + GetOptionToolbandForceReposition); + MOCK_STATIC_INIT2(ceee_module_util::SetIgnoreShowDWChanges, + SetIgnoreShowDWChanges); + MOCK_STATIC_INIT2(ceee_module_util::GetIgnoreShowDWChanges, + GetIgnoreShowDWChanges); + MOCK_STATIC_INIT2(ceee_module_util::GetExtensionPath, + GetExtensionPath); + MOCK_STATIC_INIT_END() + MOCK_STATIC1(void, , SetOptionToolbandIsHidden, bool); + MOCK_STATIC0(bool, , GetOptionToolbandIsHidden); + MOCK_STATIC1(void, , SetOptionToolbandForceReposition, bool); + MOCK_STATIC0(bool, , GetOptionToolbandForceReposition); + MOCK_STATIC1(void, , SetIgnoreShowDWChanges, bool); + MOCK_STATIC0(bool, , GetIgnoreShowDWChanges); + MOCK_STATIC0(std::wstring, , GetExtensionPath); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtils) + +} // namespace testing + +#endif // CEEE_IE_COMMON_MOCK_CEEE_MODULE_UTIL_H_ diff --git a/ceee/ie/common/mock_ie_tab_interfaces.h b/ceee/ie/common/mock_ie_tab_interfaces.h new file mode 100644 index 0000000..2a7fe22 --- /dev/null +++ b/ceee/ie/common/mock_ie_tab_interfaces.h @@ -0,0 +1,106 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ie_tab_interfaces mocks. + +#ifndef CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ +#define CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ + +#include <atlbase.h> +#include <atlcom.h> + +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "ceee/testing/utils/mock_static.h" + +namespace testing { + +MOCK_STATIC_CLASS_BEGIN(MockIeTabInterfaces) + MOCK_STATIC_INIT_BEGIN(MockIeTabInterfaces) + MOCK_STATIC_INIT2(ie_tab_interfaces::TabWindowManagerFromFrame, + TabWindowManagerFromFrame) + MOCK_STATIC_INIT_END() + + MOCK_STATIC3(HRESULT, , TabWindowManagerFromFrame, HWND, REFIID, void **) +MOCK_STATIC_CLASS_END(MockIeTabInterfaces) + +class MockITabWindowManagerIe7 + : public CComObjectRootEx<CComSingleThreadModel>, + public ITabWindowManagerIe7 { + public: + DECLARE_NOT_AGGREGATABLE(MockITabWindowManagerIe7) + BEGIN_COM_MAP(MockITabWindowManagerIe7) + COM_INTERFACE_ENTRY(ITabWindowManagerIe7) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, AddTab, + HRESULT(LPCITEMIDLIST, UINT, ULONG, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SelectTab, HRESULT(long)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, CloseAllTabs, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetActiveTab, HRESULT(IUnknown**)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetCount, HRESULT(long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetItem, HRESULT(long, IUnknown**)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, IndexFromID, HRESULT(long, long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, IndexFromHWND, HRESULT(HWND, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowserFrame, HRESULT(IUnknown**)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, AddBlankTab, + HRESULT(unsigned long, long*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, AddTabGroup, + HRESULT(LPCITEMIDLIST*, long, unsigned long)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetCurrentTabGroup, + HRESULT(LPCITEMIDLIST**, long*, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OpenHomePages, HRESULT(int)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, RepositionTab, + HRESULT(long, long, int)); +}; + +class MockITabWindowIe7 + : public CComObjectRootEx<CComSingleThreadModel>, + public ITabWindowIe7 { + public: + DECLARE_NOT_AGGREGATABLE(MockITabWindowIe7) + BEGIN_COM_MAP(MockITabWindowIe7) + COM_INTERFACE_ENTRY(ITabWindowIe7) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetID, HRESULT(long*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Close, HRESULT()); + MOCK_METHOD5_WITH_CALLTYPE(__stdcall, AsyncExec, + HRESULT(REFGUID, DWORD, DWORD, VARIANT*, VARIANT*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabWindowManager, + HRESULT(ITabWindowManagerIe7**)); + MOCK_METHOD6_WITH_CALLTYPE(__stdcall, OnBrowserCreated, + HRESULT(int, int, int, int, int, void*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, OnNewWindow, + HRESULT(ULONG, IDispatch**)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, OnBrowserClosed, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnRequestAttention, HRESULT(int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, FrameTranslateAccelerator, + HRESULT(MSG*, ULONG)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetTitle, HRESULT(LPCWSTR, int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetIcon, HRESULT(HICON, int)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetStatusBarState, HRESULT(int, long)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetTitle, HRESULT(LPWSTR, ULONG, int)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetIcon, HRESULT(HICON*, int*, int)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetLocationPidl, + HRESULT(LPCITEMIDLIST*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetNavigationState, HRESULT(ULONG*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetProgress, + HRESULT(int*, long*, long*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetFlags, HRESULT(ULONG*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowser, HRESULT(IDispatch**)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetBrowserToolbarWindow, + HRESULT(HWND*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetSearchState, HRESULT(int*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetAttentionState, HRESULT(int*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, ResampleImageAsync, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnTabImageResampled, HRESULT(HBITMAP)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetStatusBarState, + HRESULT(int*, long*)); +}; + +} // namespace testing + +#endif // CEEE_IE_COMMON_MOCK_IE_TAB_INTERFACES_H_ diff --git a/ceee/ie/common/precompile.cc b/ceee/ie/common/precompile.cc new file mode 100644 index 0000000..46a23d0 --- /dev/null +++ b/ceee/ie/common/precompile.cc @@ -0,0 +1,6 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile generator file. +#include "ceee/ie/common/precompile.h" diff --git a/ceee/ie/common/precompile.h b/ceee/ie/common/precompile.h new file mode 100644 index 0000000..5a934ee --- /dev/null +++ b/ceee/ie/common/precompile.h @@ -0,0 +1,14 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile header for CEEE. + +#ifndef CEEE_IE_COMMON_PRECOMPILE_H_ +#define CEEE_IE_COMMON_PRECOMPILE_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <atlstr.h> + +#endif // CEEE_IE_COMMON_PRECOMPILE_H_ diff --git a/ceee/ie/ie.gyp b/ceee/ie/ie.gyp new file mode 100644 index 0000000..e2ea88e --- /dev/null +++ b/ceee/ie/ie.gyp @@ -0,0 +1,146 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'targets': [ + { + 'target_name': 'ceee_ie_all', + 'type': 'none', + 'dependencies': [ + 'common/common.gyp:*', + 'broker/broker.gyp:*', + 'plugin/bho/bho.gyp:*', + 'plugin/scripting/scripting.gyp:*', + 'plugin/toolband/toolband.gyp:*', + 'ie_unittests', + 'mediumtest_ie', + ] + }, + { + 'target_name': 'ie_unittests', + 'type': 'executable', + 'sources': [ + 'broker/api_dispatcher_unittest.cc', + 'broker/broker_unittest.cc', + 'broker/cookie_api_module_unittest.cc', + 'broker/executors_manager_unittest.cc', + 'broker/infobar_api_module_unittest.cc', + 'broker/tab_api_module_unittest.cc', + 'broker/window_api_module_unittest.cc', + 'broker/window_events_funnel_unittest.cc', + 'common/chrome_frame_host_unittest.cc', + 'common/crash_reporter_unittest.cc', + 'common/extension_manifest_unittest.cc', + 'common/ceee_module_util_unittest.cc', + 'plugin/bho/browser_helper_object_unittest.cc', + 'plugin/bho/cookie_accountant_unittest.cc', + 'plugin/bho/cookie_events_funnel_unittest.cc', + 'plugin/bho/dom_utils_unittest.cc', + 'plugin/bho/events_funnel_unittest.cc', + 'plugin/bho/executor_unittest.cc', + 'plugin/bho/extension_port_manager.cc', + 'plugin/bho/frame_event_handler_unittest.cc', + 'plugin/bho/infobar_events_funnel_unittest.cc', + 'plugin/bho/tab_events_funnel_unittest.cc', + 'plugin/bho/tool_band_visibility_unittest.cc', + 'plugin/bho/webnavigation_events_funnel_unittest.cc', + 'plugin/bho/webrequest_events_funnel_unittest.cc', + 'plugin/bho/webrequest_notifier_unittest.cc', + 'plugin/bho/web_progress_notifier_unittest.cc', + 'plugin/scripting/content_script_manager.rc', + 'plugin/scripting/content_script_manager_unittest.cc', + 'plugin/scripting/content_script_native_api_unittest.cc', + 'plugin/scripting/renderer_extension_bindings_unittest.cc', + 'plugin/scripting/renderer_extension_bindings_unittest.rc', + 'plugin/scripting/script_host_unittest.cc', + 'plugin/scripting/userscripts_librarian_unittest.cc', + 'plugin/toolband/tool_band_unittest.cc', + 'plugin/toolband/toolband_module_reporting_unittest.cc', + 'testing/ie_unittest_main.cc', + 'testing/mock_broker_and_friends.h', + 'testing/mock_chrome_frame_host.h', + 'testing/mock_browser_and_friends.h', + 'testing/precompile.cc', + 'testing/precompile.h', + ], + 'configurations': { + 'Debug': { + 'msvs_settings': { + 'VCCLCompilerTool': { + # GMock and GTest appear to be really fat, so bump + # precompile header memory setting to 332 megs. + 'AdditionalOptions': ['/Zm332', '/bigobj'], + }, + }, + 'msvs_precompiled_source': 'testing/precompile.cc', + 'msvs_precompiled_header': 'testing/precompile.h', + }, + }, + 'dependencies': [ + 'common/common.gyp:ie_common', + 'common/common.gyp:ie_common_settings', + 'common/common.gyp:ie_guids', + 'broker/broker.gyp:broker', + 'plugin/bho/bho.gyp:bho', + 'plugin/scripting/scripting.gyp:javascript_bindings', + 'plugin/scripting/scripting.gyp:scripting', + 'plugin/toolband/toolband.gyp:ceee_ie_lib', + 'plugin/toolband/toolband.gyp:ie_toolband_common', + 'plugin/toolband/toolband.gyp:toolband_idl', + '../../base/base.gyp:base', + '../../breakpad/breakpad.gyp:breakpad_handler', + '../testing/sidestep/sidestep.gyp:sidestep', + '../testing/utils/test_utils.gyp:test_utils', + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + { + 'target_name': 'mediumtest_ie', + 'type': 'executable', + 'sources': [ + 'plugin/bho/mediumtest_browser_event.cc', + 'plugin/bho/mediumtest_browser_helper_object.cc', + 'testing/mediumtest_ie_common.cc', + 'testing/mediumtest_ie_common.h', + 'testing/mediumtest_ie_main.cc', + 'testing/precompile.cc', + 'testing/precompile.h', + ], + 'configurations': { + 'Debug': { + 'msvs_settings': { + 'VCCLCompilerTool': { + # GMock and GTest appear to be really fat, so bump + # precompile header memory setting to 332 megs. + 'AdditionalOptions': ['/Zm332'], + }, + }, + 'msvs_precompiled_source': 'testing/precompile.cc', + 'msvs_precompiled_header': 'testing/precompile.h', + }, + }, + 'dependencies': [ + 'common/common.gyp:ie_common', + 'common/common.gyp:ie_common_settings', + 'common/common.gyp:ie_guids', + 'plugin/bho/bho.gyp:bho', + 'plugin/scripting/scripting.gyp:scripting', + 'plugin/toolband/toolband.gyp:toolband_idl', + '../../base/base.gyp:base', + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + '../testing/utils/test_utils.gyp:test_utils', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + }, + ] +} diff --git a/ceee/ie/plugin/bho/bho.gyp b/ceee/ie/plugin/bho/bho.gyp new file mode 100644 index 0000000..fe773d39 --- /dev/null +++ b/ceee/ie/plugin/bho/bho.gyp @@ -0,0 +1,87 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + ], + 'targets': [ + { + 'target_name': 'bho', + 'type': 'static_library', + 'dependencies': [ + '../toolband/toolband.gyp:toolband_idl', + '../../broker/broker.gyp:broker', + '../../common/common.gyp:ie_common', + '../../common/common.gyp:ie_common_settings', + '../../../common/common.gyp:ceee_common', + '../../../common/common.gyp:initializing_coclass', + '../../../../base/base.gyp:base', + # For the vtable patching stuff... + '../../../../chrome_frame/chrome_frame.gyp:chrome_frame_ie', + ], + 'sources': [ + 'browser_helper_object.cc', + 'browser_helper_object.h', + 'browser_helper_object.rgs', + 'cookie_accountant.cc', + 'cookie_accountant.h', + 'cookie_events_funnel.cc', + 'cookie_events_funnel.h', + 'dom_utils.cc', + 'dom_utils.h', + 'events_funnel.cc', + 'events_funnel.h', + 'executor.cc', + 'executor.h', + 'extension_port_manager.cc', + 'extension_port_manager.h', + 'frame_event_handler.cc', + 'frame_event_handler.h', + 'http_negotiate.cc', + 'http_negotiate.h', + 'infobar_browser_window.cc', + 'infobar_browser_window.h', + 'infobar_events_funnel.cc', + 'infobar_events_funnel.h', + 'infobar_manager.cc', + 'infobar_manager.h', + 'infobar_window.cc', + 'infobar_window.h', + '../../common/precompile.cc', + '../../common/precompile.h', + 'tab_events_funnel.cc', + 'tab_events_funnel.h', + 'tab_window_manager.cc', + 'tab_window_manager.h', + 'tool_band_visibility.cc', + 'tool_band_visibility.h', + 'web_browser_events_source.h', + 'webnavigation_events_funnel.cc', + 'webnavigation_events_funnel.h', + 'webrequest_events_funnel.cc', + 'webrequest_events_funnel.h', + 'webrequest_notifier.cc', + 'webrequest_notifier.h', + 'web_progress_notifier.cc', + 'web_progress_notifier.h', + 'window_message_source.cc', + 'window_message_source.h', + + '../../../../chrome_frame/renderer_glue.cc', # needed for cf_ie.lib + '../../../../chrome/common/extensions/extension_resource.cc', + '../../../../chrome/common/extensions/extension_resource.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + ] +} diff --git a/ceee/ie/plugin/bho/browser_helper_object.cc b/ceee/ie/plugin/bho/browser_helper_object.cc new file mode 100644 index 0000000..7a9f4d7 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.cc @@ -0,0 +1,1372 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/browser_helper_object.h" + +#include <atlsafe.h> +#include <shlguid.h> + +#include <algorithm> + +#include "base/debug/trace_event.h" +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/tuple.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/cookie_accountant.h" +#include "ceee/ie/plugin/bho/http_negotiate.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "chrome/browser/automation/extension_automation_constants.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/automation/automation_constants.h" +#include "googleurl/src/gurl.h" + +#include "broker_lib.h" // NOLINT + +namespace keys = extension_tabs_module_constants; +namespace ext = extension_automation_constants; + + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatch_5variantptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 7, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO BrowserHelperObject::handler_type_idispatch_variantptr_ = { + CC_STDCALL, VT_EMPTY, 2, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF + } +}; + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatch_3variantptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 5, { + VT_DISPATCH, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_VARIANT | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO BrowserHelperObject::handler_type_idispatchptr_boolptr_ = { + CC_STDCALL, VT_EMPTY, 2, { + VT_DISPATCH | VT_BYREF, + VT_BOOL | VT_BYREF + } +}; + +_ATL_FUNC_INFO + BrowserHelperObject::handler_type_idispatchptr_boolptr_dword_2bstr_ = { + CC_STDCALL, VT_EMPTY, 5, { + VT_DISPATCH | VT_BYREF, + VT_BOOL | VT_BYREF, + VT_UI4, + VT_BSTR, + VT_BSTR + } +}; + +// Remove the need to AddRef/Release for Tasks that target this class. +DISABLE_RUNNABLE_METHOD_REFCOUNT(BrowserHelperObject); + +BrowserHelperObject::BrowserHelperObject() + : already_tried_installing_(false), + tab_window_(NULL), + tab_id_(kInvalidChromeSessionId), + fired_on_created_event_(false), + lower_bound_ready_state_(READYSTATE_UNINITIALIZED), + ie7_or_later_(false), + thread_id_(::GetCurrentThreadId()), + full_tab_chrome_frame_(false) { + TRACE_EVENT_BEGIN("ceee.bho", this, ""); + // Only the first call to this function really does anything. + CookieAccountant::GetInstance()->PatchWininetFunctions(); +} + +BrowserHelperObject::~BrowserHelperObject() { + TRACE_EVENT_END("ceee.bho", this, ""); +} + +HRESULT BrowserHelperObject::FinalConstruct() { + return S_OK; +} + +void BrowserHelperObject::FinalRelease() { + web_browser_.Release(); +} + +STDMETHODIMP BrowserHelperObject::SetSite(IUnknown* site) { + typedef IObjectWithSiteImpl<BrowserHelperObject> SuperSite; + + // From experience, we know the site may be set multiple times. + // Let's ignore second and subsequent set or unset. + if (NULL != site && NULL != m_spUnkSite.p || + NULL == site && NULL == m_spUnkSite.p) { + LOG(WARNING) << "Duplicate call to SetSite, previous site " + << m_spUnkSite.p << " new site " << site; + return S_OK; + } + + if (NULL == site) { + // We're being torn down. + TearDown(); + + FireOnRemovedEvent(); + // This call should be the last thing we send to the broker. + FireOnUnmappedEvent(); + } + + HRESULT hr = SuperSite::SetSite(site); + if (FAILED(hr)) + return hr; + + if (NULL != site) { + // We're being initialized. + hr = Initialize(site); + + // Release the site in case of failure. + if (FAILED(hr)) + SuperSite::SetSite(NULL); + } + + return hr; +} + +HRESULT BrowserHelperObject::GetParentBrowser(IWebBrowser2* browser, + IWebBrowser2** parent_browser) { + DCHECK(browser != NULL); + DCHECK(parent_browser != NULL && *parent_browser == NULL); + + // Get the parent object. + CComPtr<IDispatch> parent_disp; + HRESULT hr = browser->get_Parent(&parent_disp); + if (FAILED(hr)) { + // NO DCHECK, or even log here, the caller will handle and log the error. + return hr; + } + + // Then get the associated browser through the appropriate contortions. + CComQIPtr<IServiceProvider> parent_sp(parent_disp); + if (parent_sp == NULL) + return E_NOINTERFACE; + + CComPtr<IWebBrowser2> parent; + hr = parent_sp->QueryService(SID_SWebBrowserApp, + IID_IWebBrowser2, + reinterpret_cast<void**>(&parent)); + if (FAILED(hr)) + return hr; + + DCHECK(parent != NULL); + // IE seems to define the top-level browser as its own parent, + // hence this check and error return. + if (parent == browser) + return E_FAIL; + + LOG(INFO) << "Child: " << std::hex << browser << " -> Parent: " << + std::hex << parent.p; + + *parent_browser = parent.Detach(); + return S_OK; +} + +HRESULT BrowserHelperObject::GetBrokerRegistrar( + ICeeeBrokerRegistrar** broker) { + // This is a singleton and will not create a new one. + return ::CoCreateInstance(CLSID_CeeeBroker, NULL, CLSCTX_ALL, + IID_ICeeeBrokerRegistrar, + reinterpret_cast<void**>(broker)); +} + +HRESULT BrowserHelperObject::CreateExecutor(IUnknown** executor) { + HRESULT hr = ::CoCreateInstance( + CLSID_CeeeExecutor, NULL, CLSCTX_INPROC_SERVER, + IID_IUnknown, reinterpret_cast<void**>(executor)); + if (SUCCEEDED(hr)) { + CComQIPtr<IObjectWithSite> executor_with_site(*executor); + DCHECK(executor_with_site != NULL) + << "Executor must implement IObjectWithSite."; + if (executor_with_site != NULL) { + CComPtr<IUnknown> bho_identity; + hr = QueryInterface(IID_IUnknown, + reinterpret_cast<void**>(&bho_identity)); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (SUCCEEDED(hr)) + executor_with_site->SetSite(bho_identity); + } + } + + return hr; +} + +WebProgressNotifier* BrowserHelperObject::CreateWebProgressNotifier() { + scoped_ptr<WebProgressNotifier> web_progress_notifier( + new WebProgressNotifier()); + HRESULT hr = web_progress_notifier->Initialize(this, tab_window_, + web_browser_); + + return SUCCEEDED(hr) ? web_progress_notifier.release() : NULL; +} + +HRESULT BrowserHelperObject::Initialize(IUnknown* site) { + TRACE_EVENT_INSTANT("ceee.bho.initialize", this, ""); + + ie7_or_later_ = ie_util::GetIeVersion() > ie_util::IEVERSION_IE6; + + HRESULT hr = InitializeChromeFrameHost(); + DCHECK(SUCCEEDED(hr)) << "InitializeChromeFrameHost failed " << + com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + // Initialize the extension port manager. + extension_port_manager_.Initialize(chrome_frame_host_); + + // Preemptively feed the broker with an executor in our thread. + hr = GetBrokerRegistrar(&broker_registrar_); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create broker, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Broker. " << com::LogHr(hr); + if (SUCCEEDED(hr)) { + DCHECK(executor_ == NULL); + hr = CreateExecutor(&executor_); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + if (SUCCEEDED(hr)) { + // TODO(mad@chromium.org): Implement the proper manual/secure + // registration. + hr = broker_registrar_->RegisterTabExecutor(::GetCurrentThreadId(), + executor_); + DCHECK(SUCCEEDED(hr)) << "Registering Executor. " << com::LogHr(hr); + } + } + + // We need the service provider for both the sink connections and + // to get a handle to the tab window. + CComQIPtr<IServiceProvider> service_provider(site); + DCHECK(service_provider); + if (service_provider == NULL) { + TearDown(); + return E_FAIL; + } + + hr = ConnectSinks(service_provider); + DCHECK(SUCCEEDED(hr)) << "ConnectSinks failed " << com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + hr = GetTabWindow(service_provider); + DCHECK(SUCCEEDED(hr)) << "GetTabWindow failed " << com::LogHr(hr); + if (FAILED(hr)) { + TearDown(); + return hr; + } + + // Patch IHttpNegotiate for user-agent and cookie functionality. + HttpNegotiatePatch::Initialize(); + + CheckToolBandVisibility(web_browser_); + + web_progress_notifier_.reset(CreateWebProgressNotifier()); + DCHECK(web_progress_notifier_ != NULL) + << "Failed to initialize WebProgressNotifier"; + if (web_progress_notifier_ == NULL) { + TearDown(); + return E_FAIL; + } + + return S_OK; +} + +HRESULT BrowserHelperObject::TearDown() { + TRACE_EVENT_INSTANT("ceee.bho.teardown", this, ""); + + if (web_progress_notifier_ != NULL) { + web_progress_notifier_->TearDown(); + web_progress_notifier_.reset(NULL); + } + + ToolBandVisibility::TearDown(); + + if (executor_ != NULL) { + CComQIPtr<IObjectWithSite> executor_with_site(executor_); + DCHECK(executor_with_site != NULL) + << "Executor must implement IObjectWithSite."; + if (executor_with_site != NULL) { + executor_with_site->SetSite(NULL); + } + } + + // Unregister our executor so that the broker don't have to rely + // on the thread dying to release it and confuse COM in trying to release it. + if (broker_registrar_ != NULL) { + DCHECK(executor_ != NULL); + // Manually disconnect executor_, + // so it doesn't get called while we unregister it below. + HRESULT hr = ::CoDisconnectObject(executor_, 0); + DCHECK(SUCCEEDED(hr)) << "Couldn't disconnect Executor. " << com::LogHr(hr); + + // TODO(mad@chromium.org): Implement the proper manual/secure + // unregistration. + hr = broker_registrar_->UnregisterExecutor(::GetCurrentThreadId()); + DCHECK(SUCCEEDED(hr)) << "Unregistering Executor. " << com::LogHr(hr); + } else { + DCHECK(executor_ == NULL); + } + executor_.Release(); + + if (web_browser_) + DispEventUnadvise(web_browser_, &DIID_DWebBrowserEvents2); + web_browser_.Release(); + + HttpNegotiatePatch::Uninitialize(); + + if (chrome_frame_host_) { + chrome_frame_host_->SetEventSink(NULL); + chrome_frame_host_->TearDown(); + } + chrome_frame_host_.Release(); + + return S_OK; +} + +HRESULT BrowserHelperObject::InitializeChromeFrameHost() { + HRESULT hr = CreateChromeFrameHost(); + DCHECK(SUCCEEDED(hr) && chrome_frame_host_ != NULL); + if (FAILED(hr) || chrome_frame_host_ == NULL) { + LOG(ERROR) << "Failed to get chrome frame host " << com::LogHr(hr); + return com::AlwaysError(hr); + } + + chrome_frame_host_->SetChromeProfileName( + ceee_module_util::GetBrokerProfileNameForIe()); + chrome_frame_host_->SetEventSink(this); + hr = chrome_frame_host_->StartChromeFrame(); + DCHECK(SUCCEEDED(hr)) << "Failed to start Chrome Frame." << com::LogHr(hr); + if (FAILED(hr)) { + chrome_frame_host_->SetEventSink(NULL); + + LOG(ERROR) << "Failed to start chrome frame " << com::LogHr(hr); + return hr; + } + + return hr; +} + +HRESULT BrowserHelperObject::OnCfReadyStateChanged(LONG state) { + // If EnsureTabId() returns false, the session_id isn't available. + // This means that the ExternalTab hasn't been created yet, which is certainly + // a bug. Calling this here will also ensure we call all the deferred calls if + // they haven't been called yet. + bool id_available = EnsureTabId(); + if (!id_available) { + NOTREACHED(); + return E_UNEXPECTED; + } + + if (state == READYSTATE_COMPLETE) { + extension_path_ = ceee_module_util::GetExtensionPath(); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_) && + ceee_module_util::NeedToInstallExtension()) { + LOG(INFO) << "Installing extension: \"" << extension_path_ << "\""; + chrome_frame_host_->InstallExtension(CComBSTR(extension_path_.c_str())); + } else { + // In the case where we don't have a CRX (or we don't need to install it), + // we must ask for the currently enabled extension before we can decide + // what we need to do. + chrome_frame_host_->GetEnabledExtensions(); + } + } + + return S_OK; +} + +HRESULT BrowserHelperObject::OnCfExtensionReady(BSTR path, int response) { + TRACE_EVENT_INSTANT("ceee.bho.oncfextensionready", this, ""); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath( + FilePath(extension_path_)); + } + + // Now list enabled extensions so that we can properly start it whether + // it's a CRX file or an exploded folder. + // + // Note that we do this even if Chrome says installation failed, + // as that is the error code it uses when we try to install an + // older version of the extension than it already has, which happens + // on overinstall when Chrome has already auto-updated. + // + // If it turns out no extension is installed, we will handle that + // error in the OnCfGetEnabledExtensionsComplete callback. + chrome_frame_host_->GetEnabledExtensions(); + return S_OK; +} + +void BrowserHelperObject::StartExtension(const wchar_t* base_dir) { + chrome_frame_host_->SetUrl(CComBSTR(chrome::kAboutBlankURL)); + + LoadManifestFile(base_dir); + + // There is a race between launching Chrome to get the directory of + // the extension, and the first page this BHO is attached to being loaded. + // If we hadn't loaded the manifest file when injection of scripts and + // CSS should have been done for that page, then do it now as it is the + // earliest opportunity. + BrowserHandlerMap::const_iterator it = browsers_.begin(); + for (; it != browsers_.end(); ++it) { + DCHECK(it->second.m_T != NULL); + it->second.m_T->RedoDoneInjections(); + } + + // Now we should know the extension id and can pass it to the executor. + if (extension_id_.empty()) { + LOG(ERROR) << "Have no extension id after loading the extension."; + } else if (executor_ != NULL) { + CComPtr<ICeeeInfobarExecutor> infobar_executor; + HRESULT hr = executor_->QueryInterface(IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&infobar_executor)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ICeeeInfobarExecutor interface " << + com::LogHr(hr); + if (SUCCEEDED(hr)) { + infobar_executor->SetExtensionId(CComBSTR(extension_id_.c_str())); + } + } +} + +HRESULT BrowserHelperObject::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* base_dirs) { + CComSafeArray<BSTR> directories; + directories.Attach(base_dirs); // MUST DETACH BEFORE RETURNING + + // TODO(joi@chromium.org) Deal with multiple extensions. + if (directories.GetCount() > 0) { + // If our extension_path is not a CRX, it MUST be the same as the installed + // extension path which would be an exploded extension. + // If you get this DCHECK, you may have changed your registry settings to + // debug with an exploded extension, but you didn't uninstall the previous + // extension, either via the Chrome UI or by simply wiping out your + // profile folder. + DCHECK(ceee_module_util::IsCrxOrEmpty(extension_path_) || + extension_path_ == std::wstring(directories.GetAt(0))); + StartExtension(directories.GetAt(0)); + } else if (!ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // We have an extension path that isn't a CRX and we don't have any + // enabled extension, so we must load the exploded extension from this + // given path. WE MUST DO THIS BEFORE THE NEXT ELSE IF because it assumes + // a CRX file. + chrome_frame_host_->LoadExtension(CComBSTR(extension_path_.c_str())); + } else if (!already_tried_installing_ && !extension_path_.empty()) { + // We attempt to install the .crx file from the CEEE folder; in the + // default case this will happen only once after installation. + already_tried_installing_ = true; + chrome_frame_host_->InstallExtension(CComBSTR(extension_path_.c_str())); + } + + // If no extension is installed, we do nothing. The toolband handles + // this error and will show an explanatory message to the user. + directories.Detach(); + return S_OK; +} + +HRESULT BrowserHelperObject::OnCfGetExtensionApisToAutomate( + BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +HRESULT BrowserHelperObject::OnCfChannelError() { + return S_FALSE; +} + +bool BrowserHelperObject::EnsureTabId() { + if (tab_id_ != kInvalidChromeSessionId) { + return true; + } + + HRESULT hr = chrome_frame_host_->GetSessionId(&tab_id_); + DCHECK(SUCCEEDED(hr)); + if (hr == S_FALSE) { + // The server returned false, the session_id isn't available yet + return false; + } + + // At this point if tab_id_ is still invalid we have a problem. + if (tab_id_ == kInvalidChromeSessionId) { + // TODO(hansl@google.com): uncomment the following code when the CF change + // has landed. + //// Something really bad happened. + //NOTREACHED(); + //return false; + tab_id_ = reinterpret_cast<int>(tab_window_); + } + + CeeeWindowHandle handle = reinterpret_cast<CeeeWindowHandle>(tab_window_); + hr = broker_registrar_->SetTabIdForHandle(tab_id_, handle); + if (FAILED(hr)) { + DCHECK(SUCCEEDED(hr)) << "An error occured when setting the tab_id: " << + com::LogHr(hr); + tab_id_ = kInvalidChromeSessionId; + return false; + } + VLOG(2) << "TabId(" << tab_id_ << ") set for Handle(" << handle << ")"; + + // Call back all the methods we deferred. In order, please. + while (!deferred_tab_id_call_.empty()) { + // We pop here so that if an error happens in the call we don't recall the + // faulty method. This has the side-effect of losing events. + DeferredCallListType::value_type call = deferred_tab_id_call_.front(); + deferred_tab_id_call_.pop_front(); + call->Run(); + delete call; + } + + return true; +} + +// Fetch and remembers the tab window we are attached to. +HRESULT BrowserHelperObject::GetTabWindow(IServiceProvider* service_provider) { + CComQIPtr<IOleWindow> ole_window; + HRESULT hr = service_provider->QueryService( + SID_SShellBrowser, IID_IOleWindow, reinterpret_cast<void**>(&ole_window)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ole window " << com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + + hr = ole_window->GetWindow(&tab_window_); + DCHECK(SUCCEEDED(hr)) << "Failed to get window from ole window " << + com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + DCHECK(tab_window_ != NULL); + + // Initialize our executor to the right HWND + if (executor_ == NULL) + return E_POINTER; + CComPtr<ICeeeTabExecutor> tab_executor; + hr = executor_->QueryInterface(IID_ICeeeTabExecutor, + reinterpret_cast<void**>(&tab_executor)); + if (SUCCEEDED(hr)) { + CeeeWindowHandle handle = reinterpret_cast<CeeeWindowHandle>(tab_window_); + hr = tab_executor->Initialize(handle); + } + return hr; +} + +// Connect for notifications. +HRESULT BrowserHelperObject::ConnectSinks(IServiceProvider* service_provider) { + HRESULT hr = service_provider->QueryService( + SID_SWebBrowserApp, IID_IWebBrowser2, + reinterpret_cast<void**>(&web_browser_)); + DCHECK(SUCCEEDED(hr)) << "Failed to get web browser " << com::LogHr(hr); + if (FAILED(hr)) { + return hr; + } + + // Start sinking events from the web browser object. + hr = DispEventAdvise(web_browser_, &DIID_DWebBrowserEvents2); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to set event sink " << com::LogHr(hr); + return hr; + } + return hr; +} + +HRESULT BrowserHelperObject::CreateChromeFrameHost() { + return ChromeFrameHost::CreateInitializedIID(IID_IChromeFrameHost, + &chrome_frame_host_); +} + +HRESULT BrowserHelperObject::FireOnCreatedEvent(BSTR url) { + DCHECK(url != NULL); + DCHECK(tab_window_ != NULL); + DCHECK(!fired_on_created_event_); + HRESULT hr = tab_events_funnel().OnCreated(tab_window_, url, + lower_bound_ready_state_ == READYSTATE_COMPLETE); + fired_on_created_event_ = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the tab.onCreated event " << + com::LogHr(hr); + return hr; +} + +HRESULT BrowserHelperObject::FireOnRemovedEvent() { + DCHECK(tab_window_ != NULL); + HRESULT hr = + tab_events_funnel().OnRemoved(tab_window_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the tab.onRemoved event " << + com::LogHr(hr); + return hr; +} + +HRESULT BrowserHelperObject::FireOnUnmappedEvent() { + DCHECK(tab_window_ != NULL); + DCHECK(tab_id_ != kInvalidChromeSessionId); + HRESULT hr = tab_events_funnel().OnTabUnmapped(tab_window_, tab_id_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the ceee.onTabUnmapped event " << + com::LogHr(hr); + return hr; +} + +void BrowserHelperObject::CloseAll(IContentScriptNativeApi* instance) { + extension_port_manager_.CloseAll(instance); +} + +HRESULT BrowserHelperObject::OpenChannelToExtension( + IContentScriptNativeApi* instance, const std::string& extension, + const std::string& channel_name, int cookie) { + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OpenChannelToExtension, + instance, extension, channel_name, cookie)); + return S_OK; + } + + scoped_ptr<DictionaryValue> tab_info(new DictionaryValue()); + + DCHECK(tab_id_ != kInvalidChromeSessionId); + tab_info->SetInteger(keys::kIdKey, tab_id_); + + return extension_port_manager_.OpenChannelToExtension(instance, + extension, + channel_name, + tab_info.release(), + cookie); +} + +HRESULT BrowserHelperObject::PostMessage(int port_id, + const std::string& message) { + return extension_port_manager_.PostMessage(port_id, message); +} + +HRESULT BrowserHelperObject::OnCfPrivateMessage(BSTR msg, + BSTR origin, + BSTR target) { + // OnPortMessage uses tab_id_, so we need to check here. + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnCfPrivateMessage, + CComBSTR(msg), origin, target)); + return S_OK; + } + + const wchar_t* start = com::ToString(target); + const wchar_t* end = start + ::SysStringLen(target); + if (LowerCaseEqualsASCII(start, end, ext::kAutomationPortRequestTarget) || + LowerCaseEqualsASCII(start, end, ext::kAutomationPortResponseTarget)) { + extension_port_manager_.OnPortMessage(msg); + return S_OK; + } + + // TODO(siggi@chromium.org): What to do here? + LOG(ERROR) << "Unexpected message: '" << msg << "' to invalid target: " + << target; + return E_UNEXPECTED; +} + + +STDMETHODIMP_(void) BrowserHelperObject::OnBeforeNavigate2( + IDispatch* webbrowser_disp, VARIANT* url, VARIANT* /*flags*/, + VARIANT* /*target_frame_name*/, VARIANT* /*post_data*/, + VARIANT* /*headers*/, VARIANT_BOOL* /*cancel*/) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnBeforeNavigate2 got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + VARIANT* null_param = NULL; + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnBeforeNavigate2, + webbrowser_disp, url, null_param, null_param, null_param, null_param, + reinterpret_cast<VARIANT_BOOL*>(NULL))); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnBeforeNavigate2 failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnBeforeNavigate2 url VT=" << V_VT(url); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnBeforeNavigate(webbrowser, url->bstrVal); + } + + // Notify the infobar executor on the event but only for the main browser. + // TODO(vadimb@google.com): Refactor this so that the executor can just + // register self as WebBrowserEventsSource::Sink. Right now this is + // problematic because the executor is COM object. + CComPtr<IWebBrowser2> parent_browser; + if (executor_ != NULL && web_browser_ != NULL && + web_browser_.IsEqualObject(webbrowser_disp)) { + CComPtr<ICeeeInfobarExecutor> infobar_executor; + HRESULT hr = executor_->QueryInterface(IID_ICeeeInfobarExecutor, + reinterpret_cast<void**>(&infobar_executor)); + DCHECK(SUCCEEDED(hr)) << "Failed to get ICeeeInfobarExecutor interface " << + com::LogHr(hr); + if (SUCCEEDED(hr)) { + infobar_executor->OnTopFrameBeforeNavigate(CComBSTR(url->bstrVal)); + } + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnDocumentComplete( + IDispatch* webbrowser_disp, VARIANT* url) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnDocumentComplete got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnDocumentComplete, webbrowser_disp, url)); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnDocumentComplete failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnDocumentComplete url VT=" << V_VT(url); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnDocumentComplete(webbrowser, url->bstrVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNavigateComplete2( + IDispatch* webbrowser_disp, VARIANT* url) { + if (webbrowser_disp == NULL || url == NULL) { + LOG(ERROR) << "OnNavigateComplete2 got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnNavigateComplete2, webbrowser_disp, url)); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnNavigateComplete2 failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnNavigateComplete2 url VT=" << V_VT(url); + return; + } + + HandleNavigateComplete(webbrowser, url->bstrVal); + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNavigateComplete(webbrowser, url->bstrVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNavigateError( + IDispatch* webbrowser_disp, VARIANT* url, VARIANT* /*target_frame_name*/, + VARIANT* status_code, VARIANT_BOOL* /*cancel*/) { + if (webbrowser_disp == NULL || url == NULL || status_code == NULL) { + LOG(ERROR) << "OnNavigateError got invalid parameter(s)"; + return; + } + + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + VARIANT* null_param = NULL; + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::OnNavigateError, + webbrowser_disp, url, null_param, status_code, + reinterpret_cast<VARIANT_BOOL*>(NULL))); + return; + } + + CComPtr<IWebBrowser2> webbrowser; + HRESULT hr = webbrowser_disp->QueryInterface(&webbrowser); + if (FAILED(hr)) { + LOG(ERROR) << "OnNavigateError failed to QI " << com::LogHr(hr); + return; + } + + if (V_VT(url) != VT_BSTR) { + LOG(ERROR) << "OnNavigateError url VT=" << V_VT(url); + return; + } + + if (V_VT(status_code) != VT_I4) { + LOG(ERROR) << "OnNavigateError status_code VT=" << V_VT(status_code); + return; + } + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNavigateError(webbrowser, url->bstrVal, status_code->lVal); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNewWindow2( + IDispatch** /*webbrowser_disp*/, VARIANT_BOOL* /*cancel*/) { + // When a new window/tab is created, IE7 or later version of IE will also + // fire NewWindow3 event, which extends NewWindow2 with additional + // information. As a result, we ignore NewWindow2 event in that case. + if (ie7_or_later_) + return; + + CComBSTR url_context(L""); + CComBSTR url(L""); + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNewWindow(url_context, url); + } +} + +STDMETHODIMP_(void) BrowserHelperObject::OnNewWindow3( + IDispatch** /*webbrowser_disp*/, VARIANT_BOOL* /*cancel*/, DWORD /*flags*/, + BSTR url_context, BSTR url) { + // IE6 uses NewWindow2 event. + if (!ie7_or_later_) + return; + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); + iter != sinks_.end(); ++iter) { + (*iter)->OnNewWindow(url_context, url); + } +} + +bool BrowserHelperObject::BrowserContainsChromeFrame(IWebBrowser2* browser) { + CComPtr<IDispatch> document_disp; + HRESULT hr = browser->get_Document(&document_disp); + if (FAILED(hr)) { + // This should never happen. + NOTREACHED() << "IWebBrowser2::get_Document failed " << com::LogHr(hr); + return false; + } + + CComQIPtr<IPersist> document_persist(document_disp); + if (document_persist != NULL) { + CLSID clsid = {}; + hr = document_persist->GetClassID(&clsid); + if (SUCCEEDED(hr) && clsid == CLSID_ChromeFrame) { + return true; + } + } + return false; +} + +HRESULT BrowserHelperObject::AttachBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler) { + // We're not attached yet, figure out whether we're attaching + // to the top-level browser or a frame, and looukup the parentage + // in the latter case. + CComPtr<IWebBrowser2> parent_browser; + HRESULT hr = S_OK; + bool is_top_frame = web_browser_.IsEqualObject(webbrowser); + if (!is_top_frame) { + // Not the top-level browser, so find the parent. If this fails, + // we assume webbrowser is orphaned, and will not attach to it. + // This can happen when a FRAME/IFRAME is removed from the DOM tree. + hr = GetParentBrowser(webbrowser, &parent_browser); + if (FAILED(hr)) + LOG(WARNING) << "Failed to get parent browser " << com::LogHr(hr); + } + + // Attempt to attach to the web browser. + if (SUCCEEDED(hr)) { + hr = CreateFrameEventHandler(webbrowser, parent_browser, handler); + bool document_is_mshtml = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr) || hr == E_DOCUMENT_NOT_MSHTML) << + "Unexpected error creating a frame handler " << com::LogHr(hr); + + if (is_top_frame) { + // Check if it is a chrome frame. + bool is_chrome_frame = BrowserContainsChromeFrame(webbrowser); + + if (is_chrome_frame) { + fired_on_created_event_ = true; + full_tab_chrome_frame_ = true; + // Send a tabs.onRemoved event to make the extension believe the tab is + // dead. + hr = FireOnRemovedEvent(); + DCHECK(SUCCEEDED(hr)); + } else if (document_is_mshtml) { + // We know it's MSHTML. We check if the last page was chrome frame. + if (full_tab_chrome_frame_) { + // This will send a tabs.onCreated event later to balance the + // onRemoved event above. + fired_on_created_event_ = false; + full_tab_chrome_frame_ = false; + } + } + } + } + + return hr; +} + +void BrowserHelperObject::HandleNavigateComplete(IWebBrowser2* webbrowser, + BSTR url) { + // If the top-level document or a sub-frame is navigated, we'll already + // be attached to the browser in question, so don't reattach. + HRESULT hr = S_OK; + CComPtr<IFrameEventHandler> handler; + if (FAILED(GetBrowserHandler(webbrowser, &handler))) { + hr = AttachBrowserHandler(webbrowser, &handler); + + DCHECK(SUCCEEDED(hr)) << "Failed to attach ourselves to the web browser " << + com::LogHr(hr); + } + + bool is_hash_change = false; + if (handler) { + // Find out if this is a hash change. + CComBSTR prev_url; + handler->GetUrl(&prev_url); + is_hash_change = IsHashChange(url, prev_url); + + // Notify the handler of the current URL. + hr = handler->SetUrl(url); + DCHECK(SUCCEEDED(hr)) << "Failed setting the handler URL " << + com::LogHr(hr); + } + + // SetUrl returns S_FALSE if the URL didn't change. + if (SUCCEEDED(hr) && hr != S_FALSE) { + // And we should only fire events for URL changes on the top frame. + if (web_browser_.IsEqualObject(webbrowser)) { + // We can assume that we are NOT in a complete state when we get + // a navigation complete. + lower_bound_ready_state_ = READYSTATE_UNINITIALIZED; + + // At this point, we should have all the tab windows created, + // including the proper lower bound ready state set just before, + // so setup the tab info if it has not been set yet. + if (!fired_on_created_event_) { + hr = FireOnCreatedEvent(url); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab created event " << + com::LogHr(hr); + } + + // The onUpdate event usually gets fired after the onCreated, + // which is fired from FireOnCreatedEvent above. + DCHECK(tab_window_ != NULL); + hr = tab_events_funnel().OnUpdated(tab_window_, url, + lower_bound_ready_state_); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab updated event " << + com::LogHr(hr); + + // If this is a hash change, we manually fire the OnUpdated for the + // complete ready state as we don't receive ready state notifications + // for hash changes. + if (is_hash_change) { + hr = tab_events_funnel().OnUpdated(tab_window_, url, + READYSTATE_COMPLETE); + DCHECK(SUCCEEDED(hr)) << "Failed to fire tab updated event " << + com::LogHr(hr); + } + } + } +} + +HRESULT BrowserHelperObject::CreateFrameEventHandler( + IWebBrowser2* browser, IWebBrowser2* parent_browser, + IFrameEventHandler** handler) { + return FrameEventHandler::CreateInitializedIID( + browser, parent_browser, this, IID_IFrameEventHandler, handler); +} + +HRESULT BrowserHelperObject::AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + DCHECK(browser); + DCHECK(handler); + // Get the identity unknown of the browser. + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + std::pair<BrowserHandlerMap::iterator, bool> inserted = + browsers_.insert(std::make_pair(browser_identity, handler)); + // We shouldn't have a previous entry for any inserted browser. + // map::insert().second is true iff an item was inserted. + DCHECK(inserted.second); + + // Now try and find a parent event handler for this browser. + // If we have a parent browser, locate its handler + // and notify it of the association. + if (parent_browser) { + CComPtr<IFrameEventHandler> parent_handler; + hr = GetBrowserHandler(parent_browser, &parent_handler); + + // Notify the parent of its new underling. + if (parent_handler != NULL) { + hr = parent_handler->AddSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "AddSubHandler()" << com::LogHr(hr); + } else { + LOG(INFO) << "Received an orphan handler: " << std::hex << handler << + com::LogHr(hr); + // Lets see if we can find an ancestor up the chain of parent browsers + // that we could connect to our existing hierarchy of handlers. + CComQIPtr<IWebBrowser2> grand_parent_browser; + hr = GetParentBrowser(parent_browser, &grand_parent_browser); + if (FAILED(hr)) + LOG(WARNING) << "Failed to get parent browser " << com::LogHr(hr); + bool valid_grand_parent = (grand_parent_browser != NULL && + !grand_parent_browser.IsEqualObject(parent_browser)); + DCHECK(valid_grand_parent) << + "Orphan handler without a valid grand parent!"; + LOG_IF(ERROR, !valid_grand_parent) << "Orphan handler: " << std::hex << + handler << ", with parent browser: " << std::hex << parent_browser; + if (grand_parent_browser != NULL && + !grand_parent_browser.IsEqualObject(parent_browser)) { + DCHECK(!web_browser_.IsEqualObject(parent_browser)); + // We have a grand parent IWebBrowser2, so create a handler for the + // parent we were given that doesn't have a handler already. + CComBSTR parent_browser_url; + parent_browser->get_LocationURL(&parent_browser_url); + DLOG(INFO) << "Creating handler for parent browser: " << std::hex << + parent_browser << ", at URL: " << parent_browser_url; + hr = CreateFrameEventHandler(parent_browser, grand_parent_browser, + &parent_handler); + // If we have a handler for the child, we must be able to create one for + // the parent... And CreateFrameEventHandler should have attached it + // to the parent by calling us again via IFrameEventHandler::Init... + DCHECK(SUCCEEDED(hr) && parent_handler != NULL) << com::LogHr(hr); + if (FAILED(hr) || parent_handler == NULL) + return hr; + + // When we create a handler, we must set its URL. + hr = parent_handler->SetUrl(parent_browser_url); + DCHECK(SUCCEEDED(hr)) << "Handler::SetUrl()" << com::LogHr(hr); + if (FAILED(hr)) + return hr; + + // And now that we have a fully created parent handler, we can add + // the handler that looked orphan, to its newly created parent. + hr = parent_handler->AddSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "AddSubHandler()" << com::LogHr(hr); + } else { + // No grand parent for the orphan handler? + return E_UNEXPECTED; + } + } + } + + if (inserted.second) + return S_OK; + else + return E_FAIL; +} + +HRESULT BrowserHelperObject::DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown of the browser. + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << "QueryInterface for IUnknown failed!!!" << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + // If we have a parent browser, locate its handler + // and notify it of the disassociation. + if (parent_browser) { + CComPtr<IFrameEventHandler> parent_handler; + + hr = GetBrowserHandler(parent_browser, &parent_handler); + LOG_IF(WARNING, FAILED(hr) || parent_handler == NULL) << "No Parent " << + "Handler" << com::LogHr(hr); + + // Notify the parent of its underling removal. + if (parent_handler != NULL) { + hr = parent_handler->RemoveSubHandler(handler); + DCHECK(SUCCEEDED(hr)) << "RemoveSubHandler" << com::LogHr(hr); + } + } + + BrowserHandlerMap::iterator it = browsers_.find(browser_identity); + DCHECK(it != browsers_.end()); + if (it == browsers_.end()) + return E_FAIL; + + browsers_.erase(it); + return S_OK; +} + +HRESULT BrowserHelperObject::GetTopLevelBrowser(IWebBrowser2** browser) { + DCHECK(browser != NULL); + return web_browser_.CopyTo(browser); +} + +HRESULT BrowserHelperObject::OnReadyStateChanged(READYSTATE ready_state) { + // We make sure to always keep the lowest ready state of all the handlers + // and only fire an event when we get from not completed to completed or + // vice versa. + READYSTATE new_lowest_ready_state = READYSTATE_COMPLETE; + BrowserHandlerMap::const_iterator it = browsers_.begin(); + for (; it != browsers_.end(); ++it) { + DCHECK(it->second.m_T != NULL); + READYSTATE this_ready_state = it->second.m_T->GetReadyState(); + if (this_ready_state < new_lowest_ready_state) { + new_lowest_ready_state = this_ready_state; + } + } + + return HandleReadyStateChanged(lower_bound_ready_state_, + new_lowest_ready_state); +} + +HRESULT BrowserHelperObject::GetReadyState(READYSTATE* ready_state) { + DCHECK(ready_state != NULL); + if (ready_state != NULL) { + *ready_state = lower_bound_ready_state_; + return S_OK; + } else { + return E_POINTER; + } +} + +HRESULT BrowserHelperObject::GetExtensionId(std::wstring* extension_id) { + *extension_id = extension_id_; + return S_OK; +} + +HRESULT BrowserHelperObject::GetExtensionPath(std::wstring* extension_path) { + *extension_path = extension_base_dir_; + return S_OK; +} + +HRESULT BrowserHelperObject::GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) { + GetUnknown()->AddRef(); + *messaging_provider = this; + return S_OK; +} + +HRESULT BrowserHelperObject::InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) { + // TODO(hansl@google.com): separate this method into an event and an Impl. + if (EnsureTabId() == false) { + deferred_tab_id_call_.push_back(NewRunnableMethod( + this, &BrowserHelperObject::InsertCode, + code, file, all_frames, type)); + return S_OK; + } + + // If all_frames is false, we execute only in the top level frame. Otherwise + // we do the top level frame as well as all the inner frames. + if (all_frames) { + // Make a copy of the browser handler map since it could potentially be + // modified in the loop. + BrowserHandlerMap browsers_copy(browsers_.begin(), browsers_.end()); + BrowserHandlerMap::const_iterator it = browsers_copy.begin(); + for (; it != browsers_copy.end(); ++it) { + DCHECK(it->second.m_T != NULL); + if (it->second.m_T != NULL) { + HRESULT hr = it->second.m_T->InsertCode(code, file, type); + DCHECK(SUCCEEDED(hr)) << "IFrameEventHandler::InsertCode()" << + com::LogHr(hr); + } + } + } else if (web_browser_ != NULL) { + CComPtr<IFrameEventHandler> handler; + HRESULT hr = GetBrowserHandler(web_browser_, &handler); + DCHECK(SUCCEEDED(hr) && handler != NULL) << com::LogHr(hr); + + if (handler != NULL) { + hr = handler->InsertCode(code, file, type); + // TODO(joi@chromium.org) We don't DCHECK for now, because Chrome may have + // multiple extensions loaded whereas CEEE only knows about a single + // extension. Clean this up once we support multiple extensions. + } + } + + return S_OK; +} + +HRESULT BrowserHelperObject::HandleReadyStateChanged(READYSTATE old_state, + READYSTATE new_state) { + if (old_state == new_state) + return S_OK; + + // Remember the new lowest ready state as our current one. + lower_bound_ready_state_ = new_state; + + if (old_state == READYSTATE_COMPLETE || new_state == READYSTATE_COMPLETE) { + // The new ready state got us to or away from complete, so fire the event. + DCHECK(tab_window_ != NULL); + return tab_events_funnel().OnUpdated(tab_window_, NULL, new_state); + } + return S_OK; +} + +HRESULT BrowserHelperObject::GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) { + return librarian_.GetMatchingUserScriptsCssContent(url, require_all_frames, + css_content); +} + +HRESULT BrowserHelperObject::GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list) { + return librarian_.GetMatchingUserScriptsJsContent(url, location, + require_all_frames, + js_file_list); +} + +HRESULT BrowserHelperObject::GetBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler) { + DCHECK(webbrowser != NULL); + DCHECK(handler != NULL && *handler == NULL); + CComPtr<IUnknown> browser_identity; + HRESULT hr = webbrowser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)) << com::LogHr(hr); + + BrowserHandlerMap::iterator found(browsers_.find(browser_identity)); + if (found != browsers_.end()) { + found->second.m_T.CopyTo(handler); + return S_OK; + } + + return E_FAIL; +} + +void BrowserHelperObject::LoadManifestFile(const std::wstring& base_dir) { + // TODO(siggi@chromium.org): Generalize this to the possibility of + // multiple extensions. + FilePath extension_path(base_dir); + if (extension_path.empty()) { + // expected case if no extensions registered/found + return; + } + + extension_base_dir_ = base_dir; + + ExtensionManifest manifest; + if (SUCCEEDED(manifest.ReadManifestFile(extension_path, true))) { + extension_id_ = UTF8ToWide(manifest.extension_id()); + librarian_.AddUserScripts(manifest.content_scripts()); + } +} + +void BrowserHelperObject::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +LRESULT BrowserHelperObject::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + return 0; +} + +bool BrowserHelperObject::IsHashChange(BSTR url1, BSTR url2) { + if (::SysStringLen(url1) == 0 || ::SysStringLen(url2) == 0) { + return false; + } + + GURL gurl1(url1); + GURL gurl2(url2); + + // The entire URL should be the same except for the hash. + // We could compare gurl1.ref() to gurl2.ref() on the last step, but this + // doesn't differentiate between URLs like http://a/ and http://a/#. + return gurl1.scheme() == gurl2.scheme() && + gurl1.username() == gurl2.username() && + gurl1.password() == gurl2.password() && + gurl1.host() == gurl2.host() && + gurl1.port() == gurl2.port() && + gurl1.path() == gurl2.path() && + gurl1.query() == gurl2.query() && + gurl1 != gurl2; +} + +void BrowserHelperObject::RegisterSink(Sink* sink) { + DCHECK(thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + // Although the registration logic guarantees that the same sink won't be + // registered twice, we prefer to use std::vector rather than std::set. + // Using std::vector, we could keep "first-registered-first-notified". + // + // With std::set, however, the notifying order can only be decided at + // run-time. Moreover, in different runs, the notifying order may be + // different, since the value of the pointer to each sink is changing. The may + // cause unstable behavior and hard-to-debug issues. + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter == sinks_.end()) + sinks_.push_back(sink); +} + +void BrowserHelperObject::UnregisterSink(Sink* sink) { + DCHECK(thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter != sinks_.end()) + sinks_.erase(iter); +} diff --git a/ceee/ie/plugin/bho/browser_helper_object.h b/ceee/ie/plugin/bho/browser_helper_object.h new file mode 100644 index 0000000..bf4afb9 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.h @@ -0,0 +1,345 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#ifndef CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ +#define CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <mshtml.h> // Needed for exdisp.h +#include <exdisp.h> +#include <exdispid.h> +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/scoped_ptr.h" +#include "base/task.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "ceee/ie/common/chrome_frame_host.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/bho/extension_port_manager.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/web_progress_notifier.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/toolband/resource.h" +#include "broker_lib.h" // NOLINT +#include "toolband.h" // NOLINT + +// Implementation of an IE browser helper object. +class ATL_NO_VTABLE BrowserHelperObject + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<BrowserHelperObject, &CLSID_BrowserHelperObject>, + public IObjectWithSiteImpl<BrowserHelperObject>, + public IDispEventSimpleImpl<0, + BrowserHelperObject, + &DIID_DWebBrowserEvents2>, + public IFrameEventHandlerHost, + public IExtensionPortMessagingProvider, + public IChromeFrameHostEvents, + public ToolBandVisibility, + public WebBrowserEventsSource { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_BROWSERHELPEROBJECT) + DECLARE_NOT_AGGREGATABLE(BrowserHelperObject) + + BEGIN_COM_MAP(BrowserHelperObject) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandlerHost, IFrameEventHandlerHost) + END_COM_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + BEGIN_SINK_MAP(BrowserHelperObject) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2, + OnBeforeNavigate2, + &handler_type_idispatch_5variantptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, + OnDocumentComplete, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, + OnNavigateComplete2, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATEERROR, + OnNavigateError, + &handler_type_idispatch_3variantptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NEWWINDOW2, + OnNewWindow2, &handler_type_idispatchptr_boolptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NEWWINDOW3, + OnNewWindow3, + &handler_type_idispatchptr_boolptr_dword_2bstr_) + END_SINK_MAP() + + BrowserHelperObject(); + ~BrowserHelperObject(); + + HRESULT FinalConstruct(); + void FinalRelease(); + + // @name IObjectWithSite override. + STDMETHOD(SetSite)(IUnknown* site); + + // @name IExtensionPortMessagingProvider implementation + // @{ + virtual void CloseAll(IContentScriptNativeApi* instance); + virtual HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie); + virtual HRESULT PostMessage(int port_id, const std::string& message); + // @} + + // @name IChromeFrameHostEvents implementation + virtual HRESULT OnCfReadyStateChanged(LONG state); + virtual HRESULT OnCfPrivateMessage(BSTR msg, BSTR origin, BSTR target); + virtual HRESULT OnCfExtensionReady(BSTR path, int response); + virtual HRESULT OnCfGetEnabledExtensionsComplete( + SAFEARRAY* tab_delimited_paths); + virtual HRESULT OnCfGetExtensionApisToAutomate(BSTR* functions_enabled); + virtual HRESULT OnCfChannelError(); + + // @name WebBrowser event handlers + // @{ + STDMETHOD_(void, OnBeforeNavigate2)(IDispatch* webbrowser_disp, VARIANT* url, + VARIANT* flags, + VARIANT* target_frame_name, + VARIANT* post_data, VARIANT* headers, + VARIANT_BOOL* cancel); + STDMETHOD_(void, OnDocumentComplete)(IDispatch* webbrowser_disp, + VARIANT* url); + STDMETHOD_(void, OnNavigateComplete2)(IDispatch* webbrowser_disp, + VARIANT* url); + STDMETHOD_(void, OnNavigateError)(IDispatch* webbrowser_disp, VARIANT* url, + VARIANT* target_frame_name, + VARIANT* status_code, VARIANT_BOOL* cancel); + STDMETHOD_(void, OnNewWindow2)(IDispatch** webbrowser_disp, + VARIANT_BOOL* cancel); + STDMETHOD_(void, OnNewWindow3)(IDispatch** webbrowser_disp, + VARIANT_BOOL* cancel, DWORD flags, + BSTR url_context, BSTR url); + // @} + + // @name IFrameEventHandlerHost + // @{ + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler); + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler); + virtual HRESULT GetTopLevelBrowser(IWebBrowser2** browser); + virtual HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content); + virtual HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list); + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state); + virtual HRESULT GetReadyState(READYSTATE* ready_state); + virtual HRESULT GetExtensionId(std::wstring* extension_id); + virtual HRESULT GetExtensionPath(std::wstring* extension_path); + virtual HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider); + virtual HRESULT InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type); + // @} + + // @name WebBrowserEventsSource + // @{ + // Both RegisterSink and UnregisterSink are supposed to be called from the + // main browser thread of the tab to which this BHO is attached. Sinks will + // receive notifications on the same thread. + virtual void RegisterSink(Sink* sink); + virtual void UnregisterSink(Sink* sink); + // @} + + protected: + // Finds the handler attached to webbrowser. + // @returns S_OK if handler is found. + HRESULT GetBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler); + + virtual void HandleNavigateComplete(IWebBrowser2* webbrowser, BSTR url); + virtual HRESULT HandleReadyStateChanged(READYSTATE old_state, + READYSTATE new_state); + + // Unit testing seems to create the frame event handler. + virtual HRESULT CreateFrameEventHandler(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler); + + // Unit testing seems to get the parent of a browser. + virtual HRESULT GetParentBrowser(IWebBrowser2* browser, + IWebBrowser2** parent_browser); + + // Unit testing seems to create the broker registrar. + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker); + + // Unit testing seems to create an executor. + virtual HRESULT CreateExecutor(IUnknown** executor); + + // Unit testing seems to create a WebProgressNotifier instance. + virtual WebProgressNotifier* CreateWebProgressNotifier(); + + // Initializes the BHO to the given site. + // Called from SetSite. + HRESULT Initialize(IUnknown* site); + + // Tears down an initialized bho. + // Called from SetSite. + HRESULT TearDown(); + + // Creates and initializes the chrome frame host. + HRESULT InitializeChromeFrameHost(); + + // Fetch and remembers the tab window we are attached to. + // Virtual for testing purposes. + virtual HRESULT GetTabWindow(IServiceProvider* service_provider); + + // Connect for notifications. + HRESULT ConnectSinks(IServiceProvider* service_provider); + + // Isolate the creation of the host so we can overload it to mock + // the Chrome Frame Host in our tests. + virtual HRESULT CreateChromeFrameHost(); + + // Accessor so that we can mock it in unit tests. + virtual TabEventsFunnel& tab_events_funnel() { return tab_events_funnel_; } + + // Fires the tab.onCreated event via the tab event funnel. + virtual HRESULT FireOnCreatedEvent(BSTR url); + + // Fires the tab.onRemoved event via the tab event funnel. + virtual HRESULT FireOnRemovedEvent(); + + // Fires the private message to unmap a tab to its BHO. + virtual HRESULT FireOnUnmappedEvent(); + + // Loads our manifest and initialize our librarian. + virtual void LoadManifestFile(const std::wstring& base_dir); + + // Called when we know the base directory of our extension. + void StartExtension(const wchar_t* base_dir); + + // Our ToolBandVisibility window maintains a refcount on us for the duration + // of its lifetime. The self-reference is managed with these two methods. + virtual void OnFinalMessage(HWND window); + virtual LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + // Compares two URLs and returns whether they represent a hash change. + virtual bool IsHashChange(BSTR url1, BSTR url2); + + // Ensure that the tab ID is correct. On the first time it's set, it will + // call all deferred methods added to deferred_tab_id_call_. + // This method should be called by every method that send a message or use + // the tab event funnel, as they need the tab_id to be mapped. + // If this method returns false, the caller should defer itself using the + // deferred_tab_id_call_ list. + virtual bool EnsureTabId(); + + // Returns true if the browser interface passed in contains a full tab + // chrome frame. + virtual bool BrowserContainsChromeFrame(IWebBrowser2* browser); + + // Attach ourselves and the event handler to the browser, and launches the + // right events when going to and from a Full Tab Chrome Frame. + virtual HRESULT AttachBrowserHandler(IWebBrowser2* webbrowser, + IFrameEventHandler** handler); + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_5variantptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + static _ATL_FUNC_INFO handler_type_idispatch_3variantptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatchptr_boolptr_; + static _ATL_FUNC_INFO handler_type_idispatchptr_boolptr_dword_2bstr_; + + // The top-level web browser (window) we're attached to. NULL before SetSite. + CComPtr<IWebBrowser2> web_browser_; + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<IChromeFrameHost> chrome_frame_host_; + + // The Broker Registrar we use to un/register executors for our thread. + CComPtr<ICeeeBrokerRegistrar> broker_registrar_; + + // We keep a reference to the executor we registered so that we can + // manually disconnect it, so it doesn't get called while we unregister it. + CComPtr<IUnknown> executor_; + + // Maintains a map from browser (top-level and sub-browsers) to the + // attached FrameEventHandlers. + typedef std::map<CAdapt<CComPtr<IUnknown> >, + CAdapt<CComPtr<IFrameEventHandler> > > BrowserHandlerMap; + BrowserHandlerMap browsers_; + + // Initialized by LoadManifestFile() at + // OnCfGetEnabledExtensionsComplete-time. Valid from that point forward. + UserScriptsLibrarian librarian_; + + // Filesystem path to the .crx we will install (or have installed), or the + // empty string, or (if not ending in .crx) the path to an exploded extension + // directory to load (or which we have loaded). + std::wstring extension_path_; + + // The extension we're associated with. Set at + // OnCfGetEnabledExtensionsComplete-time. + // TODO(siggi@chromium.org): Generalize this to multiple extensions. + std::wstring extension_id_; + + // The base directory of the extension we're associated with. + // Set at OnCfGetEnabledExtensionsComplete time. + std::wstring extension_base_dir_; + + // Extension port messaging and management is delegated to this. + ExtensionPortManager extension_port_manager_; + + // Used to dispatch tab events back to Chrome. + TabEventsFunnel tab_events_funnel_; + + // Remember the tab window handle so that we can use it. + HWND tab_window_; + + // Remember the tab id so we can pass it to the underlying Chrome. + int tab_id_; + + // Makes sure we fire the onCreated event only once. + bool fired_on_created_event_; + + // True if we found no enabled extensions and tried to install one. + bool already_tried_installing_; + + // The last known ready state lower bound, so that we decide when to fire a + // tabs.onUpdated event, which only when we go from all frames completed to + // at least one of them not completed, and vice versa (from incomplete to + // fully completely completed :-)... + READYSTATE lower_bound_ready_state_; + + // Consumers of WebBrowser events. + std::vector<Sink*> sinks_; + + // Used to generate and fire Web progress notifications. + scoped_ptr<WebProgressNotifier> web_progress_notifier_; + + // True if the user is running IE7 or later. + bool ie7_or_later_; + + // The thread we are running into. + DWORD thread_id_; + + // Indicates if the current shown page is a full-tab chrome frame. + bool full_tab_chrome_frame_; + + private: + // Used during initialization to get the tab information from Chrome and + // register ourselves with the broker. + HRESULT RegisterTabInfo(); + + typedef std::deque<Task*> DeferredCallListType; + DeferredCallListType deferred_tab_id_call_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_BROWSER_HELPER_OBJECT_H_ diff --git a/ceee/ie/plugin/bho/browser_helper_object.rgs b/ceee/ie/plugin/bho/browser_helper_object.rgs new file mode 100644 index 0000000..f7331a6 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object.rgs @@ -0,0 +1,28 @@ +HKCR { +NoRemove CLSID { + ForceRemove {E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC} = s 'Google Chrome Extensions Execution Environment Helper' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + 'TypeLib' = s '{7C09079D-F9CB-4E9E-9293-D224B071D8BA}' + } + } +} + +HKLM { + NoRemove SOFTWARE { + NoRemove Microsoft { + NoRemove Windows { + NoRemove CurrentVersion { + NoRemove Explorer { + NoRemove 'Browser Helper Objects' { + ForceRemove '{E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC}' = s 'Google Chrome Extensions Execution Environment Helper' { + val 'NoExplorer' = d '1' + } + } + } + } + } + } + } +} diff --git a/ceee/ie/plugin/bho/browser_helper_object_unittest.cc b/ceee/ie/plugin/bho/browser_helper_object_unittest.cc new file mode 100644 index 0000000..285a733 --- /dev/null +++ b/ceee/ie/plugin/bho/browser_helper_object_unittest.cc @@ -0,0 +1,743 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/browser_helper_object.h" + +#include <exdisp.h> +#include <shlguid.h> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_browser_and_friends.h" +#include "ceee/ie/testing/mock_chrome_frame_host.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::_; +using testing::CopyBSTRToArgument; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::GetConnectionCount; +using testing::InstanceCountMixin; +using testing::MockChromeFrameHost; +using testing::MockDispatchEx; +using testing::MockIOleWindow; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::TestBrowser; +using testing::TestBrowserSite; + +// Tab Ids passed to the API +const int kGoodTabId = 1; +const CeeeWindowHandle kGoodTabHandle = kGoodTabId + 1; +const HWND kGoodTab = (HWND)kGoodTabHandle; + +class TestFrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<StrictMock<TestFrameEventHandler> >, + public InstanceCountMixin<TestFrameEventHandler>, + public IFrameEventHandler { + public: + BEGIN_COM_MAP(TestFrameEventHandler) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandler, IFrameEventHandler) + END_COM_MAP() + + HRESULT Initialize(TestFrameEventHandler** self) { + *self = this; + return S_OK; + } + + MOCK_METHOD1(GetUrl, void(BSTR* url)); + MOCK_METHOD1(SetUrl, HRESULT(BSTR url)); + // no need to mock it yet and it is called from a DCHECK so... + virtual READYSTATE GetReadyState() { return READYSTATE_COMPLETE; } + MOCK_METHOD1(AddSubHandler, HRESULT(IFrameEventHandler* handler)); + MOCK_METHOD1(RemoveSubHandler, HRESULT(IFrameEventHandler* handler)); + MOCK_METHOD0(TearDown, void()); + MOCK_METHOD3(InsertCode, HRESULT(BSTR code, BSTR file, + CeeeTabCodeType type)); + MOCK_METHOD0(RedoDoneInjections, void()); +}; + +class TestingBrowserHelperObject + : public BrowserHelperObject, + public InstanceCountMixin<TestingBrowserHelperObject>, + public InitializingCoClass<TestingBrowserHelperObject> { + public: + HRESULT Initialize(TestingBrowserHelperObject** self) { + // Make sure this is done early so we can mock it. + EXPECT_HRESULT_SUCCEEDED(MockChromeFrameHost::CreateInitializedIID( + &mock_chrome_frame_host_, IID_IChromeFrameHost, + &mock_chrome_frame_host_keeper_)); + chrome_frame_host_ = mock_chrome_frame_host_; + *self = this; + return S_OK; + } + + virtual TabEventsFunnel& tab_events_funnel() { + return mock_tab_events_funnel_; + } + + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker) { + broker_keeper_.CopyTo(broker); + return S_OK; + } + + virtual HRESULT CreateExecutor(IUnknown** executor) { + executor_keeper_.CopyTo(executor); + return S_OK; + } + + virtual HRESULT CreateChromeFrameHost() { + EXPECT_TRUE(chrome_frame_host_ != NULL); + return S_OK; + } + + virtual HRESULT GetTabWindow(IServiceProvider* service_provider) { + tab_window_ = reinterpret_cast<HWND>(kGoodTab); + return S_OK; + } + + virtual void SetTabId(int tab_id) { + tab_id_ = tab_id; + } + + virtual WebProgressNotifier* CreateWebProgressNotifier() { + // Without calling Initialize(), the class won't do anything. + return new WebProgressNotifier(); + } + + MOCK_METHOD0(SetupNewTabInfo, bool()); + MOCK_METHOD3(CreateFrameEventHandler, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler)); + MOCK_METHOD1(BrowserContainsChromeFrame, bool(IWebBrowser2* browser)); + + MOCK_METHOD2(IsHashChange, bool(BSTR, BSTR)); + bool CallIsHashChange(BSTR url1, BSTR url2) { + return BrowserHelperObject::IsHashChange(url1, url2); + } + + MOCK_METHOD2(GetParentBrowser, HRESULT(IWebBrowser2*, IWebBrowser2**)); + + // Pulicize + using BrowserHelperObject::HandleNavigateComplete; + + StrictMock<testing::MockTabEventsFunnel> mock_tab_events_funnel_; + MockChromeFrameHost* mock_chrome_frame_host_; + CComPtr<IChromeFrameHost> mock_chrome_frame_host_keeper_; + + testing::MockExecutorIUnknown* executor_; + CComPtr<IUnknown> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; +}; + +class BrowserHelperObjectTest: public testing::Test { + public: + BrowserHelperObjectTest() : bho_(NULL), site_(NULL), browser_(NULL) { + } + ~BrowserHelperObjectTest() { + } + + virtual void SetUp() { + // Create the instance to test. + ASSERT_HRESULT_SUCCEEDED( + TestingBrowserHelperObject::CreateInitialized(&bho_, &bho_with_site_)); + bho_with_site_ = bho_; + + // TODO(mad@chromium.org): Test this method. + EXPECT_CALL(*bho_, SetupNewTabInfo()).WillRepeatedly(Return(true)); + + // We always go beyond Chrome Frame start and event funnel init. + // Create the broker registrar related objects + ASSERT_HRESULT_SUCCEEDED(testing::MockExecutorIUnknown::CreateInitialized( + &bho_->executor_, &bho_->executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED(testing::MockBroker::CreateInitialized( + &bho_->broker_, &bho_->broker_keeper_)); + + // We always go beyond Chrome Frame start, broker reg and event funnel init. + // TODO(mad@chromium.org): Also cover failure cases from those. + ExpectBrokerRegistration(); + ExpectChromeFrameStart(); + + // Assert on successful TearDown. + ExpectBrokerUnregistration(); + ExpectChromeFrameTearDown(); + } + + virtual void TearDown() { + bho_->executor_ = NULL; + bho_->executor_keeper_.Release(); + + bho_->broker_ = NULL; + bho_->broker_keeper_.Release(); + + bho_ = NULL; + bho_with_site_.Release(); + + site_ = NULL; + site_keeper_.Release(); + + browser_ = NULL; + browser_keeper_.Release(); + + handler_ = NULL; + handler_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateSite() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, &site_keeper_)); + } + + void CreateBrowser() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser_, &browser_keeper_)); + + // Fail get_Parent calls for the root. + EXPECT_CALL(*bho_, GetParentBrowser(browser_keeper_.p, NotNull())). + WillRepeatedly(Return(E_NOTIMPL)); + + if (site_) + site_->browser_ = browser_keeper_; + } + + void CreateHandler() { + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler_, IID_IFrameEventHandler, &handler_keeper_)); + } + + bool BhoHasSite() { + // Check whether BHO has a site set. + CComPtr<IUnknown> site; + if (SUCCEEDED(bho_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&site)))) + return true; + if (site != NULL) + return true; + + return false; + } + + void ExpectChromeFrameStart() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetEventSink(_)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetChromeProfileName(_)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), StartChromeFrame()). + WillOnce(Return(S_OK)); + } + + CComBSTR CreateTabInfo(int tab_id) { + std::ostringstream iss; + iss << L"{\"id\":" << tab_id << L"}"; + return CComBSTR(iss.str().c_str()); + } + void ExpectChromeFrameGetSessionId() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), GetSessionId(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(kGoodTabId), Return(S_OK))); + EXPECT_CALL(*(bho_->broker_), SetTabIdForHandle(kGoodTabId, + kGoodTabHandle)).WillOnce(Return(S_OK)); + } + + void ExpectChromeFrameTearDown() { + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), SetEventSink(NULL)). + Times(1); + EXPECT_CALL(*(bho_->mock_chrome_frame_host_), TearDown()). + WillOnce(Return(S_OK)); + } + + void ExpectBrokerRegistration() { + EXPECT_CALL(*bho_->broker_, RegisterTabExecutor(::GetCurrentThreadId(), + bho_->executor_keeper_.p)).WillOnce(Return(S_OK)); + } + + void ExpectBrokerUnregistration() { + EXPECT_CALL(*bho_->broker_, UnregisterExecutor(::GetCurrentThreadId())). + WillOnce(Return(S_OK)); + } + + void ExpectHandleNavigation(TestFrameEventHandler* handler, + bool hash_change) { + EXPECT_CALL(*handler, GetUrl(_)).Times(1). + WillOnce(CopyBSTRToArgument<0>(kUrl2)); + EXPECT_CALL(*bho_, IsHashChange(StrEq(kUrl1), StrEq(kUrl2))). + WillOnce(Return(hash_change)); + // We should get the URL poked at the handler. + EXPECT_CALL(*handler, SetUrl(StrEq(kUrl1))).WillOnce(Return(S_OK)); + } + + void ExpectTopBrowserNavigation(bool hash_change, bool first_call) { + // We also get a tab update notification. + if (first_call) { + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnCreated(_, StrEq(kUrl1), false)).Times(1); + } + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_UNINITIALIZED)).Times(1); + if (hash_change) { + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_COMPLETE)).Times(1); + } + } + + void ExpectFireOnRemovedEvent() { + EXPECT_CALL(bho_->mock_tab_events_funnel_, OnRemoved(_)); + } + + void ExpectFireOnUnmappedEvent() { + EXPECT_CALL(bho_->mock_tab_events_funnel_, OnTabUnmapped(_, _)); + } + + static const wchar_t* kUrl1; + static const wchar_t* kUrl2; + + // Logging quenched for all tests. + testing::LogDisabler no_dchecks_; + + TestingBrowserHelperObject* bho_; + CComPtr<IObjectWithSite> bho_with_site_; + + testing::TestBrowserSite* site_; + CComPtr<IUnknown> site_keeper_; + + TestBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + TestFrameEventHandler* handler_; + CComPtr<IFrameEventHandler> handler_keeper_; +}; + +const wchar_t* BrowserHelperObjectTest::kUrl1 = +L"http://www.google.com/search?q=Google+Buys+Iceland"; +const wchar_t* BrowserHelperObjectTest::kUrl2 = L"http://www.google.com"; + + +// Setting the BHO site with a non-service provider fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNoServiceProviderFails) { + // Create an object that doesn't implement IServiceProvider. + MockDispatchEx* site = NULL; + CComPtr<IUnknown> site_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&site, + &site_keeper)); + // Setting a site that doesn't implement IServiceProvider fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with no browser fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNullBrowserFails) { + CreateSite(); + + // Setting a site with no browser fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with a non-browser fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNonBrowserFails) { + CreateSite(); + + // Endow the site with a non-browser service. + MockDispatchEx* mock_non_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_non_browser, + &site_->browser_)); + // Setting a site with a non-browser fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +// Setting the BHO site with a browser that doesn't implement the +// DIID_DWebBrowserEvents2 connection point fails. +TEST_F(BrowserHelperObjectTest, SetSiteWithNoEventsFails) { + CreateSite(); + CreateBrowser(); + + // Disable the connection point. + browser_->no_events_ = true; + + // No connection point site fails. + ASSERT_HRESULT_FAILED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(BhoHasSite()); +} + +TEST_F(BrowserHelperObjectTest, SetSiteWithBrowserSucceeds) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + // Check that the we set up a connection. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site == site_keeper_); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); + + // And check that the connection was severed. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); +} + +TEST_F(BrowserHelperObjectTest, OnNavigateCompleteHandled) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + EXPECT_CALL(*bho_, CreateFrameEventHandler(browser_, NULL, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler_keeper_), + Return(S_OK))); + EXPECT_CALL(*bho_, BrowserContainsChromeFrame(browser_)). + WillOnce(Return(false)); + ExpectHandleNavigation(handler_, true); + ExpectTopBrowserNavigation(true, true); + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, RenavigationNotifiesUrl) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Make as if a handler has been attached to the browser. + ASSERT_HRESULT_SUCCEEDED( + bho_->AttachBrowser(browser_, NULL, handler_keeper_)); + + EXPECT_CALL(*handler_, GetUrl(_)).Times(1). + WillOnce(CopyBSTRToArgument<0>(kUrl2)); + EXPECT_CALL(*bho_, IsHashChange(StrEq(kUrl1), StrEq(kUrl2))). + WillOnce(Return(false)); + // We should get the "new" URL poked at the handler. + EXPECT_CALL(*handler_, SetUrl(StrEq(kUrl1))).Times(1); + + // We also get a tab update notification. + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnCreated(_, StrEq(kUrl1), false)).Times(1); + EXPECT_CALL(bho_->mock_tab_events_funnel_, + OnUpdated(_, StrEq(kUrl1), READYSTATE_UNINITIALIZED)).Times(1); + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +// Test that we filter OnNavigateComplete invocations with +// non-IWebBrowser2 or non BSTR arguments. +TEST_F(BrowserHelperObjectTest, OnNavigateCompleteUnhandled) { + CreateSite(); + CreateBrowser(); + ExpectChromeFrameGetSessionId(); + + // Create an object that doesn't implement IWebBrowser2. + MockDispatchEx* non_browser = NULL; + CComPtr<IDispatch> non_browser_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized( + &non_browser, &non_browser_keeper)); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // HandleNavigateComplete should not be called by the invocations below. + EXPECT_CALL(*bho_, CreateFrameEventHandler(_, _, _)).Times(0); + + // Non-browser target. + browser_->FireOnNavigateComplete(non_browser, &CComVariant(kUrl1)); + + // Non-BSTR url parameter. + browser_->FireOnNavigateComplete(browser_, &CComVariant(non_browser)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, HandleNavigateComplete) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + EXPECT_CALL(*bho_, CreateFrameEventHandler(browser_, NULL, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler_keeper_), + Return(S_OK))); + EXPECT_CALL(*bho_, BrowserContainsChromeFrame(browser_)). + WillOnce(Return(false)); + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, true); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + // Now handle the case without the creation of a handler. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, false); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + // Now navigate a sub-frame. + TestBrowser* browser2; + CComPtr<IWebBrowser2> browser2_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser2, &browser2_keeper)); + EXPECT_CALL(*bho_, GetParentBrowser(browser2, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<1>(browser_keeper_), + Return(S_OK))); + TestFrameEventHandler* handler2; + CComPtr<IFrameEventHandler> handler2_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler2, IID_IFrameEventHandler, &handler2_keeper)); + + EXPECT_CALL(*bho_, + CreateFrameEventHandler(browser2, browser_, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler2_keeper), + Return(S_OK))); + + ExpectHandleNavigation(handler2, false); + bho_->HandleNavigateComplete(browser2, CComBSTR(kUrl1)); + EXPECT_CALL(*handler_, AddSubHandler(handler2)). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser2, browser_, handler2)); + + // Now, navigating the top browser again. + ExpectHandleNavigation(handler_, false); + ExpectTopBrowserNavigation(false, false); + bho_->HandleNavigateComplete(browser_, CComBSTR(kUrl1)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, AttachOrphanedBrowser) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + // The site needs to return the top-level browser. + site_->browser_ = browser_; + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Attach the root. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Now attach an apparent orphan which is actually the grand child of an + // existing frame which parent wasn't seen yet. + TestBrowser* browser3; + CComPtr<IWebBrowser2> browser3_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser3, &browser3_keeper)); + + TestFrameEventHandler* handler3; + CComPtr<IFrameEventHandler> handler_keeper_3; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler3, IID_IFrameEventHandler, &handler_keeper_3)); + + TestBrowser* browser3_parent; + CComPtr<IWebBrowser2> browser3_parent_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser3_parent, + &browser3_parent_keeper)); + + TestFrameEventHandler* handler3_parent; + CComPtr<IFrameEventHandler> handler3_parent_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler3_parent, IID_IFrameEventHandler, &handler3_parent_keeper)); + + EXPECT_CALL(*bho_, GetParentBrowser(browser3_parent, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<1>(browser_keeper_.p), + Return(S_OK))); + EXPECT_CALL(*browser3_parent, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_CALL(*bho_, + CreateFrameEventHandler(browser3_parent, browser_keeper_.p, NotNull())). + WillOnce(DoAll(CopyInterfaceToArgument<2>(handler3_parent_keeper), + Return(S_OK))); + EXPECT_CALL(*handler3_parent, SetUrl(StrEq(kUrl1))).WillOnce(Return(S_OK)); + EXPECT_CALL(*handler3_parent, AddSubHandler(handler3)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser3, browser3_parent, + handler3)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, IFrameEventHandlerHost) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + // Detaching a non-attached browser should fail. + EXPECT_HRESULT_FAILED(bho_->DetachBrowser(browser_, NULL, handler_)); + + // First-time attach should succeed. + EXPECT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + // Second attach should fail. + EXPECT_HRESULT_FAILED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Subsequent detach should succeed. + EXPECT_HRESULT_SUCCEEDED(bho_->DetachBrowser(browser_, NULL, handler_)); + // But not twice. + EXPECT_HRESULT_FAILED(bho_->DetachBrowser(browser_, NULL, handler_)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); + + // TODO(siggi@chromium.org): test hierarchial attach/detach/TearDown. +} + +TEST_F(BrowserHelperObjectTest, InsertCode) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(BhoHasSite()); + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + CComBSTR code; + CComBSTR file; + EXPECT_CALL(*handler_, InsertCode(_, _, kCeeeTabCodeTypeCss)) + .WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED(bho_->InsertCode(code, file, FALSE, + kCeeeTabCodeTypeCss)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, InsertCodeAllFrames) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + ExpectChromeFrameGetSessionId(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(BhoHasSite()); + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_, NULL, handler_)); + + // Add a second browser to the BHO and make sure that both get called. + TestBrowser* browser2; + CComPtr<IWebBrowser2> browser_keeper_2; + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser2, &browser_keeper_2)); + + TestFrameEventHandler* handler2; + CComPtr<IFrameEventHandler> handler_keeper_2; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitializedIID( + &handler2, IID_IFrameEventHandler, &handler_keeper_2)); + + ASSERT_HRESULT_SUCCEEDED(bho_->AttachBrowser(browser_keeper_2, + NULL, + handler_keeper_2)); + + CComBSTR code; + CComBSTR file; + EXPECT_CALL(*handler_, InsertCode(_, _, kCeeeTabCodeTypeJs)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*handler2, InsertCode(_, _, kCeeeTabCodeTypeJs)) + .WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED(bho_->InsertCode(code, file, TRUE, + kCeeeTabCodeTypeJs)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +TEST_F(BrowserHelperObjectTest, IsHashChange) { + CreateSite(); + CreateBrowser(); + CreateHandler(); + + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(site_keeper_)); + + CComBSTR url1("http://www.google.com/"); + CComBSTR url2("http://www.google.com/#"); + CComBSTR url3("http://www.google.com/#test"); + CComBSTR url4("http://www.google.com/#bingo"); + CComBSTR url5("http://www.bingo.com/"); + CComBSTR url6("http://www.twitter.com/#test"); + CComBSTR empty; + + // Passing cases. + EXPECT_TRUE(bho_->CallIsHashChange(url1, url2)); + EXPECT_TRUE(bho_->CallIsHashChange(url1, url3)); + EXPECT_TRUE(bho_->CallIsHashChange(url2, url3)); + EXPECT_TRUE(bho_->CallIsHashChange(url3, url4)); + + // Failing cases. + EXPECT_FALSE(bho_->CallIsHashChange(url1, empty)); + EXPECT_FALSE(bho_->CallIsHashChange(empty, url1)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url1)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url5)); + EXPECT_FALSE(bho_->CallIsHashChange(url1, url6)); + EXPECT_FALSE(bho_->CallIsHashChange(url3, url6)); + EXPECT_FALSE(bho_->CallIsHashChange(url5, url6)); + + ExpectFireOnRemovedEvent(); + ExpectFireOnUnmappedEvent(); + ASSERT_HRESULT_SUCCEEDED(bho_with_site_->SetSite(NULL)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/cookie_accountant.cc b/ceee/ie/plugin/bho/cookie_accountant.cc new file mode 100644 index 0000000..7453bb9 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant.cc @@ -0,0 +1,226 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CookieAccountant implementation. +#include "ceee/ie/plugin/bho/cookie_accountant.h" + +#include <atlbase.h> +#include <wininet.h> + +#include <set> + +#include "base/format_macros.h" // For PRIu64. +#include "base/logging.h" +#include "base/process_util.h" +#include "base/string_tokenizer.h" +#include "base/time.h" +#include "base/utf_string_conversions.h" +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/common/ceee_module_util.h" + +namespace { + +static const char kSetCookieHeaderName[] = "Set-Cookie:"; +static const char kHttpResponseHeaderDelimiter[] = "\n"; +static const wchar_t kMsHtmlModuleName[] = L"mshtml.dll"; +static const char kWinInetModuleName[] = "wininet.dll"; +static const char kInternetSetCookieExAFunctionName[] = "InternetSetCookieExA"; +static const char kInternetSetCookieExWFunctionName[] = "InternetSetCookieExW"; + +} // namespace + +CookieAccountant* CookieAccountant::singleton_instance_ = NULL; + +CookieAccountant::~CookieAccountant() { + if (internet_set_cookie_ex_a_patch_.is_patched()) + internet_set_cookie_ex_a_patch_.Unpatch(); + if (internet_set_cookie_ex_w_patch_.is_patched()) + internet_set_cookie_ex_w_patch_.Unpatch(); +} + +ProductionCookieAccountant::ProductionCookieAccountant() { +} + +CookieAccountant* CookieAccountant::GetInstance() { + // Unit tests can set singleton_instance_ directly. + if (singleton_instance_ == NULL) + singleton_instance_ = ProductionCookieAccountant::get(); + return singleton_instance_; +} + +DWORD WINAPI CookieAccountant::InternetSetCookieExAPatch( + LPCSTR url, LPCSTR cookie_name, LPCSTR cookie_data, + DWORD flags, DWORD_PTR reserved) { + base::Time current_time = base::Time::Now(); + DWORD cookie_state = ::InternetSetCookieExA(url, cookie_name, cookie_data, + flags, reserved); + CookieAccountant::GetInstance()->RecordCookie(url, cookie_data, + current_time); + return cookie_state; +} + +DWORD WINAPI CookieAccountant::InternetSetCookieExWPatch( + LPCWSTR url, LPCWSTR cookie_name, LPCWSTR cookie_data, + DWORD flags, DWORD_PTR reserved) { + base::Time current_time = base::Time::Now(); + DWORD cookie_state = ::InternetSetCookieExW(url, cookie_name, cookie_data, + flags, reserved); + CookieAccountant::GetInstance()->RecordCookie( + std::string(CW2A(url)), std::string(CW2A(cookie_data)), current_time); + return cookie_state; +} + +class CurrentProcessFilter : public base::ProcessFilter { + public: + CurrentProcessFilter() : current_process_id_(base::GetCurrentProcId()) { + } + + virtual bool Includes(const base::ProcessEntry& entry) const { + return entry.pid() == current_process_id_; + } + + private: + base::ProcessId current_process_id_; + + DISALLOW_COPY_AND_ASSIGN(CurrentProcessFilter); +}; + +// TODO(cindylau@chromium.org): Make this function more robust. +void CookieAccountant::RecordCookie( + const std::string& url, const std::string& cookie_data, + const base::Time& current_time) { + cookie_api::CookieInfo cookie; + std::string cookie_data_string = cookie_data; + net::CookieMonster::ParsedCookie parsed_cookie(cookie_data_string); + DCHECK(parsed_cookie.IsValid()); + if (!parsed_cookie.IsValid()) + return; + + // Fill the cookie info from the parsed cookie. + // TODO(cindylau@chromium.org): Add a helper function to convert an + // std::string to a BSTR. + cookie.name = ::SysAllocString(ASCIIToWide(parsed_cookie.Name()).c_str()); + cookie.value = ::SysAllocString(ASCIIToWide(parsed_cookie.Value()).c_str()); + SetScriptCookieDomain(parsed_cookie, &cookie); + SetScriptCookiePath(parsed_cookie, &cookie); + cookie.secure = parsed_cookie.IsSecure() ? TRUE : FALSE; + cookie.http_only = parsed_cookie.IsHttpOnly() ? TRUE : FALSE; + SetScriptCookieExpirationDate(parsed_cookie, current_time, &cookie); + SetScriptCookieStoreId(&cookie); + + // Send the cookie event to the broker. + // TODO(cindylau@chromium.org): Set the removed parameter properly. + cookie_events_funnel().OnChanged(false, cookie); +} + +void CookieAccountant::SetScriptCookieDomain( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie) { + if (parsed_cookie.HasDomain()) { + cookie->domain = ::SysAllocString( + ASCIIToWide(parsed_cookie.Domain()).c_str()); + cookie->host_only = FALSE; + } else { + // TODO(cindylau@chromium.org): If the domain is not provided, get + // it from the URL. + cookie->host_only = TRUE; + } +} + +void CookieAccountant::SetScriptCookiePath( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie) { + // TODO(cindylau@chromium.org): If the path is not provided, get it + // from the URL. + if (parsed_cookie.HasPath()) + cookie->path = ::SysAllocString(ASCIIToWide(parsed_cookie.Path()).c_str()); +} + +void CookieAccountant::SetScriptCookieExpirationDate( + const net::CookieMonster::ParsedCookie& parsed_cookie, + const base::Time& current_time, + cookie_api::CookieInfo* cookie) { + // First, try the Max-Age attribute. + uint64 max_age = 0; + if (parsed_cookie.HasMaxAge() && + sscanf_s(parsed_cookie.MaxAge().c_str(), " %" PRIu64, &max_age) == 1) { + cookie->session = FALSE; + base::Time expiration_time = current_time + + base::TimeDelta::FromSeconds(max_age); + cookie->expiration_date = expiration_time.ToDoubleT(); + } else if (parsed_cookie.HasExpires()) { + cookie->session = FALSE; + base::Time expiration_time = net::CookieMonster::ParseCookieTime( + parsed_cookie.Expires()); + cookie->expiration_date = expiration_time.ToDoubleT(); + } else { + cookie->session = TRUE; + } +} + +void CookieAccountant::SetScriptCookieStoreId(cookie_api::CookieInfo* cookie) { + // The store ID is either the current process ID, or the process ID of the + // parent process, if that parent process is an IE frame process. + // First collect all IE process IDs. + std::set<base::ProcessId> ie_pids; + base::NamedProcessIterator ie_iter( + ceee_module_util::kInternetExplorerModuleName, NULL); + while (const base::ProcessEntry* process_entry = ie_iter.NextProcessEntry()) { + ie_pids.insert(process_entry->pid()); + } + // Now get the store ID process by finding the current process, and seeing if + // its parent process is an IE process. + DWORD process_id = 0; + CurrentProcessFilter filter; + base::ProcessIterator it(&filter); + while (const base::ProcessEntry* process_entry = it.NextProcessEntry()) { + // There should only be one matching process entry. + DCHECK_EQ(process_id, DWORD(0)); + if (ie_pids.find(process_entry->parent_pid()) != ie_pids.end()) { + process_id = process_entry->parent_pid(); + } else { + DCHECK(ie_pids.find(process_entry->pid()) != ie_pids.end()); + process_id = process_entry->pid(); + } + } + DCHECK_NE(process_id, DWORD(0)); + std::ostringstream store_id_stream; + store_id_stream << process_id; + // The broker is responsible for checking that the store ID is registered. + cookie->store_id = + ::SysAllocString(ASCIIToWide(store_id_stream.str()).c_str()); +} + +void CookieAccountant::RecordHttpResponseCookies( + const std::string& response_headers, const base::Time& current_time) { + StringTokenizer t(response_headers, kHttpResponseHeaderDelimiter); + while (t.GetNext()) { + std::string header_line = t.token(); + size_t name_pos = header_line.find(kSetCookieHeaderName); + if (name_pos == std::string::npos) + continue; // Skip non-cookie headers. + std::string cookie_data = header_line.substr( + name_pos + std::string(kSetCookieHeaderName).size()); + // TODO(cindylau@chromium.org): Get the URL for the HTTP request from + // IHttpNegotiate::BeginningTransaction. + RecordCookie(std::string(), cookie_data, current_time); + } +} + +void CookieAccountant::PatchWininetFunctions() { + if (!internet_set_cookie_ex_a_patch_.is_patched()) { + DWORD error = internet_set_cookie_ex_a_patch_.Patch( + kMsHtmlModuleName, kWinInetModuleName, + kInternetSetCookieExAFunctionName, InternetSetCookieExAPatch); + DCHECK(error == NO_ERROR || !internet_set_cookie_ex_a_patch_.is_patched()); + } + if (!internet_set_cookie_ex_w_patch_.is_patched()) { + DWORD error = internet_set_cookie_ex_w_patch_.Patch( + kMsHtmlModuleName, kWinInetModuleName, + kInternetSetCookieExWFunctionName, InternetSetCookieExWPatch); + DCHECK(error == NO_ERROR || !internet_set_cookie_ex_w_patch_.is_patched()); + } + DCHECK(internet_set_cookie_ex_a_patch_.is_patched() || + internet_set_cookie_ex_w_patch_.is_patched()); +} diff --git a/ceee/ie/plugin/bho/cookie_accountant.h b/ceee/ie/plugin/bho/cookie_accountant.h new file mode 100644 index 0000000..f26b102 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant.h @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Defines the CookieAccountant class, which is responsible for observing +// and recording all cookie-related information generated by a particular +// IE browser session. It records and fires cookie change events, it provides +// access to session and persistent cookies. + +#ifndef CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ +#define CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ + +#include <string> + +#include "app/win/iat_patch_function.h" +#include "base/singleton.h" +#include "base/time.h" +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "net/base/cookie_monster.h" + +// The class that accounts for all cookie-related activity for a single IE +// browser session context. There should only need to be one of these allocated +// per process; use ProductionCookieAccountant instead of using this class +// directly. +class CookieAccountant { + public: + // Patch cookie-related functions to observe IE session cookies. + void PatchWininetFunctions(); + + // Record Set-Cookie changes coming from the HTTP response headers. + void RecordHttpResponseCookies( + const std::string& response_headers, const base::Time& current_time); + + // An accessor for the singleton (useful for unit testing). + static CookieAccountant* GetInstance(); + + // InternetSetCookieExA function patch implementation for recording scripted + // cookie changes. + static DWORD WINAPI InternetSetCookieExAPatch( + LPCSTR lpszURL, LPCSTR lpszCookieName, LPCSTR lpszCookieData, + DWORD dwFlags, DWORD_PTR dwReserved); + + // InternetSetCookieExW function patch implementation for recording scripted + // cookie changes. + static DWORD WINAPI InternetSetCookieExWPatch( + LPCWSTR lpszURL, LPCWSTR lpszCookieName, LPCWSTR lpszCookieData, + DWORD dwFlags, DWORD_PTR dwReserved); + + protected: + // Exposed to subclasses mainly for unit testing purposes; production code + // should use the ProductionCookieAccountant class instead. + CookieAccountant() {} + virtual ~CookieAccountant(); + + // Records the modification or creation of a cookie. Fires off a + // cookies.onChanged event to Chrome Frame. + virtual void RecordCookie( + const std::string& url, const std::string& cookie_data, + const base::Time& current_time); + + // Unit test seam. + virtual CookieEventsFunnel& cookie_events_funnel() { + return cookie_events_funnel_; + } + + // Function patches that allow us to intercept scripted cookie changes. + app::win::IATPatchFunction internet_set_cookie_ex_a_patch_; + app::win::IATPatchFunction internet_set_cookie_ex_w_patch_; + + // Cached singleton instance. Useful for unit testing. + static CookieAccountant* singleton_instance_; + + private: + // Helper functions for extracting cookie information from a scripted cookie + // being set, to pass to the cookie onChanged event. + + // Sets the cookie domain for a script cookie event. + void SetScriptCookieDomain( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie); + + // Sets the cookie path for a script cookie event. + void SetScriptCookiePath( + const net::CookieMonster::ParsedCookie& parsed_cookie, + cookie_api::CookieInfo* cookie); + + // Sets the cookie expiration date for a script cookie event. + void SetScriptCookieExpirationDate( + const net::CookieMonster::ParsedCookie& parsed_cookie, + const base::Time& current_time, + cookie_api::CookieInfo* cookie); + + // Sets the cookie store ID for a script cookie event. + void SetScriptCookieStoreId(cookie_api::CookieInfo* cookie); + + // The funnel for sending cookie events to the broker. + CookieEventsFunnel cookie_events_funnel_; + + DISALLOW_COPY_AND_ASSIGN(CookieAccountant); +}; + +// A singleton that initializes and keeps the CookieAccountant used by +// production code. This class is separate so that CookieAccountant can still +// be accessed for unit testing. +class ProductionCookieAccountant : public CookieAccountant, + public Singleton<ProductionCookieAccountant> { + private: + // This ensures no construction is possible outside of the class itself. + friend struct DefaultSingletonTraits<ProductionCookieAccountant>; + DISALLOW_IMPLICIT_CONSTRUCTORS(ProductionCookieAccountant); +}; + +#endif // CEEE_IE_PLUGIN_BHO_COOKIE_ACCOUNTANT_H_ diff --git a/ceee/ie/plugin/bho/cookie_accountant_unittest.cc b/ceee/ie/plugin/bho/cookie_accountant_unittest.cc new file mode 100644 index 0000000..6bd00ec --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_accountant_unittest.cc @@ -0,0 +1,141 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE browser helper object implementation. +#include "ceee/ie/plugin/bho/cookie_accountant.h" + +#include <wininet.h> + +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::MockCookieEventsFunnel; +using testing::Return; + +bool StringsNullOrEqual(wchar_t* a, wchar_t* b) { + if (a == NULL && b == NULL) + return true; + if (a != NULL && b != NULL && wcscmp(a, b) == 0) + return true; + return false; +} + +MATCHER_P(CookiesEqual, cookie, "") { + return + StringsNullOrEqual(arg.name, cookie->name) && + StringsNullOrEqual(arg.value, cookie->value) && + StringsNullOrEqual(arg.domain, cookie->domain) && + arg.host_only == cookie->host_only && + StringsNullOrEqual(arg.path, cookie->path) && + arg.secure == cookie->secure && + arg.http_only == cookie->http_only && + arg.session == cookie->session && + arg.expiration_date == cookie->expiration_date; +} + +// Mock WinInet functions. +MOCK_STATIC_CLASS_BEGIN(MockWinInet) + MOCK_STATIC_INIT_BEGIN(MockWinInet) + MOCK_STATIC_INIT(InternetSetCookieExA); + MOCK_STATIC_INIT(InternetSetCookieExW); + MOCK_STATIC_INIT_END() + + MOCK_STATIC5(DWORD, CALLBACK, InternetSetCookieExA, LPCSTR, LPCSTR, LPCSTR, + DWORD, DWORD_PTR); + MOCK_STATIC5(DWORD, CALLBACK, InternetSetCookieExW, LPCWSTR, LPCWSTR, + LPCWSTR, DWORD, DWORD_PTR); +MOCK_STATIC_CLASS_END(MockWinInet) + +class MockCookieAccountant : public CookieAccountant { + public: + MOCK_METHOD3(RecordCookie, + void(const std::string&, const std::string&, + const base::Time&)); + + void CallRecordCookie(const std::string& url, + const std::string& cookie_data, + const base::Time& current_time) { + CookieAccountant::RecordCookie(url, cookie_data, current_time); + } + + virtual CookieEventsFunnel& cookie_events_funnel() { + return mock_cookie_events_funnel_; + } + + static void set_singleton_instance(CookieAccountant* instance) { + singleton_instance_ = instance; + } + + MockCookieEventsFunnel mock_cookie_events_funnel_; +}; + +class CookieAccountantTest : public testing::Test { +}; + +TEST_F(CookieAccountantTest, SetCookiePatchFiresCookieEvent) { + MockWinInet mock_wininet; + MockCookieAccountant cookie_accountant; + MockCookieAccountant::set_singleton_instance(&cookie_accountant); + + EXPECT_CALL(mock_wininet, InternetSetCookieExA(_, _, _, _, _)). + WillOnce(Return(5)); + EXPECT_CALL(cookie_accountant, RecordCookie("foo.com", "foo=bar", _)); + EXPECT_EQ(5, CookieAccountant::InternetSetCookieExAPatch( + "foo.com", NULL, "foo=bar", 0, NULL)); + + EXPECT_CALL(mock_wininet, InternetSetCookieExW(_, _, _, _, _)). + WillOnce(Return(6)); + EXPECT_CALL(cookie_accountant, RecordCookie("foo.com", "foo=bar", _)); + EXPECT_EQ(6, CookieAccountant::InternetSetCookieExWPatch( + L"foo.com", NULL, L"foo=bar", 0, NULL)); +} + +TEST_F(CookieAccountantTest, RecordCookie) { + testing::LogDisabler no_dchecks; + MockCookieAccountant cookie_accountant; + cookie_api::CookieInfo expected_cookie; + expected_cookie.name = ::SysAllocString(L"FOO"); + expected_cookie.value = ::SysAllocString(L"bar"); + expected_cookie.host_only = TRUE; + expected_cookie.http_only = TRUE; + expected_cookie.expiration_date = 1278201600; + EXPECT_CALL(cookie_accountant.mock_cookie_events_funnel_, + OnChanged(false, CookiesEqual(&expected_cookie))); + cookie_accountant.CallRecordCookie( + "http://www.google.com", + "FOO=bar; httponly; expires=Sun, 4 Jul 2010 00:00:00 UTC", + base::Time::Now()); + + cookie_api::CookieInfo expected_cookie2; + expected_cookie2.name = ::SysAllocString(L""); + expected_cookie2.value = ::SysAllocString(L"helloworld"); + expected_cookie2.domain = ::SysAllocString(L"omg.com"); + expected_cookie2.path = ::SysAllocString(L"/leaping/lizards"); + expected_cookie2.secure = TRUE; + expected_cookie2.session = TRUE; + EXPECT_CALL(cookie_accountant.mock_cookie_events_funnel_, + OnChanged(false, CookiesEqual(&expected_cookie2))); + cookie_accountant.CallRecordCookie( + "http://www.omg.com", + "helloworld; path=/leaping/lizards; secure; domain=omg.com", + base::Time::Now()); +} + +TEST_F(CookieAccountantTest, RecordHttpResponseCookies) { + testing::LogDisabler no_dchecks; + MockCookieAccountant cookie_accountant; + EXPECT_CALL(cookie_accountant, RecordCookie("", " foo=bar", _)); + EXPECT_CALL(cookie_accountant, RecordCookie("", "HELLO=world235", _)); + cookie_accountant.RecordHttpResponseCookies( + "HTTP/1.1 200 OK\nSet-Cookie: foo=bar\nCookie: not_a=cookie\n" + "Set-Cookie:HELLO=world235", base::Time::Now()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/cookie_events_funnel.cc b/ceee/ie/plugin/bho/cookie_events_funnel.cc new file mode 100644 index 0000000..39ea727 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel.cc @@ -0,0 +1,28 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Cookie Events to the Broker. + +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" + +HRESULT CookieEventsFunnel::OnChanged(bool removed, + const cookie_api::CookieInfo& cookie) { + DictionaryValue change_info; + change_info.SetBoolean(extension_cookies_api_constants::kRemovedKey, + removed); + cookie_api::CookieApiResult api_result( + cookie_api::CookieApiResult::kNoRequestId); + bool success = api_result.CreateCookieValue(cookie); + DCHECK(success); + if (!success) { + return E_FAIL; + } + change_info.Set(extension_cookies_api_constants::kCookieKey, + api_result.value()->DeepCopy()); + return SendEvent(extension_cookies_api_constants::kOnChanged, change_info); +} diff --git a/ceee/ie/plugin/bho/cookie_events_funnel.h b/ceee/ie/plugin/bho/cookie_events_funnel.h new file mode 100644 index 0000000..06ada896 --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel.h @@ -0,0 +1,28 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Cookie Events. + +#ifndef CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ + +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send cookie related events to the Broker. +class CookieEventsFunnel : public EventsFunnel { + public: + CookieEventsFunnel() : EventsFunnel(false) {} + + // Sends the cookies.onChanged event to the Broker. + // @param removed True if the cookie was removed vs. set. + // @param cookie Information about the cookie that was set or removed. + virtual HRESULT OnChanged(bool removed, + const cookie_api::CookieInfo& cookie); + + private: + DISALLOW_COPY_AND_ASSIGN(CookieEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_COOKIE_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc b/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc new file mode 100644 index 0000000..650e31d --- /dev/null +++ b/ceee/ie/plugin/bho/cookie_events_funnel_unittest.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for CookieEventsFunnel. + +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "chrome/browser/extensions/extension_cookies_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestCookieEventsFunnel : public CookieEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(CookieEventsFunnelTest, OnChanged) { + TestCookieEventsFunnel cookie_events_funnel; + + bool removed = true; + cookie_api::CookieInfo cookie_info; + cookie_info.name = ::SysAllocString(L"FOO"); + cookie_info.value = ::SysAllocString(L"BAR"); + cookie_info.secure = TRUE; + cookie_info.session = TRUE; + cookie_info.store_id = ::SysAllocString(L"a store id!!"); + + DictionaryValue dict; + dict.SetBoolean(extension_cookies_api_constants::kRemovedKey, removed); + DictionaryValue* cookie = new DictionaryValue(); + cookie->SetString(extension_cookies_api_constants::kNameKey, "FOO"); + cookie->SetString(extension_cookies_api_constants::kValueKey, "BAR"); + cookie->SetString(extension_cookies_api_constants::kDomainKey, ""); + cookie->SetBoolean(extension_cookies_api_constants::kHostOnlyKey, false); + cookie->SetString(extension_cookies_api_constants::kPathKey, ""); + cookie->SetBoolean(extension_cookies_api_constants::kSecureKey, true); + cookie->SetBoolean(extension_cookies_api_constants::kHttpOnlyKey, false); + cookie->SetBoolean(extension_cookies_api_constants::kSessionKey, true); + cookie->SetString(extension_cookies_api_constants::kStoreIdKey, + "a store id!!"); + dict.Set(extension_cookies_api_constants::kCookieKey, cookie); + + EXPECT_CALL(cookie_events_funnel, SendEvent( + StrEq(extension_cookies_api_constants::kOnChanged), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED( + cookie_events_funnel.OnChanged(removed, cookie_info)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/dom_utils.cc b/ceee/ie/plugin/bho/dom_utils.cc new file mode 100644 index 0000000..714e108 --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils.cc @@ -0,0 +1,126 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A collection of utility functions that interrogate or mutate the IE DOM. +#include "ceee/ie/plugin/bho/dom_utils.h" + +#include <atlbase.h> + +#include "ceee/common/com_utils.h" +#include "base/logging.h" + +HRESULT DomUtils::InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code) { + DCHECK(document != NULL); + DCHECK(head_node != NULL); + + CComPtr<IHTMLElement> elem; + HRESULT hr = document->createElement(CComBSTR(L"style"), &elem); + if (FAILED(hr)) { + LOG(ERROR) << "Could not create style element " << com::LogHr(hr); + return hr; + } + + CComQIPtr<IHTMLStyleElement> style_elem(elem); + DCHECK(style_elem != NULL) << + "Could not QueryInterface for IHTMLStyleElement"; + + hr = style_elem->put_type(CComBSTR(L"text/css")); + DCHECK(SUCCEEDED(hr)) << "Could not set type of style element" << + com::LogHr(hr); + + CComPtr<IHTMLStyleSheet> style_sheet; + hr = style_elem->get_styleSheet(&style_sheet); + DCHECK(SUCCEEDED(hr)) << "Could not get styleSheet of style element." << + com::LogHr(hr); + + hr = style_sheet->put_cssText(CComBSTR(code)); + if (FAILED(hr)) { + LOG(ERROR) << "Could not set cssText of styleSheet." << com::LogHr(hr); + return hr; + } + + CComQIPtr<IHTMLDOMNode> style_node(style_elem); + DCHECK(style_node != NULL) << "Could not query interface for IHTMLDomNode."; + + CComPtr<IHTMLDOMNode> dummy; + hr = head_node->appendChild(style_node, &dummy); + if (FAILED(hr)) + LOG(ERROR) << "Could not append style node to head node." << com::LogHr(hr); + + return hr; +} + +HRESULT DomUtils::GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** head_node) { + DCHECK(document != NULL); + DCHECK(head_node != NULL && *head_node == NULL); + + // Find the HEAD element through document.getElementsByTagName. + CComQIPtr<IHTMLDocument3> document3(document); + CComPtr<IHTMLElementCollection> head_elements; + DCHECK(document3 != NULL); // Should be there on IE >= 5 + if (document3 == NULL) { + LOG(ERROR) << L"Unable to retrieve IHTMLDocument3 interface"; + return E_NOINTERFACE; + } + HRESULT hr = GetElementsByTagName(document3, CComBSTR(L"head"), + &head_elements, NULL); + if (FAILED(hr)) { + LOG(ERROR) << "Could not retrieve head elements collection " + << com::LogHr(hr); + return hr; + } + + return GetElementFromCollection(head_elements, 0, IID_IHTMLDOMNode, + reinterpret_cast<void**>(head_node)); +} + +HRESULT DomUtils::GetElementsByTagName(IHTMLDocument3* document, + BSTR tag_name, + IHTMLElementCollection** elements, + long* length) { + DCHECK(document != NULL); + DCHECK(tag_name != NULL); + DCHECK(elements != NULL && *elements == NULL); + + HRESULT hr = document->getElementsByTagName(tag_name, elements); + if (FAILED(hr) || *elements == NULL) { + hr = com::AlwaysError(hr); + LOG(ERROR) << "Could not retrieve elements collection " << com::LogHr(hr); + return hr; + } + + if (length != NULL) { + hr = (*elements)->get_length(length); + if (FAILED(hr)) { + (*elements)->Release(); + *elements = NULL; + LOG(ERROR) << "Could not retrieve collection length " << com::LogHr(hr); + } + } + return hr; +} + +HRESULT DomUtils::GetElementFromCollection(IHTMLElementCollection* collection, + long index, + REFIID id, + void** element) { + DCHECK(collection != NULL); + DCHECK(element != NULL && *element == NULL); + + CComPtr<IDispatch> item; + CComVariant index_variant(index, VT_I4); + HRESULT hr = collection->item(index_variant, index_variant, &item); + // As per http://msdn.microsoft.com/en-us/library/aa703930(VS.85).aspx + // item may still be NULL even if S_OK is returned. + if (FAILED(hr) || item == NULL) { + hr = com::AlwaysError(hr); + LOG(ERROR) << "Could not access item " << com::LogHr(hr); + return hr; + } + + return item->QueryInterface(id, element); +} diff --git a/ceee/ie/plugin/bho/dom_utils.h b/ceee/ie/plugin/bho/dom_utils.h new file mode 100644 index 0000000..e988a68 --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils.h @@ -0,0 +1,53 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A collection of utility functions that interrogate or mutate the IE DOM. +#ifndef CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ +#define CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ + +#include <mshtml.h> +#include <string> + +// This class is a namespace for hosting utility functions that interrogate +// or mutate the IE DOM. +// TODO(siggi@chromium.org): should this be a namespace? +class DomUtils { + public: + // Inject a style tag into the head of the document. + // @param document The DOM document object. + // @param head_node The HEAD DOM node from |document|. + // @param code The CSS code to inject. + static HRESULT InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code); + + // Retrieve the "HEAD" DOM node from @p document. + // @param document the DOM document object. + // @param head_node on success returns the "HEAD" DOM node. + static HRESULT GetHeadNode(IHTMLDocument* document, IHTMLDOMNode** head_node); + + // Retrieves all elements with the specified tag name from the document. + // @param document The document object. + // @param tag_name The tag name. + // @param elements On success returns a collection of elements. + // @param length On success returns the number of elements in the collection. + // The caller could pass in NULL to indicate that length + // information is not needed. + static HRESULT GetElementsByTagName(IHTMLDocument3* document, + BSTR tag_name, + IHTMLElementCollection** elements, + long* length); + + // Retrieves an element from the collection. + // @param collection The collection object. + // @param index The zero-based index of the element to retrieve. + // @param id The interface ID of the element to retrieve. + // @param element On success returns the element. + static HRESULT GetElementFromCollection(IHTMLElementCollection* collection, + long index, + REFIID id, + void** element); +}; + +#endif // CEEE_IE_PLUGIN_BHO_DOM_UTILS_H_ diff --git a/ceee/ie/plugin/bho/dom_utils_unittest.cc b/ceee/ie/plugin/bho/dom_utils_unittest.cc new file mode 100644 index 0000000..180c1cc --- /dev/null +++ b/ceee/ie/plugin/bho/dom_utils_unittest.cc @@ -0,0 +1,197 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unittests for DOM utils. +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/test_utils.h" + +namespace { + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::StrEq; +using testing::VariantEq; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl>, + public StrictMock<IHTMLDocument3MockImpl> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLDocument2) + COM_INTERFACE_ENTRY(IHTMLDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + COM_INTERFACE_ENTRY(IHTMLDocument3) + END_COM_MAP() + + HRESULT Initialize(MockDocument** self) { + *self = this; + return S_OK; + } +}; + +class MockElementNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockElementNode>, + public StrictMock<IHTMLElementMockImpl>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockElementNode) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize(MockElementNode** self) { + *self = this; + return S_OK; + } +}; + +class MockStyleElementNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockStyleElementNode>, + public StrictMock<IHTMLElementMockImpl>, + public StrictMock<IHTMLStyleElementMockImpl>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockStyleElementNode) + COM_INTERFACE_ENTRY2(IDispatch, IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLElement) + COM_INTERFACE_ENTRY(IHTMLStyleElement) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize(MockStyleElementNode** self) { + *self = this; + return S_OK; + } +}; + +class MockStyleSheet + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockStyleSheet>, + public StrictMock<IHTMLStyleSheetMockImpl> { + BEGIN_COM_MAP(MockStyleSheet) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLStyleSheet) + END_COM_MAP() + + HRESULT Initialize(MockStyleSheet** self) { + *self = this; + return S_OK; + } +}; + +class DomUtilsTest: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockElementNode::CreateInitialized(&head_node_, &head_node_keeper_)); + } + + virtual void TearDown() { + document_ = NULL; + document_keeper_.Release(); + head_node_ = NULL; + head_node_keeper_.Release(); + } + + protected: + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + MockElementNode* head_node_; + CComPtr<IHTMLDOMNode> head_node_keeper_; +}; + +TEST_F(DomUtilsTest, InjectStyleTag) { + MockStyleElementNode* style_node; + CComPtr<IHTMLElement> style_node_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockStyleElementNode::CreateInitialized(&style_node, &style_node_keeper)); + + MockStyleSheet* style_sheet; + CComPtr<IHTMLStyleSheet> style_sheet_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockStyleSheet::CreateInitialized(&style_sheet, &style_sheet_keeper)); + + EXPECT_CALL(*document_, createElement(StrEq(L"style"), _)). + WillOnce(DoAll(CopyInterfaceToArgument<1>(style_node_keeper), + Return(S_OK))); + + EXPECT_CALL(*style_node, put_type(StrEq(L"text/css"))). + WillOnce(Return(S_OK)); + + EXPECT_CALL(*style_node, get_styleSheet(_)). + WillOnce(DoAll(CopyInterfaceToArgument<0>(style_sheet_keeper), + Return(S_OK))); + + EXPECT_CALL(*style_sheet, put_cssText(StrEq(L"foo"))).WillOnce(Return(S_OK)); + + EXPECT_CALL(*head_node_, appendChild(style_node, _)). + WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + DomUtils::InjectStyleTag(document_keeper_, head_node_keeper_, L"foo")); +} + +class MockElementCollection + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockElementCollection>, + public StrictMock<IHTMLElementCollectionMockImpl> { + public: + BEGIN_COM_MAP(MockElementCollection) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLElementCollection) + END_COM_MAP() + + HRESULT Initialize(MockElementCollection** self) { + *self = this; + return S_OK; + } +}; + +TEST_F(DomUtilsTest, GetHeadNode) { + MockElementCollection* collection; + CComPtr<IHTMLElementCollection> collection_keeper; + ASSERT_HRESULT_SUCCEEDED( + MockElementCollection::CreateInitialized(&collection, + &collection_keeper)); + + EXPECT_CALL(*document_, getElementsByTagName(StrEq(L"head"), _)) + .WillRepeatedly(DoAll(CopyInterfaceToArgument<1>(collection_keeper), + Return(S_OK))); + + CComVariant zero(0L); + // First verify that we gracefuly fail when there are no heads. + // bb2333090 + EXPECT_CALL(*collection, item(_, VariantEq(zero), _)) + .WillOnce(Return(S_OK)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_FAILED(DomUtils::GetHeadNode(document_, &head_node)); + ASSERT_EQ(static_cast<IHTMLDOMNode*>(NULL), head_node); + + // And now properly return a valid head node. + EXPECT_CALL(*collection, item(_, VariantEq(zero), _)) + .WillOnce(DoAll(CopyInterfaceToArgument<2>( + static_cast<IDispatch*>(head_node_keeper_)), Return(S_OK))); + + ASSERT_HRESULT_SUCCEEDED( + DomUtils::GetHeadNode(document_, &head_node)); + + ASSERT_TRUE(head_node == head_node_); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/events_funnel.cc b/ceee/ie/plugin/bho/events_funnel.cc new file mode 100644 index 0000000..dfa0cdc --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel.cc @@ -0,0 +1,42 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common base class for funnels of Chrome Extension events that originate +// from the BHO and are sent to the Broker. + +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "ceee/ie/common/ceee_module_util.h" + + +EventsFunnel::EventsFunnel(bool keep_broker_alive) + : keep_broker_alive_(keep_broker_alive) { + if (keep_broker_alive_) + ceee_module_util::AddRefModuleWorkerThread(); +} + +EventsFunnel::~EventsFunnel() { + if (keep_broker_alive_) + ceee_module_util::ReleaseModuleWorkerThread(); +} + +HRESULT EventsFunnel::SendEvent(const char* event_name, + const Value& event_args) { + // Event arguments for FireEventToBroker always need to be stored in a list. + std::string event_args_str; + if (event_args.IsType(Value::TYPE_LIST)) { + base::JSONWriter::Write(&event_args, false, &event_args_str); + } else { + ListValue list; + list.Append(event_args.DeepCopy()); + base::JSONWriter::Write(&list, false, &event_args_str); + } + + EventsFunnel thread_locker(!keep_broker_alive_); + ceee_module_util::FireEventToBroker(event_name, event_args_str); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/events_funnel.h b/ceee/ie/plugin/bho/events_funnel.h new file mode 100644 index 0000000..add53f8 --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel.h @@ -0,0 +1,38 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Common base class for funnels of Chrome Extension events that originate +// from the BHO. + +#ifndef CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ + +#include <windows.h> + +#include "base/basictypes.h" + +class Value; + +// Defines a base class for sending events to the Broker. +class EventsFunnel { + protected: + // @param keep_broker_alive If true broker will be alive during + // lifetime of this funnel, otherwise only during SendEvent. + explicit EventsFunnel(bool keep_broker_alive); + virtual ~EventsFunnel(); + + // Send the given event to the Broker. + // @param event_name The name of the event. + // @param event_args The arguments to be sent with the event. + // protected virtual for testability... + virtual HRESULT SendEvent(const char* event_name, const Value& event_args); + + private: + // If true constructor/destructor of class increments/decrements ref counter + // of broker thread. Otherwise SendEvent does it for every event. + const bool keep_broker_alive_; + DISALLOW_COPY_AND_ASSIGN(EventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/events_funnel_unittest.cc b/ceee/ie/plugin/bho/events_funnel_unittest.cc new file mode 100644 index 0000000..64f7061 --- /dev/null +++ b/ceee/ie/plugin/bho/events_funnel_unittest.cc @@ -0,0 +1,62 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for EventsFunnel. + +#include "base/json/json_writer.h" +#include "base/values.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/events_funnel.h" +#include "ceee/testing/utils/mock_static.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::StrEq; + +MOCK_STATIC_CLASS_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT_BEGIN(MockCeeeModuleUtils) + MOCK_STATIC_INIT2(ceee_module_util::FireEventToBroker, + FireEventToBroker); + MOCK_STATIC_INIT_END() + MOCK_STATIC2(void, , FireEventToBroker, const std::string&, + const std::string&); +MOCK_STATIC_CLASS_END(MockCeeeModuleUtils) + +// Test subclass used to provide access to protected functionality in the +// EventsFunnel class. +class TestEventsFunnel : public EventsFunnel { + public: + TestEventsFunnel() : EventsFunnel(true) {} + + HRESULT CallSendEvent(const char* event_name, const Value& event_args) { + return SendEvent(event_name, event_args); + } +}; + +TEST(EventsFunnelTest, SendEvent) { + TestEventsFunnel events_funnel; + MockCeeeModuleUtils mock_ceee_module; + + static const char* kEventName = "MADness"; + DictionaryValue event_args; + event_args.SetInteger("Answer to the Ultimate Question of Life," + "the Universe, and Everything", 42); + event_args.SetString("AYBABTU", "All your base are belong to us"); + event_args.SetReal("www.unrealtournament.com", 3.0); + + ListValue args_list; + args_list.Append(event_args.DeepCopy()); + + std::string event_args_str; + base::JSONWriter::Write(&args_list, false, &event_args_str); + + EXPECT_CALL(mock_ceee_module, FireEventToBroker(StrEq(kEventName), + StrEq(event_args_str))).Times(1); + EXPECT_HRESULT_SUCCEEDED( + events_funnel.CallSendEvent(kEventName, event_args)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/executor.cc b/ceee/ie/plugin/bho/executor.cc new file mode 100644 index 0000000..d7719f1 --- /dev/null +++ b/ceee/ie/plugin/bho/executor.cc @@ -0,0 +1,771 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// CeeeExecutor implementation +// +// We use interfaces named ITabWindowManager and ITabWindow +// (documented at +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindowmanager.htm +// and +// http://www.geoffchappell.com/viewer.htm?doc=studies/windows/ie/ieframe/interfaces/itabwindow.htm) +// to help implement this API. These are available in IE7, IE8 and +// IE9 (with minor differences between browser versions), so we use a +// wrapper class that takes care of delegating to the available +// interface. +// +// Alternate approach considered: Using the IAccessible interface to find out +// about the order (indexes) of tabs, create new tabs and close tabs in a +// reliable way. The main drawback was that currently, the only way we've +// found to go from the IAccessible interface to the tab window itself (and +// hence the IWebBrowser2 object) is to match the description +// fetched using IAccessible::get_accDescription(), which contains the title +// and URL of the tab, to the title and URL retrieved via the IWebBrowser2 +// object. This limitation would mean that tab indexes could be incorrect +// when two or more tabs are navigated to the same page (and have the same +// title). + +#include "ceee/ie/plugin/bho/executor.h" + +#include <atlcomcli.h> +#include <mshtml.h> +#include <wininet.h> + +#include <vector> + +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "base/scoped_ptr.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/bho/infobar_manager.h" +#include "ceee/ie/plugin/bho/tab_window_manager.h" +#include "chrome_frame/utils.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +// Static per-process variable to indicate whether the process has been +// registered as a cookie store yet. +static bool g_cookie_store_is_registered = false; + +// INTERNET_COOKIE_HTTPONLY is only available for IE8 or later, which allows +// Wininet API to read cookies that are marked as HTTPOnly. +#ifndef INTERNET_COOKIE_HTTPONLY +#define INTERNET_COOKIE_HTTPONLY 0x00002000 +#endif + +// Default maximum height of the infobar. From the experience with the design of +// infobars this value is found to provide enough space and not to be too +// restrictive - for example this is approximately the height of Chrome infobar. +const int kMaxInfobarHeight = 39; +} // namespace + +// The message which will be posted to the destination thread. +const UINT CeeeExecutorCreator::kCreateWindowExecutorMessage = + ::RegisterWindowMessage( + L"CeeeExecutor{D91E23A6-1C2E-4984-8528-1F1771004F37}"); + +CeeeExecutorCreator::CeeeExecutorCreator() + : current_thread_id_(0), hook_(NULL) { +} + +void CeeeExecutorCreator::FinalRelease() { + if (hook_ != NULL) { + HRESULT hr = Teardown(current_thread_id_); + DCHECK(SUCCEEDED(hr)) << "Self-Tearing down. " << com::LogHr(hr); + } +} + +HRESULT CeeeExecutorCreator::CreateWindowExecutor(long thread_id, + CeeeWindowHandle window) { + DCHECK_EQ(0, current_thread_id_); + current_thread_id_ = thread_id; + // Verify we're a window, not just a tab. + DCHECK_EQ(window_utils::GetTopLevelParent(reinterpret_cast<HWND>(window)), + reinterpret_cast<HWND>(window)); + + hook_ = ::SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, + static_cast<HINSTANCE>(_AtlBaseModule.GetModuleInstance()), thread_id); + if (hook_ == NULL) { + LOG(ERROR) << "Couldn't hook into thread: " << thread_id << " " << + com::LogWe(); + current_thread_id_ = 0; + return E_FAIL; + } + + // We unfortunately can't Send a synchronous message here. + // If we do, any calls back to the broker fail with the following error: + // "An outgoing call cannot be made since the application is dispatching an + // input synchronous call." + BOOL success = ::PostThreadMessage(thread_id, kCreateWindowExecutorMessage, + 0, static_cast<LPARAM>(window)); + if (success) + return S_OK; + else + return HRESULT_FROM_WIN32(::GetLastError()); +} + +HRESULT CeeeExecutorCreator::Teardown(long thread_id) { + if (hook_ != NULL) { + DCHECK(current_thread_id_ == thread_id); + current_thread_id_ = 0; + // Don't return the failure since it may fail when we get called after + // the destination thread/module we hooked to vanished into thin air. + BOOL success = ::UnhookWindowsHookEx(hook_); + LOG_IF(INFO, !success) << "Failed to unhook. " << com::LogWe(); + hook_ = NULL; + } + return S_OK; +} + +LRESULT CeeeExecutorCreator::GetMsgProc(int code, WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION) { + MSG* message_data = reinterpret_cast<MSG*>(lparam); + if (message_data != NULL && + message_data->message == kCreateWindowExecutorMessage) { + // Remove the message from the queue so that we don't get called again + // while we wait for CoCreateInstance to complete, since COM will run + // a message loop in there. And some loop don't PM_REMOVE us (some do). + if (wparam != PM_REMOVE) { + MSG dummy; + BOOL success = ::PeekMessage(&dummy, NULL, kCreateWindowExecutorMessage, + kCreateWindowExecutorMessage, PM_REMOVE); + DCHECK(success) << "Peeking Hook Message. " << com::LogWe(); + // We must return here since we will get called again from within + // PeekMessage, and with PM_REMOVE this time (so no, we won't + // infinitely recurse :-), so this ensure that we get called just once. + return 0; + } + + CComPtr<ICeeeWindowExecutor> executor; + HRESULT hr = executor.CoCreateInstance(CLSID_CeeeExecutor); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + CeeeWindowHandle window = static_cast<CeeeWindowHandle>( + message_data->lParam); + if (window) { + hr = executor->Initialize(window); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create Executor, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Executor. " << com::LogHr(hr); + } + + CComPtr<ICeeeBrokerRegistrar> broker; + hr = broker.CoCreateInstance(CLSID_CeeeBroker); + LOG_IF(ERROR, FAILED(hr)) << "Failed to create broker, hr=" << + com::LogHr(hr); + DCHECK(SUCCEEDED(hr)) << "CoCreating Broker. " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + hr = broker->RegisterWindowExecutor(::GetCurrentThreadId(), executor); + DCHECK(SUCCEEDED(hr)) << "Registering Executor. " << com::LogHr(hr); + } + } + return 0; + } + } + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +HRESULT CeeeExecutor::Initialize(CeeeWindowHandle hwnd) { + DCHECK(hwnd); + hwnd_ = reinterpret_cast<HWND>(hwnd); + + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // If this is tab window then create the infobab manager. + // TODO(mad@chromium.org): We are starting to need to have different classes + // for the different executors. + if (window_utils::GetTopLevelParent(hwnd_) != hwnd_) + infobar_manager_.reset(new infobar_api::InfobarManager(hwnd_)); + + return S_OK; +} + +HRESULT CeeeExecutor::GetWebBrowser(IWebBrowser2** browser) { + DCHECK(browser); + CComPtr<IFrameEventHandlerHost> frame_handler_host; + HRESULT hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " << + com::LogHr(hr); + return hr; + } + return frame_handler_host->GetTopLevelBrowser(browser); +} + +STDMETHODIMP CeeeExecutor::GetWindow(BOOL populate_tabs, + CeeeWindowInfo* window_info) { + DCHECK(window_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Zero the window info. + ZeroMemory(window_info, sizeof(CeeeWindowInfo)); + + // The window we use to represent IE windows is the top-level (parentless) + // frame for the collection of windows that make up a logical "window" in the + // sense of the chrome.window.* API. Therefore, compare the provided window + // with the top-level parent of the current foreground (focused) window to + // see if the logical window has focus. + HWND top_level = window_utils::GetTopLevelParent(::GetForegroundWindow()); + window_info->focused = (top_level == hwnd_); + + if (!::GetWindowRect(hwnd_, &window_info->rect)) { + DWORD we = ::GetLastError(); + DCHECK(false) << "GetWindowRect failed " << com::LogWe(we); + return HRESULT_FROM_WIN32(we); + } + + if (populate_tabs) { + return GetTabs(&window_info->tab_list); + } + + return S_OK; +} + +BOOL CALLBACK CeeeExecutor::GetTabsEnumProc(HWND window, LPARAM param) { + if (window_utils::IsWindowClass(window, windows::kIeTabWindowClass)) { + std::vector<HWND>* tab_windows = + reinterpret_cast<std::vector<HWND>*>(param); + DCHECK(tab_windows); + tab_windows->push_back(window); + } + return TRUE; +} + +STDMETHODIMP CeeeExecutor::GetTabs(BSTR* tab_list) { + DCHECK(tab_list); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + std::vector<HWND> tab_windows; + ::EnumChildWindows(hwnd_, GetTabsEnumProc, + reinterpret_cast<LPARAM>(&tab_windows)); + // We don't DCHECK that we found as many windows as the tab window manager + // GetCount(), because there are cases where it sees more than we do... :-( + // When we navigate to a new page, IE8 actually creates a new temporary page + // (not sure if it always do it, or just in some cases), and the + // TabWindowManager actually count this temporary tab, even though we can't + // see it when we enumerate the kIeTabWindowClass windows. + ListValue tabs_list; + for (size_t index = 0; index < tab_windows.size(); ++index) { + HWND tab_window = tab_windows[index]; + long tab_index = -1; + hr = manager->IndexFromHWND(tab_window, &tab_index); + if (SUCCEEDED(hr)) { + tabs_list.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_window))); + tabs_list.Append(Value::CreateIntegerValue(static_cast<int>(tab_index))); + // The tab window may have died by the time we get here. + // Simply ignore that tab in this case. + } else if (::IsWindow(tab_window)) { + // But if it's still alive, then something wrong happened. + return hr; + } + } + std::string tabs_json; + base::JSONWriter::Write(&tabs_list, false, &tabs_json); + *tab_list = ::SysAllocString(CA2W(tabs_json.c_str())); + if (*tab_list == NULL) { + return E_OUTOFMEMORY; + } + return S_OK; +} + +STDMETHODIMP CeeeExecutor::UpdateWindow( + long left, long top, long width, long height, + CeeeWindowInfo* window_info) { + DCHECK(window_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Any part of the window rect can optionally be set by the caller. + // We need the original rect if only some dimensions are set by caller. + RECT rect = { 0 }; + BOOL success = ::GetWindowRect(hwnd_, &rect); + DCHECK(success); + + if (left == -1) { + left = rect.left; + } + if (top == -1) { + top = rect.top; + } + if (width == -1) { + width = rect.right - left; + } + if (height == -1) { + height = rect.bottom - top; + } + + // In IE8 this would yield ERROR_ACCESS_DENIED when called from another + // thread/process and protected mode is enabled, because the process owning + // the frame window is medium integrity. See UIPI in MSDN. + // So this is why we must do this via an injected executor. + success = ::MoveWindow(hwnd_, left, top, width, height, TRUE); + DCHECK(success) << "Failed to move the window to the update rect. " << + com::LogWe(); + return GetWindow(FALSE, window_info); +} + +STDMETHODIMP CeeeExecutor::RemoveWindow() { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + return manager->CloseAllTabs(); +} + +STDMETHODIMP CeeeExecutor::GetTabInfo(CeeeTabInfo* tab_info) { + DCHECK(tab_info); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + // Zero the info. + ZeroMemory(tab_info, sizeof(CeeeTabInfo)); + + CComPtr<IFrameEventHandlerHost> frame_handler_host; + hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " << + com::LogHr(hr); + return hr; + } + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + hr = frame_handler_host->GetReadyState(&ready_state); + if (FAILED(hr)) { + NOTREACHED() << "Can't get ReadyState, Wazzup???. " << com::LogHr(hr); + return hr; + } + + tab_info->status = kCeeeTabStatusComplete; + if (ready_state != READYSTATE_COMPLETE) { + // Chrome only has two states, so all incomplete states are "loading". + tab_info->status = kCeeeTabStatusLoading; + } + + CComPtr<IWebBrowser2> browser; + hr = frame_handler_host->GetTopLevelBrowser(&browser); + if (FAILED(hr)) { + NOTREACHED(); + return hr; + } + + hr = browser->get_LocationURL(&tab_info->url); + DCHECK(SUCCEEDED(hr)) << "get_LocationURL()" << com::LogHr(hr); + + hr = browser->get_LocationName(&tab_info->title); + DCHECK(SUCCEEDED(hr)) << "get_LocationName()" << com::LogHr(hr); + + // TODO(mad@chromium.org): Favicon support (see Chrome + // implementation, kFavIconUrlKey). AFAJoiCT, this is only set if + // there is a <link rel="icon" ...> tag, so we could parse this out + // of the IHTMLDocument2::get_links() collection. + tab_info->fav_icon_url = NULL; + return S_OK; +} + +STDMETHODIMP CeeeExecutor::GetTabIndex(CeeeWindowHandle tab, long* index) { + DCHECK(index); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), index); + DCHECK(SUCCEEDED(hr)) << "Couldn't get index for tab: " << + tab << ", " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::MoveTab(CeeeWindowHandle tab, long index) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long src_index = 0; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &src_index); + if (FAILED(hr)) { + NOTREACHED() << "Failed IndexFromHWND " << com::LogHr(hr); + return hr; + } + + LONG num_tabs = -1; + hr = manager->GetCount(&num_tabs); + if (FAILED(hr)) { + NOTREACHED() << "Failed to GetCount " << com::LogHr(hr); + return hr; + } + + // Clamp new index (as per Chrome implementation) so that extension authors + // can for convenience sakes use index=999 (or some such) to move the tab + // to the far right. + if (index >= num_tabs) { + index = num_tabs - 1; + } + + // Clamp current index so we can move newly-created tabs easily. + if (src_index >= num_tabs) { + src_index = num_tabs - 1; + } + + if (index == src_index) + return S_FALSE; // nothing to be done + + scoped_ptr<TabWindow> dest_tab; + hr = manager->GetItemWrapper(index, &dest_tab); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetItem or QI on dest tab " << com::LogHr(hr); + return hr; + } + + long dest_id = -1; + hr = dest_tab->GetID(&dest_id); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetID on dest tab " << com::LogHr(hr); + return hr; + } + + scoped_ptr<TabWindow> moving_tab; + hr = manager->GetItemWrapper(src_index, &moving_tab); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetItem or QI on moving tab " << com::LogHr(hr); + return hr; + } + + long moving_id = -1; + hr = moving_tab->GetID(&moving_id); + if (FAILED(hr)) { + NOTREACHED() << "Failed GetID on moving tab " << com::LogHr(hr); + return hr; + } + + hr = manager->RepositionTab(moving_id, dest_id, 0); + if (FAILED(hr)) { + NOTREACHED() << "Failed to reposition tab " << com::LogHr(hr); + return hr; + } + + return hr; +} + +STDMETHODIMP CeeeExecutor::Navigate(BSTR url, long flags, BSTR target) { + DCHECK(url); + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + CComPtr<IWebBrowser2> tab_browser; + hr = GetWebBrowser(&tab_browser); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get browser " << com::LogHr(hr); + return hr; + } + + CComBSTR current_url; + hr = tab_browser->get_LocationURL(¤t_url); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get URL " << com::LogHr(hr); + return hr; + } + + if (current_url == url && + 0 != lstrcmpW(L"_blank", com::ToString(target))) { + LOG(INFO) << "Got update request, but URL & target is unchanged: " << url; + return S_FALSE; + } + + hr = tab_browser->Navigate(url, &CComVariant(flags), &CComVariant(target), + &CComVariant(), &CComVariant()); + // We don't DCHECK here since there are cases where we get an error + // 0x800700aa "The requested resource is in use." if the main UI + // thread is currently blocked... and sometimes... it is blocked by + // us... if we are too slow to respond (e.g. because too busy + // navigating when the user performs an extension action that causes + // navigation again and again and again)... So we might as well + // abandon ship and let the UI thread be happy... + LOG_IF(ERROR, FAILED(hr)) << "Failed to navigate tab: " << hwnd_ << + " to " << url << ". " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::RemoveTab(CeeeWindowHandle tab) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long index = -1; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &index); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get index of tab " << com::LogHr(hr); + return hr; + } + + scoped_ptr<TabWindow> tab_item; + hr = manager->GetItemWrapper(index, &tab_item); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get tab object " << com::LogHr(hr); + return hr; + } + + hr = tab_item->Close(); + DCHECK(SUCCEEDED(hr)) << "Failed to close tab " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::SelectTab(CeeeWindowHandle tab) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + scoped_ptr<TabWindowManager> manager; + hr = CreateTabWindowManager(hwnd_, &manager); + DCHECK(SUCCEEDED(hr)) << "Failed to initialize TabWindowManager."; + if (FAILED(hr)) { + return hr; + } + + long index = -1; + hr = manager->IndexFromHWND(reinterpret_cast<HWND>(tab), &index); + if (FAILED(hr)) { + NOTREACHED() << "Failed to get index of tab, wnd=" << + std::hex << hwnd_ << " " << com::LogHr(hr); + return hr; + } + + hr = manager->SelectTab(index); + DCHECK(SUCCEEDED(hr)) << "Failed to select window, wnd=" << + std::hex << hwnd_ << " " << com::LogHr(hr); + return hr; +} + +STDMETHODIMP CeeeExecutor::InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) { + HRESULT hr = EnsureWindowThread(); + if (FAILED(hr)) { + return hr; + } + + CComPtr<IFrameEventHandlerHost> frame_handler_host; + hr = GetSite(IID_IFrameEventHandlerHost, + reinterpret_cast<void**>(&frame_handler_host)); + if (FAILED(hr)) { + NOTREACHED() << "No frame event handler host for executor. " + << com::LogHr(hr); + return hr; + } + + hr = frame_handler_host->InsertCode(code, file, all_frames, type); + if (FAILED(hr)) { + NOTREACHED() << "Failed to insert code. " << com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT CeeeExecutor::GetCookieValue(BSTR url, BSTR name, BSTR* value) { + DCHECK(value); + if (!value) + return E_POINTER; + + // INTERNET_COOKIE_HTTPONLY only works for IE8+. + DWORD flags = 0; + if (ie_util::GetIeVersion() > ie_util::IEVERSION_IE7) + flags |= INTERNET_COOKIE_HTTPONLY; + + // First find out the size of the cookie data. + DWORD size = 0; + BOOL cookie_found = ::InternetGetCookieExW( + url, name, NULL, &size, flags, NULL); + if (!cookie_found) { + if (::GetLastError() == ERROR_NO_MORE_ITEMS) + return S_FALSE; + else + return E_FAIL; + } else if (size == 0) { + return E_FAIL; + } + + // Now retrieve the data. + std::vector<wchar_t> cookie_data(size + 1); + cookie_found = ::InternetGetCookieExW( + url, name, &cookie_data[0], &size, flags, NULL); + DCHECK(cookie_found); + if (!cookie_found) + return E_FAIL; + + // Copy the data to the output parameter. + cookie_data[size] = 0; + std::wstring cookie_data_string(&cookie_data[0], size); + std::wstring data_prefix(name); + data_prefix.append(L"="); + if (cookie_data_string.find(data_prefix) != 0) { + DCHECK(false) << "The cookie name or data format does not match the " + << "expected 'name=value'. Name: " << name << ", Data: " + << cookie_data_string; + return E_FAIL; + } + *value = ::SysAllocString( + cookie_data_string.substr(data_prefix.size()).c_str()); + return S_OK; +} + +STDMETHODIMP CeeeExecutor::GetCookie(BSTR url, BSTR name, + CeeeCookieInfo* cookie_info) { + DCHECK(cookie_info); + if (!cookie_info) + return E_POINTER; + HRESULT hr = GetCookieValue(url, name, &cookie_info->value); + if (hr == S_OK) { + cookie_info->name = ::SysAllocString(name); + } + DCHECK(hr == S_OK || cookie_info->value == NULL); + return hr; +} + +void CeeeExecutor::set_cookie_store_is_registered(bool is_registered) { + g_cookie_store_is_registered = is_registered; +} + +STDMETHODIMP CeeeExecutor::RegisterCookieStore() { + set_cookie_store_is_registered(true); + return S_OK; +} + +HRESULT CeeeExecutor::EnsureWindowThread() { + if (!window_utils::IsWindowThread(hwnd_)) { + LOG(ERROR) << "Executor not running in appropriate thread for window: " << + hwnd_; + return E_UNEXPECTED; + } + + return S_OK; +} + +STDMETHODIMP CeeeExecutor::CookieStoreIsRegistered() { + return g_cookie_store_is_registered ? S_OK : S_FALSE; +} + +STDMETHODIMP CeeeExecutor::SetExtensionId(BSTR extension_id) { + DCHECK(extension_id); + if (extension_id == NULL) + return E_FAIL; + + WideToUTF8(extension_id, SysStringLen(extension_id), &extension_id_); + return S_OK; +} + +STDMETHODIMP CeeeExecutor::ShowInfobar(BSTR url, + CeeeWindowHandle* window_handle) { + DCHECK(infobar_manager_ != NULL) << "infobar_manager_ is not initialized"; + if (infobar_manager_ == NULL) + return E_FAIL; + + // Consider infobar navigation to an empty url as the request to hide it. + // Note that this is not a part of the spec so it is up to the implementation + // how to treat this. + size_t url_string_length = SysStringLen(url); + if (0 == url_string_length) { + infobar_manager_->HideAll(); + return S_OK; + } + + // Translate relative path to the absolute path using our extension URL + // as the root. + std::string url_utf8; + WideToUTF8(url, url_string_length, &url_utf8); + if (extension_id_.empty()) { + LOG(ERROR) << "Extension id is not set before the request to show infobar."; + } else { + url_utf8 = ResolveURL( + StringPrintf("chrome-extension://%s", extension_id_.c_str()), url_utf8); + } + std::wstring full_url; + UTF8ToWide(url_utf8.c_str(), url_utf8.size(), &full_url); + + // Show and navigate the infobar window. + HRESULT hr = infobar_manager_->Show(infobar_api::TOP_INFOBAR, + kMaxInfobarHeight, full_url, true); + if (SUCCEEDED(hr) && window_handle != NULL) { + *window_handle = reinterpret_cast<CeeeWindowHandle>( + window_utils::GetTopLevelParent(hwnd_)); + } + + return hr; +} + +STDMETHODIMP CeeeExecutor::OnTopFrameBeforeNavigate(BSTR url) { + DCHECK(infobar_manager_ != NULL) << "infobar_manager_ is not initialized"; + if (infobar_manager_ == NULL) + return E_FAIL; + + // According to the specification, tab navigation closes the infobar. + infobar_manager_->HideAll(); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/executor.h b/ceee/ie/plugin/bho/executor.h new file mode 100644 index 0000000..379ba32 --- /dev/null +++ b/ceee/ie/plugin/bho/executor.h @@ -0,0 +1,166 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// CeeeExecutor & CeeeExecutorCreator implementation, interfaces to +// execute code in other threads which can be running in other another process. + +#ifndef CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ +#define CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <string.h> + +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/infobar_manager.h" +#include "ceee/ie/plugin/toolband/resource.h" + +#include "toolband.h" // NOLINT + +struct IWebBrowser2; +namespace infobar_api { + class InfobarManager; +}; + +// The executor creator hooks itself in the destination thread where +// the executor will then be created and register in the CeeeBroker. + +// The creator of CeeeExecutors. +class ATL_NO_VTABLE CeeeExecutorCreator + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<CeeeExecutorCreator, + &CLSID_CeeeExecutorCreator>, + public ICeeeExecutorCreator { + public: + CeeeExecutorCreator(); + void FinalRelease(); + + DECLARE_REGISTRY_RESOURCEID(IDR_EXECUTOR_CREATOR) + + DECLARE_NOT_AGGREGATABLE(CeeeExecutorCreator) + BEGIN_COM_MAP(CeeeExecutorCreator) + COM_INTERFACE_ENTRY(ICeeeExecutorCreator) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + + // @name ICeeeExecutorCreator implementation. + // @{ + STDMETHOD(CreateWindowExecutor)(long thread_id, CeeeWindowHandle window); + STDMETHOD(Teardown)(long thread_id); + // @} + + protected: + // The registered message we use to communicate with the destination thread. + static const UINT kCreateWindowExecutorMessage; + + // The function that will be hooked in the destination thread. + // See http://msdn.microsoft.com/en-us/library/ms644981(VS.85).aspx + // for more details. + static LRESULT CALLBACK GetMsgProc(int code, WPARAM wparam, LPARAM lparam); + + // We must remember the hook so that we can unhook when we are done. + HHOOK hook_; + + // We can only work for one thread at a time. Used to validate that the + // call to ICeeeExecutorCreator::Teardown are balanced to a previous call + // to ICeeeExecutorCreator::CreateExecutor. + long current_thread_id_; +}; + +// The executor object that is instantiated in the destination thread and +// then called to... execute stuff... +class ATL_NO_VTABLE CeeeExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<CeeeExecutor, &CLSID_CeeeExecutor>, + public IObjectWithSiteImpl<CeeeExecutor>, + public ICeeeWindowExecutor, + public ICeeeTabExecutor, + public ICeeeCookieExecutor, + public ICeeeInfobarExecutor { + public: + DECLARE_REGISTRY_RESOURCEID(IDR_EXECUTOR) + + DECLARE_NOT_AGGREGATABLE(CeeeExecutor) + BEGIN_COM_MAP(CeeeExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(ICeeeWindowExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(ICeeeCookieExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT() + DECLARE_CLASSFACTORY() + + // @name ICeeeWindowExecutor implementation. + // @{ + STDMETHOD(Initialize)(CeeeWindowHandle hwnd); + STDMETHOD(GetWindow)(BOOL populate_tabs, CeeeWindowInfo* window_info); + STDMETHOD(GetTabs)(BSTR* tab_list); + STDMETHOD(UpdateWindow)(long left, long top, long width, long height, + CeeeWindowInfo* window_info); + STDMETHOD(RemoveWindow)(); + STDMETHOD(GetTabIndex)(CeeeWindowHandle tab, long* index); + STDMETHOD(MoveTab)(CeeeWindowHandle tab, long index); + STDMETHOD(RemoveTab)(CeeeWindowHandle tab); + STDMETHOD(SelectTab)(CeeeWindowHandle tab); + // @} + + // @name ICeeeTabExecutor implementation. + // @{ + // Initialize was already declared in ICeeeWindowExecutor, so we don't + // add it here, even if it's part of the interface. + STDMETHOD(GetTabInfo)(CeeeTabInfo* tab_info); + STDMETHOD(Navigate)(BSTR url, long flags, BSTR target); + STDMETHOD(InsertCode)(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type); + // @} + + // @name ICeeeCookieExecutor implementation. + // @{ + STDMETHOD(GetCookie)(BSTR url, BSTR name, CeeeCookieInfo* cookie_info); + STDMETHOD(RegisterCookieStore)(); + STDMETHOD(CookieStoreIsRegistered)(); + // @} + + // @name ICeeeInfobarExecutor implementation. + // @{ + STDMETHOD(SetExtensionId)(BSTR extension_id); + STDMETHOD(ShowInfobar)(BSTR url, CeeeWindowHandle* window_handle); + STDMETHOD(OnTopFrameBeforeNavigate)(BSTR url); + // @} + + CeeeExecutor() : hwnd_(NULL) {} + + protected: + // Get the IWebBrowser2 interface of the + // frame event host that was set as our site. + virtual HRESULT GetWebBrowser(IWebBrowser2** browser); + + // Used via EnumChildWindows to get all tabs. + static BOOL CALLBACK GetTabsEnumProc(HWND window, LPARAM param); + + // Ensure we're running inside the right thread. + HRESULT EnsureWindowThread(); + + // The HWND of the tab/window we are associated to. + HWND hwnd_; + + // Extension id. + std::string extension_id_; + + // Get the value of the cookie with the given name, associated with the given + // URL. Returns S_FALSE if the cookie does not exist, and returns an error + // code if something unexpected occurs. + virtual HRESULT GetCookieValue(BSTR url, BSTR name, BSTR* value); + + // Mainly for unit testing purposes. + void set_cookie_store_is_registered(bool is_registered); + + // Instance of InfobarManager for the tab associated with the thread to which + // the executor is attached. + scoped_ptr<infobar_api::InfobarManager> infobar_manager_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_EXECUTOR_H_ diff --git a/ceee/ie/plugin/bho/executor.rgs b/ceee/ie/plugin/bho/executor.rgs new file mode 100644 index 0000000..4fed5bc --- /dev/null +++ b/ceee/ie/plugin/bho/executor.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{057FCFE3-F872-483d-86B0-0430E375E41F}' = s 'Google CEEE Executor' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + } + } +}
\ No newline at end of file diff --git a/ceee/ie/plugin/bho/executor_creator.rgs b/ceee/ie/plugin/bho/executor_creator.rgs new file mode 100644 index 0000000..3a81441 --- /dev/null +++ b/ceee/ie/plugin/bho/executor_creator.rgs @@ -0,0 +1,9 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{4A562910-2D54-4e98-B87F-D4A7F5F5D0B9}' = s 'Google CEEE Executor Creator' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Free' + } + } + } +} diff --git a/ceee/ie/plugin/bho/executor_unittest.cc b/ceee/ie/plugin/bho/executor_unittest.cc new file mode 100644 index 0000000..e260fd3 --- /dev/null +++ b/ceee/ie/plugin/bho/executor_unittest.cc @@ -0,0 +1,1080 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Executor implementation unit tests. + +// MockWin32 must not be included after atlwin, which is included by some +// headers in here, so we need to put it at the top: +#include "ceee/testing/utils/mock_win32.h" // NOLINT + +#include <wininet.h> + +#include <vector> + +#include "ceee/ie/broker/cookie_api_module.h" +#include "ceee/ie/broker/common_api_module.h" +#include "ceee/ie/broker/tab_api_module.h" +#include "ceee/ie/broker/window_api_module.h" +#include "ceee/ie/common/ie_util.h" +#include "ceee/ie/common/mock_ie_tab_interfaces.h" +#include "ceee/ie/plugin/bho/executor.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/mock_static.h" +#include "ceee/testing/utils/mock_window_utils.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::_; +using testing::AddRef; +using testing::CopyInterfaceToArgument; +using testing::CopyBSTRToArgument; +using testing::InstanceCountMixin; +using testing::Invoke; +using testing::IsNull; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; + +const int kGoodWindowId = 42; +const HWND kGoodWindow = reinterpret_cast<HWND>(kGoodWindowId); +const int kOtherGoodWindowId = 84; +const HWND kOtherGoodWindow = reinterpret_cast<HWND>(kOtherGoodWindowId); +const int kTabIndex = 26; +const int kOtherTabIndex = 62; + +const wchar_t* kUrl1 = L"http://www.google.com"; +const wchar_t* kUrl2 = L"http://myintranet"; +const wchar_t* kTitle1 = L"MyLord"; +const wchar_t* kTitle2 = L"Your MADness"; + +class TestingMockExecutorCreatorTeardown + : public CeeeExecutorCreator, + public InitializingCoClass< + StrictMock<TestingMockExecutorCreatorTeardown>> { + public: + // TODO(mad@chromium.org): Add reference counting testing/validation. + TestingMockExecutorCreatorTeardown() { + hook_ = reinterpret_cast<HHOOK>(1); + current_thread_id_ = 42L; + } + HRESULT Initialize(StrictMock<TestingMockExecutorCreatorTeardown>** self) { + // Yes, this seems fishy, but it is called from InitializingCoClass + // which does it on the class we pass it as a template, so we are OK. + *self = static_cast<StrictMock<TestingMockExecutorCreatorTeardown>*>(this); + return S_OK; + } + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Teardown, HRESULT(long)); +}; + +TEST(ExecutorCreator, ProperTearDownOnDestruction) { + StrictMock<TestingMockExecutorCreatorTeardown>* executor_creator = NULL; + CComPtr<ICeeeExecutorCreator> executor_creator_keeper; + ASSERT_HRESULT_SUCCEEDED(StrictMock<TestingMockExecutorCreatorTeardown>:: + CreateInitialized(&executor_creator, &executor_creator_keeper)); + EXPECT_CALL(*executor_creator, Teardown(42L)).WillOnce(Return(S_OK)); + // The Release of the last reference should call FinalRelease. +} + +// Mock object for some functions used for hooking. +MOCK_STATIC_CLASS_BEGIN(MockHooking) + MOCK_STATIC_INIT_BEGIN(MockHooking) + MOCK_STATIC_INIT(SetWindowsHookEx); + MOCK_STATIC_INIT(UnhookWindowsHookEx); + MOCK_STATIC_INIT(PostThreadMessage); + MOCK_STATIC_INIT(PeekMessage); + MOCK_STATIC_INIT(CallNextHookEx); + MOCK_STATIC_INIT(CoCreateInstance); + MOCK_STATIC_INIT_END() + + MOCK_STATIC4(HHOOK, CALLBACK, SetWindowsHookEx, int, HOOKPROC, HINSTANCE, + DWORD); + MOCK_STATIC1(BOOL, CALLBACK, UnhookWindowsHookEx, HHOOK); + MOCK_STATIC4(BOOL, CALLBACK, PostThreadMessage, DWORD, UINT, WPARAM, LPARAM); + MOCK_STATIC5(BOOL, CALLBACK, PeekMessage, LPMSG, HWND, UINT, UINT, UINT); + MOCK_STATIC4(LRESULT, CALLBACK, CallNextHookEx, HHOOK, int, WPARAM, LPARAM); + MOCK_STATIC5(HRESULT, CALLBACK, CoCreateInstance, REFCLSID, LPUNKNOWN, + DWORD, REFIID, LPVOID*); +MOCK_STATIC_CLASS_END(MockHooking) + +class TestingExecutorCreator : public CeeeExecutorCreator { + public: + // TODO(mad@chromium.org): Add reference counting testing/validation. + STDMETHOD_(ULONG, AddRef)() { return 1; } + STDMETHOD_(ULONG, Release)() { return 1; } + STDMETHOD (QueryInterface)(REFIID, LPVOID*) { return S_OK; } + + // Accessorize... :-) + long current_thread_id() const { return current_thread_id_; } + void set_current_thread_id(long thread_id) { current_thread_id_ = thread_id; } + HHOOK hook() const { return hook_; } + void set_hook(HHOOK hook) { hook_ = hook; } + + // Publicize... + using CeeeExecutorCreator::kCreateWindowExecutorMessage; + using CeeeExecutorCreator::GetMsgProc; +}; + +TEST(ExecutorCreator, CreateExecutor) { + testing::LogDisabler no_dchecks; + + // Start with hooking failure. + StrictMock<MockHooking> mock_hooking; + EXPECT_CALL(mock_hooking, SetWindowsHookEx(WH_GETMESSAGE, _, _, 42L)). + WillOnce(Return(static_cast<HHOOK>(NULL))); + TestingExecutorCreator executor_creator; + EXPECT_HRESULT_FAILED(executor_creator.CreateWindowExecutor(42L, 42L)); + EXPECT_EQ(0L, executor_creator.current_thread_id()); + EXPECT_EQ(NULL, executor_creator.hook()); + + // Then succeed hooking but fail message posting. + EXPECT_CALL(mock_hooking, SetWindowsHookEx(WH_GETMESSAGE, _, _, 42L)). + WillRepeatedly(Return(reinterpret_cast<HHOOK>(1))); + EXPECT_CALL(mock_hooking, PostThreadMessage(42L, _, 0, 0)). + WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_ACCESS); + EXPECT_HRESULT_FAILED(executor_creator.CreateWindowExecutor(42L, 0L)); + ::SetLastError(ERROR_SUCCESS); + + // Success!!! + EXPECT_CALL(mock_hooking, PostThreadMessage(42L, _, 0, 0)). + WillRepeatedly(Return(TRUE)); + EXPECT_HRESULT_SUCCEEDED(executor_creator.CreateWindowExecutor(42L, 0L)); +} + +TEST(ExecutorCreator, Teardown) { + testing::LogDisabler no_dchecks; + + // Start with nothing to do. + TestingExecutorCreator executor_creator; + EXPECT_HRESULT_SUCCEEDED(executor_creator.Teardown(0)); + + // OK check that we properly unhook now... + StrictMock<MockHooking> mock_hooking; + HHOOK fake_hook = reinterpret_cast<HHOOK>(1); + EXPECT_CALL(mock_hooking, UnhookWindowsHookEx(fake_hook)). + WillOnce(Return(TRUE)); + executor_creator.set_current_thread_id(42L); + executor_creator.set_hook(fake_hook); + EXPECT_HRESULT_SUCCEEDED(executor_creator.Teardown(42L)); + EXPECT_EQ(0L, executor_creator.current_thread_id()); + EXPECT_EQ(NULL, executor_creator.hook()); +} + +class ExecutorCreatorTest : public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED(testing::MockWindowExecutor::CreateInitialized( + &executor_, &executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED(testing::MockBroker::CreateInitialized( + &broker_, &broker_keeper_)); + } + + virtual void TearDown() { + executor_ = NULL; + executor_keeper_.Release(); + + broker_ = NULL; + broker_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + protected: + testing::MockWindowExecutor* executor_; + CComPtr<ICeeeWindowExecutor> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; +}; + +TEST_F(ExecutorCreatorTest, GetMsgProc) { + testing::LogDisabler no_dchecks; + + // Start with nothing to do. + StrictMock<MockHooking> mock_hooking; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + + TestingExecutorCreator executor_creator; + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_SKIP, 0, 0)); + + // NULL message. + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, 0)); + + // Not our message. + MSG message; + message.message = WM_TIMER; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).WillOnce(Return(0)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, + reinterpret_cast<LPARAM>(&message))); + + // OK check our own code paths now. + message.message = executor_creator.kCreateWindowExecutorMessage; + EXPECT_CALL(mock_hooking, CallNextHookEx(_, _, _, _)).Times(0); + + // Not a PM_REMOVE message, delegates to PeekMessage. + EXPECT_CALL(mock_hooking, PeekMessage(_, _, _, _, _)).WillOnce(Return(TRUE)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, 0, + reinterpret_cast<LPARAM>(&message))); + + // With a PM_REMOVE, we get the job done. + // But lets see if we can silently handle a CoCreateInstance Failure + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Now fail getting the broker registrar. + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(Return(REGDB_E_CLASSNOTREG)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Now fail the registration itself. + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(DoAll(SetArgumentPointee<4>(broker_keeper_.p), + AddRef(broker_keeper_.p), Return(S_OK))); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + DWORD current_thread_id = ::GetCurrentThreadId(); + EXPECT_CALL(*broker_, RegisterWindowExecutor(current_thread_id, + executor_keeper_.p)).WillOnce(Return(E_FAIL)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); + + // Success!!! + message.lParam = reinterpret_cast<LPARAM>(kGoodWindow); + EXPECT_CALL(mock_hooking, CoCreateInstance(_, _, _, _, _)). + WillOnce(DoAll(SetArgumentPointee<4>(executor_keeper_.p), + AddRef(executor_keeper_.p), Return(S_OK))). + WillOnce(DoAll(SetArgumentPointee<4>(broker_keeper_.p), + AddRef(broker_keeper_.p), Return(S_OK))); + EXPECT_CALL(*executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(*broker_, RegisterWindowExecutor(current_thread_id, + executor_keeper_.p)).WillOnce(Return(S_OK)); + EXPECT_EQ(0, executor_creator.GetMsgProc(HC_ACTION, PM_REMOVE, + reinterpret_cast<LPARAM>(&message))); +} + +MOCK_STATIC_CLASS_BEGIN(MockWinInet) + MOCK_STATIC_INIT_BEGIN(MockWinInet) + MOCK_STATIC_INIT(InternetGetCookieExW); + MOCK_STATIC_INIT_END() + + MOCK_STATIC6(BOOL, CALLBACK, InternetGetCookieExW, LPCWSTR, LPCWSTR, LPWSTR, + LPDWORD, DWORD, LPVOID); +MOCK_STATIC_CLASS_END(MockWinInet) + + +// Mock object for some functions used for hooking. +MOCK_STATIC_CLASS_BEGIN(MockIeUtil) + MOCK_STATIC_INIT_BEGIN(MockIeUtil) + MOCK_STATIC_INIT2(ie_util::GetIeVersion, GetIeVersion); + MOCK_STATIC_INIT_END() + + MOCK_STATIC0(ie_util::IeVersion, , GetIeVersion); +MOCK_STATIC_CLASS_END(MockIeUtil) + +class TestingExecutor + : public CeeeExecutor, + public InitializingCoClass<TestingExecutor> { + public: + HRESULT Initialize(TestingExecutor** self) { + *self = this; + return S_OK; + } + static void set_tab_windows(std::vector<HWND> tab_windows) { + tab_windows_ = tab_windows; + } + void set_id(HWND hwnd) { + hwnd_ = hwnd; + } + static BOOL MockEnumChildWindows(HWND, WNDENUMPROC, LPARAM p) { + std::vector<HWND>* tab_windows = reinterpret_cast<std::vector<HWND>*>(p); + *tab_windows = tab_windows_; + return TRUE; + } + static void set_cookie_data(const std::wstring& cookie_data) { + cookie_data_ = cookie_data; + } + static BOOL MockInternetGetCookieExW(LPCWSTR, LPCWSTR, LPWSTR data, + LPDWORD size, DWORD, LPVOID) { + EXPECT_TRUE(data != NULL); + EXPECT_TRUE(*size > cookie_data_.size()); + wcscpy_s(data, *size, cookie_data_.data()); + *size = cookie_data_.size() + 1; + return TRUE; + } + + MOCK_METHOD1(GetWebBrowser, HRESULT(IWebBrowser2** browser)); + HRESULT CallGetWebBrowser(IWebBrowser2** browser) { + return CeeeExecutor::GetWebBrowser(browser); + } + + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabs, HRESULT(BSTR*)); + HRESULT CallGetTabs(BSTR* tab_list) { + return CeeeExecutor::GetTabs(tab_list); + } + + MOCK_METHOD3(GetCookieValue, HRESULT(BSTR, BSTR, BSTR*)); + HRESULT CallGetCookieValue(BSTR url, BSTR name, BSTR* value) { + return CeeeExecutor::GetCookieValue(url, name, value); + } + + // Publicize... + using CeeeExecutor::GetTabsEnumProc; + using CeeeExecutor::MoveTab; + using CeeeExecutor::set_cookie_store_is_registered; + private: + static std::vector<HWND> tab_windows_; + static std::wstring cookie_data_; +}; +std::vector<HWND> TestingExecutor::tab_windows_; +std::wstring TestingExecutor::cookie_data_; + +// Override to handle DISPID_READYSTATE. +class ExecutorTests: public testing::Test { + public: + void SetUp() { + ASSERT_HRESULT_SUCCEEDED(TestingExecutor::CreateInitialized( + &executor_, &executor_keeper_)); + + browser_ = NULL; + manager_ = NULL; + } + + void MockBrowser() { + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance( + &browser_); + DCHECK(browser_ != NULL); + browser_keeper_ = browser_; + EXPECT_CALL(*executor_, GetWebBrowser(NotNull())). + WillRepeatedly(DoAll(CopyInterfaceToArgument<0>(browser_keeper_), + Return(S_OK))); + } + + void MockSite() { + ASSERT_HRESULT_SUCCEEDED( + testing::MockFrameEventHandlerHost::CreateInitializedIID( + &mock_site_, IID_IUnknown, &mock_site_keeper_)); + executor_->SetSite(mock_site_keeper_); + } + + void MockTabManager() { + CComObject<StrictMock<testing::MockITabWindowManagerIe7>>::CreateInstance( + &manager_); + DCHECK(manager_ != NULL); + manager_keeper_ = manager_; + EXPECT_CALL(ie_tab_interfaces_, TabWindowManagerFromFrame(_, _, _)). + WillRepeatedly(DoAll(AddRef(manager_keeper_.p), SetArgumentPointee<2>( + reinterpret_cast<void*>(manager_keeper_.p)), Return(S_OK))); + + EXPECT_CALL(*manager_, IndexFromHWND(_, _)).WillRepeatedly(DoAll( + SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + } + + void NotRunningInThisWindowThread(HWND window) { + EXPECT_CALL(mock_window_utils_, IsWindowThread(window)). + WillOnce(Return(false)); + } + + void RepeatedlyRunningInThisWindowThread(HWND window) { + EXPECT_CALL(mock_window_utils_, IsWindowThread(window)). + WillRepeatedly(Return(true)); + } + + void RepeatedlyRunningInThisParentWindowThread(HWND child_window, + HWND parent_window) { + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(child_window)). + WillRepeatedly(Return(parent_window)); + EXPECT_CALL(mock_window_utils_, IsWindowThread(parent_window)). + WillRepeatedly(Return(true)); + } + protected: + TestingExecutor* executor_; + StrictMock<testing::MockWindowUtils> mock_window_utils_; + + + // TODO(mad@chromium.org): We should standardize on the Mock COM + // objects creation. + // Using InitializingCoClass would probably be better. + CComObject<StrictMock<testing::MockIWebBrowser2>>* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + CComObject<StrictMock<testing::MockITabWindowManagerIe7>>* manager_; + + testing::MockFrameEventHandlerHost* mock_site_; + + StrictMock<testing::MockUser32> user32_; + StrictMock<testing::MockIeTabInterfaces> ie_tab_interfaces_; + + private: + CComPtr<IUnknown> executor_keeper_; + CComPtr<IUnknown> mock_site_keeper_; + CComPtr<ITabWindowManagerIe7> manager_keeper_; +}; + +TEST_F(ExecutorTests, GetWebBrowser) { + MockSite(); + CComPtr<IWebBrowser2> browser; + { + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->CallGetWebBrowser(&browser)); + } + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(DoAll(SetArgumentPointee<0>(browser_), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetWebBrowser(&browser)); + EXPECT_EQ(browser, browser.p); +} + +TEST_F(ExecutorTests, GetWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->GetWindow(FALSE, NULL)); + + // Fail getting the window RECT. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(user32_, GetForegroundWindow()). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_ACCESS); + common_api::WindowInfo window_info; + EXPECT_HRESULT_FAILED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + ::SetLastError(ERROR_SUCCESS); + + // Success without tab population. + RECT window_rect = {1, 2, 3, 5}; + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + EXPECT_HRESULT_SUCCEEDED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(TRUE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Try the not focused case and a bigger rect. + window_rect.left = 8; + window_rect.top = 13; + window_rect.right = 21; + window_rect.bottom = 34; + EXPECT_CALL(user32_, GetWindowRect(kOtherGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + // Different parent, means we are not focused. + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kOtherGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + executor_->set_id(kOtherGoodWindow); + EXPECT_HRESULT_SUCCEEDED(executor_->GetWindow(FALSE, &window_info)); + EXPECT_EQ(FALSE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Fail with tab population. We'll test tab population with GetTabs later. + // GetTabs will fail but at least we confirm that it gets called :-)... + EXPECT_CALL(*executor_, GetTabs(NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->GetWindow(TRUE, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); +} + +TEST_F(ExecutorTests, GetTabsEnumProc) { + std::vector<HWND> tab_windows; + HWND bad_window1 = reinterpret_cast<HWND>(666); + HWND bad_window2 = reinterpret_cast<HWND>(999); + EXPECT_CALL(mock_window_utils_, IsWindowClass(kGoodWindow, _)). + WillOnce(Return(true)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(kOtherGoodWindow, _)). + WillOnce(Return(true)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(bad_window1, _)). + WillOnce(Return(false)); + EXPECT_CALL(mock_window_utils_, IsWindowClass(bad_window2, _)). + WillOnce(Return(false)); + LPARAM param = reinterpret_cast<LPARAM>(&tab_windows); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(kGoodWindow, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(bad_window1, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(kOtherGoodWindow, param)); + EXPECT_TRUE(TestingExecutor::GetTabsEnumProc(bad_window2, param)); + EXPECT_EQ(2, tab_windows.size()); + EXPECT_EQ(kGoodWindow, tab_windows[0]); + EXPECT_EQ(kOtherGoodWindow, tab_windows[1]); +} + +TEST_F(ExecutorTests, GetTabs) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->CallGetTabs(NULL)); + + // We already tested the case where we don't have a tab manager. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + MockTabManager(); + EXPECT_CALL(user32_, EnumChildWindows(kGoodWindow, _, _)). + WillRepeatedly(Invoke(TestingExecutor::MockEnumChildWindows)); + + // No tabs case. + CComBSTR result; + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + EXPECT_STREQ(L"[]", result.m_str); + result.Empty(); + + static const int kBadWindowId = 21; + static const HWND kBadWindow = reinterpret_cast<HWND>(kBadWindowId); + + // Fail to get a tab index. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + std::vector<HWND> tab_windows; + tab_windows.push_back(kGoodWindow); + EXPECT_CALL(user32_, IsWindow(kGoodWindow)).WillOnce(Return(TRUE)); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_FAILED(executor_->CallGetTabs(&result)); + + // Successfully return 1 tab. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wchar_t expected_result[16]; + wnsprintf(expected_result, 16, L"[%d,%d]", kGoodWindowId, kTabIndex); + EXPECT_STREQ(expected_result, result.m_str); + result.Empty(); + + // Successfully return 2 tabs. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kOtherGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kOtherTabIndex), Return(S_OK))); + tab_windows.push_back(kOtherGoodWindow); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wnsprintf(expected_result, 16, L"[%d,%d,%d,%d]", kGoodWindowId, kTabIndex, + kOtherGoodWindowId, kOtherTabIndex); + EXPECT_STREQ(expected_result, result.m_str); + + // Successfully return 2 out of 3 tabs. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kOtherGoodWindow, NotNull())). + WillOnce(DoAll(SetArgumentPointee<1>(kOtherTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, IndexFromHWND(kBadWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_CALL(user32_, IsWindow(kBadWindow)).WillOnce(Return(FALSE)); + tab_windows.push_back(kBadWindow); + executor_->set_tab_windows(tab_windows); + EXPECT_HRESULT_SUCCEEDED(executor_->CallGetTabs(&result)); + wnsprintf(expected_result, 16, L"[%d,%d,%d,%d]", kGoodWindowId, kTabIndex, + kOtherGoodWindowId, kOtherTabIndex); + EXPECT_STREQ(expected_result, result.m_str); +} + +TEST_F(ExecutorTests, UpdateWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->UpdateWindow(0, 0, 0, 0, NULL)); + // No other failure path, go straight to success... + RepeatedlyRunningInThisWindowThread(kGoodWindow); + + RECT window_rect = {1, 2, 3, 5}; + EXPECT_CALL(user32_, GetWindowRect(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(window_rect), Return(TRUE))); + // These will be called from GetWindow which is called at the end of Update. + EXPECT_CALL(user32_, GetForegroundWindow()). + WillRepeatedly(Return(kGoodWindow)); + EXPECT_CALL(mock_window_utils_, GetTopLevelParent(kGoodWindow)). + WillRepeatedly(Return(kGoodWindow)); + // Try with no change at first. + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 2, 3, TRUE)).Times(1); + common_api::WindowInfo window_info; + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, -1, -1, &window_info)); + EXPECT_EQ(TRUE, window_info.focused); + EXPECT_EQ(window_rect.top, window_info.rect.top); + EXPECT_EQ(window_rect.left, window_info.rect.left); + EXPECT_EQ(window_rect.bottom, window_info.rect.bottom); + EXPECT_EQ(window_rect.right, window_info.rect.right); + EXPECT_EQ(NULL, window_info.tab_list); + + // Now try with some changes incrementally. + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 2, 30, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, -1, 30, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 2, 20, 3, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, -1, 20, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 1, 0, 2, 5, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + -1, 0, -1, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 0, 2, 3, 3, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + 0, -1, -1, -1, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); + + EXPECT_CALL(user32_, MoveWindow(kGoodWindow, 8, 13, 21, 34, TRUE)).Times(1); + EXPECT_HRESULT_SUCCEEDED(executor_->UpdateWindow( + 8, 13, 21, 34, &window_info)); + EXPECT_EQ(NULL, window_info.tab_list); +} + +TEST_F(ExecutorTests, RemoveWindow) { + testing::LogDisabler no_dchecks; + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->RemoveWindow()); + + // Now let the manager succeed. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + MockTabManager(); + EXPECT_CALL(*manager_, CloseAllTabs()).WillOnce(Return(S_OK)); + EXPECT_CALL(user32_, PostMessage(kGoodWindow, WM_CLOSE, 0, 0)). + Times(0); + EXPECT_HRESULT_SUCCEEDED(executor_->RemoveWindow()); +} + +TEST_F(ExecutorTests, GetTabInfo) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(NULL)); + + // Now can't get ready state. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(Return(E_FAIL)); + tab_api::TabInfo tab_info; + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(&tab_info)); + DCHECK_EQ((BSTR)NULL, tab_info.url); + DCHECK_EQ((BSTR)NULL, tab_info.title); + DCHECK_EQ((BSTR)NULL, tab_info.fav_icon_url); + + // And can't get the top level browser. + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_COMPLETE), Return(S_OK))); + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillOnce(Return(E_FAIL)); + tab_info.Clear(); + EXPECT_HRESULT_FAILED(executor_->GetTabInfo(&tab_info)); + DCHECK_EQ((BSTR)NULL, tab_info.url); + DCHECK_EQ((BSTR)NULL, tab_info.title); + DCHECK_EQ((BSTR)NULL, tab_info.fav_icon_url); + + // Success time! + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_COMPLETE), Return(S_OK))); + CComObject<StrictMock<testing::MockIWebBrowser2>>::CreateInstance(&browser_); + DCHECK(browser_ != NULL); + browser_keeper_ = browser_; + EXPECT_CALL(*mock_site_, GetTopLevelBrowser(NotNull())). + WillRepeatedly(DoAll(CopyInterfaceToArgument<0>(browser_keeper_), + Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationName(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kTitle1), Return(S_OK))); + + tab_info.Clear(); + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabInfo(&tab_info)); + EXPECT_STREQ(kUrl1, tab_info.url); + EXPECT_STREQ(kTitle1, tab_info.title); + EXPECT_EQ(kCeeeTabStatusComplete, tab_info.status); + + // With other values + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + // Reset the HWND + executor_->set_id(kOtherGoodWindow); + EXPECT_CALL(*mock_site_, GetReadyState(NotNull())).WillOnce(DoAll( + SetArgumentPointee<0>(READYSTATE_LOADING), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kUrl2), Return(S_OK))); + EXPECT_CALL(*browser_, get_LocationName(NotNull())). + WillOnce(DoAll(CopyBSTRToArgument<0>(kTitle2), Return(S_OK))); + + tab_info.Clear(); + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabInfo(&tab_info)); + EXPECT_STREQ(kUrl2, tab_info.url); + EXPECT_STREQ(kTitle2, tab_info.title); + EXPECT_EQ(kCeeeTabStatusLoading, tab_info.status); +} + +TEST_F(ExecutorTests, GetTabIndex) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + EXPECT_CALL(mock_window_utils_, IsWindowThread(kGoodWindow)). + WillOnce(Return(false)); + EXPECT_HRESULT_FAILED(executor_->GetTabIndex(kGoodWindowId, NULL)); + + // Success. + EXPECT_CALL(mock_window_utils_, IsWindowThread(kGoodWindow)). + WillOnce(Return(true)); + MockTabManager(); + long index = 0; + EXPECT_HRESULT_SUCCEEDED(executor_->GetTabIndex(kOtherGoodWindowId, &index)); + EXPECT_EQ(kTabIndex, index); +} + +TEST_F(ExecutorTests, Navigate) { + testing::LogDisabler no_dchecks; + MockSite(); + executor_->set_id(kGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kGoodWindow); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now make GetWebBrowser fail. + RepeatedlyRunningInThisWindowThread(kGoodWindow); + EXPECT_CALL(*executor_, GetWebBrowser(NotNull())). + WillRepeatedly(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now get URL fails. + MockBrowser(); + EXPECT_CALL(*browser_, get_LocationURL(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(NULL, 0, NULL)); + + // Now succeed by not needing to navigate since same URL. + EXPECT_CALL(*browser_, get_LocationURL(NotNull())). + WillRepeatedly(DoAll(CopyBSTRToArgument<0>(kUrl1), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->Navigate(CComBSTR(kUrl1), 0, NULL)); + + // And finally succeed completely. + EXPECT_CALL(*browser_, Navigate(_, _, _, _, _)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->Navigate(CComBSTR(kUrl2), 0, NULL)); + // And fail if navigate fails. + EXPECT_CALL(*browser_, Navigate(_, _, _, _, _)).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->Navigate(CComBSTR(kUrl2), 0, NULL)); +} + +TEST_F(ExecutorTests, RemoveTab) { + testing::LogDisabler no_dchecks; + MockSite(); + + // Since we're in a WindowExecutor and not a TabExecutor, we don't get our + // HWND and there's no call to parent. + executor_->set_id(kOtherGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Fail to get the tab index. + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_CALL(user32_, GetParent(_)).WillRepeatedly(Return(kGoodWindow)); + MockTabManager(); + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Fail to get the tab interface. + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, GetItem(kTabIndex, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); + + // Success. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance(&mock_tab); + CComPtr<ITabWindowIe7> mock_tab_holder(mock_tab); + EXPECT_CALL(*manager_, GetItem(kTabIndex, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab), AddRef(mock_tab), Return(S_OK))); + EXPECT_CALL(*mock_tab, Close()).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->RemoveTab(kGoodWindowId)); + + // Failure to close. + EXPECT_CALL(*mock_tab, Close()).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->RemoveTab(kGoodWindowId)); +} + +TEST_F(ExecutorTests, SelectTab) { + testing::LogDisabler no_dchecks; + MockSite(); + + executor_->set_id(kOtherGoodWindow); + + // Not running in appropriate thread. + NotRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); + + // Fail to get the tab index. + RepeatedlyRunningInThisWindowThread(kOtherGoodWindow); + EXPECT_CALL(user32_, GetParent(_)).WillRepeatedly(Return(kGoodWindow)); + MockTabManager(); + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); + + // Success! + EXPECT_CALL(*manager_, IndexFromHWND(kGoodWindow, NotNull())). + WillRepeatedly(DoAll(SetArgumentPointee<1>(kTabIndex), Return(S_OK))); + EXPECT_CALL(*manager_, SelectTab(kTabIndex)).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->SelectTab(kGoodWindowId)); + + // Failure to Select. + EXPECT_CALL(*manager_, SelectTab(kTabIndex)).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->SelectTab(kGoodWindowId)); +} + +TEST_F(ExecutorTests, MoveTab) { + testing::LogDisabler no_dchecks; + MockTabManager(); + MockSite(); + RepeatedlyRunningInThisWindowThread(kGoodWindow); + executor_->set_id(kGoodWindow); + + // Fail to get tab count. + EXPECT_CALL(*manager_, GetCount(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Nothing to be done. + LONG nb_tabs = 3; + EXPECT_CALL(*manager_, GetCount(NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<0>(nb_tabs), Return(S_OK))); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 2)); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 99)); + + // Fail to get the first tab interface. + EXPECT_CALL(*manager_, GetItem(0, NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the tab id. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab0; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance( + &mock_tab0); + CComPtr<ITabWindowIe7> mock_tab_holder0(mock_tab0); + EXPECT_CALL(*manager_, GetItem(0, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab0), AddRef(mock_tab0), Return(S_OK))); + EXPECT_CALL(*mock_tab0, GetID(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the second tab interface. + EXPECT_CALL(*mock_tab0, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, GetItem(nb_tabs - 1, NotNull())) + .WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to get the second tab id. + CComObject<StrictMock<testing::MockITabWindowIe7>>* mock_tab1; + CComObject<StrictMock<testing::MockITabWindowIe7>>::CreateInstance( + &mock_tab1); + CComPtr<ITabWindowIe7> mock_tab_holder1(mock_tab1); + EXPECT_CALL(*manager_, GetItem(nb_tabs - 1, NotNull())).WillRepeatedly(DoAll( + SetArgumentPointee<1>(mock_tab1), AddRef(mock_tab1), Return(S_OK))); + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Fail to reposition. + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kOtherGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, RepositionTab(kOtherGoodWindowId, kGoodWindowId, 0)) + .WillOnce(Return(E_FAIL)); + EXPECT_HRESULT_FAILED(executor_->MoveTab(kOtherGoodWindowId, 0)); + + // Success!! + EXPECT_CALL(*mock_tab1, GetID(NotNull())).WillRepeatedly( + DoAll(SetArgumentPointee<0>(kOtherGoodWindowId), Return(S_OK))); + EXPECT_CALL(*manager_, RepositionTab(kOtherGoodWindowId, kGoodWindowId, 0)) + .WillRepeatedly(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(executor_->MoveTab(kOtherGoodWindowId, 0)); +} + +TEST_F(ExecutorTests, GetCookieValue) { + testing::LogDisabler no_dchecks; + MockWinInet mock_wininet; + + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + // Bad parameters. + EXPECT_HRESULT_FAILED(executor_->CallGetCookieValue(url, name, NULL)); + + // Failure to get cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillOnce(Return(FALSE)); + ::SetLastError(ERROR_INVALID_PARAMETER); + CComBSTR value; + EXPECT_HRESULT_FAILED(executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Nonexistent cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillOnce(Return(FALSE)); + ::SetLastError(ERROR_NO_MORE_ITEMS); + EXPECT_EQ(S_FALSE, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Malformed cookie. + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, _, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, _, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + executor_->set_cookie_data(L"malformed_cookie_data"); + EXPECT_EQ(E_FAIL, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + executor_->set_cookie_data(L"AnotherCookie=FOOBAR"); + EXPECT_EQ(E_FAIL, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_EQ((BSTR)NULL, value); + + // Well-behaved cookie. + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_STREQ(L"1234567890", value); + executor_->set_cookie_data(L"=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, CComBSTR(L""), &value)); + EXPECT_STREQ(L"1234567890", value); + executor_->set_cookie_data(L"HELLOWORLD="); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + EXPECT_STREQ(L"", value); + executor_->set_cookie_data(L"="); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, CComBSTR(L""), &value)); + EXPECT_STREQ(L"", value); +} + +TEST_F(ExecutorTests, GetCookieValueFlagsByIeVersion) { + testing::LogDisabler no_dchecks; + MockWinInet mock_wininet; + MockIeUtil mock_ie_util; + + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + + // Test IE7 and below. + DWORD expected_flags = 0; + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, + expected_flags, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, + expected_flags, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE6)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + CComBSTR value; + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE7)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + // Test IE8 and above. + expected_flags = INTERNET_COOKIE_HTTPONLY; + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, IsNull(), _, + expected_flags, _)) + .WillRepeatedly(DoAll(SetArgumentPointee<3>(100), Return(TRUE))); + EXPECT_CALL(mock_wininet, InternetGetCookieExW(_, _, NotNull(), _, + expected_flags, _)) + .WillRepeatedly(Invoke(TestingExecutor::MockInternetGetCookieExW)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE8)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + + EXPECT_CALL(mock_ie_util, GetIeVersion()) + .WillOnce(Return(ie_util::IEVERSION_IE9)); + executor_->set_cookie_data(L"HELLOWORLD=1234567890"); + EXPECT_EQ(S_OK, executor_->CallGetCookieValue(url, name, &value)); + +} + +TEST_F(ExecutorTests, GetCookie) { + testing::LogDisabler no_dchecks; + CComBSTR url(L"http://foobar.com"); + CComBSTR name(L"HELLOWORLD"); + + EXPECT_HRESULT_FAILED(executor_->GetCookie(url, name, NULL)); + + // Failure to get cookie. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillOnce(Return(E_FAIL)); + cookie_api::CookieInfo cookie_info; + cookie_info.name = NULL; + cookie_info.value = NULL; + EXPECT_HRESULT_FAILED(executor_->GetCookie(url, name, &cookie_info)); + EXPECT_EQ((BSTR)NULL, cookie_info.name); + EXPECT_EQ((BSTR)NULL, cookie_info.value); + + // Nonexistent cookie. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillOnce(Return(S_FALSE)); + EXPECT_EQ(S_FALSE, executor_->GetCookie(url, name, &cookie_info)); + EXPECT_EQ((BSTR)NULL, cookie_info.name); + EXPECT_EQ((BSTR)NULL, cookie_info.value); + + // Success. + EXPECT_CALL(*executor_, GetCookieValue(_, _, NotNull())) + .WillRepeatedly(DoAll( + SetArgumentPointee<2>(::SysAllocString(L"abcde")), + Return(S_OK))); + EXPECT_EQ(S_OK, executor_->GetCookie(url, name, &cookie_info)); + EXPECT_STREQ(L"HELLOWORLD", cookie_info.name); + EXPECT_STREQ(L"abcde", cookie_info.value); + EXPECT_EQ(S_OK, executor_->GetCookie(url, CComBSTR("ABC"), &cookie_info)); + EXPECT_STREQ(L"ABC", cookie_info.name); + EXPECT_STREQ(L"abcde", cookie_info.value); +} + +TEST_F(ExecutorTests, RegisterCookieStore) { + testing::LogDisabler no_dchecks; + + executor_->set_cookie_store_is_registered(false); + EXPECT_EQ(S_FALSE, executor_->CookieStoreIsRegistered()); + EXPECT_HRESULT_SUCCEEDED(executor_->RegisterCookieStore()); + EXPECT_EQ(S_OK, executor_->CookieStoreIsRegistered()); + EXPECT_HRESULT_SUCCEEDED(executor_->RegisterCookieStore()); + EXPECT_EQ(S_OK, executor_->CookieStoreIsRegistered()); +} + +// TODO(vadimb@google.com): Add unit tests for infobar APIs. + +} // namespace diff --git a/ceee/ie/plugin/bho/extension_port_manager.cc b/ceee/ie/plugin/bho/extension_port_manager.cc new file mode 100644 index 0000000..9bc28f5 --- /dev/null +++ b/ceee/ie/plugin/bho/extension_port_manager.cc @@ -0,0 +1,193 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Extension port manager takes care of managing the state of +// connecting and connected ports. +#include "ceee/ie/plugin/bho/extension_port_manager.h" +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "ceee/ie/common/chrome_frame_host.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "chrome/browser/automation/extension_automation_constants.h" + +namespace ext = extension_automation_constants; + +ExtensionPortManager::ExtensionPortManager() { +} + +ExtensionPortManager::~ExtensionPortManager() { +} + +void ExtensionPortManager::Initialize(IChromeFrameHost* chrome_frame_host) { + chrome_frame_host_ = chrome_frame_host; +} + +void ExtensionPortManager::CloseAll(IContentScriptNativeApi* instance) { + DCHECK(instance != NULL); + + // TODO(siggi@chromium.org): Deal better with these cases. Connected + // ports probably ought to be closed, and connecting ports may + // need to hang around until we get the connected message, to be + // terminated at that point. + ConnectedPortMap::iterator it(connected_ports_.begin()); + while (it != connected_ports_.end()) { + if (it->second.instance = instance) { + connected_ports_.erase(it++); + } else { + ++it; + } + } + + ConnectingPortMap::iterator jt(connecting_ports_.begin()); + while (jt != connecting_ports_.end()) { + if (jt->second.instance = instance) { + connecting_ports_.erase(jt++); + } else { + ++jt; + } + } +} + +HRESULT ExtensionPortManager::OpenChannelToExtension( + IContentScriptNativeApi* instance, const std::string& extension, + const std::string& channel_name, Value* tab, int cookie) { + int connection_id = next_connection_id_++; + + // Prepare the connection request. + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + if (dict.get() == NULL) + return E_OUTOFMEMORY; + + dict->SetInteger(ext::kAutomationRequestIdKey, ext::OPEN_CHANNEL); + dict->SetInteger(ext::kAutomationConnectionIdKey, connection_id); + dict->SetString(ext::kAutomationExtensionIdKey, extension); + dict->SetString(ext::kAutomationChannelNameKey, channel_name); + dict->Set(ext::kAutomationTabJsonKey, tab); + + // JSON encode it. + std::string request_json; + base::JSONWriter::Write(dict.get(), false, &request_json); + // And fire it off. + HRESULT hr = PostMessageToHost(request_json, + ext::kAutomationPortRequestTarget); + if (FAILED(hr)) + return hr; + + ConnectingPort connecting_port = { instance, cookie }; + connecting_ports_[connection_id] = connecting_port; + + return S_OK; +} + +HRESULT ExtensionPortManager::PostMessage(int port_id, + const std::string& message) { + // Wrap the message for sending as a port request. + scoped_ptr<DictionaryValue> dict(new DictionaryValue()); + if (dict.get() == NULL) + return E_OUTOFMEMORY; + + dict->SetInteger(ext::kAutomationRequestIdKey, ext::POST_MESSAGE); + dict->SetInteger(ext::kAutomationPortIdKey, port_id); + dict->SetString(ext::kAutomationMessageDataKey, message); + + // JSON encode it. + std::string message_json; + base::JSONWriter::Write(dict.get(), false, &message_json); + + // And fire it off. + return PostMessageToHost(message_json, + std::string(ext::kAutomationPortRequestTarget)); +} + +void ExtensionPortManager::OnPortMessage(BSTR message) { + std::string message_json = CW2A(message); + scoped_ptr<Value> value(base::JSONReader::Read(message_json, true)); + if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) { + NOTREACHED(); + LOG(ERROR) << "Invalid message"; + return; + } + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + int request = -1; + if (!dict->GetInteger(ext::kAutomationRequestIdKey, &request)) { + NOTREACHED(); + LOG(ERROR) << "Request ID missing"; + return; + } + + if (request == ext::CHANNEL_OPENED) { + int connection_id = -1; + if (!dict->GetInteger(ext::kAutomationConnectionIdKey, &connection_id)) { + NOTREACHED(); + LOG(ERROR) << "Connection ID missing"; + return; + } + + int port_id = -1; + if (!dict->GetInteger(ext::kAutomationPortIdKey, &port_id)) { + NOTREACHED(); + LOG(ERROR) << "Port ID missing"; + return; + } + + ConnectingPortMap::iterator it(connecting_ports_.find(connection_id)); + if (it == connecting_ports_.end()) { + // TODO(siggi@chromium.org): This can happen legitimately on a + // race between connect and document unload. We should + // probably respond with a close port message here. + NOTREACHED(); + LOG(ERROR) << "No such connection id " << connection_id; + return; + } + ConnectingPort port = it->second; + connecting_ports_.erase(it); + // Did it connect successfully? + if (port_id != -1) + connected_ports_[port_id].instance = port.instance; + + port.instance->OnChannelOpened(port.cookie, port_id); + return; + } else if (request == ext::POST_MESSAGE) { + int port_id = -1; + if (!dict->GetInteger(ext::kAutomationPortIdKey, &port_id)) { + NOTREACHED(); + LOG(ERROR) << "No port id"; + return; + } + + std::string data; + if (!dict->GetString(ext::kAutomationMessageDataKey, &data)) { + NOTREACHED(); + LOG(ERROR) << "No message data"; + return; + } + + ConnectedPortMap::iterator it(connected_ports_.find(port_id)); + if (it == connected_ports_.end()) { + NOTREACHED(); + LOG(ERROR) << "No such port " << port_id; + return; + } + + it->second.instance->OnPostMessage(port_id, data); + return; + } else if (request == ext::CHANNEL_CLOSED) { + // TODO(siggi@chromium.org): handle correctly. + return; + } + + NOTREACHED(); +} + +HRESULT ExtensionPortManager::PostMessageToHost(const std::string& message, + const std::string& target) { + // Post our message through the ChromeFrameHost. We allow queueing, + // because we don't synchronize to the destination extension loading. + return chrome_frame_host_->PostMessage(CComBSTR(message.c_str()), + CComBSTR(target.c_str())); +} diff --git a/ceee/ie/plugin/bho/extension_port_manager.h b/ceee/ie/plugin/bho/extension_port_manager.h new file mode 100644 index 0000000..dc156da --- /dev/null +++ b/ceee/ie/plugin/bho/extension_port_manager.h @@ -0,0 +1,86 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Extension port manager takes care of managing the state of +// connecting and connected ports. +#ifndef CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <map> +#include <string> +#include "base/values.h" + +// Fwd. +class IContentScriptNativeApi; +class IChromeFrameHost; + +// The extension port manager takes care of: +// - Managing the state of connecting and connected ports +// - Transforming connection and post requests to outgoing message traffic. +// - Processing incoming message traffic and routing it to the appropriate +// Native API instances for handling. +class ExtensionPortManager { + public: + ExtensionPortManager(); + virtual ~ExtensionPortManager(); + + void Initialize(IChromeFrameHost* host); + + // Cleanup ports on native API uninitialization. + // @param instance the instance that's going away. + void CloseAll(IContentScriptNativeApi* instance); + + // Request opening a chnnel to an extension. + // @param instance the API instance that will be handling this connection. + // @param extension the extension to connect to. + // @param tab info on the tab we're initiating a connection from. + // @param cookie an opaque cookie that will be handed back to the + // instance once the connection completes or fails. + HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + Value* tab, + int cookie); + + // Post a message on an open port. + // @param port_id the port's ID. + // @param message the message to send. + HRESULT PostMessage(int port_id, const std::string& message); + + // Process an incoming automation port message from the host. + // @param message the automation message to process. + void OnPortMessage(BSTR message); + + private: + virtual HRESULT PostMessageToHost(const std::string& message, + const std::string& target); + + // Represents a connected port. + struct ConnectedPort { + CComPtr<IContentScriptNativeApi> instance; + }; + typedef std::map<int, ConnectedPort> ConnectedPortMap; + + // Represents a connecting port. + struct ConnectingPort { + CComPtr<IContentScriptNativeApi> instance; + int cookie; + }; + typedef std::map<int, ConnectingPort> ConnectingPortMap; + + // Map from port_id to page api instance. + ConnectedPortMap connected_ports_; + + // Map from connection_id to page api and callback instances. + ConnectingPortMap connecting_ports_; + + // The next connection id we'll assign. + int next_connection_id_; + + CComPtr<IChromeFrameHost> chrome_frame_host_; +}; + +#endif // CEEE_IE_PLUGIN_BHO_EXTENSION_PORT_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/frame_event_handler.cc b/ceee/ie/plugin/bho/frame_event_handler.cc new file mode 100644 index 0000000..a32cc6f --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler.cc @@ -0,0 +1,518 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler implementation. +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include <mshtml.h> +#include <shlguid.h> +#include "base/file_util.h" +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/com_utils.h" +#include "chrome/common/extensions/extension_resource.h" +#include "third_party/activscp/activdbg.h" +#include "toolband.h" // NOLINT + +// {242CD33B-B386-44a7-B685-3A9999B95420} +const GUID IID_IFrameEventHandler = + { 0x242cd33b, 0xb386, 0x44a7, + { 0xb6, 0x85, 0x3a, 0x99, 0x99, 0xb9, 0x54, 0x20 } }; + +// {E68D8538-0E0F-4d42-A4A2-1A635675F376} +const GUID IID_IFrameEventHandlerHost = + { 0xe68d8538, 0xe0f, 0x4d42, + { 0xa4, 0xa2, 0x1a, 0x63, 0x56, 0x75, 0xf3, 0x76 } }; + + +FrameEventHandler::FrameEventHandler() + : property_notify_sink_cookie_(kInvalidCookie), + advise_sink_cookie_(kInvalidCookie), + document_ready_state_(READYSTATE_UNINITIALIZED), + initialized_debugging_(false), + loaded_css_(false), + loaded_start_scripts_(false), + loaded_end_scripts_(false) { +} + +FrameEventHandler::~FrameEventHandler() { + DCHECK_EQ(property_notify_sink_cookie_, kInvalidCookie); + DCHECK_EQ(advise_sink_cookie_, kInvalidCookie); +} + +HRESULT FrameEventHandler::Initialize(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + DCHECK(browser != NULL); + DCHECK(host != NULL); + + InitializeContentScriptManager(); + + CComPtr<IDispatch> document_disp; + HRESULT hr = browser->get_Document(&document_disp); + if (FAILED(hr)) { + // This should never happen. + NOTREACHED() << "IWebBrowser2::get_Document failed " << com::LogHr(hr); + return hr; + } + + // We used to check for whether the document implements IHTMLDocument2 + // to see whether we have an HTML document instance, but that check + // proved not to be specific enough, as e.g. Google Chrome Frame implements + // IHTMLDocument2. So instead we check specifically for MSHTMLs CLSID. + CComQIPtr<IPersist> document_persist(document_disp); + bool document_is_mshtml = false; + if (document_persist != NULL) { + CLSID clsid = {}; + hr = document_persist->GetClassID(&clsid); + if (SUCCEEDED(hr) && clsid == CLSID_HTMLDocument) + document_is_mshtml = true; + } + + // Check that the document is an MSHTML instance, as opposed to e.g. a + // PDF document or a ChromeFrame server. + if (document_is_mshtml) { + document_ = document_disp; + DCHECK(document_ != NULL); + + // Attach to the document, any error here is abnormal + // and should be returned to our caller. + hr = AttachToHtmlDocument(browser, parent_browser, host); + + if (SUCCEEDED(hr)) + // If we have a parent browser, then this frame is an iframe and we only + // want to match content scripts where all_frames is true. + hr = content_script_manager_->Initialize(host, parent_browser != NULL); + + if (FAILED(hr)) + TearDown(); + + return hr; + } + + // It wasn't an HTML document, we kindly decline to attach to this one. + return E_DOCUMENT_NOT_MSHTML; +} + +HRESULT FrameEventHandler::AttachToHtmlDocument(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + DCHECK(browser); + DCHECK(host); + + // Check that we can retrieve the document's ready state. + READYSTATE new_ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = GetDocumentReadyState(&new_ready_state); + DCHECK(SUCCEEDED(hr)) << "Failed to retrieve document ready state " + << com::LogHr(hr); + + // Set up the advise sink. + if (SUCCEEDED(hr)) { + CComQIPtr<IOleObject> ole_object(document_); + DCHECK(ole_object != NULL) << "Document is not an OLE Object"; + + hr = ole_object->Advise(this, &advise_sink_cookie_); + } + + if (SUCCEEDED(hr)) { + DCHECK(advise_sink_cookie_ != kInvalidCookie); + } else { + DCHECK(advise_sink_cookie_ == kInvalidCookie); + LOG(ERROR) << "IOleObject::Advise failed " << com::LogHr(hr); + } + + // Set up the property notify sink. + if (SUCCEEDED(hr)) { + hr = AtlAdvise(document_, + GetUnknown(), + IID_IPropertyNotifySink, + &property_notify_sink_cookie_); + if (SUCCEEDED(hr)) { + DCHECK(property_notify_sink_cookie_ != kInvalidCookie); + } else { + DCHECK(property_notify_sink_cookie_ == kInvalidCookie); + LOG(ERROR) << "Subscribing IPropertyNotifySink failed " + << com::LogHr(hr); + } + } + + if (SUCCEEDED(hr)) { + // We're all good. + browser_ = browser; + parent_browser_ = parent_browser; + host_ = host; + + if (!initialized_debugging_) { + initialized_debugging_ = true; + ScriptHost::default_debug_application()->Initialize(document_); + } + + // Notify the host that we've attached this browser instance. + hr = host_->AttachBrowser(browser_, parent_browser_, this); + } else { + // We had some sort of failure, tear down any initialization we've done. + TearDown(); + } + + return hr; +} + +HRESULT FrameEventHandler::AddSubHandler(IFrameEventHandler* handler) { + std::pair<SubHandlerSet::iterator, bool> ret = + sub_handlers_.insert(CAdapt<CComPtr<IFrameEventHandler> >(handler)); + DCHECK(ret.second == true) << "Double notification for a sub handler"; + + return S_OK; +} + +HRESULT FrameEventHandler::RemoveSubHandler(IFrameEventHandler* handler) { + size_t removed = + sub_handlers_.erase(CAdapt<CComPtr<IFrameEventHandler> >(handler)); + DCHECK_NE(0U, removed) << "Errant removal notification for a sub handler"; + DCHECK_EQ(1U, removed); // There can be only one in a set. + + return S_OK; +} + +void FrameEventHandler::InitializeContentScriptManager() { + content_script_manager_.reset(new ContentScriptManager); +} + +HRESULT FrameEventHandler::GetExtensionResourceContents(const FilePath& file, + std::string* contents) { + DCHECK(contents); + + std::wstring extension_path; + host_->GetExtensionPath(&extension_path); + DCHECK(extension_path.size()); + + FilePath file_path(ExtensionResource::GetFilePath( + FilePath(extension_path), file)); + + if (!file_util::ReadFileToString(file_path, contents)) { + return STG_E_FILENOTFOUND; + } + + return S_OK; +} + +HRESULT FrameEventHandler::GetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents) { + DCHECK(contents); + + // Must have one of code or file, but not both. + bool has_code = ::SysStringLen(code) > 0; + bool has_file = ::SysStringLen(file) > 0; + if (!(has_code ^ has_file)) + return E_INVALIDARG; + + if (has_code) { + *contents = code; + } else { + std::string code_string_a; + HRESULT hr = GetExtensionResourceContents(FilePath(file), &code_string_a); + if (FAILED(hr)) + return hr; + + *contents = CA2W(code_string_a.c_str()); + } + + return S_OK; +} + +void FrameEventHandler::TearDownSubHandlers() { + // Copy the set to avoid problems with reentrant + // modification of the set during teardown. + SubHandlerSet sub_handlers(sub_handlers_); + SubHandlerSet::iterator it(sub_handlers.begin()); + SubHandlerSet::iterator end(sub_handlers.end()); + for (; it != end; ++it) { + DCHECK(it->m_T); + it->m_T->TearDown(); + } + + // In the case where we tear down subhandlers on a readystate + // drop, it appears that by this time, the child->parent relationship + // between the sub-browsers and our attached browser has been severed. + // We therefore can't expect our host to have issued RemoveSubHandler + // for our subhandlers, and should do manual cleanup. + sub_handlers_.clear(); +} + +void FrameEventHandler::TearDown() { + // Start by tearing down subframes. + TearDownSubHandlers(); + + // Flush content script state. + content_script_manager_->TearDown(); + + // Then detach all event sinks. + if (host_ != NULL) { + DCHECK(browser_ != NULL); + + // Notify our host that we're detaching the browser. + HRESULT hr = host_->DetachBrowser(browser_, parent_browser_, this); + DCHECK(SUCCEEDED(hr)); + + parent_browser_.Release(); + browser_.Release(); + host_.Release(); + } + + // Unadvise any document events we're sinking. + if (property_notify_sink_cookie_ != kInvalidCookie) { + HRESULT hr = AtlUnadvise(document_, + IID_IPropertyNotifySink, + property_notify_sink_cookie_); + DCHECK(SUCCEEDED(hr)) << "Failed to unsubscribe IPropertyNotifySink " + << com::LogHr(hr); + property_notify_sink_cookie_ = kInvalidCookie; + } + + if (advise_sink_cookie_ != kInvalidCookie) { + CComQIPtr<IOleObject> ole_object(document_); + DCHECK(ole_object != NULL); + HRESULT hr = ole_object->Unadvise(advise_sink_cookie_); + DCHECK(SUCCEEDED(hr)) << "Failed to unadvise IOleObject " << com::LogHr(hr); + advise_sink_cookie_ = kInvalidCookie; + } + + document_.Release(); +} + +HRESULT FrameEventHandler::InsertCode(BSTR code, BSTR file, + CeeeTabCodeType type) { + std::wstring extension_id; + host_->GetExtensionId(&extension_id); + if (!extension_id.size()) { + // We haven't loaded an extension yet; defer until we do. + DeferredInjection injection = {code ? code : L"", file ? file : L"", type}; + deferred_injections_.push_back(injection); + LOG(INFO) << "Deferring InsertCode"; + return S_OK; + } else { + LOG(INFO) << "Executing InsertCode"; + } + + std::wstring code_string; + HRESULT hr = GetCodeOrFileContents(code, file, &code_string); + if (FAILED(hr)) + return hr; + + if (type == kCeeeTabCodeTypeCss) { + return content_script_manager_->InsertCss(code_string.c_str(), document_); + } else if (type == kCeeeTabCodeTypeJs) { + const wchar_t* file_string; + if (::SysStringLen(file) > 0) { + file_string = OLE2W(file); + } else { + // TODO(rogerta@chromium.org): should not use a hardcoded name, + // but one either extracted from the script itself or hashed + // from the code. bb2146033. + file_string = L"ExecuteScript.code"; + } + + return content_script_manager_->ExecuteScript(code_string.c_str(), + file_string, + document_); + } + + return E_INVALIDARG; +} + +void FrameEventHandler::RedoDoneInjections() { + // Any type of injection we attempted to do before extensions were + // loaded would have been a no-op. This function is called once + // extensions have been loaded to redo the ones that have + // already been attempted. Those that have not yet been attempted will + // happen later, when appropriate (e.g. on readystate complete). + GURL match_url(com::ToString(browser_url_)); + + if (loaded_css_) { + LoadCss(match_url); + } + + if (loaded_start_scripts_) { + LoadStartScripts(match_url); + } + + if (loaded_end_scripts_) { + LoadEndScripts(match_url); + } + + // Take a copy to avoid an endless loop if we should for whatever + // reason still not know the extension dir (this is just belt and + // suspenders). + std::list<DeferredInjection> injections = deferred_injections_; + std::list<DeferredInjection>::iterator it = injections.begin(); + for (; it != injections.end(); ++it) { + InsertCode(CComBSTR(it->code.c_str()), + CComBSTR(it->file.c_str()), + it->type); + } +} + +void FrameEventHandler::FinalRelease() { + if (initialized_debugging_) + ScriptHost::default_debug_application()->Terminate(); +} + +HRESULT FrameEventHandler::GetDocumentReadyState(READYSTATE* ready_state) { + DCHECK(document_ != NULL); + + CComVariant ready_state_var; + CComDispatchDriver document(document_); + HRESULT hr = document.GetProperty(DISPID_READYSTATE, &ready_state_var); + if (FAILED(hr)) + return hr; + + if (ready_state_var.vt != VT_I4) + return E_UNEXPECTED; + + READYSTATE tmp = static_cast<READYSTATE>(ready_state_var.lVal); + DCHECK(tmp >= READYSTATE_UNINITIALIZED && tmp <= READYSTATE_COMPLETE); + *ready_state = tmp; + return S_OK; +} + +void FrameEventHandler::HandleReadyStateChange(READYSTATE old_ready_state, + READYSTATE new_ready_state) { + // We should always have been notified of our corresponding document's URL + DCHECK(browser_url_ != NULL); + DCHECK(document_ != NULL); + + if (new_ready_state <= READYSTATE_LOADING && + old_ready_state > READYSTATE_LOADING) { + ReInitialize(); + } + + GURL match_url(com::ToString(browser_url_)); + + if (new_ready_state >= READYSTATE_LOADED && !loaded_css_) { + loaded_css_ = true; + LoadCss(match_url); + } + + if (new_ready_state >= READYSTATE_LOADED && !loaded_start_scripts_) { + loaded_start_scripts_ = true; + LoadStartScripts(match_url); + } + + if (new_ready_state == READYSTATE_COMPLETE && !loaded_end_scripts_) { + loaded_end_scripts_ = true; + LoadEndScripts(match_url); + } + + // Let our host know of this change. + DCHECK(host_ != NULL); + if (host_ != NULL) { + HRESULT hr = host_->OnReadyStateChanged(new_ready_state); + DCHECK(SUCCEEDED(hr)) << com::LogHr(hr); + } +} + +void FrameEventHandler::SetNewReadyState(READYSTATE new_ready_state) { + READYSTATE old_ready_state = document_ready_state_; + if (old_ready_state != new_ready_state) { + document_ready_state_ = new_ready_state; + HandleReadyStateChange(old_ready_state, new_ready_state); + } +} + +void FrameEventHandler::ReInitialize() { + // This function should only be called when the readystate + // drops from above LOADING to LOADING (or below). + DCHECK(document_ready_state_ <= READYSTATE_LOADING); + + // A readystate drop means our associated document is being + // re-navigated or refreshed. We need to tear down all sub + // frame handlers, because they'll otherwise receive no + // notification of this event. + TearDownSubHandlers(); + + // Reset our indicators, and manager. + // We'll need to re-inject on subsquent up-transitions. + loaded_css_ = false; + loaded_start_scripts_ = false; + loaded_end_scripts_ = false; + + content_script_manager_->TearDown(); +} + +STDMETHODIMP FrameEventHandler::OnChanged(DISPID property_disp_id) { + if (property_disp_id == DISPID_READYSTATE) { + READYSTATE new_ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = GetDocumentReadyState(&new_ready_state); + DCHECK(SUCCEEDED(hr)); + SetNewReadyState(new_ready_state); + } + return S_OK; +} + +STDMETHODIMP FrameEventHandler::OnRequestEdit(DISPID property_disp_id) { + return S_OK; +} + +STDMETHODIMP_(void) FrameEventHandler::OnDataChange(FORMATETC* format, + STGMEDIUM* storage) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnViewChange(DWORD aspect, LONG index) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnRename(IMoniker* moniker) { +} + +STDMETHODIMP_(void) FrameEventHandler::OnSave() { +} + +STDMETHODIMP_(void) FrameEventHandler::OnClose() { + // TearDown may release all references to ourselves, so we have to + // maintain a self-reference over this call. + CComPtr<IUnknown> staying_alive_oooh_oooh_oooh_staying_alive(GetUnknown()); + + TearDown(); +} + +void FrameEventHandler::GetUrl(BSTR* url) { + *url = CComBSTR(browser_url_).Detach(); +} + +HRESULT FrameEventHandler::SetUrl(BSTR url) { + // This method is called by our host on NavigateComplete, at which time + // our corresponding browser is either doing first-time navigation, or + // it's being re-navigated to a different URL, or possibly to the same URL. + // Note that as there's no NavigateComplete event fired for refresh, + // we won't hit here in that case, but will rather notice that case on + // our readystate dropping on an IPropertyNotifySink change notification. + // The readystate for first-time navigation on the top-level browser + // will be interactive, whereas for subsequent navigations and for + // sub-browsers, the readystate will be loading. + // This would be a fine time to probe and act on the readystate, except + // for the fact that in some weird cases, GetDocumentReadyState incorrectly + // returns READYSTATE_COMPLETE, which means we act too soon. So instead + // we patiently wait for a property change notification and act on the + // document's ready state then. + if (browser_url_ == url) + return S_FALSE; + + browser_url_ = url; + return S_OK; +} + +void FrameEventHandler::LoadCss(const GURL& match_url) { + content_script_manager_->LoadCss(match_url, document_); +} + +void FrameEventHandler::LoadStartScripts(const GURL& match_url) { + // Run the document start scripts. + content_script_manager_->LoadStartScripts(match_url, document_); +} + +void FrameEventHandler::LoadEndScripts(const GURL& match_url) { + // Run the document end scripts. + content_script_manager_->LoadEndScripts(match_url, document_); +} diff --git a/ceee/ie/plugin/bho/frame_event_handler.h b/ceee/ie/plugin/bho/frame_event_handler.h new file mode 100644 index 0000000..16bba7d --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler.h @@ -0,0 +1,309 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler declaration. + +#ifndef CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ +#define CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <mshtml.h> // Must be before <exdisp.h> +#include <exdisp.h> +#include <ocidl.h> +#include <objidl.h> + +#include <list> +#include <set> +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/common/initializing_coclass.h" + +#include "toolband.h" // NOLINT + +// Error code to signal that a browser has a non-MSHTML document attached. +const HRESULT E_DOCUMENT_NOT_MSHTML = + MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200); + +// This is the interface the frame event handler presents to its host. +// The BHO maintains a mapping of browsers to frame event handlers, +// and takes care of notifying frame event handlers when they acquire +// a subframe. +extern const GUID IID_IFrameEventHandler; +class IFrameEventHandler: public IUnknown { + public: + // Get the frame's current URL. + virtual void GetUrl(BSTR* url) = 0; + + // Notify the frame event handler of the associated browser's current URL. + virtual HRESULT SetUrl(BSTR url) = 0; + + // Returns the current document ready state. + virtual READYSTATE GetReadyState() = 0; + + // Notify the frame event handler that |handler| has been attached + // to an immediate sub-browser of the browser it's attached to. + virtual HRESULT AddSubHandler(IFrameEventHandler* handler) = 0; + // Notify the frame event handler that |handler| has detached from + // an immediate sub-browser of the browser it's attached to. + virtual HRESULT RemoveSubHandler(IFrameEventHandler* handler) = 0; + + // A parent frame handler has seen a readystate drop, indicating + // that our associated browser instance has gone out of scope. + // @pre this frame event handler is attached to a browser. + virtual void TearDown() = 0; + + // Insert code inside a tab whether by execution or injection. + // @param code The code to insert. + // @param file A file containing the code to insert. + // @param type The type of the code to insert. + virtual HRESULT InsertCode(BSTR code, BSTR file, + CeeeTabCodeType type) = 0; + + // Re-does any injections of code or CSS that should have been already done. + // Called by the host when extensions have been loaded, as before then we + // don't have details on which scripts to load. + virtual void RedoDoneInjections() = 0; +}; + +// Fwd. +class IExtensionPortMessagingProvider; + +// The interface presented to a frame event handler by its host. +extern const GUID IID_IFrameEventHandlerHost; +class IFrameEventHandlerHost: public IUnknown { + public: + // Notify the host that |handler| has attached to |browser|, + // whose parent browser is |parent_browser|. + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) = 0; + // Notify the host that |handler| has detached from |browser|. + // whose parent browser is |parent_browser|. + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) = 0; + // Returns the top level browser associated to this frame event handler. + virtual HRESULT GetTopLevelBrowser(IWebBrowser2** browser) = 0; + + // Notify the host that our ready state has changed. + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state) = 0; + + // Get the current ready state of the host. + virtual HRESULT GetReadyState(READYSTATE* ready_state) = 0; + + // Retrieve the CSS content from user scripts that match @p url. + // @param url The URL to match. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param css_content The single stream of CSS content. + virtual HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) = 0; + + // Retrieve the JS content from user scripts that match @p url. + // @param url The URL to match. + // @param location The location where the scripts will be run at. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param js_file_list A vector of file path/content pairs. + virtual HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list) = 0; + + // Retrieve our extension ID. + // @param extension_id on success returns the extension id. + virtual HRESULT GetExtensionId(std::wstring* extension_id) = 0; + + // Retrieve our extension base dir. + // @param extension_path on success returns the extension base dir. + virtual HRESULT GetExtensionPath(std::wstring* extension_path) = 0; + + // Retrieve the native API host. + // @param host on success returns the native API host. + virtual HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) = 0; + + // Execute the given code or file in the top level frame or all frames. + // Note that only one of code or file can be non-empty. + // @param code The script to execute. + // @param file A file containing the script to execute. + // @param all_frames If true, applies to the top level frame as well as + // contained iframes. Otherwise, applies only to the + // top level frame. + // @param type The type of the code to insert. + virtual HRESULT InsertCode(BSTR code, BSTR file, BOOL all_frames, + CeeeTabCodeType type) = 0; +}; + +// The frame event handler is attached to an IWebBrowser2 instance, either +// a top-level instance or sub instances associated with frames. +// It is responsible for listening for events from the associated frame in +// order to e.g. instantiate content scripts that interact with the frame. +class FrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<FrameEventHandler>, + public IPropertyNotifySink, + public IAdviseSink, + public IFrameEventHandler { + public: + BEGIN_COM_MAP(FrameEventHandler) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + COM_INTERFACE_ENTRY(IAdviseSink) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandler, IFrameEventHandler) + END_COM_MAP() + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + FrameEventHandler(); + virtual ~FrameEventHandler(); + + // Initialize the event handler. + // @returns S_OK on success, E_DOCUMENT_NOT_MSHTML if the browser + // is not attached to an MSTHML document instance. + HRESULT Initialize(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host); + void FinalRelease(); + + // @name IPropertyNotifySink implementation + // @{ + STDMETHOD(OnChanged)(DISPID property_disp_id); + STDMETHOD(OnRequestEdit)(DISPID property_disp_id); + // @} + + // @name IAdviseSink implementation + // @{ + STDMETHOD_(void, OnDataChange)(FORMATETC* format, STGMEDIUM* storage); + STDMETHOD_(void, OnViewChange)(DWORD aspect, LONG index); + STDMETHOD_(void, OnRename)(IMoniker* moniker); + STDMETHOD_(void, OnSave)(); + // We use this event to tear down + STDMETHOD_(void, OnClose)(); + // @} + + // @name IFrameEventHandler implementation. + // @{ + virtual void GetUrl(BSTR* url); + virtual HRESULT SetUrl(BSTR url); + virtual READYSTATE GetReadyState() { return document_ready_state_; } + virtual HRESULT AddSubHandler(IFrameEventHandler* handler); + virtual HRESULT RemoveSubHandler(IFrameEventHandler* handler); + virtual void TearDown(); + virtual HRESULT InsertCode(BSTR code, BSTR file, CeeeTabCodeType type); + virtual void RedoDoneInjections(); + // @} + + BSTR browser_url() const { return browser_url_; } + + protected: + // Reinitialize state on a readystate drop to LOADING, which + // signifies that either our associated browser is being refreshed + // or is being re-navigated. + void ReInitialize(); + + // Issues a teardown call to all sub frame handlers. + void TearDownSubHandlers(); + + // Creates and initializes the content script manager for this handler. + // This method is virtual to allow overriding by tests. + virtual void InitializeContentScriptManager(); + + // Reads the contents of an extension resource. The file path is assumed + // to be relative to the root of the extension. + virtual HRESULT GetExtensionResourceContents(const FilePath& file, + std::string* contents); + + // Validates and returns the code content of either code or file. + // Used by ExecuteScript and InsertCss. + virtual HRESULT GetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents); + + // Handle a ready state change from document_ready_state_ to new_ready_state. + virtual void HandleReadyStateChange(READYSTATE old_ready_state, + READYSTATE new_ready_state); + + // Change the current document ready state to new_ready_state + // and invoke HandleReadyStateChange if the ready state changed. + void SetNewReadyState(READYSTATE new_ready_state); + + // Retrieves our document's ready state. + HRESULT GetDocumentReadyState(READYSTATE* ready_state); + + // Inject CSS for @p match_url. + virtual void LoadCss(const GURL& match_url); + + // Inject start scripts for @p match_url. + virtual void LoadStartScripts(const GURL& match_url); + + // Inject end scripts for @p match_url. + virtual void LoadEndScripts(const GURL& match_url); + + // Subscribes for events etc. + // @pre document_ is non-NULL and implements IHTMLDocument2. + HRESULT AttachToHtmlDocument(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host); + + // Sentinel value for non-subscribed cookies. + static const DWORD kInvalidCookie = -1; + + // Connection cookie for IPropertyNotifySink connection point. + DWORD property_notify_sink_cookie_; + + // Connection cookie for IAdviseSink subscription. + DWORD advise_sink_cookie_; + + // The browser we're attached to. + CComPtr<IWebBrowser2> browser_; + + // Our parent browser, if any. + CComPtr<IWebBrowser2> parent_browser_; + + // The current URL browser_ is navigated or navigating to. + CComBSTR browser_url_; + + // Our host object. + CComPtr<IFrameEventHandlerHost> host_; + + // The document object of browser_, but only if it implements + // IHTMLDocument2 - e.g. is an HTML document object. + CComPtr<IHTMLDocument2> document_; + + // The last recorded document_ ready state. + READYSTATE document_ready_state_; + + // True iff we've initialized debugging. + bool initialized_debugging_; + + // Each of these is true iff we've attempted content script + // CSS/start/end script injection. + bool loaded_css_; + bool loaded_start_scripts_; + bool loaded_end_scripts_; + + // Our content script manager. + scoped_ptr<ContentScriptManager> content_script_manager_; + + typedef std::set<CAdapt<CComPtr<IFrameEventHandler> > > SubHandlerSet; + // The sub frames handlers we've been advised of by our host. + SubHandlerSet sub_handlers_; + + struct DeferredInjection { + std::wstring code; + std::wstring file; + CeeeTabCodeType type; + }; + + // Injections we deferred until extension information is available. + std::list<DeferredInjection> deferred_injections_; + + DISALLOW_COPY_AND_ASSIGN(FrameEventHandler); +}; + +#endif // CEEE_IE_PLUGIN_BHO_FRAME_EVENT_HANDLER_H_ diff --git a/ceee/ie/plugin/bho/frame_event_handler_unittest.cc b/ceee/ie/plugin/bho/frame_event_handler_unittest.cc new file mode 100644 index 0000000..2094591 --- /dev/null +++ b/ceee/ie/plugin/bho/frame_event_handler_unittest.cc @@ -0,0 +1,740 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Frame event handler unittests. +#include "ceee/ie/plugin/bho/frame_event_handler.h" + +#include <atlctl.h> +#include <map> + +#include "base/file_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::IOleObjecMockImpl; +using testing::IWebBrowser2MockImpl; +using testing::InstanceCountMixinBase; +using testing::InstanceCountMixin; + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::DoAll; +using testing::Return; +using testing::StrEq; +using testing::StrictMock; +using testing::SetArgumentPointee; + +ScriptHost::DebugApplication debug_app(L"FrameEventHandlerUnittest"); + +// We need to implement this interface separately, because +// there are name conflicts with methods in IConnectionPointImpl, +// and we don't want to override those methods. +class TestIOleObjectImpl: public StrictMock<IOleObjecMockImpl> { + public: + // Implement the advise functions. + STDMETHOD(Advise)(IAdviseSink* sink, DWORD* advise_cookie) { + return advise_holder_->Advise(sink, advise_cookie); + } + STDMETHOD(Unadvise)(DWORD advise_cookie) { + return advise_holder_->Unadvise(advise_cookie); + } + STDMETHOD(EnumAdvise)(IEnumSTATDATA **enum_advise) { + return advise_holder_->EnumAdvise(enum_advise); + } + + HRESULT Initialize() { + return ::CreateOleAdviseHolder(&advise_holder_); + } + + public: + CComPtr<IOleAdviseHolder> advise_holder_; +}; + +class IPersistMockImpl: public IPersist { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetClassID, HRESULT(CLSID *clsid)); +}; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public InstanceCountMixin<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl>, + public StrictMock<IPersistMockImpl>, + public TestIOleObjectImpl, + public IConnectionPointContainerImpl<MockDocument>, + public IConnectionPointImpl<MockDocument, &IID_IPropertyNotifySink> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + COM_INTERFACE_ENTRY(IOleObject) + COM_INTERFACE_ENTRY(IPersist) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(MockDocument) + CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink) + END_CONNECTION_POINT_MAP() + + MockDocument() : ready_state_(READYSTATE_UNINITIALIZED) { + } + + void FireOnClose() { + EXPECT_HRESULT_SUCCEEDED(advise_holder_->SendOnClose()); + } + + // Override to handle DISPID_READYSTATE. + STDMETHOD(Invoke)(DISPID member, REFIID iid, LCID locale, WORD flags, + DISPPARAMS* params, VARIANT *result, EXCEPINFO* ex_info, + unsigned int* arg_error) { + if (member == DISPID_READYSTATE && flags == DISPATCH_PROPERTYGET) { + result->vt = VT_I4; + result->lVal = ready_state_; + return S_OK; + } + + return StrictMock<IHTMLDocument2MockImpl>::Invoke(member, iid, locale, + flags, params, result, ex_info, arg_error); + } + + STDMETHOD(get_URL)(BSTR* url) { + return url_.CopyTo(url); + } + + HRESULT Initialize(MockDocument** self) { + *self = this; + return TestIOleObjectImpl::Initialize(); + } + + + READYSTATE ready_state() const { return ready_state_; } + void set_ready_state(READYSTATE ready_state) { ready_state_ = ready_state; } + + void FireReadyStateChange() { + CFirePropNotifyEvent::FireOnChanged(GetUnknown(), DISPID_READYSTATE); + } + + // Sets our ready state and fires the change event. + void SetReadyState(READYSTATE new_ready_state) { + if (ready_state_ == new_ready_state) + return; + ready_state_ = new_ready_state; + FireReadyStateChange(); + } + + const wchar_t *url() const { return com::ToString(url_); } + void set_url(const wchar_t* url) { url_ = url; } + + protected: + CComBSTR url_; + READYSTATE ready_state_; +}; + +class MockBrowser + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockBrowser>, + public InstanceCountMixin<MockBrowser>, + public StrictMock<IWebBrowser2MockImpl> { + public: + BEGIN_COM_MAP(MockBrowser) + COM_INTERFACE_ENTRY(IWebBrowser2) + COM_INTERFACE_ENTRY(IWebBrowserApp) + COM_INTERFACE_ENTRY(IWebBrowser) + END_COM_MAP() + + HRESULT Initialize(MockBrowser** self) { + *self = this; + return S_OK; + } + + STDMETHOD(get_Parent)(IDispatch** parent) { + this->GetUnknown()->AddRef(); + *parent = this; + return S_OK; + } +}; + +class IFrameEventHandlerHostMockImpl : public IFrameEventHandlerHost { + public: + MOCK_METHOD1(GetReadyState, HRESULT(READYSTATE* readystate)); + MOCK_METHOD3(GetMatchingUserScriptsCssContent, + HRESULT(const GURL& url, bool require_all_frames, + std::string* css_content)); + MOCK_METHOD4(GetMatchingUserScriptsJsContent, + HRESULT(const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list)); + MOCK_METHOD1(GetExtensionId, HRESULT(std::wstring* extension_id)); + MOCK_METHOD1(GetExtensionPath, HRESULT(std::wstring* extension_path)); + MOCK_METHOD1(GetExtensionPortMessagingProvider, + HRESULT(IExtensionPortMessagingProvider** messaging_provider)); + MOCK_METHOD4(InsertCode, HRESULT(BSTR, BSTR, BOOL, CeeeTabCodeType)); +}; + +class TestFrameEventHandlerHost + : public testing::MockFrameEventHandlerHostBase<TestFrameEventHandlerHost> { + public: + HRESULT Initialize(TestFrameEventHandlerHost** self) { + *self = this; + return S_OK; + } + virtual HRESULT AttachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown. + CComPtr<IUnknown> browser_identity_unknown; + EXPECT_HRESULT_SUCCEEDED( + browser->QueryInterface(&browser_identity_unknown)); + + std::pair<HandlerMap::iterator, bool> result = + handlers_.insert(std::make_pair(browser_identity_unknown, handler)); + EXPECT_TRUE(result.second); + return S_OK; + } + + virtual HRESULT DetachBrowser(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler) { + // Get the identity unknown. + CComPtr<IUnknown> browser_identity_unknown; + EXPECT_HRESULT_SUCCEEDED( + + browser->QueryInterface(&browser_identity_unknown)); + EXPECT_EQ(1, handlers_.erase(browser_identity_unknown)); + return S_OK; + } + + virtual HRESULT OnReadyStateChanged(READYSTATE ready_state) { + return S_OK; + } + + bool has_browser(IUnknown* browser) { + CComPtr<IUnknown> browser_identity(browser); + + return handlers_.find(browser_identity) != handlers_.end(); + } + + FrameEventHandler* GetHandler(IUnknown* browser) { + CComPtr<IUnknown> browser_identity(browser); + + HandlerMap::iterator it(handlers_.find(browser_identity)); + if (it != handlers_.end()) + return NULL; + + return static_cast<FrameEventHandler*>(it->second); + } + + private: + typedef std::map<IUnknown*, IFrameEventHandler*> HandlerMap; + HandlerMap handlers_; +}; + +class MockContentScriptManager : public ContentScriptManager { + public: + MOCK_METHOD3(ExecuteScript, HRESULT(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document)); + MOCK_METHOD2(InsertCss, HRESULT(const wchar_t* code, + IHTMLDocument2* document)); +}; + +// This testing class is used to test the higher-level event handling +// behavior of FrameEventHandler by mocking out the implementation +// functions invoked on readystate transitions. +class TestingFrameEventHandler + : public FrameEventHandler, + public InitializingCoClass<TestingFrameEventHandler>, + public InstanceCountMixin<TestingFrameEventHandler> { + public: + TestingFrameEventHandler() {} + ~TestingFrameEventHandler() {} + + HRESULT Initialize(TestingFrameEventHandler **self, + IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandlerHost* host) { + *self = this; + return FrameEventHandler::Initialize(browser, parent_browser, host); + } + + virtual void InitializeContentScriptManager() { + content_script_manager_.reset(new MockContentScriptManager); + } + MockContentScriptManager* GetContentScriptManager() { + return reinterpret_cast<MockContentScriptManager*>( + content_script_manager_.get()); + } + + // Mock out or publicize our internal helper methods. + MOCK_METHOD2(GetExtensionResourceContents, + HRESULT(const FilePath& file, std::string* contents)); + MOCK_METHOD3(GetCodeOrFileContents, + HRESULT(BSTR code, BSTR file, std::wstring* contents)); + HRESULT CallGetCodeOrFileContents(BSTR code, BSTR file, + std::wstring* contents) { + return FrameEventHandler::GetCodeOrFileContents(code, file, contents); + } + + const std::list<DeferredInjection>& deferred_injections() { + return deferred_injections_; + } + + void SetupForRedoDoneInjectionsTest(BSTR url) { + browser_url_ = url; + loaded_css_ = true; + loaded_start_scripts_ = true; + loaded_end_scripts_ = true; + } + + // Disambiguate. + using InitializingCoClass<TestingFrameEventHandler>:: + CreateInitialized; + + // Mock out the state transition implementation functions. + MOCK_METHOD1(LoadCss, void(const GURL& match_url)); + MOCK_METHOD1(LoadStartScripts, void(const GURL& match_url)); + MOCK_METHOD1(LoadEndScripts, void(const GURL& match_url)); +}; + +class FrameEventHandlerTestBase: public testing::Test { + public: + virtual void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockBrowser::CreateInitialized(&browser_, &browser_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandlerHost::CreateInitializedIID( + &host_, IID_IUnknown, &host_keeper_)); + + ExpectGetDocument(); + } + + virtual void TearDown() { + // Fire a close event just in case. + if (document_) + document_->FireOnClose(); + + browser_ = NULL; + browser_keeper_.Release(); + document_ = NULL; + document_keeper_.Release(); + host_ = NULL; + host_keeper_.Release(); + + handler_keeper_.Release(); + + ASSERT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + void ExpectGetDocument() { + EXPECT_CALL(*browser_, get_Document(_)) + .WillRepeatedly(DoAll( + CopyInterfaceToArgument<0>(static_cast<IDispatch*>(document_)), + Return(S_OK))); + } + + protected: + MockBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + TestFrameEventHandlerHost* host_; + CComPtr<IFrameEventHandlerHost> host_keeper_; + + CComPtr<IUnknown> handler_keeper_; +}; + +class FrameEventHandlerTest: public FrameEventHandlerTestBase { + public: + typedef FrameEventHandlerTestBase Base; + + static void SetUpTestCase() { + // Never torn down as other threads in the test may need it after + // teardown. + ScriptHost::set_default_debug_application(&debug_app); + } + + void TearDown() { + handler_ = NULL; + + Base::TearDown(); + } + + void CreateHandler() { + EXPECT_CALL(*document_, GetClassID(_)).WillOnce( + DoAll(SetArgumentPointee<0>(CLSID_HTMLDocument), Return(S_OK))); + IWebBrowser2* parent_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + TestingFrameEventHandler::CreateInitialized( + &handler_, browser_, parent_browser, host_, &handler_keeper_)); + } + + protected: + TestingFrameEventHandler* handler_; +}; + +TEST_F(FrameEventHandlerTest, WillNotAttachToNonHTMLDocument) { + EXPECT_CALL(*document_, GetClassID(_)).WillOnce( + DoAll(SetArgumentPointee<0>(GUID_NULL), Return(S_OK))); + + // If the document is not MSHTML, we should not attach, and + // we should return E_DOCUMENT_NOT_MSHTML to our caller to signal this. + IWebBrowser2* parent_browser = NULL; + HRESULT hr = TestingFrameEventHandler::CreateInitialized( + &handler_, browser_, parent_browser, host_, &handler_keeper_); + + EXPECT_EQ(E_DOCUMENT_NOT_MSHTML, hr); + EXPECT_FALSE(host_->has_browser(browser_)); +} + +TEST_F(FrameEventHandlerTest, CreateAndDetachDoesNotCrash) { + ASSERT_EQ(0, TestingFrameEventHandler::instance_count()); + + CreateHandler(); + ASSERT_EQ(1, TestingFrameEventHandler::instance_count()); + + // Assert that it registered. + ASSERT_TRUE(host_->has_browser(browser_)); + + // Release the handler early to ensure its last reference will + // be released while handling FireOnClose. + handler_keeper_.Release(); + handler_ = NULL; + EXPECT_EQ(1, TestingFrameEventHandler::instance_count()); + + // Should tear down and destroy itself on this event. + document_->FireOnClose(); + ASSERT_EQ(0, TestingFrameEventHandler::instance_count()); +} + +const wchar_t kGoogleUrl[] = + L"http://www.google.com/search?q=Google+Buys+Iceland"; +const wchar_t kSlashdotUrl[] = + L"http://hardware.slashdot.org/"; + +TEST_F(FrameEventHandlerTest, InjectsCSSAndStartScriptsOnLoadedReadystate) { + CreateHandler(); + + document_->set_url(kGoogleUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + // Notify the handler of the URL. + handler_->SetUrl(CComBSTR(kGoogleUrl)); + document_->FireReadyStateChange(); + + const GURL google_url(kGoogleUrl); + // Transitioning to LOADED should load Css and start scripts. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Now make like a re-navigation. + document_->SetReadyState(READYSTATE_LOADING); + + // Transitioning back to LOADED should load Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Now navigate to a different URL. + document_->set_url(kSlashdotUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + handler_->SetUrl(CComBSTR(kSlashdotUrl)); + document_->FireReadyStateChange(); + + const GURL slashdot_url(kSlashdotUrl); + + // Transitioning back to LOADED on the new URL should load + // Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(slashdot_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(slashdot_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); +} + +TEST_F(FrameEventHandlerTest, InjectsEndScriptsOnCompleteReadystate) { + CreateHandler(); + + document_->set_url(kGoogleUrl); + document_->set_ready_state(READYSTATE_LOADING); + + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + // Notify the handler of the URL. + handler_->SetUrl(CComBSTR(kGoogleUrl)); + document_->FireReadyStateChange(); + + const GURL google_url(kGoogleUrl); + // Transitioning to LOADED should load Css and start scripts. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Transitioning to INTERACTIVE should be a no-op. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // Transitioning to COMPLETE should load end scripts. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(google_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); + + // Now make like a re-navigation. + document_->SetReadyState(READYSTATE_LOADING); + + // Transitioning back to LOADED should load Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(google_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(google_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Transitioning back to INTERACTIVE should be a no-op. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // Transitioning back to COMPLETE should load end scripts. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(google_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); + + // Now navigate to a different URL. + document_->set_url(kSlashdotUrl); + document_->set_ready_state(READYSTATE_LOADING); + + // Transitioning to loading should not cause any loads. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + handler_->SetUrl(CComBSTR(kSlashdotUrl)); + document_->FireReadyStateChange(); + + const GURL slashdot_url(kSlashdotUrl); + + // Transitioning back to LOADED on the new URL should load + // Css and start scripts again. + EXPECT_CALL(*handler_, LoadCss(slashdot_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(slashdot_url)).Times(1); + + // But not end scripts. + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + document_->SetReadyState(READYSTATE_LOADED); + + // Back to INTERACTIVE is still a noop. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(_)).Times(0); + + document_->SetReadyState(READYSTATE_INTERACTIVE); + + // And COMPLETE loads end scripts again. + EXPECT_CALL(*handler_, LoadCss(_)).Times(0); + EXPECT_CALL(*handler_, LoadStartScripts(_)).Times(0); + EXPECT_CALL(*handler_, LoadEndScripts(slashdot_url)).Times(1); + + document_->SetReadyState(READYSTATE_COMPLETE); +} + +TEST_F(FrameEventHandlerTest, InsertCodeCss) { + CreateHandler(); + MockContentScriptManager* content_script_manager = + handler_->GetContentScriptManager(); + + // Css code type + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + // Must respond with non-empty extension path or call will get deferred. + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, NULL, kCeeeTabCodeTypeCss)); + + // Js code type with no file + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + + wchar_t* default_file = L"ExecuteScript.code"; + EXPECT_CALL(*content_script_manager, ExecuteScript(_, StrEq(default_file), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, NULL, kCeeeTabCodeTypeJs)); + + // Js code type with a file + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + + wchar_t* test_file = L"test_file.js"; + EXPECT_CALL(*content_script_manager, ExecuteScript(_, StrEq(test_file), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + + CComBSTR test_file_bstr(test_file); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, test_file_bstr, kCeeeTabCodeTypeJs)); +} + +TEST_F(FrameEventHandlerTest, DeferInsertCodeCss) { + CreateHandler(); + + // Does not set extension path, so it stays empty. + EXPECT_CALL(*host_, GetExtensionId(_)).WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(L"boo", NULL, kCeeeTabCodeTypeCss)); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(NULL, L"moo", kCeeeTabCodeTypeJs)); + ASSERT_EQ(2, handler_->deferred_injections().size()); + + ASSERT_EQ(L"boo", handler_->deferred_injections().begin()->code); + ASSERT_EQ(L"", handler_->deferred_injections().begin()->file); + ASSERT_EQ(kCeeeTabCodeTypeCss, + handler_->deferred_injections().begin()->type); + + // The ++ syntax is ugly but it's either this or make DeferredInjection + // a public struct. + ASSERT_EQ(L"", (++handler_->deferred_injections().begin())->code); + ASSERT_EQ(L"moo", (++handler_->deferred_injections().begin())->file); + ASSERT_EQ(kCeeeTabCodeTypeJs, + (++handler_->deferred_injections().begin())->type); +} + +TEST_F(FrameEventHandlerTest, RedoDoneInjections) { + CreateHandler(); + MockContentScriptManager* content_script_manager = + handler_->GetContentScriptManager(); + + // Expects no calls since nothing to redo. + handler_->RedoDoneInjections(); + + CComBSTR url(L"http://www.google.com/"); + handler_->SetupForRedoDoneInjectionsTest(url); + GURL match_url(com::ToString(url)); + + // Does not set extension path, so it stays empty. + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce(Return(S_OK)); + // Will get deferred. + ASSERT_HRESULT_SUCCEEDED(handler_->InsertCode(L"boo", NULL, + kCeeeTabCodeTypeCss)); + + EXPECT_CALL(*handler_, LoadCss(match_url)).Times(1); + EXPECT_CALL(*handler_, LoadStartScripts(match_url)).Times(1); + EXPECT_CALL(*handler_, LoadEndScripts(match_url)).Times(1); + + // Expect to get this once, as we deferred it before. + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + ASSERT_HRESULT_SUCCEEDED( + handler_->InsertCode(L"boo", NULL, kCeeeTabCodeTypeCss)); + + EXPECT_CALL(*handler_, GetCodeOrFileContents(_, _, _)).WillOnce(Return(S_OK)); + EXPECT_CALL(*host_, GetExtensionId(_)).WillOnce( + DoAll(SetArgumentPointee<0>(std::wstring(L"hello")), Return(S_OK))); + EXPECT_CALL(*content_script_manager, InsertCss(_, _)).WillOnce(Return(S_OK)); + handler_->RedoDoneInjections(); +} + +TEST_F(FrameEventHandlerTest, GetCodeOrFileContents) { + CreateHandler(); + + CComBSTR code(L"test"); + CComBSTR file(L"test.js"); + CComBSTR empty; + std::wstring contents; + + // Failure cases. + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(0); + + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, NULL, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(code, file, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(empty, NULL, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, empty, + &contents)); + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(empty, empty, + &contents)); + + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)) + .WillOnce(Return(E_FAIL)); + + ASSERT_HRESULT_FAILED(handler_->CallGetCodeOrFileContents(NULL, file, + &contents)); + + // Success cases. + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(0); + + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(code, NULL, + &contents)); + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(code, empty, + &contents)); + + EXPECT_CALL(*handler_, GetExtensionResourceContents(_, _)).Times(2) + .WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(NULL, file, + &contents)); + ASSERT_HRESULT_SUCCEEDED(handler_->CallGetCodeOrFileContents(empty, file, + &contents)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/http_negotiate.cc b/ceee/ie/plugin/bho/http_negotiate.cc new file mode 100644 index 0000000..ac39ba3 --- /dev/null +++ b/ceee/ie/plugin/bho/http_negotiate.cc @@ -0,0 +1,197 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/http_negotiate.h" + +#include <atlbase.h> +#include <atlctl.h> +#include <htiframe.h> +#include <urlmon.h> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "base/time.h" + +#include "ceee/ie/plugin/bho/cookie_accountant.h" +#include "chrome_frame/vtable_patch_manager.h" +#include "chrome_frame/utils.h" +#include "base/scoped_comptr_win.h" + +static const int kHttpNegotiateBeginningTransactionIndex = 3; +static const int kHttpNegotiateOnResponseIndex = 4; + +CComAutoCriticalSection HttpNegotiatePatch::bho_instance_count_crit_; +int HttpNegotiatePatch::bho_instance_count_ = 0; + + +BEGIN_VTABLE_PATCHES(IHttpNegotiate) + VTABLE_PATCH_ENTRY(kHttpNegotiateBeginningTransactionIndex, + HttpNegotiatePatch::BeginningTransaction) + VTABLE_PATCH_ENTRY(kHttpNegotiateOnResponseIndex, + HttpNegotiatePatch::OnResponse) +END_VTABLE_PATCHES() + +namespace { + +class SimpleBindStatusCallback : public CComObjectRootEx<CComSingleThreadModel>, + public IBindStatusCallback { + public: + BEGIN_COM_MAP(SimpleBindStatusCallback) + COM_INTERFACE_ENTRY(IBindStatusCallback) + END_COM_MAP() + + // IBindStatusCallback implementation + STDMETHOD(OnStartBinding)(DWORD reserved, IBinding* binding) { + return E_NOTIMPL; + } + + STDMETHOD(GetPriority)(LONG* priority) { + return E_NOTIMPL; + } + STDMETHOD(OnLowResource)(DWORD reserved) { + return E_NOTIMPL; + } + + STDMETHOD(OnProgress)(ULONG progress, ULONG max_progress, + ULONG status_code, LPCWSTR status_text) { + return E_NOTIMPL; + } + STDMETHOD(OnStopBinding)(HRESULT result, LPCWSTR error) { + return E_NOTIMPL; + } + + STDMETHOD(GetBindInfo)(DWORD* bind_flags, BINDINFO* bind_info) { + return E_NOTIMPL; + } + + STDMETHOD(OnDataAvailable)(DWORD flags, DWORD size, FORMATETC* formatetc, + STGMEDIUM* storage) { + return E_NOTIMPL; + } + STDMETHOD(OnObjectAvailable)(REFIID iid, IUnknown* object) { + return E_NOTIMPL; + } +}; + +} // end namespace + +HttpNegotiatePatch::HttpNegotiatePatch() { +} + +HttpNegotiatePatch::~HttpNegotiatePatch() { +} + +// static +bool HttpNegotiatePatch::Initialize() { + // Patch IHttpNegotiate for user-agent and cookie functionality. + { + CComCritSecLock<CComAutoCriticalSection> lock(bho_instance_count_crit_); + bho_instance_count_++; + if (bho_instance_count_ != 1) { + return true; + } + } + + if (IS_PATCHED(IHttpNegotiate)) { + LOG(WARNING) << __FUNCTION__ << ": already patched."; + return true; + } + + // Use our SimpleBindStatusCallback class as we need a temporary object that + // implements IBindStatusCallback. + CComObjectStackEx<SimpleBindStatusCallback> request; + ScopedComPtr<IBindCtx> bind_ctx; + HRESULT hr = ::CreateAsyncBindCtx(0, &request, NULL, bind_ctx.Receive()); + + DCHECK(SUCCEEDED(hr)) << "CreateAsyncBindCtx"; + if (bind_ctx) { + ScopedComPtr<IUnknown> bscb_holder; + bind_ctx->GetObjectParam(L"_BSCB_Holder_", bscb_holder.Receive()); + if (bscb_holder) { + hr = PatchHttpNegotiate(bscb_holder); + } else { + NOTREACHED() << "Failed to get _BSCB_Holder_"; + hr = E_UNEXPECTED; + } + bind_ctx.Release(); + } + + return SUCCEEDED(hr); +} + +// static +void HttpNegotiatePatch::Uninitialize() { + CComCritSecLock<CComAutoCriticalSection> lock(bho_instance_count_crit_); + bho_instance_count_--; + DCHECK_GE(bho_instance_count_, 0); + if (bho_instance_count_ == 0) { + vtable_patch::UnpatchInterfaceMethods(IHttpNegotiate_PatchInfo); + } +} + +// static +HRESULT HttpNegotiatePatch::PatchHttpNegotiate(IUnknown* to_patch) { + DCHECK(to_patch); + DCHECK_IS_NOT_PATCHED(IHttpNegotiate); + + ScopedComPtr<IHttpNegotiate> http; + HRESULT hr = http.QueryFrom(to_patch); + if (FAILED(hr)) { + hr = DoQueryService(IID_IHttpNegotiate, to_patch, http.Receive()); + } + + if (http) { + hr = vtable_patch::PatchInterfaceMethods(http, IHttpNegotiate_PatchInfo); + DLOG_IF(ERROR, FAILED(hr)) + << StringPrintf("HttpNegotiate patch failed 0x%08X", hr); + } else { + DLOG(WARNING) + << StringPrintf("IHttpNegotiate not supported 0x%08X", hr); + } + + return hr; +} + + +// static +HRESULT HttpNegotiatePatch::BeginningTransaction( + IHttpNegotiate_BeginningTransaction_Fn original, IHttpNegotiate* me, + LPCWSTR url, LPCWSTR headers, DWORD reserved, LPWSTR* additional_headers) { + DLOG(INFO) << __FUNCTION__ << " " << url << " headers:\n" << headers; + + HRESULT hr = original(me, url, headers, reserved, additional_headers); + + if (FAILED(hr)) { + DLOG(WARNING) << __FUNCTION__ << " Delegate returned an error"; + return hr; + } + + // TODO(skare@google.com): Modify User-Agent here. + + return hr; +} + +// static +HRESULT HttpNegotiatePatch::OnResponse( + IHttpNegotiate_OnResponse_Fn original, IHttpNegotiate* me, + DWORD response_code, LPCWSTR response_headers, LPCWSTR request_headers, + LPWSTR* additional_request_headers) { + DLOG(INFO) << __FUNCTION__ << " response headers:\n" << response_headers; + + base::Time current_time = base::Time::Now(); + + HRESULT hr = original(me, response_code, response_headers, request_headers, + additional_request_headers); + + if (FAILED(hr)) { + DLOG(WARNING) << __FUNCTION__ << " Delegate returned an error"; + return hr; + } + + CookieAccountant::GetInstance()->RecordHttpResponseCookies( + std::string(CW2A(response_headers)), current_time); + + return hr; +} diff --git a/ceee/ie/plugin/bho/http_negotiate.h b/ceee/ie/plugin/bho/http_negotiate.h new file mode 100644 index 0000000..d2975d3 --- /dev/null +++ b/ceee/ie/plugin/bho/http_negotiate.h @@ -0,0 +1,69 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ +#define CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ + +#include <atlbase.h> +#include <shdeprecated.h> + +#include "base/basictypes.h" + +// Typedefs for IHttpNegotiate methods. +typedef HRESULT (STDMETHODCALLTYPE* IHttpNegotiate_BeginningTransaction_Fn)( + IHttpNegotiate* me, LPCWSTR url, LPCWSTR headers, DWORD reserved, + LPWSTR* additional_headers); +typedef HRESULT (STDMETHODCALLTYPE* IHttpNegotiate_OnResponse_Fn)( + IHttpNegotiate* me, DWORD response_code, LPCWSTR response_header, + LPCWSTR request_header, LPWSTR* additional_request_headers); + +// Typedefs for IBindStatusCallback methods. +typedef HRESULT (STDMETHODCALLTYPE* IBindStatusCallback_StartBinding_Fn)( + IBindStatusCallback* me, DWORD reserved, IBinding *binding); +typedef HRESULT (STDMETHODCALLTYPE* IBindStatusCallback_OnProgress_Fn)( + IBindStatusCallback* me, ULONG progress, ULONG progress_max, + ULONG status_code, LPCWSTR status_text); + +// Typedefs for IInternetProtocolSink methods. +typedef HRESULT (STDMETHODCALLTYPE* IInternetProtocolSink_ReportProgress_Fn)( + IInternetProtocolSink* me, ULONG status_code, LPCWSTR status_text); + +// Patches methods of urlmon's IHttpNegotiate implementation for the purposes +// of adding to the http user agent header. + +class HttpNegotiatePatch { + private: + // Class is not to be instantiated at the moment. + HttpNegotiatePatch(); + ~HttpNegotiatePatch(); + + public: + static bool Initialize(); + static void Uninitialize(); + + // IHttpNegotiate patch methods + static STDMETHODIMP BeginningTransaction( + IHttpNegotiate_BeginningTransaction_Fn original, IHttpNegotiate* me, + LPCWSTR url, LPCWSTR headers, DWORD reserved, LPWSTR* additional_headers); + static STDMETHODIMP OnResponse(IHttpNegotiate_OnResponse_Fn original, + IHttpNegotiate* me, DWORD response_code, LPCWSTR response_headers, + LPCWSTR request_headers, LPWSTR* additional_request_headers); + + protected: + static HRESULT PatchHttpNegotiate(IUnknown* to_patch); + + private: + // Count number of BHOs depending on this patch. + // Unhook when the last one goes away. + static CComAutoCriticalSection bho_instance_count_crit_; + static int bho_instance_count_; + + DISALLOW_COPY_AND_ASSIGN(HttpNegotiatePatch); +}; + +// Attempts to get to the associated browser service for an active request. +HRESULT GetBrowserServiceFromProtocolSink(IInternetProtocolSink* sink, + IBrowserService** browser_service); + +#endif // CEEE_IE_PLUGIN_BHO_HTTP_NEGOTIATE_H_ diff --git a/ceee/ie/plugin/bho/infobar_browser_window.cc b/ceee/ie/plugin/bho/infobar_browser_window.cc new file mode 100644 index 0000000..fecf5e6 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_browser_window.cc @@ -0,0 +1,205 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the infobar browser window. + +#include "ceee/ie/plugin/bho/infobar_browser_window.h" + +#include <atlapp.h> +#include <atlcrack.h> +#include <atlmisc.h> +#include <atlsafe.h> +#include <atlwin.h> + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "chrome/common/chrome_switches.h" +#include "chrome_frame/com_message_event.h" + +namespace infobar_api { + +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO InfobarBrowserWindow::handler_type_void_= + { CC_STDCALL, VT_EMPTY, 0, { } }; + +InfobarBrowserWindow::InfobarBrowserWindow() : delegate_(NULL) { +} + +InfobarBrowserWindow::~InfobarBrowserWindow() { +} + +STDMETHODIMP InfobarBrowserWindow::GetWantsPrivileged( + boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Must enable experimental extensions because we want to load html pages + // from our extension. + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetChromeProfileName(BSTR* profile_name) { + *profile_name = ::SysAllocString( + ceee_module_util::GetBrokerProfileNameForIe()); + return S_OK; +} + +STDMETHODIMP InfobarBrowserWindow::GetExtensionApisToAutomate( + BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfReadyStateChanged(LONG state) { + if (state == READYSTATE_COMPLETE) { + // We already loaded the extension, enable them in this CF. + chrome_frame_->getEnabledExtensions(); + // Also we should already have URL, navigate to it. + Navigate(); + infobar_events_funnel().OnDocumentComplete(); + } +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfExtensionReady(BSTR path, + int response) { + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath(FilePath(extension_path_)); + } + + chrome_frame_->getEnabledExtensions(); +} + +STDMETHODIMP_(void) InfobarBrowserWindow::OnCfClose() { + if (delegate_ != NULL) + delegate_->OnWindowClose(); +} + + HRESULT InfobarBrowserWindow::Initialize(HWND parent) { + HRESULT hr = InitializeAndShowWindow(parent); + if (FAILED(hr)) { + LOG(ERROR) << "Infobar browser failed to initialize its site window: " << + com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT InfobarBrowserWindow::InitializeAndShowWindow(HWND parent) { + if (NULL == Create(parent)) + return E_FAIL; + + BOOL shown = ShowWindow(SW_SHOW); + DCHECK(shown); + + return shown ? S_OK : E_FAIL; +} + +HRESULT InfobarBrowserWindow::Teardown() { + if (IsWindow()) { + // Teardown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + } + + return S_OK; +} + +void InfobarBrowserWindow::SetUrl(const std::wstring& url) { + // Navigate to the URL if the browser exists, otherwise just store the URL. + url_ = url; + Navigate(); +} + + +LRESULT InfobarBrowserWindow::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CAxHostWindow::CreateInstance(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Infobar failed to create ActiveX host window. " << + com::LogHr(hr); + return 1; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = chrome_frame_.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create the Chrome Frame instance. " << + com::LogHr(hr); + return 1; + } + + // And attach it to our window. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host. " << + com::LogHr(hr); + return 1; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink. " << com::LogHr(hr); + } + + return S_OK; +} + +void InfobarBrowserWindow::OnDestroy() { + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + chrome_frame_.Release(); + } +} + +void InfobarBrowserWindow::Navigate() { + // If the browser has not been created then just return. + if (!chrome_frame_) + return; + + if (url_.empty()) { + LOG(WARNING) << "Navigating infobar to not specified URL"; + } else { + HRESULT hr = chrome_frame_->put_src(CComBSTR(url_.c_str())); + LOG_IF(WARNING, FAILED(hr)) << + "Infobar: ChromeFrame::put_src returned: " << com::LogHr(hr); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_browser_window.h b/ceee/ie/plugin/bho/infobar_browser_window.h new file mode 100644 index 0000000..fd6c975 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_browser_window.h @@ -0,0 +1,156 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Infobar browser window. This is the window that hosts CF and navigates it +// to the infobar URL. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlmisc.h> + +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" + +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +namespace infobar_api { + +class InfobarBrowserWindow; + +typedef IDispEventSimpleImpl<0, InfobarBrowserWindow, &DIID_DIChromeFrameEvents> + ChromeFrameEvents; + +// The window that hosts CF where infobar URL is loaded. It implements a limited +// site functionality needed for CF as well as handles sink events from CF. +// TODO(vadimb@google.com): Refactor this class, ChromeFrameHost and ToolBand +// to have a common functionality supported in a common base class. +class ATL_NO_VTABLE InfobarBrowserWindow + : public CComObjectRootEx<CComSingleThreadModel>, + public IObjectWithSiteImpl<InfobarBrowserWindow>, + public IServiceProviderImpl<InfobarBrowserWindow>, + public IChromeFramePrivileged, + public ChromeFrameEvents, + public CWindowImpl<InfobarBrowserWindow> { + public: + // Class to connect this an instance of InfobarBrowserWindow with a hosting + // object who should inherit from InfobarBrowserWindow::Delegate and set it + // with set_delegate() functions. + class Delegate { + public: + virtual ~Delegate() {} + // Informs about window.close() event. + virtual void OnWindowClose() = 0; + }; + + InfobarBrowserWindow(); + ~InfobarBrowserWindow(); + + BEGIN_COM_MAP(InfobarBrowserWindow) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(InfobarBrowserWindow) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + BEGIN_SINK_MAP(InfobarBrowserWindow) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONCLOSE, + OnCfClose, &handler_type_void_) + END_SINK_MAP() + + BEGIN_MSG_MAP(InfobarBrowserWindow) + MSG_WM_CREATE(OnCreate) + MSG_WM_DESTROY(OnDestroy) + END_MSG_MAP() + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean *wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR *args); + STDMETHOD(GetChromeProfileName)(BSTR *args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR *args); + // @} + + // @name ChromeFrame event handlers. + // @{ + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfClose)(); + // @} + + // Initializes the browser window to the given site. + HRESULT Initialize(HWND parent); + // Tears down an initialized browser window. + HRESULT Teardown(); + + // Navigates the browser to the given URL if the browser has already been + // created, otherwise stores the URL to navigate later on. + void SetUrl(const std::wstring& url); + + // Set the delegate to be informed about window.close() events. + void set_delegate(Delegate* delegate) { delegate_ = delegate; } + + // Unit test seam. + virtual InfobarEventsFunnel& infobar_events_funnel() { + return infobar_events_funnel_; + } + + protected: + // @name Message handlers. + // @{ + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + void OnDestroy(); + // @} + + private: + // The funnel for sending infobar events to the broker. + InfobarEventsFunnel infobar_events_funnel_; + + // Our Chrome frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // Url to navigate infobar to. + std::wstring url_; + // Filesystem path to the .crx we will install, or the empty string, or + // (if not ending in .crx) the path to an exploded extension directory to + // load. + std::wstring extension_path_; + + // Delegate. Not owned by the instance of this object. + Delegate* delegate_; + + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_void_; + + // Subroutine of general initialization. Extracted to make testable. + virtual HRESULT InitializeAndShowWindow(HWND parent); + + // Navigate the browser to url_ if the browser has been created. + void Navigate(); + + DISALLOW_COPY_AND_ASSIGN(InfobarBrowserWindow); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_BROWSER_WINDOW_H_ diff --git a/ceee/ie/plugin/bho/infobar_events_funnel.cc b/ceee/ie/plugin/bho/infobar_events_funnel.cc new file mode 100644 index 0000000..d1a0b67 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Infobar Events to the Broker. + +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "base/json/json_writer.h" + +namespace { +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; +} + +HRESULT InfobarEventsFunnel::OnDocumentComplete() { + DictionaryValue info; + return SendEvent(kOnDocumentCompleteEventName, info); +} diff --git a/ceee/ie/plugin/bho/infobar_events_funnel.h b/ceee/ie/plugin/bho/infobar_events_funnel.h new file mode 100644 index 0000000..fcfa274d7 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel.h @@ -0,0 +1,24 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Infobar Events. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ + +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send infobar related events to the Broker. +class InfobarEventsFunnel : public EventsFunnel { + public: + InfobarEventsFunnel() : EventsFunnel(false) {} + + // Sends the infobar.onDocumentComplete event to the Broker. + virtual HRESULT OnDocumentComplete(); + + private: + DISALLOW_COPY_AND_ASSIGN(InfobarEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc b/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc new file mode 100644 index 0000000..9fe15ea --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_events_funnel_unittest.cc @@ -0,0 +1,38 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for InfobarEventsFunnel. + +#include "ceee/ie/plugin/bho/infobar_events_funnel.h" +#include "base/values.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +const char kOnDocumentCompleteEventName[] = "infobar.onDocumentComplete"; + +class TestInfobarEventsFunnel : public InfobarEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(InfobarEventsFunnelTest, OnDocumentComplete) { + TestInfobarEventsFunnel infobar_events_funnel; + DictionaryValue dict; + + EXPECT_CALL(infobar_events_funnel, SendEvent( + StrEq(kOnDocumentCompleteEventName), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(infobar_events_funnel.OnDocumentComplete()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/infobar_manager.cc b/ceee/ie/plugin/bho/infobar_manager.cc new file mode 100644 index 0000000..6b969f6 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_manager.cc @@ -0,0 +1,270 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the manager for infobar windows. + +#include "ceee/ie/plugin/bho/infobar_manager.h" + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlmisc.h> +#include <atlwin.h> + +#include "base/logging.h" +#include "ceee/common/windows_constants.h" +#include "chrome_frame/utils.h" + +namespace { + +enum ContainerWindowUserMessages { + TM_NOTIFY_UPDATE_POSITION = WM_USER + 600, + TM_DELAYED_CLOSE_INFOBAR = (TM_NOTIFY_UPDATE_POSITION + 1), +}; + +} // namespace + +namespace infobar_api { + +// ContainerWindow subclasses IE content window's container window, handles +// WM_NCCALCSIZE to resize its client area. It also handles WM_SIZE and WM_MOVE +// messages to make infobars consistent with IE content window's size and +// position. +class InfobarManager::ContainerWindow : public CWindowImpl<ContainerWindow> { + public: + ContainerWindow(HWND container, InfobarManager* manager) + : infobar_manager_(manager) { + PinModule(); + destroyed_ = !::IsWindow(container) || !SubclassWindow(container); + } + + virtual ~ContainerWindow() { + if (!destroyed_) + UnsubclassWindow(); + } + + bool destroyed() const { + return destroyed_; + } + + BEGIN_MSG_MAP_EX(ContainerWindow) + MSG_WM_NCCALCSIZE(OnNcCalcSize) + MSG_WM_SIZE(OnSize) + MSG_WM_MOVE(OnMove) + MSG_WM_DESTROY(OnDestroy) + MESSAGE_HANDLER(TM_NOTIFY_UPDATE_POSITION, OnNotifyUpdatePosition) + MESSAGE_HANDLER(TM_DELAYED_CLOSE_INFOBAR, OnDelayedCloseInfobar) + END_MSG_MAP() + + private: + // Handles WM_NCCALCSIZE message. + LRESULT OnNcCalcSize(BOOL calc_valid_rects, LPARAM lparam) { + // Adjust client area for infobar. + LRESULT ret = DefWindowProc(WM_NCCALCSIZE, + static_cast<WPARAM>(calc_valid_rects), lparam); + // Whether calc_valid_rects is true or false, we could treat beginning of + // lparam as a RECT object. + RECT* rect = reinterpret_cast<RECT*>(lparam); + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowNcCalcSize(rect); + + // If infobars reserve all the space and rect becomes empty, the container + // window won't receive subsequent WM_SIZE and WM_MOVE messages. + // In this case, we have to explicitly notify infobars to update their + // position. + if (rect->right - rect->left <= 0 || rect->bottom - rect->top <= 0) + PostMessage(TM_NOTIFY_UPDATE_POSITION, 0, 0); + return ret; + } + + // Handles WM_SIZE message. + void OnSize(UINT type, CSize size) { + DefWindowProc(WM_SIZE, static_cast<WPARAM>(type), + MAKELPARAM(size.cx, size.cy)); + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + } + + // Handles WM_MOVE message. + void OnMove(CPoint point) { + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + } + + // Handles WM_DESTROY message. + void OnDestroy() { + // When refreshing IE window, this window may be destroyed. + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowDestroy(); + if (m_hWnd && IsWindow()) + UnsubclassWindow(); + destroyed_ = true; + } + + // Handles TM_NOTIFY_UPDATE_POSITION message - delayed window resize message. + LRESULT OnNotifyUpdatePosition(UINT message, WPARAM wparam, LPARAM lparam, + BOOL& handled) { + if (infobar_manager_ != NULL) + infobar_manager_->OnContainerWindowUpdatePosition(); + + handled = TRUE; + return 0; + } + + // Handles TM_DELAYED_CLOSE_INFOBAR - delayed infobar window close request. + LRESULT OnDelayedCloseInfobar(UINT message, WPARAM wparam, LPARAM lparam, + BOOL& handled) { + if (infobar_manager_ != NULL) { + InfobarType type = static_cast<InfobarType>(wparam); + infobar_manager_->OnContainerWindowDelayedCloseInfobar(type); + } + + handled = TRUE; + return 0; + } + + // Pointer to infobar manager. This object is not owned by the class instance. + InfobarManager* infobar_manager_; + + // True is this window was destroyed or not subclassed. + bool destroyed_; + + DISALLOW_COPY_AND_ASSIGN(ContainerWindow); +}; + +InfobarManager::InfobarManager(HWND tab_window) + : tab_window_(tab_window) { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + // Note that when InfobarManager is being initialized the IE has not created + // the tab. Therefore we cannot find the container window here and have to + // pass interface for a function that finds windows to be called later. + infobars_[index].reset( + InfobarWindow::CreateInfobar(static_cast<InfobarType>(index), this)); + } +} + +HRESULT InfobarManager::Show(InfobarType type, int max_height, + const std::wstring& url, bool slide) { + if (type < FIRST_INFOBAR_TYPE || type >= END_OF_INFOBAR_TYPE || + infobars_[type] == NULL) { + return E_INVALIDARG; + } + // Set the URL. If the window is not created it will navigate there as soon as + // it is created. + infobars_[type]->Navigate(url); + // Create the window if not created. + if (!infobars_[type]->IsWindow()) { + infobars_[type]->Create(tab_window_, NULL, NULL, + WS_CHILD | WS_CLIPCHILDREN); + } + if (!infobars_[type]->IsWindow()) + return E_UNEXPECTED; + + HRESULT hr = infobars_[type]->Show(max_height, slide); + return hr; +} + +HRESULT InfobarManager::Hide(InfobarType type) { + if (type < FIRST_INFOBAR_TYPE || type >= END_OF_INFOBAR_TYPE || + infobars_[type] == NULL) { + return E_INVALIDARG; + } + // There is a choice either to hide or to destroy the infobar window. + // This implementation destroys the infobar to save resources and stop all + // scripts that possibly still run in the window. If we want to just hide the + // infobar window instead then we should change Reset to Hide here, possibly + // navigate the infobar window to "about:blank" and make sure that the code + // in Show() does not try to create the chrome frame window again. + infobars_[type]->Reset(); + return S_OK; +} + +void InfobarManager::HideAll() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) + Hide(static_cast<InfobarType>(index)); +} + +// Callback function for EnumChildWindows. lParam should be the pointer to +// HWND variable where the handle of the window which class is +// kIeTabContentParentWindowClass will be written. +static BOOL CALLBACK FindContentParentWindowsProc(HWND hwnd, LPARAM lparam) { + HWND* window_handle = reinterpret_cast<HWND*>(lparam); + if (NULL == window_handle) { + // It makes no sense to continue enumeration. + return FALSE; + } + + // Variable to hold the class name. The size does not matter as long as it + // is at least can hold kIeTabContentParentWindowClass. + wchar_t class_name[100]; + if (::GetClassName(hwnd, class_name, arraysize(class_name)) && + lstrcmpi(windows::kIeTabContentParentWindowClass, class_name) == 0) { + // We found the window. Return its handle and stop enumeration. + *window_handle = hwnd; + return FALSE; + } + return TRUE; +} + +HWND InfobarManager::GetContainerWindow() { + if (container_window_ != NULL && container_window_->destroyed()) + container_window_.reset(NULL); + + if (container_window_ == NULL) { + if (tab_window_ != NULL && ::IsWindow(tab_window_)) { + // Find the window which is the container for the HTML view (parent of + // the content). + HWND content_parent_window = NULL; + ::EnumChildWindows(tab_window_, FindContentParentWindowsProc, + reinterpret_cast<LPARAM>(&content_parent_window)); + DCHECK(content_parent_window); + if (content_parent_window != NULL) { + container_window_.reset( + new ContainerWindow(content_parent_window, this)); + } + } + } + DCHECK(container_window_ != NULL && container_window_->IsWindow()); + return container_window_->m_hWnd; +} + +void InfobarManager::OnWindowClose(InfobarType type) { + // This callback is called from CF callback so we should not destroy the + // infobar window right away as it may result on deleting the object that + // started this callback. So instead we post ourtselves the message. + if (container_window_ != NULL) + container_window_->PostMessage(TM_DELAYED_CLOSE_INFOBAR, + static_cast<WPARAM>(type), 0); +} + +void InfobarManager::OnContainerWindowNcCalcSize(RECT* rect) { + if (rect == NULL) + return; + + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->ReserveSpace(rect); + } +} + +void InfobarManager::OnContainerWindowUpdatePosition() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->UpdatePosition(); + } +} + +void InfobarManager::OnContainerWindowDelayedCloseInfobar(InfobarType type) { + // Hide the infobar window. Parameter validation is handled in Hide(). + Hide(type); +} + +void InfobarManager::OnContainerWindowDestroy() { + for (int index = 0; index < END_OF_INFOBAR_TYPE; ++index) { + if (infobars_[index] != NULL) + infobars_[index]->Reset(); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_manager.h b/ceee/ie/plugin/bho/infobar_manager.h new file mode 100644 index 0000000..dead924 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_manager.h @@ -0,0 +1,67 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Manager for infobar windows. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ + +#include "base/scoped_ptr.h" +#include "base/singleton.h" +#include "ceee/ie/plugin/bho/infobar_window.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" + +namespace infobar_api { + +// InfobarManager creates and manages infobars, which are displayed at the top +// or bottom of IE content window. +class InfobarManager : public InfobarWindow::Delegate, + public WebBrowserEventsSource::Sink { + public: + explicit InfobarManager(HWND tab_window); + + // Shows the infobar of the specified type and navigates it to the specified + // URL. + HRESULT Show(InfobarType type, int max_height, const std::wstring& url, + bool slide); + // Hides the infobar of the specified type. + HRESULT Hide(InfobarType type); + // Hides all infobars. + void HideAll(); + + // Implementation of InfobarWindow::Delegate. + // Finds the handle of the container window. + virtual HWND GetContainerWindow(); + // Informs about window.close() event. + virtual void OnWindowClose(InfobarType type); + + private: + class ContainerWindow; + + // The HWND of the tab window the infobars are associated with. + HWND tab_window_; + + // Parent window for IE content window. + scoped_ptr<ContainerWindow> container_window_; + + // Infobar windows. + scoped_ptr<InfobarWindow> infobars_[END_OF_INFOBAR_TYPE]; + + // ContainerWindow callbacks. + // Callback for WM_NCCALCSIZE. + void OnContainerWindowNcCalcSize(RECT* rect); + // Callback for messages on size or position change. + void OnContainerWindowUpdatePosition(); + // Callback for message requesting closing the infobar. + void OnContainerWindowDelayedCloseInfobar(InfobarType type); + // Callback for WM_DESTROY. + void OnContainerWindowDestroy(); + + DISALLOW_COPY_AND_ASSIGN(InfobarManager); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/infobar_window.cc b/ceee/ie/plugin/bho/infobar_window.cc new file mode 100644 index 0000000..62e9301 --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_window.cc @@ -0,0 +1,314 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of the manager for infobar windows. + +#include "ceee/ie/plugin/bho/infobar_window.h" + +#include <atlapp.h> +#include <atlcrack.h> +#include <atlmisc.h> + +#include "base/logging.h" +#include "base/scoped_ptr.h" + +namespace { + +const UINT_PTR kInfobarSlidingTimerId = 1U; +// Interval for sliding the infobar in milliseconds. +const UINT kInfobarSlidingTimerIntervalMs = 50U; +// The step when the infobar is sliding, in pixels. +const int kInfobarSlidingStep = 10; +// The default height of the infobar. See also similar constant in +// ceee/ie/plugin/bho/executor.cc which overrides this one. +const int kInfobarDefaultHeight = 39; + +} // namespace + + +namespace infobar_api { + +InfobarWindow* InfobarWindow::CreateInfobar(InfobarType type, + Delegate* delegate) { + DCHECK(delegate); + return NULL == delegate ? NULL : new InfobarWindow(type, delegate); +} + +InfobarWindow::InfobarWindow(InfobarType type, Delegate* delegate) + : type_(type), + delegate_(delegate), + show_(false), + target_height_(1), + current_height_(1), + sliding_infobar_(false) { + DCHECK(delegate); +} + +InfobarWindow::~InfobarWindow() { + Reset(); + + if (IsWindow()) { + DestroyWindow(); + } else { + NOTREACHED() << "Infobar window was not successfully created."; + } +} + +void InfobarWindow::OnWindowClose() { + // Propagate the event to the manager. + if (delegate_ != NULL) + delegate_->OnWindowClose(type_); +} + +HRESULT InfobarWindow::Show(int max_height, bool slide) { + if (url_.empty()) + return E_UNEXPECTED; + + StartUpdatingLayout(true, max_height, slide); + return S_OK; +} + +HRESULT InfobarWindow::Hide() { + StartUpdatingLayout(false, 0, false); + + return S_OK; +} + +HRESULT InfobarWindow::Navigate(const std::wstring& url) { + // If the CF exists (which means the infobar has already been created) then + // navigate it. Otherwise just store the URL, it will be passed to the CF when + // it will be created. + url_ = url; + if (chrome_frame_host_) + chrome_frame_host_->SetUrl(url_); + return S_OK; +} + +void InfobarWindow::ReserveSpace(RECT* rect) { + DCHECK(rect); + if (rect == NULL || !show_) + return; + + switch (type_) { + case TOP_INFOBAR: + rect->top += current_height_; + if (rect->top > rect->bottom) + rect->top = rect->bottom; + break; + case BOTTOM_INFOBAR: + rect->bottom -= current_height_; + if (rect->bottom < rect->top) + rect->bottom = rect->top; + break; + default: + NOTREACHED() << "Unknown InfobarType value."; + break; + } +} + +void InfobarWindow::UpdatePosition() { + // Make infobar be consistent with IE window's size. + // NOTE: Even if currently it is not visible, we still need to update its + // position, since the contents may need to decide its layout based on the + // width of the infobar. + + CRect rect = CalculatePosition(); + if (IsWindow()) + MoveWindow(&rect, TRUE); +} + +void InfobarWindow::Reset() { + Hide(); + + if (chrome_frame_host_) + chrome_frame_host_->set_delegate(NULL); + + DCHECK(!show_ && !sliding_infobar_); + if (m_hWnd != NULL) { + DestroyWindow(); + m_hWnd = NULL; + } + url_.clear(); + chrome_frame_host_.Release(); +} + +void InfobarWindow::StartUpdatingLayout(bool show, int max_height, bool slide) { + if (!IsWindow()) { + LOG(ERROR) << "Updating infobar layout when window has not been created"; + return; + } + + show_ = show; + if (show) { + int html_content_height = kInfobarDefaultHeight; + CSize html_content_size(0, 0); + if (SUCCEEDED(GetContentSize(&html_content_size)) && + html_content_size.cy > 0) { + html_content_height = html_content_size.cy; + } + target_height_ = (max_height == 0 || html_content_height < max_height) ? + html_content_height : max_height; + if (target_height_ <= 0) { + target_height_ = 1; + } + } else { + target_height_ = 1; + } + + if (!slide || !show) { + current_height_ = target_height_; + + if (sliding_infobar_) { + KillTimer(kInfobarSlidingTimerId); + sliding_infobar_ = false; + } + } else { + // If the infobar is visible and sliding effect is requested, we need to + // start expanding/shrinking the infobar according to its current height. + current_height_ = CalculateNextHeight(); + + if (!sliding_infobar_) { + SetTimer(kInfobarSlidingTimerId, kInfobarSlidingTimerIntervalMs, NULL); + sliding_infobar_ = true; + } + } + + UpdateLayout(); +} + +int InfobarWindow::CalculateNextHeight() { + if (current_height_ < target_height_) { + return std::min(current_height_ + kInfobarSlidingStep, target_height_); + } else if (current_height_ > target_height_) { + return std::max(current_height_ - kInfobarSlidingStep, target_height_); + } else { + return current_height_; + } +} + +RECT InfobarWindow::CalculatePosition() { + CRect rect(0, 0, 0, 0); + + if (NULL == delegate_) + return rect; + HWND container_window = delegate_->GetContainerWindow(); + if (container_window == NULL || !::IsWindow(container_window)) + return rect; + HWND container_parent_window = ::GetParent(container_window); + if (!::IsWindow(container_parent_window)) + return rect; + + ::GetWindowRect(container_window, &rect); + ::MapWindowPoints(NULL, container_parent_window, + reinterpret_cast<POINT*>(&rect), 2); + + switch (type_) { + case TOP_INFOBAR: + if (rect.top + current_height_ < rect.bottom) + rect.bottom = rect.top + current_height_; + break; + case BOTTOM_INFOBAR: + if (rect.bottom - current_height_ > rect.top) + rect.top = rect.bottom - current_height_; + break; + default: + NOTREACHED() << "Unknown InfobarType value."; + break; + } + return rect; +} + +void InfobarWindow::UpdateLayout() { + CRect rect = CalculatePosition(); + if (IsWindow()) { + // Set infobar's z-order, place it at the top, so that it won't be hidden by + // IE window. + SetWindowPos(HWND_TOP, &rect, show_ ? SWP_SHOWWINDOW : SWP_HIDEWINDOW); + } + + HWND container_window = NULL; + if (delegate_ != NULL) + container_window = delegate_->GetContainerWindow(); + if (container_window != NULL && ::IsWindow(container_window)) { + // Call SetWindowPos with SWP_FRAMECHANGED for IE window, then IE + // window would receive WM_NCCALCSIZE to recalculate its client size. + ::SetWindowPos(container_window, + NULL, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_FRAMECHANGED); + } +} + +HRESULT InfobarWindow::GetContentSize(SIZE* size) { + DCHECK(size); + if (NULL == size) + return E_POINTER; + + // Set the size to 0 that means we do not know it. + // TODO(vadimb@google.com): Find how to get the content size from the CF. + size->cx = 0; + size->cy = 0; + return S_OK; +} + +LRESULT InfobarWindow::OnTimer(UINT_PTR nIDEvent) { + DCHECK(nIDEvent == kInfobarSlidingTimerId); + if (show_ && sliding_infobar_ && current_height_ != target_height_) { + current_height_ = CalculateNextHeight(); + UpdateLayout(); + } else if (sliding_infobar_) { + KillTimer(kInfobarSlidingTimerId); + sliding_infobar_ = false; + } + + return S_OK; +} + +LRESULT InfobarWindow::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // TODO(vadimb@google.com): Better way to do this is to derive + // InfobarBrowserWindow from InitializingCoClass and give it an + // InitializeMethod, then create it with + // InfobarBrowserWindow::CreateInitialized(...). + CComObject<InfobarBrowserWindow>* chrome_frame_host = NULL; + CComObject<InfobarBrowserWindow>::CreateInstance(&chrome_frame_host); + if (chrome_frame_host) { + chrome_frame_host_.Attach(chrome_frame_host); + chrome_frame_host_->SetUrl(url_); + chrome_frame_host_->Initialize(m_hWnd); + chrome_frame_host_->set_delegate(this); + AdjustSize(); + } + return S_OK; +} + +void InfobarWindow::OnPaint(CDCHandle dc) { + RECT rc; + if (GetUpdateRect(&rc, FALSE)) { + PAINTSTRUCT ps = {}; + BeginPaint(&ps); + + BOOL ret = GetClientRect(&rc); + DCHECK(ret); + FillRect(ps.hdc, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH)); + ::DrawText(ps.hdc, L"Google CEEE. No Chrome Frame found!", -1, + &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); + + EndPaint(&ps); + } +} + +void InfobarWindow::OnSize(UINT type, CSize size) { + AdjustSize(); +} + +void InfobarWindow::AdjustSize() { + if (NULL != chrome_frame_host_) { + CRect rect; + GetClientRect(&rect); + chrome_frame_host_->SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(), + SWP_NOACTIVATE | SWP_NOZORDER); + } +} + +} // namespace infobar_api diff --git a/ceee/ie/plugin/bho/infobar_window.h b/ceee/ie/plugin/bho/infobar_window.h new file mode 100644 index 0000000..e745cef --- /dev/null +++ b/ceee/ie/plugin/bho/infobar_window.h @@ -0,0 +1,143 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Infobar window. + +#ifndef CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ +#define CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlmisc.h> +#include <atlwin.h> + +#include "base/singleton.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/infobar_browser_window.h" + +namespace infobar_api { + +enum InfobarType { + FIRST_INFOBAR_TYPE = 0, + TOP_INFOBAR = 0, // Infobar at the top. + BOTTOM_INFOBAR = 1, // Infobar at the bottom. + END_OF_INFOBAR_TYPE = 2 +}; + +// InfobarWindow is the window created on the top or bottom of the browser tab +// window that contains the web browser window. +class InfobarWindow : public InfobarBrowserWindow::Delegate, + public CWindowImpl<InfobarWindow, CWindow> { + public: + class Delegate { + public: + virtual ~Delegate() {} + // Returns the window handle for the HTML container window. + virtual HWND GetContainerWindow() = 0; + // Informs about window.close() event. + virtual void OnWindowClose(InfobarType type) = 0; + }; + + static InfobarWindow* CreateInfobar(InfobarType type, Delegate* delegate); + ~InfobarWindow(); + + // Implementation of InfobarBrowserWindow::Delegate. + // Informs about window.close() event. + virtual void OnWindowClose(); + + // Shows the infobar. + // NOTE: Navigate should be called before Show. + // The height of the infobar is calculated to fit the content (limited to + // max_height if the content is too high; no limit if max_height is set to + // 0). + // slide indicates whether to show sliding effect. + HRESULT Show(int max_height, bool slide); + + // Hides the infobar. + HRESULT Hide(); + + // Navigates the HTML view of the infobar. + HRESULT Navigate(const std::wstring& url); + + // Reserves space for the infobar when IE window recalculates its size. + void ReserveSpace(RECT* rect); + + // Updates the infobar size and position when IE content window size or + // position is changed. + void UpdatePosition(); + + // Destroys the browser window. + void Reset(); + + private: + BEGIN_MSG_MAP(InfobarWindow) + MSG_WM_TIMER(OnTimer); + MSG_WM_CREATE(OnCreate) + MSG_WM_PAINT(OnPaint) + MSG_WM_SIZE(OnSize) + END_MSG_MAP() + + // Type of the infobar - whether it is displayed at the top or at the bottom + // of the IE content window. + InfobarType type_; + + // Delegate, connection to the infobar manager. + Delegate* delegate_; + + // URL to navigate to. + std::wstring url_; + + // Whether the infobar is shown or not. + bool show_; + + // The target height of the infobar. + int target_height_; + + // The current height of the infobar. + int current_height_; + + // Indicates whether the infobar is sliding. + bool sliding_infobar_; + + // The Chrome Frame host handling a Chrome Frame instance for us. + CComPtr<InfobarBrowserWindow> chrome_frame_host_; + + // Constructor. + InfobarWindow(InfobarType type, Delegate* delegate); + + // If show is true, shrinks IE content window and shows the infobar + // either at the top or at the bottom. Otherwise, hides the infobar and + // restores IE content window. + void StartUpdatingLayout(bool show, int max_height, bool slide); + + // Based on the current height and the target height, decides the height of + // the next step. This is used when showing sliding effect. + int CalculateNextHeight(); + + // Calculates the position of the infobar based on its current height. + RECT CalculatePosition(); + + // Updates the layout (sizes and positions of infobar and IE content window) + // based on the current height. + void UpdateLayout(); + + HRESULT GetContentSize(SIZE* size); + + // Event handlers. + LRESULT OnTimer(UINT_PTR nIDEvent); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + void OnPaint(CDCHandle dc); + void OnSize(UINT type, CSize size); + + void AdjustSize(); + + DISALLOW_COPY_AND_ASSIGN(InfobarWindow); +}; + +} // namespace infobar_api + +#endif // CEEE_IE_PLUGIN_BHO_INFOBAR_WINDOW_H_ diff --git a/ceee/ie/plugin/bho/mediumtest_browser_event.cc b/ceee/ie/plugin/bho/mediumtest_browser_event.cc new file mode 100644 index 0000000..3288e9d --- /dev/null +++ b/ceee/ie/plugin/bho/mediumtest_browser_event.cc @@ -0,0 +1,495 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A test that hosts and excercises the webbrowser control to test +// its event firing behavior. +#include <atlcrack.h> +#include <atlsync.h> +#include <atlwin.h> +#include <set> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "ceee/ie/testing/mediumtest_ie_common.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + + +namespace { + +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; + +using testing::BrowserEventSinkBase; +using testing::GetTestUrl; +using testing::GetTempPath; +using testing::ShellBrowserTestImpl; + +// {AFF1D082-6B03-4b29-9521-E52240F6333B} +const GUID IID_Dummy = + { 0xaff1d082, 0x6b03, 0x4b29, + { 0x95, 0x21, 0xe5, 0x22, 0x40, 0xf6, 0x33, 0x3b } }; + +class TestBrowserEventSink; + +class TestFrameEventHandler + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<TestFrameEventHandler>, + public InstanceCountMixin<TestFrameEventHandler>, + public IPropertyNotifySink, + public IAdviseSink { + public: + BEGIN_COM_MAP(TestFrameEventHandler) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + COM_INTERFACE_ENTRY(IAdviseSink) + END_COM_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + TestFrameEventHandler() : event_sink_(NULL), + document_property_notify_sink_cookie_(-1), + advise_sink_cookie_(-1) { + } + + virtual ~TestFrameEventHandler() { + ATLTRACE("~TestFrameEventHandler[%ws]\n", url_ ? url_ : L""); + } + + void DetachFromSink(); + + STDMETHOD(OnChanged)(DISPID changed_property); + STDMETHOD(OnRequestEdit)(DISPID changed_property) { + ATLTRACE("OnRequestEdit(%d)\n", changed_property); + return S_OK; + } + + // IAdviseSink + STDMETHOD_(void, OnDataChange)(FORMATETC *pFormatetc, STGMEDIUM *pStgmed) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnViewChange)(DWORD dwAspect, LONG lindex) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnRename)(IMoniker *pmk) { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnSave)() { + ATLTRACE("%s\n", __FUNCTION__); + } + STDMETHOD_(void, OnClose)(); + + virtual void GetDescription(std::string* description) const { + description->clear(); + description->append("TestFrameEventHandler"); + // TODO(siggi@chromium.org): append URL + } + + HRESULT Initialize(TestBrowserEventSink* event_sink, + IWebBrowser2* browser, + BSTR url); + void FinalRelease(); + + void set_url(const wchar_t* url) { url_ = url; } + const wchar_t* url() const { return url_ ? url_ : L""; } + + template <class Interface> + HRESULT GetBrowser(Interface** browser) { + return browser_.QueryInterface(browser); + } + + private: + CComBSTR url_; + CComDispatchDriver document_; + CComPtr<IWebBrowser2> browser_; + TestBrowserEventSink* event_sink_; + + DWORD advise_sink_cookie_; + DWORD document_property_notify_sink_cookie_; +}; + +class TestBrowserEventSink + : public BrowserEventSinkBase, + public InitializingCoClass<TestBrowserEventSink> { + public: + // Disambiguate. + using InitializingCoClass<TestBrowserEventSink>::CreateInitialized; + + HRESULT Initialize(TestBrowserEventSink** self, IWebBrowser2* browser) { + *self = this; + return BrowserEventSinkBase::Initialize(browser); + } + + // We have seen cases where we get destroyed while the ATL::CAxHostWindow + // may still hold references to frame handlers. + void FinalRelease() { + FrameHandlerMap::iterator it(frame_handlers_.begin()); + FrameHandlerMap::iterator end(frame_handlers_.end()); + + // Since they will detach from us as we clear them, we must not do it + // while we loop. + std::vector<TestFrameEventHandler*> to_be_detached; + for (; it != end; ++it) { + to_be_detached.push_back(it->second); + } + for (size_t index = 0; index < to_be_detached.size(); ++index) { + to_be_detached[index]->DetachFromSink(); + } + ASSERT_EQ(0, frame_handlers_.size()); + } + + virtual void GetDescription(std::string* description) const { + description->clear(); + description->append("TestBrowserEventSink"); + } + + void AttachHandler(IWebBrowser2* browser, TestFrameEventHandler* handler) { + ASSERT_TRUE(browser != NULL && handler != NULL); + CComPtr<IUnknown> browser_unk; + ASSERT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + + // We shouldn't already have one. + ASSERT_TRUE(NULL == FindHandlerForBrowser(browser)); + + frame_handlers_.insert(std::make_pair(browser_unk, handler)); + } + + void DetachHandler(IWebBrowser2* browser, TestFrameEventHandler* handler) { + ASSERT_TRUE(browser != NULL && handler != NULL); + + // It should already be registered. + ASSERT_TRUE(NULL != FindHandlerForBrowser(browser)); + + CComPtr<IUnknown> browser_unk; + ASSERT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + ASSERT_EQ(1, frame_handlers_.erase(browser_unk)); + } + + TestFrameEventHandler* FindHandlerForBrowser(IDispatch* browser) { + CComPtr<IUnknown> browser_unk; + EXPECT_HRESULT_SUCCEEDED(browser->QueryInterface(&browser_unk)); + + FrameHandlerMap::iterator it(frame_handlers_.find(browser_unk)); + if (it == frame_handlers_.end()) + return NULL; + + return it->second; + } + + TestFrameEventHandler* FindHandlerForUrl(const std::wstring& url) { + FrameHandlerMap::iterator it(frame_handlers_.begin()); + FrameHandlerMap::iterator end(frame_handlers_.end()); + + for (; it != end; ++it) { + CComQIPtr<IWebBrowser2> browser(it->first); + EXPECT_TRUE(browser != NULL); + CComBSTR location_url; + EXPECT_HRESULT_SUCCEEDED(browser->get_LocationURL(&location_url)); + + if (0 == ::UrlCompare(url.c_str(), location_url, TRUE)) + return it->second; + } + + return NULL; + } + + // Override. + STDMETHOD_(void, OnNavigateComplete)(IDispatch* browser_disp, + VARIANT* url_var) { + CComBSTR url; + if (V_VT(url_var) == VT_BSTR) + url = V_BSTR(url_var); + + TestFrameEventHandler* frame_handler = FindHandlerForBrowser(browser_disp); + if (!frame_handler) { + CComQIPtr<IWebBrowser2> browser(browser_disp); + ASSERT_TRUE(browser != NULL); + + CComPtr<IUnknown> frame_handler_keeper; + ASSERT_HRESULT_SUCCEEDED( + TestFrameEventHandler::CreateInitialized(this, + browser, + url, + &frame_handler_keeper)); + } else { + ATLTRACE("FrameHandler[%ws] -> %ws\n", frame_handler->url(), url); + frame_handler->set_url(url); + } + } + + private: + typedef std::map<IUnknown*, TestFrameEventHandler*> FrameHandlerMap; + // Keeps a map from a frame or top-level browser's identifying + // IUnknown to the frame event handler instance attached. + FrameHandlerMap frame_handlers_; +}; + + +STDMETHODIMP TestFrameEventHandler::OnChanged(DISPID changed_property) { + ATLTRACE("OnChanged(%d)\n", changed_property); + + if (changed_property == DISPID_READYSTATE) { + CComVariant ready_state; + CComDispatchDriver document(document_); + EXPECT_TRUE(document != NULL); + EXPECT_HRESULT_SUCCEEDED(document.GetProperty(DISPID_READYSTATE, + &ready_state)); + EXPECT_EQ(V_VT(&ready_state), VT_I4); + ATLTRACE("READYSTATE Frame[%ws]: %d\n", url_, ready_state.lVal); + + TestBrowserEventSink::add_state(static_cast<READYSTATE>(ready_state.lVal)); + } + + return S_OK; +} + +HRESULT TestFrameEventHandler::Initialize(TestBrowserEventSink* event_sink, + IWebBrowser2* browser, + BSTR url) { + EXPECT_HRESULT_SUCCEEDED(browser->get_Document(&document_)); + + CComQIPtr<IHTMLDocument2> html_document2(document_); + if (html_document2 != NULL) { + event_sink_ = event_sink; + browser_ = browser; + url_ = url; + EXPECT_HRESULT_SUCCEEDED(AtlAdvise(document_, + GetUnknown(), + IID_IPropertyNotifySink, + &document_property_notify_sink_cookie_)); + + ATLTRACE("TestFrameEventHandler::Initialize[%ws]\n", url_ ? url_ : L""); + + CComQIPtr<IOleObject> document_ole_object(document_); + EXPECT_TRUE(document_ole_object != NULL); + EXPECT_HRESULT_SUCCEEDED( + document_ole_object->Advise(this, &advise_sink_cookie_)); + + event_sink_->AttachHandler(browser_, this); + } else { + // This happens when we're navigated to e.g. a PDF doc or a folder. + } + + return S_OK; +} + + +void TestFrameEventHandler::DetachFromSink() { + ASSERT_TRUE(event_sink_ != NULL); + event_sink_->DetachHandler(browser_, this); + event_sink_ = NULL; +} + +void TestFrameEventHandler::FinalRelease() { + if (event_sink_ && browser_) + event_sink_->DetachHandler(browser_, this); + browser_.Release(); + document_.Release(); +} + +STDMETHODIMP_(void) TestFrameEventHandler::OnClose() { + EXPECT_HRESULT_SUCCEEDED(AtlUnadvise(document_, + IID_IPropertyNotifySink, + document_property_notify_sink_cookie_)); + + CComQIPtr<IOleObject> document_ole_object(document_); + EXPECT_TRUE(document_ole_object != NULL); + EXPECT_HRESULT_SUCCEEDED( + document_ole_object->Unadvise(advise_sink_cookie_)); +} + +class BrowserEventTest: public ShellBrowserTestImpl<TestBrowserEventSink> { +}; + +const wchar_t* kSimplePage = L"simple_page.html"; + +TEST_F(BrowserEventTest, RefreshTopLevelBrowserRetainsFrameHandler) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kSimplePage))); + + // We should have only one frame at this point. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Refreshing the top-level browser retains it. + EXPECT_HRESULT_SUCCEEDED(browser_->Refresh()); + EXPECT_TRUE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // Still there after refresh. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); +} + +const wchar_t* kTwoFramesPage = L"two_frames.html"; +const wchar_t* kFrameOne = L"frame_one.html"; +const wchar_t* kFrameTwo = L"frame_two.html"; + +TEST_F(BrowserEventTest, NavigateToFrames) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kTwoFramesPage))); + + // We should have three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // We should have a handler for each of these. + TestFrameEventHandler* two_frames = + event_sink()->FindHandlerForUrl(GetTestUrl(kTwoFramesPage)); + TestFrameEventHandler* frame_one = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameOne)); + TestFrameEventHandler* frame_two = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + ASSERT_TRUE(two_frames != NULL); + ASSERT_TRUE(frame_one != NULL); + ASSERT_TRUE(frame_two != NULL); + + // Noteworthy fact: the top level browser implements an + // IPropertyNotifySink connection point, but the sub-browsers + // for the frames do not. + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(two_frames->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_SUCCEEDED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(frame_one->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_FAILED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + { + CComQIPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&cpc)); + ASSERT_TRUE(cpc != NULL); + CComPtr<IConnectionPoint> cp; + EXPECT_HRESULT_FAILED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } + + // Test sub-frame document IPropertyNotifySink. + { + CComPtr<IWebBrowser2> browser; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&browser)); + + CComPtr<IDispatch> document_disp; + ASSERT_HRESULT_SUCCEEDED(browser->get_Document(&document_disp)); + + CComPtr<IConnectionPointContainer> cpc; + ASSERT_HRESULT_SUCCEEDED(document_disp->QueryInterface(&cpc)); + CComPtr<IConnectionPoint> cp; + ASSERT_HRESULT_SUCCEEDED( + cpc->FindConnectionPoint(IID_IPropertyNotifySink, &cp)); + } +} + +TEST_F(BrowserEventTest, ReNavigateToSamePageRetainsEventHandler) { + const std::wstring url(GetTestUrl(kSimplePage)); + EXPECT_TRUE(NavigateBrowser(url)); + + // We should have a frame handler attached now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve it and make sure it doesn't die. + TestFrameEventHandler* handler_before = + event_sink()->FindHandlerForUrl(url); + + ASSERT_TRUE(handler_before != NULL); + CComPtr<IUnknown> handler_before_keeper(handler_before->GetUnknown()); + + // Re-navigate the browser to the same page. + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kSimplePage))); + // Note: on re-navigation we don't see the top-level + // browser's readystate drop, I guess that only happens + // on transitions between content types. E.g. when a + // navigation requires the shell browser to instantiate + // a new type of document, such as going from a + // HTML doc to a PDF doc or the like. + EXPECT_FALSE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should only have the one frame handler in existence now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve the new one. + TestFrameEventHandler* handler_after = + event_sink()->FindHandlerForUrl(GetTestUrl(kSimplePage)); + + ASSERT_EQ(handler_before, handler_after); + + // Release the old one, it should still stay around + handler_before_keeper.Release(); + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); +} + +TEST_F(BrowserEventTest, NavigateToDifferentPageRetainsEventHandler) { + const std::wstring first_url(GetTestUrl(kSimplePage)); + EXPECT_TRUE(NavigateBrowser(first_url)); + + // We should have a frame handler attached now. + EXPECT_EQ(1, TestFrameEventHandler::instance_count()); + + // Retrieve it and make sure it doesn't die. + TestFrameEventHandler* handler_before = + event_sink()->FindHandlerForUrl(first_url); + + ASSERT_TRUE(handler_before != NULL); + CComPtr<IUnknown> handler_before_keeper(handler_before->GetUnknown()); + + // Navigate the browser to another page. + const std::wstring second_url(GetTestUrl(kTwoFramesPage)); + EXPECT_TRUE(NavigateBrowser(second_url)); + EXPECT_FALSE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should have the three frame handlers in existence now. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // Retrieve the new one for the top-level browser. + TestFrameEventHandler* handler_after = + event_sink()->FindHandlerForUrl(second_url); + + ASSERT_EQ(handler_before, handler_after); + + // Release the old one, it should still stay around + handler_before_keeper.Release(); + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); +} + +TEST_F(BrowserEventTest, RefreshFrameBrowserRetainsHandler) { + EXPECT_TRUE(NavigateBrowser(GetTestUrl(kTwoFramesPage))); + + // We should have three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + + // Get one of the frames. + TestFrameEventHandler* frame_two = + event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + ASSERT_TRUE(frame_two != NULL); + + // Now refresh a sub-browser instance, let it settle, + // observe its frame event handler is still around and + // has signalled a readystate transition. + CComPtr<IWebBrowser2> browser; + ASSERT_HRESULT_SUCCEEDED(frame_two->GetBrowser(&browser)); + ASSERT_HRESULT_SUCCEEDED(browser->Refresh()); + + EXPECT_TRUE(WaitForReadystateLoading()); + EXPECT_TRUE(WaitForReadystateComplete()); + + // We should have all three frame handlers at this point. + EXPECT_EQ(3, TestFrameEventHandler::instance_count()); + frame_two = event_sink()->FindHandlerForUrl(GetTestUrl(kFrameTwo)); + EXPECT_TRUE(frame_two != NULL); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc b/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc new file mode 100644 index 0000000..fd433508 --- /dev/null +++ b/ceee/ie/plugin/bho/mediumtest_browser_helper_object.cc @@ -0,0 +1,531 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A test that hosts and excercises the webbrowser control to test +// its event firing behavior. +#include <guiddef.h> +#include <mshtml.h> +#include <shlguid.h> +#include "base/utf_string_conversions.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/browser_helper_object.h" +#include "ceee/ie/testing/mediumtest_ie_common.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/ie/testing/mock_chrome_frame_host.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "net/base/net_util.h" + +namespace { + +using testing::_; +using testing::AnyNumber; +using testing::BrowserEventSinkBase; +using testing::GetTestUrl; +using testing::DoAll; +using testing::InstanceCountMixin; +using testing::kAnotherFrameOne; +using testing::kAnotherFrameTwo; +using testing::kAnotherTwoFramesPage; +using testing::kDeepFramesPage; +using testing::kFrameOne; +using testing::kFrameTwo; +using testing::kLevelOneFrame; +using testing::kLevelTwoFrame; +using testing::kOrphansPage; +using testing::kSimplePage; +using testing::kTwoFramesPage; +using testing::MockChromeFrameHost; +using testing::NotNull; +using testing::Return; +using testing::SetArgumentPointee; +using testing::ShellBrowserTestImpl; +using testing::StrictMock; + +ScriptHost::DebugApplication debug_app(L"FrameEventHandlerUnittest"); + +class TestingFrameEventHandler + : public FrameEventHandler, + public InstanceCountMixin<TestingFrameEventHandler>, + public InitializingCoClass<TestingFrameEventHandler> { + public: + // Disambiguate. + using InitializingCoClass<TestingFrameEventHandler>::CreateInitializedIID; + + IWebBrowser2* browser() const { return browser_; } + IHTMLDocument2* document() const { return document_; } +}; + +class TestingBrowserHelperObject + : public BrowserHelperObject, + public InitializingCoClass<TestingBrowserHelperObject>, + public InstanceCountMixin<TestingBrowserHelperObject> { + public: + TestingBrowserHelperObject() : mock_chrome_frame_host_(NULL) { + } + + HRESULT Initialize(TestingBrowserHelperObject** self) { + *self = this; + return S_OK; + } + + HRESULT CreateFrameEventHandler(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler** handler) { + return TestingFrameEventHandler::CreateInitializedIID( + browser, parent_browser, this, IID_IFrameEventHandler, handler); + } + + virtual TabEventsFunnel& tab_events_funnel() { + return mock_tab_events_funnel_; + } + + virtual HRESULT GetBrokerRegistrar(ICeeeBrokerRegistrar** broker) { + broker_keeper_.CopyTo(broker); + return S_OK; + } + + virtual HRESULT CreateExecutor(IUnknown** executor) { + CComPtr<IUnknown> unknown(executor_keeper_); + unknown.CopyTo(executor); + return S_OK; + } + + HRESULT CreateChromeFrameHost() { + HRESULT hr = MockChromeFrameHost::CreateInitializedIID( + &mock_chrome_frame_host_, IID_IChromeFrameHost, &chrome_frame_host_); + + // Neuter the functions we know are going to be called. + if (SUCCEEDED(hr)) { + EXPECT_CALL(*mock_chrome_frame_host_, SetChromeProfileName(_)) + .Times(1); + EXPECT_CALL(*mock_chrome_frame_host_, StartChromeFrame()) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, SetEventSink(NotNull())) + .Times(1); + EXPECT_CALL(*mock_chrome_frame_host_, SetEventSink(NULL)) + .Times(1); + + EXPECT_CALL(*mock_chrome_frame_host_, PostMessage(_, _)) + .WillRepeatedly(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, TearDown()) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*mock_chrome_frame_host_, GetSessionId(NotNull())) + .WillOnce(DoAll(SetArgumentPointee<0>(44), Return(S_OK))); + EXPECT_CALL(*broker_, SetTabIdForHandle(44, _)) + .WillOnce(Return(S_OK)); + } + + return hr; + } + + // Stub content script manifest loading. + void LoadManifestFile() {} + + // Make type public in this class. + typedef BrowserHelperObject::BrowserHandlerMap BrowserHandlerMap; + + BrowserHandlerMap::const_iterator browsers_begin() const { + return browsers_.begin(); + }; + BrowserHandlerMap::const_iterator browsers_end() const { + return browsers_.end(); + }; + + TestingFrameEventHandler* FindHandlerForUrl(const std::wstring& url) { + BrowserHandlerMap::iterator it(browsers_.begin()); + BrowserHandlerMap::iterator end(browsers_.end()); + + for (; it != end; ++it) { + CComQIPtr<IWebBrowser2> browser(it->first.m_T); + EXPECT_TRUE(browser != NULL); + CComBSTR location_url; + EXPECT_HRESULT_SUCCEEDED(browser->get_LocationURL(&location_url)); + + if (0 == ::UrlCompare(url.c_str(), location_url, TRUE)) + return static_cast<TestingFrameEventHandler*>(it->second.m_T.p); + } + + return NULL; + } + + // Returns true iff we have exactly |num_frames| registering + // the urls |resources[0..num_frames)|. + bool ExpectHasFrames(size_t num_frames, const std::wstring* resources) { + typedef BrowserHandlerMap::const_iterator iterator; + iterator it(browsers_.begin()); + + size_t count = 0; + for (; it != browsers_.end(); ++it) { + ++count; + + FrameEventHandler* handler = + static_cast<FrameEventHandler*>(it->second.m_T.p); + std::wstring url(handler->browser_url()); + const std::wstring* resources_end = resources + num_frames; + const std::wstring* found = std::find(resources, resources_end, url); + + if (resources_end == found) { + // A browser navigated to a file: URL reports the + // raw file path as its URL, convert the file path + // to a URL and search again. + FilePath path(handler->browser_url()); + + url = UTF8ToWide(net::FilePathToFileURL(path).spec()); + found = std::find(resources, resources_end, url); + } + + EXPECT_TRUE(resources_end != found) + << " unexpected frame URL " << url; + } + + EXPECT_EQ(num_frames, count); + return num_frames == count; + } + + template <size_t N> + bool ExpectHasFrames(const std::wstring (&resources)[N]) { + return ExpectHasFrames(N, resources); + } + + MockChromeFrameHost* mock_chrome_frame_host() const { + return mock_chrome_frame_host_; + } + + testing::MockTabEventsFunnel* mock_tab_events_funnel() { + return &mock_tab_events_funnel_; + } + + // We should use the executor mock that supports infobar in this test because + // OnBeforeNavigate2 queries the executor for infobar interface. + testing::MockTabInfobarExecutor* executor_; + CComPtr<ICeeeTabExecutor> executor_keeper_; + + testing::MockBroker* broker_; + CComPtr<ICeeeBrokerRegistrar> broker_keeper_; + + private: + MockChromeFrameHost* mock_chrome_frame_host_; + StrictMock<testing::MockTabEventsFunnel> mock_tab_events_funnel_; +}; + +class TestBrowserSite + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowserSite>, + public InitializingCoClass<TestBrowserSite>, + public IServiceProviderImpl<TestBrowserSite> { + public: + BEGIN_COM_MAP(TestBrowserSite) + COM_INTERFACE_ENTRY(IServiceProvider) + END_COM_MAP() + + BEGIN_SERVICE_MAP(TestBrowserSite) + SERVICE_ENTRY_CHAIN(browser_) + END_SERVICE_MAP() + + HRESULT Initialize(TestBrowserSite **self, IWebBrowser2* browser) { + *self = this; + browser_ = browser; + return S_OK; + } + + public: + CComPtr<IWebBrowser> browser_; +}; + +class BrowserEventSink + : public BrowserEventSinkBase, + public InitializingCoClass<BrowserEventSink> { + public: + // Disambiguate. + using InitializingCoClass<BrowserEventSink>::CreateInitialized; + + HRESULT Initialize(BrowserEventSink** self, IWebBrowser2* browser) { + *self = this; + return BrowserEventSinkBase::Initialize(browser); + } +}; + +class BrowerHelperObjectTest: public ShellBrowserTestImpl<BrowserEventSink> { + public: + typedef ShellBrowserTestImpl<BrowserEventSink> Super; + + BrowerHelperObjectTest() : bho_(NULL), site_(NULL) { + } + + virtual void SetUp() { + Super::SetUp(); + + // Never torn down as other threads in the test may need it after + // teardown. + ScriptHost::set_default_debug_application(&debug_app); + + ASSERT_HRESULT_SUCCEEDED( + TestingBrowserHelperObject::CreateInitialized(&bho_, &bho_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, browser_, &site_keeper_)); + + // Create and set expectations for the broker registrar related objects. + ASSERT_HRESULT_SUCCEEDED(testing::MockTabInfobarExecutor::CreateInitialized( + &bho_->executor_, &bho_->executor_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + testing::MockBroker::CreateInitialized(&bho_->broker_, + &bho_->broker_keeper_)); + EXPECT_CALL(*bho_->broker_, RegisterTabExecutor(_, + bho_->executor_keeper_.p)).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*bho_->broker_, UnregisterExecutor(_)). + WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnCreated(_, _, _)). + Times(AnyNumber()); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnUpdated(_, _, _)). + Times(AnyNumber()); + EXPECT_CALL(*bho_->executor_, Initialize(_)).WillOnce(Return(S_OK)); + EXPECT_CALL(*bho_->executor_, OnTopFrameBeforeNavigate(_)). + WillRepeatedly(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED(bho_keeper_->SetSite(site_->GetUnknown())); + } + + virtual void TearDown() { + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnRemoved(_)); + EXPECT_CALL(*bho_->mock_tab_events_funnel(), OnTabUnmapped(_, _)); + ASSERT_HRESULT_SUCCEEDED(bho_keeper_->SetSite(NULL)); + + site_ = NULL; + site_keeper_.Release(); + + bho_->executor_ = NULL; + bho_->executor_keeper_.Release(); + + bho_->broker_ = NULL; + bho_->broker_keeper_.Release(); + + bho_ = NULL; + bho_keeper_.Release(); + + Super::TearDown(); + } + + protected: + TestingBrowserHelperObject* bho_; + CComPtr<IObjectWithSite> bho_keeper_; + + TestBrowserSite* site_; + CComPtr<IServiceProvider> site_keeper_; +}; + +// This test navigates a webbrowser control instance back and forth +// between a set of resources with our BHO attached. On every navigation +// we're asserting on the urls and number of frame event handlers the BHO +// has recorded, as well as the instance count of the testing frame +// event handler class. +// This is to ensure that: +// 1. Our frame event handlers get attached to all created frames. +// 2. That any "recycled" frame event handlers track their associated +// document's URL changes. +// 3. That we don't leak discarded frame event handlers. +// 4. That we don't crash during any of this. +TEST_F(BrowerHelperObjectTest, FrameHandlerCreationAndDestructionOnNavigation) { + const std::wstring two_frames_resources[] = { + GetTestUrl(kTwoFramesPage), + GetTestUrl(kFrameOne), + GetTestUrl(kFrameTwo)}; + + const std::wstring another_two_frames_resources[] = { + GetTestUrl(kAnotherTwoFramesPage), + GetTestUrl(kAnotherFrameOne), + GetTestUrl(kAnotherFrameTwo)}; + + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + + EXPECT_TRUE(NavigateBrowser(two_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(two_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(two_frames_resources), + TestingFrameEventHandler::instance_count()); + + EXPECT_TRUE(NavigateBrowser(another_two_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(another_two_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(another_two_frames_resources), + TestingFrameEventHandler::instance_count()); + + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); +} + +// What motivated this test is the fact that at teardown time, +// sometimes the child->parent relationship between the browser +// instances we've encountered has been disrupted. +// So if you start with a frame hierarchy like +// A <- B <- C +// if you query the parent for C at the timepoint when B is +// reporting a COMPLETE->LOADING readystate change you'll find this: +// A <- B +// A <- C +// e.g. the C frame has been re-parented to the topmost webbrowser. +// +// Strangely I can't get this to repro under programmatic control +// against the webbrowser control. I suspect conditions are simply +// different in IE proper, or else there's something special to +// navigating by user event. I'm still leaving this code in as +// I hope to find a viable repro for this later, and because this +// code exercises some modes of navigation that the above test does +// not. +TEST_F(BrowerHelperObjectTest, DeepFramesAreCorrectlyHandled) { + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + const std::wstring deep_frames_resources[] = { + GetTestUrl(kDeepFramesPage), + GetTestUrl(kLevelOneFrame), + GetTestUrl(kLevelTwoFrame), + GetTestUrl(kFrameOne) + }; + + // Navigate to a deep frame hierarchy. + EXPECT_TRUE(NavigateBrowser(deep_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate to a simple page with only a top-level frame. + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); + + // And back to a deep frame hierarchy. + EXPECT_TRUE(NavigateBrowser(deep_frames_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + + // Refresh a mid-way frame. + TestingFrameEventHandler* handler = + bho_->FindHandlerForUrl(GetTestUrl(kLevelOneFrame)); + ASSERT_TRUE(handler); + EXPECT_HRESULT_SUCCEEDED(handler->browser()->Refresh()); + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // We should still have the same set of frames. + EXPECT_TRUE(bho_->ExpectHasFrames(deep_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(deep_frames_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate a mid-way frame to a new resource. + CComVariant empty; + EXPECT_HRESULT_SUCCEEDED( + browser_->Navigate2(&CComVariant(GetTestUrl(kTwoFramesPage).c_str()), + &empty, + &CComVariant(L"level_one"), + &empty, &empty)); + + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // This should now be our resource set. + const std::wstring mixed_frames_resources[] = { + GetTestUrl(kDeepFramesPage), + GetTestUrl(kLevelOneFrame), + GetTestUrl(kTwoFramesPage), + GetTestUrl(kFrameOne), + GetTestUrl(kFrameTwo), + }; + EXPECT_TRUE(bho_->ExpectHasFrames(mixed_frames_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(mixed_frames_resources), + TestingFrameEventHandler::instance_count()); +} + +// What motivated this test is the fact that some webpage can dynamically +// create frames that don't get navigated. We first saw this on the +// http://www.zooborns.com site and found that it has some javascript that +// creates and iframe and manually fill its innerHTML which contains an +// iframe with a src that is navigated to. Our code used to assume that +// when a frame is navigated, its parent was previously navigated so we could +// attach the new frame to its parent that we had seen before. So we needed to +// add code to create a handler for the ancestors of such orphans. +// +TEST_F(BrowerHelperObjectTest, OrphanFrame) { + const std::wstring simple_page_resources[] = { GetTestUrl(kSimplePage) }; + const std::wstring orphan_page_resources[] = { + GetTestUrl(kOrphansPage), + GetTestUrl(kOrphansPage), + GetTestUrl(kFrameOne) + }; + + // Navigate to an orphanage. + EXPECT_TRUE(NavigateBrowser(orphan_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); + + // Navigate to a simple page with only a top-level frame. + EXPECT_TRUE(NavigateBrowser(simple_page_resources[0])); + EXPECT_TRUE(bho_->ExpectHasFrames(simple_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(simple_page_resources), + TestingFrameEventHandler::instance_count()); + + // And back to the orphanage. + EXPECT_TRUE(NavigateBrowser(orphan_page_resources[0])); + // On fast machines, we don't wait long enough for everything to be completed. + // So we may already be OK, or we may need to wait for an extra COMPLETE. + // When we re-navigate to the ophans page like this, for some reason, we first + // get one COMPLETE ready state, and then a LOADING and then another COMPLETE. + if (arraysize(orphan_page_resources) != + TestingFrameEventHandler::instance_count()) { + ASSERT_TRUE(WaitForReadystateComplete()); + } + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); + + // Refresh a deep frame. + TestingFrameEventHandler* handler = + bho_->FindHandlerForUrl(GetTestUrl(kFrameOne)); + ASSERT_TRUE(handler); + EXPECT_HRESULT_SUCCEEDED(handler->browser()->Refresh()); + ASSERT_FALSE(WaitForReadystateLoading()); + ASSERT_TRUE(WaitForReadystateComplete()); + + // We should still have the same set of frames. + EXPECT_TRUE(bho_->ExpectHasFrames(orphan_page_resources)); + + // One handler per resource. + EXPECT_EQ(arraysize(orphan_page_resources), + TestingFrameEventHandler::instance_count()); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/tab_events_funnel.cc b/ceee/ie/plugin/bho/tab_events_funnel.cc new file mode 100644 index 0000000..275da40 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel.cc @@ -0,0 +1,85 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from whereever through the Broker. + +#include "ceee/ie/plugin/bho/tab_events_funnel.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/common/constants.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +HRESULT TabEventsFunnel::OnCreated(HWND tab_handle, BSTR url, bool complete) { + DictionaryValue tab_values; + tab_values.SetInteger(keys::kIdKey, reinterpret_cast<int>(tab_handle)); + tab_values.SetString(keys::kUrlKey, url); + tab_values.SetString(keys::kStatusKey, complete ? keys::kStatusValueComplete : + keys::kStatusValueLoading); + return SendEvent(ext_event_names::kOnTabCreated, tab_values); +} + +HRESULT TabEventsFunnel::OnMoved(HWND tab_handle, int window_id, int from_index, + int to_index) { + // For tab moves, the args are an array of two values, the tab id as an int + // and then a dictionary with window id, from and to indexes. + ListValue tab_moved_args; + tab_moved_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + dict->SetInteger(keys::kFromIndexKey, from_index); + dict->SetInteger(keys::kFromIndexKey, to_index); + tab_moved_args.Append(dict); + return SendEvent(ext_event_names::kOnTabMoved, tab_moved_args); +} + +HRESULT TabEventsFunnel::OnRemoved(HWND tab_handle) { + scoped_ptr<Value> args(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + return SendEvent(ext_event_names::kOnTabRemoved, *args.get()); +} + +HRESULT TabEventsFunnel::OnSelectionChanged(HWND tab_handle, int window_id) { + // For tab selection changes, the args are an array of two values, the tab id + // as an int and then a dictionary with only the window id in it. + ListValue tab_selection_changed_args; + tab_selection_changed_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + tab_selection_changed_args.Append(dict); + return SendEvent(ext_event_names::kOnTabSelectionChanged, + tab_selection_changed_args); +} + +HRESULT TabEventsFunnel::OnUpdated(HWND tab_handle, BSTR url, + READYSTATE ready_state) { + // For tab updates, the args are an array of two values, the tab id as an int + // and then a dictionary with an optional url field as well as a mandatory + // status string value. + ListValue tab_update_args; + tab_update_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + DictionaryValue* dict = new DictionaryValue; + if (url != NULL) + dict->SetString(keys::kUrlKey, url); + dict->SetString(keys::kStatusKey, (ready_state == READYSTATE_COMPLETE) ? + keys::kStatusValueComplete : keys::kStatusValueLoading); + tab_update_args.Append(dict); + return SendEvent(ext_event_names::kOnTabUpdated, tab_update_args); +} + +HRESULT TabEventsFunnel::OnTabUnmapped(HWND tab_handle, int tab_id) { + ListValue tab_unmapped_args; + tab_unmapped_args.Append(Value::CreateIntegerValue( + reinterpret_cast<int>(tab_handle))); + tab_unmapped_args.Append(Value::CreateIntegerValue(tab_id)); + return SendEvent(ceee_event_names::kCeeeOnTabUnmapped, tab_unmapped_args); +} diff --git a/ceee/ie/plugin/bho/tab_events_funnel.h b/ceee/ie/plugin/bho/tab_events_funnel.h new file mode 100644 index 0000000..7a9a2c4 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel.h @@ -0,0 +1,61 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Tab Events. + +#ifndef CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ + +#include <ocidl.h> // for READYSTATE + +#include "ceee/ie/plugin/bho/events_funnel.h" + +// Implements a set of methods to send tab related events to the Broker. +class TabEventsFunnel : public EventsFunnel { + public: + TabEventsFunnel() : EventsFunnel(true) {} + + // Sends the tabs.onMoved event to the Broker. + // @param tab_handle The HWND of the tab that moved. + // @param window_id The identifier of the window containing the moving tab. + // @param from_index The index from which the tab moved away. + // @param to_index The index where the tab moved to. + virtual HRESULT OnMoved(HWND tab_handle, int window_id, + int from_index, int to_index); + + // Sends the tabs.onRemoved event to the Broker. + // @param tab_handle The identifier of the tab that was removed. + virtual HRESULT OnRemoved(HWND tab_handle); + + // Sends the tabs.onSelectionChanged event to the Broker. + // @param tab_handle The HWND of the tab was selected. + // @param window_id The identifier of the window containing the selected tab. + virtual HRESULT OnSelectionChanged(HWND tab_handle, int window_id); + + // Sends the tabs.onCreated :b+event to the Broker. + // @param tab_handle The HWND of the tab that was created. + // @param url The current URL of the page. + // @param completed If true, the status of the page is completed, otherwise, + // it is any othe other status values. + virtual HRESULT OnCreated(HWND tab_handle, BSTR url, bool completed); + + // Sends the tabs.onUpdated event to the Broker. + // @param tab_handle The HWND of the tab that was updated. + // @param url The [optional] url where the tab was navigated to. + // @param ready_state The ready state of the tab. + virtual HRESULT OnUpdated(HWND tab_handle, BSTR url, + READYSTATE ready_state); + + // Sends the private message to unmap a tab to its BHO. This is the last + // message a BHO should send, as its tab_id will no longer be mapped afterward + // and will assert if used. + // @param tab_handle The HWND of the tab to unmap. + // @param tab_id The id of the tab to unmap. + virtual HRESULT OnTabUnmapped(HWND tab_handle, int tab_id); + + private: + DISALLOW_COPY_AND_ASSIGN(TabEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_TAB_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc b/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc new file mode 100644 index 0000000..dc2726d --- /dev/null +++ b/ceee/ie/plugin/bho/tab_events_funnel_unittest.cc @@ -0,0 +1,141 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for TabEventsFunnel. + +#include <atlcomcli.h> + +#include "base/scoped_ptr.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "chrome/browser/extensions/extension_event_names.h" +#include "chrome/browser/extensions/extension_tabs_module_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace ext_event_names = extension_event_names; +namespace keys = extension_tabs_module_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestTabEventsFunnel : public TabEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(TabEventsFunnelTest, OnTabCreated) { + TestTabEventsFunnel tab_events_funnel; + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + std::string url("http://www.google.com"); + std::string status(keys::kStatusValueComplete); + + DictionaryValue dict; + dict.SetInteger(keys::kIdKey, tab_id); + dict.SetString(keys::kUrlKey, url); + dict.SetString(keys::kStatusKey, status); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabCreated), ValuesEqual(&dict))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnCreated(tab_handle, + CComBSTR(url.c_str()), + true)); +} + +TEST(TabEventsFunnelTest, OnTabMoved) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + int window_id = 24; + int from_index = 12; + int to_index = 21; + + ListValue tab_moved_args; + tab_moved_args.Append(Value::CreateIntegerValue(tab_id)); + + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + dict->SetInteger(keys::kFromIndexKey, from_index); + dict->SetInteger(keys::kFromIndexKey, to_index); + tab_moved_args.Append(dict); + + EXPECT_CALL(tab_events_funnel, SendEvent(StrEq(ext_event_names::kOnTabMoved), + ValuesEqual(&tab_moved_args))).WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnMoved(tab_handle, window_id, + from_index, to_index)); +} + +TEST(TabEventsFunnelTest, OnTabRemoved) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + scoped_ptr<Value> args(Value::CreateIntegerValue(tab_id)); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabRemoved), ValuesEqual(args.get()))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnRemoved(tab_handle)); +} + +TEST(TabEventsFunnelTest, OnTabSelectionChanged) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 42; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + int window_id = 24; + + ListValue args; + args.Append(Value::CreateIntegerValue(tab_id)); + DictionaryValue* dict = new DictionaryValue; + dict->SetInteger(keys::kWindowIdKey, window_id); + args.Append(dict); + + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabSelectionChanged), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnSelectionChanged(tab_handle, + window_id)); +} + +TEST(TabEventsFunnelTest, OnTabUpdated) { + TestTabEventsFunnel tab_events_funnel; + + int tab_id = 24; + HWND tab_handle = reinterpret_cast<HWND>(tab_id); + READYSTATE ready_state = READYSTATE_INTERACTIVE; + + ListValue args; + args.Append(Value::CreateIntegerValue(tab_id)); + DictionaryValue* dict = new DictionaryValue; + dict->SetString(keys::kStatusKey, keys::kStatusValueLoading); + args.Append(dict); + + // Without a URL. + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabUpdated), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnUpdated(tab_handle, NULL, + ready_state)); + // With a URL. + CComBSTR url(L"http://imbored.com"); + dict->SetString(keys::kUrlKey, url.m_str); + EXPECT_CALL(tab_events_funnel, SendEvent( + StrEq(ext_event_names::kOnTabUpdated), ValuesEqual(&args))). + WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(tab_events_funnel.OnUpdated(tab_handle, url, + ready_state)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/tab_window_manager.cc b/ceee/ie/plugin/bho/tab_window_manager.cc new file mode 100644 index 0000000..c557436 --- /dev/null +++ b/ceee/ie/plugin/bho/tab_window_manager.cc @@ -0,0 +1,244 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/tab_window_manager.h" + +#include "base/logging.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/common/window_utils.h" +#include "ceee/ie/common/ie_tab_interfaces.h" + +namespace { + +// Adapter for tab-window interfaces on IE7/IE8. +template <class IeTabWindow> +class TabWindowAdapter : public TabWindow { + public: + explicit TabWindowAdapter(IeTabWindow* tab_window) + : tab_window_(tab_window) { + } + + STDMETHOD(GetBrowser)(IDispatch** browser) { + DCHECK(tab_window_ != NULL); + DCHECK(browser != NULL); + DCHECK(*browser == NULL); + return tab_window_->GetBrowser(browser); + } + + STDMETHOD(GetID)(long* id) { + DCHECK(tab_window_ != NULL); + DCHECK(id != NULL); + return tab_window_->GetID(id); + } + + STDMETHOD(Close)() { + DCHECK(tab_window_ != NULL); + return tab_window_->Close(); + } + + private: + CComPtr<IeTabWindow> tab_window_; + DISALLOW_COPY_AND_ASSIGN(TabWindowAdapter); +}; + +// Adapter for tab-window-manager interfaces on IE7/IE8. +template <class IeTabWindowManager, class IeTabWindow> +class TabWindowManagerAdapter : public TabWindowManager { + public: + explicit TabWindowManagerAdapter(IeTabWindowManager* manager) + : manager_(manager) { + } + + STDMETHOD(IndexFromHWND)(HWND window, long* index) { + DCHECK(manager_ != NULL); + DCHECK(index != NULL); + return manager_->IndexFromHWND(window, index); + } + + STDMETHOD(SelectTab)(long index) { + DCHECK(manager_ != NULL); + return manager_->SelectTab(index); + } + + STDMETHOD(GetCount)(long* count) { + DCHECK(manager_ != NULL); + DCHECK(count != NULL); + return manager_->GetCount(count); + } + + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) { + DCHECK(manager_ != NULL); + DCHECK(tab_window != NULL); + + CComPtr<IUnknown> tab_unk; + HRESULT hr = E_FAIL; + hr = manager_->GetItem(index, &tab_unk); + DCHECK(SUCCEEDED(hr)) << "GetItem failed for index: " << index << ". " << + com::LogHr(hr); + if (FAILED(hr)) + return hr; + + CComPtr<IeTabWindow> ie_tab_window; + hr = tab_unk.QueryInterface(&ie_tab_window); + DCHECK(SUCCEEDED(hr)) << "QI failed IeTabWindow. " << com::LogHr(hr); + if (FAILED(hr)) + return hr; + + tab_window->reset(new TabWindowAdapter<IeTabWindow>(ie_tab_window)); + return S_OK; + } + + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) { + DCHECK(manager_ != NULL); + return manager_->RepositionTab(moving_id, dest_id, unused); + } + + STDMETHOD(CloseAllTabs)() { + DCHECK(manager_ != NULL); + return manager_->CloseAllTabs(); + } + + private: + CComPtr<IeTabWindowManager> manager_; + DISALLOW_COPY_AND_ASSIGN(TabWindowManagerAdapter); +}; + +typedef TabWindowManagerAdapter<ITabWindowManagerIe9, ITabWindowIe9> + TabWindowManagerAdapter9; +typedef TabWindowManagerAdapter<ITabWindowManagerIe8, ITabWindowIe8_1> + TabWindowManagerAdapter8_1; +typedef TabWindowManagerAdapter<ITabWindowManagerIe8, ITabWindowIe8> + TabWindowManagerAdapter8; +typedef TabWindowManagerAdapter<ITabWindowManagerIe7, ITabWindowIe7> + TabWindowManagerAdapter7; + +// Faked tab-window class for IE6. +class TabWindow6 : public TabWindow { + public: + explicit TabWindow6(TabWindowManager *manager) : manager_(manager) {} + + STDMETHOD(GetBrowser)(IDispatch** browser) { + // Currently nobody calls this method. + NOTREACHED(); + return E_NOTIMPL; + } + + STDMETHOD(GetID)(long* id) { + DCHECK(id != NULL); + *id = 0; + return S_OK; + } + + STDMETHOD(Close)() { + DCHECK(manager_ != NULL); + // IE6 has only one tab for each frame window. + // So closing one tab means closing all the tabs. + return manager_->CloseAllTabs(); + } + + private: + TabWindowManager* manager_; + DISALLOW_COPY_AND_ASSIGN(TabWindow6); +}; + +// Faked tab-window-manager class for IE6. +class TabWindowManager6 : public TabWindowManager { + public: + explicit TabWindowManager6(HWND frame_window) : frame_window_(frame_window) {} + + STDMETHOD(IndexFromHWND)(HWND window, long* index) { + DCHECK(window != NULL); + DCHECK(index != NULL); + *index = 0; + return S_OK; + } + + STDMETHOD(SelectTab)(long index) { + DCHECK_EQ(0, index); + return S_OK; + } + + STDMETHOD(GetCount)(long* count) { + DCHECK(count != NULL); + *count = 1; + return S_OK; + } + + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) { + DCHECK(tab_window != NULL); + DCHECK_EQ(0, index); + tab_window->reset(new TabWindow6(this)); + return S_OK; + } + + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) { + DCHECK_EQ(0, moving_id); + DCHECK_EQ(0, dest_id); + return S_OK; + } + + STDMETHOD(CloseAllTabs)() { + DCHECK(IsWindow(frame_window_)); + ::PostMessage(frame_window_, WM_CLOSE, 0, 0); + return S_OK; + } + private: + HWND frame_window_; +}; + +} // anonymous namespace + +HRESULT CreateTabWindowManager(HWND frame_window, + scoped_ptr<TabWindowManager>* manager) { + CComPtr<IUnknown> manager_unknown; + HRESULT hr = ie_tab_interfaces::TabWindowManagerFromFrame( + frame_window, + __uuidof(IUnknown), + reinterpret_cast<void**>(&manager_unknown)); + if (SUCCEEDED(hr)) { + DCHECK(manager_unknown != NULL); + CComQIPtr<ITabWindowManagerIe9> manager_ie9(manager_unknown); + if (manager_ie9 != NULL) { + manager->reset(new TabWindowManagerAdapter9(manager_ie9)); + return S_OK; + } + + CComQIPtr<ITabWindowManagerIe8> manager_ie8(manager_unknown); + if (manager_ie8 != NULL) { + // On IE8, there was a version change that introduced a new version + // of the ITabWindow interface even though the ITabWindowManager didn't + // change. So we must find which one before we make up our mind. + CComPtr<IUnknown> tab_window_punk; + hr = manager_ie8->GetItem(0, &tab_window_punk); + DCHECK(SUCCEEDED(hr) && tab_window_punk != NULL) << com::LogHr(hr); + CComQIPtr<ITabWindowIe8> tab_window8(tab_window_punk); + if (tab_window8 != NULL) { + manager->reset(new TabWindowManagerAdapter8(manager_ie8)); + return S_OK; + } + CComQIPtr<ITabWindowIe8_1> tab_window8_1(tab_window_punk); + if (tab_window8_1 != NULL) { + manager->reset(new TabWindowManagerAdapter8_1(manager_ie8)); + return S_OK; + } + NOTREACHED() << "Found an ITabWindow Punk that is not known by us!!!"; + return E_UNEXPECTED; + } + + CComQIPtr<ITabWindowManagerIe7> manager_ie7(manager_unknown); + if (manager_ie7 != NULL) { + manager->reset(new TabWindowManagerAdapter7(manager_ie7)); + return S_OK; + } + + // Maybe future IE would have another interface. Consider it as IE6 anyway. + NOTREACHED(); + } + + LOG(WARNING) << + "Could not create a sensible brower interface, defaulting to IE6"; + manager->reset(new TabWindowManager6(frame_window)); + return S_OK; +} diff --git a/ceee/ie/plugin/bho/tab_window_manager.h b/ceee/ie/plugin/bho/tab_window_manager.h new file mode 100644 index 0000000..4f1ef2c --- /dev/null +++ b/ceee/ie/plugin/bho/tab_window_manager.h @@ -0,0 +1,39 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ +#define CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ + +#include <atlbase.h> + +#include "base/scoped_ptr.h" + +// A unified tab-window interface for IE6/IE7/IE8. +class TabWindow { + public: + STDMETHOD(GetBrowser)(IDispatch** browser) = 0; + STDMETHOD(GetID)(long* id) = 0; + STDMETHOD(Close)() = 0; + virtual ~TabWindow() {} +}; + +// A unified tab-window-manager interface for IE6/IE7/IE8. +class TabWindowManager { + public: + STDMETHOD(IndexFromHWND)(HWND window, long* index) = 0; + STDMETHOD(SelectTab)(long index) = 0; + STDMETHOD(GetCount)(long* count) = 0; + STDMETHOD(GetItemWrapper)(long index, scoped_ptr<TabWindow>* tab_window) = 0; + STDMETHOD(RepositionTab)(long moving_id, long dest_id, int unused) = 0; + STDMETHOD(CloseAllTabs)() = 0; + virtual ~TabWindowManager() {} +}; + +// Creates a TabWindowManager object for the specified IEFrame window. +// @param frame_window The top-level frame window you wish to manage. +// @param manager The created TabWindowManager object. +HRESULT CreateTabWindowManager(HWND frame_window, + scoped_ptr<TabWindowManager>* manager); + +#endif // CEEE_IE_PLUGIN_BHO_TAB_WINDOW_MANAGER_H_ diff --git a/ceee/ie/plugin/bho/tool_band_visibility.cc b/ceee/ie/plugin/bho/tool_band_visibility.cc new file mode 100644 index 0000000..8952457 --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ceee/ie/plugin/bho/tool_band_visibility.h" + +#include "base/logging.h" +#include "ceee/ie/common/ie_tab_interfaces.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/toolband/tool_band.h" + +// The ToolBandVisibility class allows us to recover from a number of +// features of IE that can cause our toolband to become invisible +// unexpectedly. + +// A brief discussion of toolband visibility in IE. +// +// See MS knowledge base article Q219427 for more detail. +// +// IE does some interesting tricks to cache toolband layout information. One +// side effect of this is that sometimes it can get confused about whether a +// toolband should be shown or not. +// +// In theory all that is needed to recover from this is a call to +// IWebBrowser2::ShowBrowserBar. Unfortunately, there are some +// gotchas. +// It's not easy to tell when IE has refused to display your +// toolband. The only way to be sure is to either call +// ShowBrowserBar on every startup or wait for some reasonable +// time to see if IE showed the toolband and then kick it if it +// didn't. +// In IE6, a single call to ShowBrowserBar is often not enough to +// unhide a hidden toolband. It's sometimes necessary to call +// ShowBrowserBar THREE times (show, then hide, then show) to get +// things into a sane state. +// TODO(cindylau@chromium.org): In IE6, just calling ShowBrowserBar +// will usually cause IE to scrunch our toolband up at the end of +// a line instead of giving it its own line. We can combat this +// by requesting to be shown on our own line, but if we do that +// in all cases we'll anger users who WANT our toolband to share +// a line with others. +// Some other toolbands (notably SnagIt versions 6 and 7) will +// cause toolbands to get hidden when opening a new tab in IE7. +// Visibility must be checked on every new tab and window to be +// sure. +// Calls to ShowBrowserBar are slow and we should avoid them +// whenever possible. +// Calls to ShowBrowserBar should be made from the same UI thread +// responsible for the browser object we use to unhide the +// toolband. Failure to do this can cause the toolband to +// believe it belongs to a different thread than it does. +// TODO(cindylau@chromium.org): IE tracks layout information in the +// registry. When toolbands are added or removed the installing +// toolband MUST clear the registry (badness, including possible +// crashes in IE will result if not). When this layout +// information is cleared all third party toolbands are hidden. + +// This code attempts to address all of these issues. +// The core is the VisibilityUtil class. +// When VisibilityUtil::CheckVisibility is called it checks for common +// indicators that a toolband is hidden. If it sees a smoking gun +// it unhides the toolband. Otherwise it creates a notification window and +// a timer. If a toolband doesn't report itself as active for a particular +// browser window before the timer fires it unhides the toolband. + +namespace { +const int kVisibilityTimerId = 1; +const DWORD kVisibilityCheckDelay = 2000; // 2 seconds. +} // anonymous namespace + +std::set<IUnknown*> ToolBandVisibility::visibility_set_; +CComAutoCriticalSection ToolBandVisibility::visibility_set_crit_; + +ToolBandVisibility::ToolBandVisibility() + : web_browser_(NULL) { +} + +ToolBandVisibility::~ToolBandVisibility() { + DCHECK(m_hWnd == NULL); +} + +void ToolBandVisibility::ReportToolBandVisible(IWebBrowser2* web_browser) { + DCHECK(web_browser); + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return; + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + visibility_set_.insert(browser_identity); +} + +bool ToolBandVisibility::IsToolBandVisible(IWebBrowser2* web_browser) { + DCHECK(web_browser); + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return false; + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + return visibility_set_.count(browser_identity) != 0; +} + +void ToolBandVisibility::ClearCachedVisibility(IWebBrowser2* web_browser) { + CComCritSecLock<CComAutoCriticalSection> lock(visibility_set_crit_); + if (web_browser) { + CComQIPtr<IUnknown, &IID_IUnknown> browser_identity(web_browser); + DCHECK(browser_identity != NULL); + if (browser_identity == NULL) + return; + visibility_set_.erase(browser_identity); + } else { + visibility_set_.clear(); + } +} + +void ToolBandVisibility::CheckToolBandVisibility(IWebBrowser2* web_browser) { + DCHECK(web_browser); + web_browser_ = web_browser; + + if (!ceee_module_util::GetOptionToolbandIsHidden() && + CreateNotificationWindow()) { + SetWindowTimer(kVisibilityTimerId, kVisibilityCheckDelay); + } +} + +void ToolBandVisibility::TearDown() { + if (web_browser_ != NULL) { + ClearCachedVisibility(web_browser_); + } + if (m_hWnd != NULL) { + CloseNotificationWindow(); + } +} + +bool ToolBandVisibility::CreateNotificationWindow() { + return Create(HWND_MESSAGE) != NULL; +} + +void ToolBandVisibility::CloseNotificationWindow() { + DestroyWindow(); +} + +void ToolBandVisibility::SetWindowTimer(UINT timer_id, UINT delay) { + SetTimer(timer_id, delay, NULL); +} + +void ToolBandVisibility::KillWindowTimer(UINT timer_id) { + KillTimer(timer_id); +} + +void ToolBandVisibility::OnTimer(UINT_PTR nIDEvent) { + DCHECK(nIDEvent == kVisibilityTimerId); + KillWindowTimer(nIDEvent); + + if (!IsToolBandVisible(web_browser_)) { + UnhideToolBand(); + } + ClearCachedVisibility(web_browser_); + CloseNotificationWindow(); +} + +void ToolBandVisibility::UnhideToolBand() { + // Ignore ShowDW calls that are triggered by our calls here to + // ShowBrowserBar. + ceee_module_util::SetIgnoreShowDWChanges(true); + CComVariant toolband_class(CLSID_ToolBand); + CComVariant show(false); + CComVariant empty; + show = true; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + + // Force IE to ignore bad caching. This is a problem generally before IE7, + // and at least sometimes even in IE7 (bb1291042). + show = false; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + show = true; + web_browser_->ShowBrowserBar(&toolband_class, &show, &empty); + + ceee_module_util::SetIgnoreShowDWChanges(false); +} diff --git a/ceee/ie/plugin/bho/tool_band_visibility.h b/ceee/ie/plugin/bho/tool_band_visibility.h new file mode 100644 index 0000000..f6e5767 --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ +#define CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ + +#include <atlbase.h> +#include <atlcrack.h> +#include <atlwin.h> +#include <mshtml.h> // Needed for exdisp.h +#include <exdisp.h> +#include <set> + +#include "base/basictypes.h" + +// The ToolBandVisibility class allows us to recover from a number of +// features of IE that can cause our toolband to become invisible +// unexpectedly. See the .cc file for more details. +class ATL_NO_VTABLE ToolBandVisibility + : public CWindowImpl<ToolBandVisibility> { + public: + // Inform ToolBandVisibility that the toolband has been created for this + // browser instance. + static void ReportToolBandVisible(IWebBrowser2* web_browser); + + BEGIN_MSG_MAP(ToolBandVisibility) + MSG_WM_CREATE(OnCreate) + MSG_WM_TIMER(OnTimer) + END_MSG_MAP() + + protected: + // Returns true iff ReportToolBandVisible has been called for the given web + // browser. + static bool IsToolBandVisible(IWebBrowser2* web_browser); + + // Cleans up the visibility set entry for the given browser that was stored + // when the browser called ReportToolBandVisible. If the pointer passed in is + // NULL, all items in the entire visibility set are deleted (this is useful + // for testing). + static void ClearCachedVisibility(IWebBrowser2* web_browser); + + ToolBandVisibility(); + virtual ~ToolBandVisibility(); + + // Checks toolband visibility, and forces the toolband to be shown if it + // isn't, and the user hasn't explicitly hidden the toolband. + void CheckToolBandVisibility(IWebBrowser2* web_browser); + + // Cleans up toolband visibility data when the BHO is being torn down. + void TearDown(); + + // Set up the notification window used for processing ToolBandVisibilityWindow + // messages. + // Unfortunately, we need to create a notification window to handle a delayed + // check for the toolband. Other methods (like creating a new thread and + // sleeping) will not work. + // Returns true on success. + // Also serves as a unit testing seam. + virtual bool CreateNotificationWindow(); + + // Unit testing seam for destroying the window. + virtual void CloseNotificationWindow(); + + // Unit testing seam for setting the timer for the visibility window. + virtual void SetWindowTimer(UINT timer_id, UINT delay); + + // Unit testing seam for killing the timer for the visibility window. + virtual void KillWindowTimer(UINT timer_id); + + // @name Message handlers. + // @{ + // The OnCreate handler is empty; subclasses can override it for more + // functionality. + virtual LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct) { + return 0; + } + void OnTimer(UINT_PTR nIDEvent); + // @} + + // Forces the toolband to be shown. + void UnhideToolBand(); + + // The web browser instance for which we are tracking toolband visibility. + CComPtr<IWebBrowser2> web_browser_; + + private: + // The set of browser windows that have visible toolbands. + // The BHO for each browser window is responsible for cleaning up its + // own entry in this set when it's torn down; see ClearToolBandVisibility. + // This collection does not hold references to the objects it stores. + static std::set<IUnknown*> visibility_set_; + static CComAutoCriticalSection visibility_set_crit_; + + DISALLOW_COPY_AND_ASSIGN(ToolBandVisibility); +}; + +#endif // CEEE_IE_PLUGIN_BHO_TOOL_BAND_VISIBILITY_H_ diff --git a/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc b/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc new file mode 100644 index 0000000..0876c9d --- /dev/null +++ b/ceee/ie/plugin/bho/tool_band_visibility_unittest.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tests for ToolBandVisibility. + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "ceee/testing/utils/mock_com.h" +#include "gtest/gtest.h" + +namespace { +using testing::_; +using testing::Return; +using testing::StrictMock; + +class TestingToolBandVisibility : public ToolBandVisibility { + public: + TestingToolBandVisibility() {} + virtual ~TestingToolBandVisibility() {} + + IWebBrowser2* GetWindowBrowser() const { + return web_browser_; + } + + using ToolBandVisibility::IsToolBandVisible; + using ToolBandVisibility::ClearCachedVisibility; + using ToolBandVisibility::CheckToolBandVisibility; + using ToolBandVisibility::OnTimer; + + MOCK_METHOD0(CreateNotificationWindow, bool()); + MOCK_METHOD0(CloseNotificationWindow, void()); + MOCK_METHOD2(SetWindowTimer, void(UINT, UINT)); + MOCK_METHOD1(KillWindowTimer, void(UINT)); +}; + +class MockBrowser + : public testing::MockIWebBrowser2, + public InitializingCoClass<MockBrowser> { + public: + HRESULT Initialize(MockBrowser** browser) { + *browser = this; + return S_OK; + } +}; + +class ToolBandVisibilityTest : public testing::Test { + public: + virtual void TearDown() { + TestingToolBandVisibility::ClearCachedVisibility(NULL); + } + + void CreateMockBrowser(MockBrowser** browser, IWebBrowser2** browser_keeper) { + ASSERT_TRUE(browser && browser_keeper); + ASSERT_HRESULT_SUCCEEDED( + MockBrowser::CreateInitialized(browser, browser_keeper)); + } + + void ExpectCheckToolBandVisibilitySucceeded( + TestingToolBandVisibility* visibility) { + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(false)); + EXPECT_CALL(*visibility, CreateNotificationWindow()) + .WillOnce(Return(true)); + EXPECT_CALL(*visibility, SetWindowTimer(1, 2000)).Times(1); + } + + StrictMock<testing::MockCeeeModuleUtils> ceee_module_utils_; + TestingToolBandVisibility visibility; +}; + +TEST_F(ToolBandVisibilityTest, ReportToolBandVisibleSucceeds) { + MockBrowser* browser1; + MockBrowser* browser2; + MockBrowser* browser3; + CComPtr<IWebBrowser2> browser1_keeper, browser2_keeper, browser3_keeper; + CreateMockBrowser(&browser1, &browser1_keeper); + CreateMockBrowser(&browser2, &browser2_keeper); + CreateMockBrowser(&browser3, &browser3_keeper); + + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + TestingToolBandVisibility::ReportToolBandVisible(browser2_keeper); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + TestingToolBandVisibility::ReportToolBandVisible(browser3_keeper); + TestingToolBandVisibility::ClearCachedVisibility(browser2_keeper); + + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); + + // Clearing visibility for a browser that isn't visible is essentially a + // no-op. + TestingToolBandVisibility::ClearCachedVisibility(browser1_keeper); + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser1_keeper)); + ASSERT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser2_keeper)); + ASSERT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser3_keeper)); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilityHiddenToolband) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(true)); + EXPECT_CALL(visibility, CreateNotificationWindow()).Times(0); + visibility.CheckToolBandVisibility(browser_keeper); + EXPECT_EQ(browser_keeper, visibility.GetWindowBrowser()); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilityCreateFailed) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandIsHidden()) + .WillOnce(Return(false)); + EXPECT_CALL(visibility, CreateNotificationWindow()) + .WillOnce(Return(false)); + EXPECT_CALL(visibility, SetWindowTimer(_, _)).Times(0); + visibility.CheckToolBandVisibility(browser_keeper); +} + +TEST_F(ToolBandVisibilityTest, CheckToolBandVisibilitySucceeded) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); +} + +TEST_F(ToolBandVisibilityTest, OnTimerVisibleToolBand) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + TestingToolBandVisibility::ReportToolBandVisible(browser_keeper); + EXPECT_TRUE(TestingToolBandVisibility::IsToolBandVisible(browser_keeper)); + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); + + EXPECT_CALL(visibility, KillWindowTimer(1)).Times(1); + EXPECT_CALL(visibility, CloseNotificationWindow()).Times(1); + visibility.OnTimer(1); + + EXPECT_FALSE(TestingToolBandVisibility::IsToolBandVisible(browser_keeper)); +} + +TEST_F(ToolBandVisibilityTest, OnTimerInvisibleToolBand) { + MockBrowser* browser; + CComPtr<IWebBrowser2> browser_keeper; + CreateMockBrowser(&browser, &browser_keeper); + TestingToolBandVisibility visibility; + + ExpectCheckToolBandVisibilitySucceeded(&visibility); + visibility.CheckToolBandVisibility(browser_keeper); + + EXPECT_CALL(visibility, KillWindowTimer(1)).Times(1); + EXPECT_CALL(ceee_module_utils_, SetIgnoreShowDWChanges(true)).Times(1); + EXPECT_CALL(*browser, ShowBrowserBar(_, _, _)).Times(3); + EXPECT_CALL(ceee_module_utils_, SetIgnoreShowDWChanges(false)).Times(1); + EXPECT_CALL(visibility, CloseNotificationWindow()).Times(1); + visibility.OnTimer(1); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/web_browser_events_source.h b/ceee/ie/plugin/bho/web_browser_events_source.h new file mode 100644 index 0000000..8cdec9c --- /dev/null +++ b/ceee/ie/plugin/bho/web_browser_events_source.h @@ -0,0 +1,34 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Interface of WebBrowser events source. +#ifndef CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ +#define CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ + +#include <exdisp.h> + +// WebBrowserEventsSource defines the interface of a WebBrowser event publisher, +// which is used to register/unregister event consumers and fire WebBrowser +// events to them. +class WebBrowserEventsSource { + public: + // The interface of WebBrowser event consumers. + class Sink { + public: + virtual ~Sink() {} + virtual void OnBeforeNavigate(IWebBrowser2* browser, BSTR url) {} + virtual void OnDocumentComplete(IWebBrowser2* browser, BSTR url) {} + virtual void OnNavigateComplete(IWebBrowser2* browser, BSTR url) {} + virtual void OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code) {} + virtual void OnNewWindow(BSTR url_context, BSTR url) {} + }; + + virtual ~WebBrowserEventsSource() {} + + virtual void RegisterSink(Sink* sink) = 0; + virtual void UnregisterSink(Sink* sink) = 0; +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEB_BROWSER_EVENTS_SOURCE_H_ diff --git a/ceee/ie/plugin/bho/web_progress_notifier.cc b/ceee/ie/plugin/bho/web_progress_notifier.cc new file mode 100644 index 0000000..c32ac70 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier.cc @@ -0,0 +1,653 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web progress notifier implementation. +#include "ceee/ie/plugin/bho/web_progress_notifier.h" + +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/ie/plugin/bho/dom_utils.h" + +namespace { + +// In milliseconds. It defines the "effective period" of user action. A user +// action is considered as a possible cause of the next navigation if the +// navigation happens in this period. +// This is a number we feel confident of based on past experience. +const int kUserActionTimeThresholdMs = 500; + +// String constants for the values of TransitionQualifier. +const char kClientRedirect[] = "client_redirect"; +const char kServerRedirect[] = "server_redirect"; +const char kForwardBack[] = "forward_back"; +const char kRedirectMetaRefresh[] = "redirect_meta_refresh"; +const char kRedirectOnLoad[] = "redirect_onload"; +const char kRedirectJavaScript[] = "redirect_javascript"; + +} // namespace + +WebProgressNotifier::WebProgressNotifier() + : web_browser_events_source_(NULL), + main_frame_info_(PageTransition::LINK, 0, kMainFrameId), + tab_handle_(NULL), + next_subframe_id_(1), + main_frame_document_complete_(true), + tracking_content_window_action_(false), + tracking_browser_ui_action_(false), + has_potential_javascript_redirect_(false), + cached_webrequest_notifier_(NULL), + webrequest_notifier_initialized_(false), + create_thread_id_(::GetCurrentThreadId()) { +} + +WebProgressNotifier::~WebProgressNotifier() { + DCHECK(!webrequest_notifier_initialized_); + DCHECK(web_browser_events_source_ == NULL); + DCHECK(window_message_source_ == NULL); + DCHECK(main_browser_ == NULL); + DCHECK(travel_log_ == NULL); + DCHECK(tab_handle_ == NULL); +} + +HRESULT WebProgressNotifier::Initialize( + WebBrowserEventsSource* web_browser_events_source, + HWND tab_window, + IWebBrowser2* main_browser) { + if (web_browser_events_source == NULL || tab_window == NULL || + main_browser == NULL) { + return E_INVALIDARG; + } + + tab_handle_ = reinterpret_cast<CeeeWindowHandle>(tab_window); + main_browser_ = main_browser; + + CComQIPtr<IServiceProvider> service_provider(main_browser); + if (service_provider == NULL || + FAILED(service_provider->QueryService( + SID_STravelLogCursor, IID_ITravelLogStg, + reinterpret_cast<void**>(&travel_log_))) || + travel_log_ == NULL) { + TearDown(); + return E_FAIL; + } + + web_browser_events_source_ = web_browser_events_source; + web_browser_events_source_->RegisterSink(this); + + window_message_source_.reset(CreateWindowMessageSource()); + if (window_message_source_ == NULL) { + TearDown(); + return E_FAIL; + } + window_message_source_->RegisterSink(this); + + if (!webrequest_notifier()->RequestToStart()) + NOTREACHED() << "Failed to start the WebRequestNotifier service."; + webrequest_notifier_initialized_ = true; + return S_OK; +} + +void WebProgressNotifier::TearDown() { + if (webrequest_notifier_initialized_) { + webrequest_notifier()->RequestToStop(); + webrequest_notifier_initialized_ = false; + } + if (web_browser_events_source_ != NULL) { + web_browser_events_source_->UnregisterSink(this); + web_browser_events_source_ = NULL; + } + if (window_message_source_ != NULL) { + window_message_source_->UnregisterSink(this); + window_message_source_->TearDown(); + window_message_source_.reset(NULL); + } + + main_browser_.Release(); + travel_log_.Release(); + tab_handle_ = NULL; +} + +void WebProgressNotifier::OnBeforeNavigate(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::BEFORE_NAVIGATE)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + // TODO(yzshen@google.com): add support for requestId. + HRESULT hr = webnavigation_events_funnel().OnBeforeNavigate( + tab_handle_, url, frame_info->frame_id, -1, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onBeforeNavigate event " + << com::LogHr(hr); + + if (frame_info->IsMainFrame()) { + frame_info->ClearTransition(); + + // The order in which we set these transitions is **very important.** + // If there was no DocumentComplete, then there are two likely options: + // the transition was a JavaScript redirect, or the user navigated to a + // second page before the first was done loading. We initialize the + // transition to JavaScript redirect first. If there are other signals such + // as the user clicked/typed, we'll overwrite this value with the + // appropriate value. + if (!main_frame_document_complete_ || + wcsncmp(url, L"javascript:", wcslen(L"javascript:")) == 0 || + has_potential_javascript_redirect_) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_JAVASCRIPT); + } + + // Override the transition if there is user action in the tab content window + // or browser UI. + if (IsPossibleUserActionInContentWindow()) { + frame_info->SetTransition(PageTransition::LINK, 0); + } else if (IsPossibleUserActionInBrowserUI()) { + frame_info->SetTransition(PageTransition::TYPED, 0); + } + + // Override the transition if we find some signals that we are more + // confident about. + if (InOnLoadEvent(browser)) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_ONLOAD); + } else if (IsMetaRefresh(browser, url)) { + frame_info->SetTransition(PageTransition::LINK, + CLIENT_REDIRECT | REDIRECT_META_REFRESH); + } + + // Assume that user actions don't have long-lasting effect: user actions + // before the current navigation may be the cause of this navigation; but + // they can not affect any subsequent navigation. + // + // Under this assumption, we don't need to remember previous user actions. + tracking_content_window_action_ = false; + tracking_browser_ui_action_ = false; + } +} + +void WebProgressNotifier::OnDocumentComplete(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::DOCUMENT_COMPLETE)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + if (frame_info->IsMainFrame()) { + main_frame_document_complete_ = true; + + has_potential_javascript_redirect_ = + HasPotentialJavaScriptRedirect(browser); + } + + HRESULT hr = webnavigation_events_funnel().OnCompleted( + tab_handle_, url, frame_info->frame_id, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the webNavigation.onCompleted event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnNavigateComplete(IWebBrowser2* browser, BSTR url) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::NAVIGATE_COMPLETE)) { + filtering_info_.pending_navigate_complete_browser = browser; + filtering_info_.pending_navigate_complete_url = url; + filtering_info_.pending_navigate_complete_timestamp = base::Time::Now(); + } else { + HandleNavigateComplete(browser, url, base::Time::Now()); + } +} + +void WebProgressNotifier::HandleNavigateComplete( + IWebBrowser2* browser, + BSTR url, + const base::Time& timestamp) { + // NOTE: For the first OnNavigateComplete event in a tab/window, this method + // may not be called at the moment when IE fires the event. + // As a result, be careful if you need to query the browser state in this + // method, because the state may have changed after IE fired the event. + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + if (frame_info->IsMainFrame()) { + main_frame_document_complete_ = false; + + if (IsForwardBack(url)) { + frame_info->SetTransition(PageTransition::AUTO_BOOKMARK, FORWARD_BACK); + } + } + + HRESULT hr = webnavigation_events_funnel().OnCommitted( + tab_handle_, url, frame_info->frame_id, + PageTransition::CoreTransitionString(frame_info->transition_type), + TransitionQualifiersString(frame_info->transition_qualifiers).c_str(), + timestamp); + DCHECK(SUCCEEDED(hr)) << "Failed to fire the webNavigation.onCommitted event " + << com::LogHr(hr); + + if (frame_info->IsMainFrame()) + subframe_map_.clear(); +} + +void WebProgressNotifier::OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code) { + if (browser == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(browser, FilteringInfo::NAVIGATE_ERROR)) + return; + + FrameInfo* frame_info = NULL; + if (!GetFrameInfo(browser, &frame_info)) + return; + + HRESULT hr = webnavigation_events_funnel().OnErrorOccurred( + tab_handle_, url, frame_info->frame_id, CComBSTR(L""), base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onErrorOccurred event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnNewWindow(BSTR url_context, BSTR url) { + if (url_context == NULL || url == NULL) + return; + + if (FilterOutWebBrowserEvent(NULL, FilteringInfo::NEW_WINDOW)) + return; + + HRESULT hr = webnavigation_events_funnel().OnBeforeRetarget( + tab_handle_, url_context, url, base::Time::Now()); + DCHECK(SUCCEEDED(hr)) + << "Failed to fire the webNavigation.onBeforeRetarget event " + << com::LogHr(hr); +} + +void WebProgressNotifier::OnHandleMessage( + WindowMessageSource::MessageType type, + const MSG* message_info) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + // This is called when a user input message is about to be handled by any + // window procedure on the current thread, we should not do anything expensive + // here that would degrade user experience. + switch (type) { + case WindowMessageSource::TAB_CONTENT_WINDOW: { + if (IsUserActionMessage(message_info->message)) { + tracking_content_window_action_ = true; + last_content_window_action_time_ = base::Time::Now(); + } + break; + } + case WindowMessageSource::BROWSER_UI_SAME_THREAD: { + if (IsUserActionMessage(message_info->message)) { + tracking_browser_ui_action_ = true; + last_browser_ui_action_time_ = base::Time::Now(); + } + break; + } + default: { + NOTREACHED(); + break; + } + } +} + +WindowMessageSource* WebProgressNotifier::CreateWindowMessageSource() { + scoped_ptr<WindowMessageSource> source(new WindowMessageSource()); + + return source->Initialize() ? source.release() : NULL; +} + +std::string WebProgressNotifier::TransitionQualifiersString( + TransitionQualifiers qualifiers) { + std::string result; + for (unsigned int current_qualifier = FIRST_TRANSITION_QUALIFIER; + current_qualifier <= LAST_TRANSITION_QUALIFIER; + current_qualifier = current_qualifier << 1) { + if ((qualifiers & current_qualifier) != 0) { + if (!result.empty()) + result.append("|"); + switch (current_qualifier) { + case CLIENT_REDIRECT: + result.append(kClientRedirect); + break; + case SERVER_REDIRECT: + result.append(kServerRedirect); + break; + case FORWARD_BACK: + result.append(kForwardBack); + break; + case REDIRECT_META_REFRESH: + result.append(kRedirectMetaRefresh); + break; + case REDIRECT_ONLOAD: + result.append(kRedirectOnLoad); + break; + case REDIRECT_JAVASCRIPT: + result.append(kRedirectJavaScript); + break; + default: + NOTREACHED(); + break; + } + } + } + return result; +} + +bool WebProgressNotifier::GetFrameInfo(IWebBrowser2* browser, + FrameInfo** frame_info) { + DCHECK(browser != NULL && frame_info != NULL); + + if (IsMainFrame(browser)) { + *frame_info = &main_frame_info_; + return true; + } + + CComPtr<IUnknown> browser_identity; + HRESULT hr = browser->QueryInterface(&browser_identity); + DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) + return false; + + SubframeMap::iterator iter = subframe_map_.find(browser_identity); + if (iter != subframe_map_.end()) { + *frame_info = &iter->second; + } else { + // PageTransition::MANUAL_SUBFRAME, as well as transition qualifiers for + // subframes, is not supported. + subframe_map_.insert(std::make_pair(browser_identity, + FrameInfo(PageTransition::AUTO_SUBFRAME, + 0, next_subframe_id_++))); + *frame_info = &subframe_map_[browser_identity]; + } + return true; +} + +bool WebProgressNotifier::GetDocument(IWebBrowser2* browser, + REFIID id, + void** document) { + DCHECK(browser != NULL && document != NULL); + + CComPtr<IDispatch> document_disp; + if (FAILED(browser->get_Document(&document_disp)) || document_disp == NULL) + return false; + return SUCCEEDED(document_disp->QueryInterface(id, document)) && + *document != NULL; +} + +bool WebProgressNotifier::IsForwardBack(BSTR url) { + DWORD length = 0; + DWORD position = 0; + + if (FAILED(travel_log_->GetCount(TLEF_RELATIVE_BACK | TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE, + &length))) { + length = -1; + } else { + length++; // Add 1 for the current entry. + } + + if (FAILED(travel_log_->GetCount(TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE, + &position))) { + position = -1; + } + + // Consider this is a forward/back navigation, if: + // (1) state of the forward/back list has been successfully retrieved, and + // (2) the length of the forward/back list is not changed, and + // (3) (a) the current position is not the newest entry of the + // forward/back list, or + // (b) we are not at the newest entry of the list before the current + // navigation and the URL of the newest entry is not changed by the + // current navigation. + bool is_forward_back = + length != -1 && previous_travel_log_info_.length != -1 && + position != -1 && previous_travel_log_info_.position != -1 && + length == previous_travel_log_info_.length && + (position != 0 || + (previous_travel_log_info_.position != 0 && + previous_travel_log_info_.newest_url == url)); + + previous_travel_log_info_.length = length; + previous_travel_log_info_.position = position; + if (position == 0 && !is_forward_back) + previous_travel_log_info_.newest_url = url; + + return is_forward_back; +} + +bool WebProgressNotifier::InOnLoadEvent(IWebBrowser2* browser) { + DCHECK(browser != NULL); + + CComPtr<IHTMLDocument2> document; + if (!GetDocument(browser, IID_IHTMLDocument2, + reinterpret_cast<void**>(&document))) { + return false; + } + + CComPtr<IHTMLWindow2> window; + if (FAILED(document->get_parentWindow(&window)) || window == NULL) + return false; + + CComPtr<IHTMLEventObj> event_obj; + if (FAILED(window->get_event(&event_obj)) || event_obj == NULL) + return false; + + CComBSTR type; + if (FAILED(event_obj->get_type(&type)) || wcscmp(type, L"load") != 0) + return false; + else + return true; +} + +bool WebProgressNotifier::IsMetaRefresh(IWebBrowser2* browser, BSTR url) { + DCHECK(browser != NULL && url != NULL); + + CComPtr<IHTMLDocument3> document; + if (!GetDocument(browser, IID_IHTMLDocument3, + reinterpret_cast<void**>(&document))) { + return false; + } + + std::wstring dest_url(url); + StringToLowerASCII(&dest_url); + + static const wchar_t slash[] = { L'/' }; + // IE can add/remove a slash to/from the URL specified in the meta refresh + // tag. No redirect occurs as a result of this URL change, so we just compare + // without slashes here. + TrimString(dest_url, slash, &dest_url); + + CComPtr<IHTMLElementCollection> meta_elements; + long length = 0; + if (FAILED(DomUtils::GetElementsByTagName(document, CComBSTR(L"meta"), + &meta_elements, &length))) { + return false; + } + + for (long index = 0; index < length; ++index) { + CComPtr<IHTMLMetaElement> meta_element; + if (FAILED(DomUtils::GetElementFromCollection( + meta_elements, index, IID_IHTMLMetaElement, + reinterpret_cast<void**>(&meta_element)))) { + continue; + } + + CComBSTR http_equiv; + if (FAILED(meta_element->get_httpEquiv(&http_equiv)) || + http_equiv == NULL || _wcsicmp(http_equiv, L"refresh") != 0) { + continue; + } + + CComBSTR content_bstr; + if (FAILED(meta_element->get_content(&content_bstr)) || + content_bstr == NULL) + continue; + std::wstring content(content_bstr); + StringToLowerASCII(&content); + size_t pos = content.find(L"url"); + if (pos == std::wstring::npos) + continue; + pos = content.find(L"=", pos + 3); + if (pos == std::wstring::npos) + continue; + + std::wstring content_url(content.begin() + pos + 1, content.end()); + TrimWhitespace(content_url, TRIM_ALL, &content_url); + TrimString(content_url, slash, &content_url); + + // It is possible that the meta tag specifies a relative URL. + if (!content_url.empty() && EndsWith(dest_url, content_url, true)) + return true; + } + return false; +} + +bool WebProgressNotifier::HasPotentialJavaScriptRedirect( + IWebBrowser2* browser) { + DCHECK(browser != NULL); + + CComPtr<IHTMLDocument3> document; + if (!GetDocument(browser, IID_IHTMLDocument3, + reinterpret_cast<void**>(&document))) { + return false; + } + + CComPtr<IHTMLElementCollection> script_elements; + long length = 0; + if (FAILED(DomUtils::GetElementsByTagName(document, CComBSTR(L"script"), + &script_elements, &length))) { + return false; + } + + for (long index = 0; index < length; ++index) { + CComPtr<IHTMLScriptElement> script_element; + if (FAILED(DomUtils::GetElementFromCollection( + script_elements, index, IID_IHTMLScriptElement, + reinterpret_cast<void**>(&script_element)))) { + continue; + } + + CComBSTR text; + if (FAILED(script_element->get_text(&text)) || text == NULL) + continue; + + if (wcsstr(text, L"location.href") != NULL || + wcsstr(text, L"location.replace") != NULL || + wcsstr(text, L"location.assign") != NULL || + wcsstr(text, L"location.reload") != NULL) { + return true; + } + } + + return false; +} + +bool WebProgressNotifier::IsPossibleUserActionInContentWindow() { + if (tracking_content_window_action_) { + base::TimeDelta delta = base::Time::Now() - + last_content_window_action_time_; + if (delta.InMilliseconds() < kUserActionTimeThresholdMs) + return true; + } + + return false; +} + +bool WebProgressNotifier::IsPossibleUserActionInBrowserUI() { + if (tracking_browser_ui_action_) { + base::TimeDelta delta = base::Time::Now() - last_browser_ui_action_time_; + if (delta.InMilliseconds() < kUserActionTimeThresholdMs) + return true; + } + + // TODO(yzshen@google.com): The windows of the browser UI live in + // different threads or even processes, for example: + // 1) The menu bar, add-on toolbands, as well as the status bar at the bottom, + // live in the same thread as the tab content window. + // 2) In IE7, the browser frame lives in a different thread other than the one + // hosting the tab content window; in IE8, it lives in a different process. + // 3) Our extension UI (rendered by Chrome) lives in another process. + // Currently WebProgressNotifier only handles case (1). I need to find out a + // solution that can effectively handle case (2) and (3). + return false; +} + +bool WebProgressNotifier::FilterOutWebBrowserEvent(IWebBrowser2* browser, + FilteringInfo::Event event) { + if (!IsMainFrame(browser)) { + if (filtering_info_.state == FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + HandleNavigateComplete( + filtering_info_.pending_navigate_complete_browser, + filtering_info_.pending_navigate_complete_url, + filtering_info_.pending_navigate_complete_timestamp); + } + } else { + switch (filtering_info_.state) { + case FilteringInfo::END: { + break; + } + case FilteringInfo::START: { + if (event == FilteringInfo::BEFORE_NAVIGATE) + filtering_info_.state = FilteringInfo::FIRST_BEFORE_NAVIGATE; + + break; + } + case FilteringInfo::FIRST_BEFORE_NAVIGATE: { + if (event == FilteringInfo::BEFORE_NAVIGATE || + event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + } else if (event == FilteringInfo::DOCUMENT_COMPLETE) { + filtering_info_.state = FilteringInfo::SUSPICIOUS_DOCUMENT_COMPLETE; + return true; + } + + break; + } + case FilteringInfo::SUSPICIOUS_DOCUMENT_COMPLETE: { + if (event == FilteringInfo::BEFORE_NAVIGATE) { + filtering_info_.state = FilteringInfo::END; + } else if (event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE; + return true; + } + + break; + } + case FilteringInfo::SUSPICIOUS_NAVIGATE_COMPLETE: { + if (event == FilteringInfo::NAVIGATE_COMPLETE) { + filtering_info_.state = FilteringInfo::END; + // Ignore the pending OnNavigateComplete event. + } else { + filtering_info_.state = FilteringInfo::END; + HandleNavigateComplete( + filtering_info_.pending_navigate_complete_browser, + filtering_info_.pending_navigate_complete_url, + filtering_info_.pending_navigate_complete_timestamp); + } + break; + } + default: { + NOTREACHED() << "Unknown state type."; + break; + } + } + } + return false; +} diff --git a/ceee/ie/plugin/bho/web_progress_notifier.h b/ceee/ie/plugin/bho/web_progress_notifier.h new file mode 100644 index 0000000..3c364c8 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier.h @@ -0,0 +1,366 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web progress notifier implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ +#define CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ + +#include <atlbase.h> +#include <tlogstg.h> + +#include <map> +#include <string> + +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "ceee/ie/plugin/bho/webrequest_notifier.h" +#include "ceee/ie/plugin/bho/window_message_source.h" +#include "chrome/common/page_transition_types.h" + +// WebProgressNotifier sends to the Broker various Web progress events, +// including Web page navigation events and HTTP request/response events. +class WebProgressNotifier : public WebBrowserEventsSource::Sink, + public WindowMessageSource::Sink { + public: + WebProgressNotifier(); + virtual ~WebProgressNotifier(); + + HRESULT Initialize( + WebBrowserEventsSource* web_browser_events_source, + HWND tab_window, + IWebBrowser2* main_browser); + void TearDown(); + + // @name WebBrowserEventsSource::Sink implementation + // @{ + virtual void OnBeforeNavigate(IWebBrowser2* browser, BSTR url); + virtual void OnDocumentComplete(IWebBrowser2* browser, BSTR url); + virtual void OnNavigateComplete(IWebBrowser2* browser, BSTR url); + virtual void OnNavigateError(IWebBrowser2* browser, BSTR url, + long status_code); + virtual void OnNewWindow(BSTR url_context, BSTR url); + // @} + + // @name WindowMessageSource::Sink implementation + // @{ + virtual void OnHandleMessage(WindowMessageSource::MessageType type, + const MSG* message_info); + // @} + + protected: + // The main frame ID. + static const int kMainFrameId = 0; + + // Sometimes IE fires unexpected events, which could possibly corrupt the + // internal state of WebProgressNotifier and lead to incorrect webNavigation + // event sequence. Here is a common issue: + // + // When a URL is opened in a new tab/window (CTRL + mouse click, or using + // "_blank" target in <a> element/IHTMLDocument2::open), + // (1) if the URL results in server-initiated redirect, the event sequence is: + // (1-1) OnNewWindow + // (1-2) OnBeforeNavigate http://www.originalurl.com/ + // (1-3) OnDocumentComplete http://www.originalurl.com/ + // (1-4) OnNavigateComplete http://www.originalurl.com/ + // (1-5) OnNavigateComplete http://www.redirecturl.com/ + // (1-6) OnDocumentComplete http://www.redirecturl.com/ + // (2) otherwise, the event sequence is: + // (2-1) OnNewWindow + // (2-2) OnBeforeNavigate http://www.url.com/ + // (2-3) OnDocumentComplete http://www.url.com/ + // (2-4) OnNavigateComplete http://www.url.com/ + // (2-5) OnDocumentComplete http://www.url.com/ + // (NOTE: HTTP responses with status code 304 fall into the 2nd category.) + // + // Event 1-3, 1-4 and 2-3 are undesired, comparing with the (normal) event + // sequence of opening a link in the current tab/window. + // FilteringInfo is used to filter out these events. + // + // It is easy to get rid of event 1-3 and 2-3. If we observe an + // OnDocumentComplete event immediately after OnBeforeNavigate, we know that + // it should be ignored. + // However, it is hard to get rid of event 1-4, since we have no way to tell + // the difference between 1-4 and 2-4, until we get the next event. + // (At the first glance, we could tell 1-4 from 2-4 by observing + // INTERNET_STATUS_REDIRECT to see whether the navigation involves + // server-initiated redirect. However, INTERNET_STATUS_REDIRECT actually + // happens *after* event 1-4.) As a result, when we receive an + // OnNavigateComplete event after an undesired OnDocumentComplete event, we + // have to postpone making the decision of processing it or not until we + // receive the next event. We process it only if the next event is not another + // OnNavigateComplete. + struct FilteringInfo { + enum State { + // The tab/window is newly created. + START, + // The first OnBeforeNavigate in the main frame has been observed. + FIRST_BEFORE_NAVIGATE, + // A suspicious OnDocumentComplete in the main frame has been observed. + SUSPICIOUS_DOCUMENT_COMPLETE, + // A suspicious OnNavigateComplete in the main frame has been observed. + SUSPICIOUS_NAVIGATE_COMPLETE, + // Filtering has finished. + END + }; + + enum Event { + // An OnBeforeNavigate has been fired. + BEFORE_NAVIGATE, + // An OnDocumentComplete has been fired. + DOCUMENT_COMPLETE, + // An OnNavigateComplete has been fired. + NAVIGATE_COMPLETE, + // An OnNavigateError has been fired. + NAVIGATE_ERROR, + // An OnNewWindow has been fired. + NEW_WINDOW + }; + + FilteringInfo() : state(START) {} + + State state; + + // Arguments of a pending OnNavigateComplete event. + CComBSTR pending_navigate_complete_url; + CComPtr<IWebBrowser2> pending_navigate_complete_browser; + base::Time pending_navigate_complete_timestamp; + }; + + // Any transition type can be augmented by qualifiers, which further define + // the navigation. + // Transition types could be found in chrome/common/page_transition_types.h. + // We are not using the qualifiers defined in that file, since we need to + // define a few IE/FF specific qualifiers for now. + enum TransitionQualifier { + // Redirects caused by JavaScript or a meta refresh tag on the page. + CLIENT_REDIRECT = 0x1, + // Redirects sent from the server by HTTP headers. + SERVER_REDIRECT = 0x2, + // Users use Forward or Back button to navigate among browsing history. + FORWARD_BACK = 0x4, + // Client redirects caused by <meta http-equiv="refresh">. (IE/FF specific) + REDIRECT_META_REFRESH = 0x8, + // Client redirects happening in JavaScript onload event handler. + // (IE/FF specific) + REDIRECT_ONLOAD = 0x10, + // Non-onload JavaScript redirects. (IE/FF specific) + REDIRECT_JAVASCRIPT = 0x20, + FIRST_TRANSITION_QUALIFIER = CLIENT_REDIRECT, + LAST_TRANSITION_QUALIFIER = REDIRECT_JAVASCRIPT + }; + // Represents zero or more transition qualifiers. + typedef unsigned int TransitionQualifiers; + + // Information related to a frame. + struct FrameInfo { + FrameInfo() : transition_type(PageTransition::LINK), + transition_qualifiers(0), + frame_id(-1) { + } + + FrameInfo(PageTransition::Type in_transition_type, + TransitionQualifiers in_transition_qualifiers, + int in_frame_id) + : transition_type(in_transition_type), + transition_qualifiers(in_transition_qualifiers), + frame_id(in_frame_id) { + } + + // Clears transition type as well as qualifiers. + void ClearTransition() { + SetTransition(PageTransition::LINK, 0); + } + + // Sets transition type and qualifiers. + void SetTransition(PageTransition::Type type, + TransitionQualifiers qualifiers) { + transition_type = type; + transition_qualifiers = qualifiers; + } + + // Appends more transition qualifiers. + void AppendTransitionQualifiers(TransitionQualifiers qualifiers) { + transition_qualifiers |= qualifiers; + } + + // Whether this FrameInfo instance is associated with the main frame. + bool IsMainFrame() const { + return frame_id == kMainFrameId; + } + + // The transition type for the current navigation in this frame. + PageTransition::Type transition_type; + // The transition qualifiers for the current navigation in this frame. + TransitionQualifiers transition_qualifiers; + // The frame ID. + const int frame_id; + }; + + // Accessor so that we can mock it in unit tests. + virtual WebNavigationEventsFunnel& webnavigation_events_funnel() { + return webnavigation_events_funnel_; + } + + // Accessor so that we can mock WebRequestNotifier in unit tests. + virtual WebRequestNotifier* webrequest_notifier() { + if (cached_webrequest_notifier_ == NULL) { + cached_webrequest_notifier_ = ProductionWebRequestNotifier::get(); + } + return cached_webrequest_notifier_; + } + + // Unit testing seems to create a WindowMessageSource instance. + virtual WindowMessageSource* CreateWindowMessageSource(); + + // Whether the current navigation is a navigation among the browsing history + // (forward/back list). + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsForwardBack(BSTR url); + + // Whether we are currently inside the onload event handler of the page. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool InOnLoadEvent(IWebBrowser2* browser); + + // Whether there is meta refresh tag on the current page. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsMetaRefresh(IWebBrowser2* browser, BSTR url); + + // Whether there is JavaScript code on the current page that could possibly + // cause a navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool HasPotentialJavaScriptRedirect(IWebBrowser2* browser); + + // Whether there is user action in the content window that could possibly + // cause a navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsPossibleUserActionInContentWindow(); + + // Whether there is user action in the browser UI that could possibly cause a + // navigation. + // The method is made virtual so that we could easily mock it in unit tests. + virtual bool IsPossibleUserActionInBrowserUI(); + + // Converts a set of transition qualifier values into a string. + std::string TransitionQualifiersString(TransitionQualifiers qualifiers); + + // Whether the IWebBrowser2 interface belongs to the main frame. + bool IsMainFrame(IWebBrowser2* browser) { + return browser != NULL && main_browser_.IsEqualObject(browser); + } + + // Gets the information related to a frame. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param frame_info An output parameter to return the information. The caller + // doesn't take ownership of the returned object. The FrameInfo + // instance for the main frame will live as long as the + // WebProgressNotifier instance; all FrameInfo instances for subframes + // will be deleted when the main frame navigates. + // @return Whether the operation is successful or not. + bool GetFrameInfo(IWebBrowser2* browser, FrameInfo** frame_info); + + // Gets the document of the frame. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param id The IID of the document interface to return. + // @param document An output parameter to return the interface pointer. + // @return Whether the operation is successful or not. + bool GetDocument(IWebBrowser2* browser, REFIID id, void** document); + + // Whether the specified Windows message represents a user action. + bool IsUserActionMessage(UINT message) { + return message == WM_LBUTTONUP || message == WM_KEYUP || + message == WM_KEYDOWN; + } + + // Handles OnNavigateComplete events. + // @param browser The corresponding IWebBrowser2 interface of a frame. + // @param url The URL that the frame navigated to. + // @param timestamp The time when the OnNavigateComplete event was fired. + void HandleNavigateComplete(IWebBrowser2* browser, + BSTR url, + const base::Time& timestamp); + + // Decides whether to filter out a navigation event. + // The method may call HandleNavigateComplete to handle delayed + // OnNavigateComplete events. + // @param browser The frame in which the navigation event happens. + // @param event The navigation event. + // @return Returns true if the event should be ignored. + bool FilterOutWebBrowserEvent(IWebBrowser2* browser, + FilteringInfo::Event event); + + // This class doesn't have ownership of the object that + // web_browser_events_source_ points to. + WebBrowserEventsSource* web_browser_events_source_; + // Publisher of events about Windows message handling. + scoped_ptr<WindowMessageSource> window_message_source_; + + // The funnel for sending webNavigation events to the broker. + WebNavigationEventsFunnel webnavigation_events_funnel_; + + // Information related to the main frame. + FrameInfo main_frame_info_; + + // IWebBrowser2 interface pointer of the main frame. + CComPtr<IWebBrowser2> main_browser_; + // ITravelLogStg interface pointer to manage the forward/back list. + CComPtr<ITravelLogStg> travel_log_; + // Window handle of the tab. + CeeeWindowHandle tab_handle_; + + // The ID to assign to the next subframe. + int next_subframe_id_; + // Maintains a map from subframes and their corresponding FrameInfo instances. + typedef std::map<CAdapt<CComPtr<IUnknown> >, FrameInfo> SubframeMap; + SubframeMap subframe_map_; + + // Information related to the forward/back list. + struct TravelLogInfo { + TravelLogInfo() : length(-1), position(-1) { + } + // The length of the forward/back list, including the current entry. + DWORD length; + // The current position within the forward/back list, defined as the + // distance between the current entry and the newest entry in the + // forward/back list. That is, if the current entry is the newest one + // in the forward/back list then the position is 0. + DWORD position; + // The URL of the newest entry in the forward/back list. + CComBSTR newest_url; + }; + // The state of the forward/back list before the current navigation. + TravelLogInfo previous_travel_log_info_; + + // Whether the previous navigation of the main frame reaches DocumentComplete. + bool main_frame_document_complete_; + + // If tracking_content_window_action_ is true, consider user action in the tab + // content window as a possible cause for the next navigation. + bool tracking_content_window_action_; + // The last time when the user took action in the content window. + base::Time last_content_window_action_time_; + + // If tracking_browser_ui_action_ is true, consider user action in the browser + // UI (except the tab content window) as a possible cause for the next + // navigation. + bool tracking_browser_ui_action_; + // The last time when the user took action in the browser UI. + base::Time last_browser_ui_action_time_; + + // Whether there is JavaScript code on the current page that can possibly + // cause a navigation. + bool has_potential_javascript_redirect_; + + // A cached pointer to the singleton object. + WebRequestNotifier* cached_webrequest_notifier_; + bool webrequest_notifier_initialized_; + + DWORD create_thread_id_; + + FilteringInfo filtering_info_; + private: + DISALLOW_COPY_AND_ASSIGN(WebProgressNotifier); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEB_PROGRESS_NOTIFIER_H_ diff --git a/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc b/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc new file mode 100644 index 0000000..46a1e57 --- /dev/null +++ b/ceee/ie/plugin/bho/web_progress_notifier_unittest.cc @@ -0,0 +1,593 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit test for WebProgressNotifier class. +#include "ceee/ie/plugin/bho/web_progress_notifier.h" + +#include "ceee/ie/plugin/bho/web_browser_events_source.h" +#include "ceee/ie/plugin/bho/window_message_source.h" +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::AddRef; +using testing::AnyNumber; +using testing::InSequence; +using testing::MockFunction; +using testing::NiceMock; +using testing::NotNull; +using testing::Return; +using testing::SaveArg; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +class FakeWebBrowserEventsSource : public WebBrowserEventsSource { + public: + FakeWebBrowserEventsSource() : sink_(NULL) {} + virtual ~FakeWebBrowserEventsSource() { + EXPECT_EQ(NULL, sink_); + } + + virtual void RegisterSink(Sink* sink) { + ASSERT_TRUE(sink != NULL && sink_ == NULL); + sink_ = sink; + } + + virtual void UnregisterSink(Sink* sink) { + ASSERT_TRUE(sink == sink_); + sink_ = NULL; + } + + void FireOnBeforeNavigate(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnBeforeNavigate(browser, url); + } + void FireOnDocumentComplete(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnDocumentComplete(browser, url); + } + void FireOnNavigateComplete(IWebBrowser2* browser, BSTR url) { + if (sink_ != NULL) + sink_->OnNavigateComplete(browser, url); + } + void FireOnNavigateError(IWebBrowser2* browser, BSTR url, long status_code) { + if (sink_ != NULL) + sink_->OnNavigateError(browser, url, status_code); + } + void FireOnNewWindow(BSTR url_context, BSTR url) { + if (sink_ != NULL) + sink_->OnNewWindow(url_context, url); + } + + private: + Sink* sink_; +}; + +class FakeWindowMessageSource : public WindowMessageSource { + public: + FakeWindowMessageSource() : sink_(NULL) {} + virtual ~FakeWindowMessageSource() { + EXPECT_EQ(NULL, sink_); + } + + virtual void RegisterSink(Sink* sink) { + ASSERT_TRUE(sink != NULL && sink_ == NULL); + sink_ = sink; + } + + virtual void UnregisterSink(Sink* sink) { + ASSERT_TRUE(sink == sink_); + sink_ = NULL; + } + + void FireOnHandleMessage(MessageType type, + const MSG* message_info) { + if (sink_ != NULL) + sink_->OnHandleMessage(type, message_info); + } + + private: + Sink* sink_; +}; + +class TestWebProgressNotifier : public WebProgressNotifier { + public: + TestWebProgressNotifier() + : mock_is_forward_back_(false), + mock_in_onload_event_(false), + mock_is_meta_refresh_(false), + mock_has_potential_javascript_redirect_(false), + mock_is_possible_user_action_in_content_window_(false), + mock_is_possible_user_action_in_browser_ui_(false) {} + + virtual WebNavigationEventsFunnel& webnavigation_events_funnel() { + return mock_webnavigation_events_funnel_; + } + + virtual WindowMessageSource* CreateWindowMessageSource() { + return new FakeWindowMessageSource(); + } + + virtual bool IsForwardBack(BSTR /*url*/) { return mock_is_forward_back_; } + virtual bool InOnLoadEvent(IWebBrowser2* /*browser*/) { + return mock_in_onload_event_; + } + virtual bool IsMetaRefresh(IWebBrowser2* /*browser*/, BSTR /*url*/) { + return mock_is_meta_refresh_; + } + virtual bool HasPotentialJavaScriptRedirect(IWebBrowser2* /*browser*/) { + return mock_has_potential_javascript_redirect_; + } + virtual bool IsPossibleUserActionInContentWindow() { + return mock_is_possible_user_action_in_content_window_; + } + virtual bool IsPossibleUserActionInBrowserUI() { + return mock_is_possible_user_action_in_browser_ui_; + } + + bool CallRealIsForwardBack(BSTR url) { + return WebProgressNotifier::IsForwardBack(url); + } + + TravelLogInfo& previous_travel_log_info() { + return previous_travel_log_info_; + } + + StrictMock<testing::MockWebNavigationEventsFunnel> + mock_webnavigation_events_funnel_; + bool mock_is_forward_back_; + bool mock_in_onload_event_; + bool mock_is_meta_refresh_; + bool mock_has_potential_javascript_redirect_; + bool mock_is_possible_user_action_in_content_window_; + bool mock_is_possible_user_action_in_browser_ui_; +}; + +class WebProgressNotifierTestFixture : public testing::Test { + protected: + WebProgressNotifierTestFixture() : mock_web_browser_(NULL), + mock_travel_log_stg_(NULL) { + } + + virtual void SetUp() { + CComObject<testing::MockIWebBrowser2>::CreateInstance( + &mock_web_browser_); + ASSERT_TRUE(mock_web_browser_ != NULL); + web_browser_ = mock_web_browser_; + + CComObject<testing::MockITravelLogStg>::CreateInstance( + &mock_travel_log_stg_); + ASSERT_TRUE(mock_travel_log_stg_ != NULL); + travel_log_stg_ = mock_travel_log_stg_; + + // We cannot use CopyInterfaceToArgument here because QueryService takes + // void** as argument. + EXPECT_CALL(*mock_web_browser_, + QueryService(SID_STravelLogCursor, IID_ITravelLogStg, + NotNull())) + .WillRepeatedly(DoAll(SetArgumentPointee<2>(travel_log_stg_.p), + AddRef(travel_log_stg_.p), + Return(S_OK))); + + web_browser_events_source_.reset(new FakeWebBrowserEventsSource()); + + web_progress_notifier_.reset(new TestWebProgressNotifier()); + ASSERT_HRESULT_SUCCEEDED(web_progress_notifier_->Initialize( + web_browser_events_source_.get(), reinterpret_cast<HWND>(1024), + web_browser_)); + } + + virtual void TearDown() { + web_progress_notifier_->TearDown(); + web_progress_notifier_.reset(NULL); + web_browser_events_source_.reset(NULL); + mock_web_browser_ = NULL; + web_browser_.Release(); + mock_travel_log_stg_ = NULL; + travel_log_stg_.Release(); + } + + void IgnoreCallsToEventsFunnelExceptOnCommitted() { + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeRetarget(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnDOMContentLoaded(_, _, _, _)) + .Times(AnyNumber()); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnErrorOccurred(_, _, _, _, _)) + .Times(AnyNumber()); + } + + void FireNavigationEvents(IWebBrowser2* browser, BSTR url) { + web_browser_events_source_->FireOnBeforeNavigate(browser, url); + web_browser_events_source_->FireOnNavigateComplete(browser, url); + web_browser_events_source_->FireOnDocumentComplete(browser, url); + } + + HRESULT CreateMockWebBrowser(IWebBrowser2** web_browser) { + EXPECT_TRUE(web_browser != NULL); + CComObject<testing::MockIWebBrowser2>* mock_web_browser = NULL; + CComObject<testing::MockIWebBrowser2>::CreateInstance(&mock_web_browser); + EXPECT_TRUE(mock_web_browser != NULL); + + *web_browser = mock_web_browser; + (*web_browser)->AddRef(); + return S_OK; + } + + CComObject<testing::MockIWebBrowser2>* mock_web_browser_; + CComPtr<IWebBrowser2> web_browser_; + CComObject<testing::MockITravelLogStg>* mock_travel_log_stg_; + CComPtr<ITravelLogStg> travel_log_stg_; + scoped_ptr<FakeWebBrowserEventsSource> web_browser_events_source_; + scoped_ptr<TestWebProgressNotifier> web_progress_notifier_; +}; + +TEST_F(WebProgressNotifierTestFixture, FireEvents) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)); + EXPECT_CALL(check, Call(1)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeRetarget(_, _, _, _)); + EXPECT_CALL(check, Call(2)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(check, Call(3)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(4)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnErrorOccurred(_, _, _, _, _)); + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(1); + web_browser_events_source_->FireOnNewWindow( + CComBSTR(L"http://www.google.com/"), + CComBSTR(L"http://mail.google.com/")); + check.Call(2); + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(3); + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + check.Call(4); + web_browser_events_source_->FireOnNavigateError( + web_browser_, CComBSTR(L"http://www.google.com/"), 400); + } +} + +TEST_F(WebProgressNotifierTestFixture, FilterAbnormalWebBrowserEvents) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCompleted(_, _, _, _)); + } + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + // Undesired event. + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + // Undesired event. + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://mail.google.com/")); + web_browser_events_source_->FireOnDocumentComplete( + web_browser_, CComBSTR(L"http://mail.google.com/")); +} + +TEST_F(WebProgressNotifierTestFixture, TestFrameId) { + int subframe_id_1 = -1; + int current_frame_id = -1; + + CComPtr<IWebBrowser2> subframe_1; + ASSERT_HRESULT_SUCCEEDED(CreateMockWebBrowser(&subframe_1)); + + CComPtr<IWebBrowser2> subframe_2; + ASSERT_HRESULT_SUCCEEDED(CreateMockWebBrowser(&subframe_2)); + + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(1)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(2)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(3)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + EXPECT_CALL(check, Call(4)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnBeforeNavigate(_, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(¤t_frame_id), + Return(S_OK))); + + web_browser_events_source_->FireOnBeforeNavigate( + web_browser_, CComBSTR(L"http://www.google.com/")); + // The main frame has 0 as frame ID. + EXPECT_EQ(0, current_frame_id); + check.Call(1); + + current_frame_id = -1; + web_browser_events_source_->FireOnNavigateComplete( + web_browser_, CComBSTR(L"http://www.google.com/")); + // The main frame has 0 as frame ID. + EXPECT_EQ(0, current_frame_id); + check.Call(2); + + current_frame_id = -1; + web_browser_events_source_->FireOnBeforeNavigate( + subframe_1, CComBSTR(L"http://www.google.com/")); + subframe_id_1 = current_frame_id; + // A subframe should not have 0 as frame ID. + EXPECT_NE(0, subframe_id_1); + check.Call(3); + + current_frame_id = -1; + web_browser_events_source_->FireOnNavigateComplete( + subframe_1, CComBSTR(L"http://www.google.com/")); + // The frame ID of a subframe remains the same. + EXPECT_EQ(subframe_id_1, current_frame_id); + check.Call(4); + + current_frame_id = -1; + web_browser_events_source_->FireOnBeforeNavigate( + subframe_2, CComBSTR(L"http://www.google.com/")); + // Different subframes have different frame IDs. + EXPECT_NE(subframe_id_1, current_frame_id); + } +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionClientRedirect) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + CComBSTR javascript_url(L"javascript:someScript();"); + const char* link = "link"; + const char* redirect_javascript = "client_redirect|redirect_javascript"; + const char* redirect_onload = "client_redirect|redirect_onload"; + const char* redirect_meta_refresh = "client_redirect|redirect_meta_refresh"; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, _, _, _)); + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_javascript), + _)); + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_onload), _)); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(redirect_meta_refresh), + _)); + } + + // If target URL starts with "javascript:" and no other signals are found, + // consider the transition as JavaScript redirect. + web_browser_events_source_->FireOnBeforeNavigate(web_browser_, + javascript_url); + web_browser_events_source_->FireOnNavigateComplete(web_browser_, + javascript_url); + check.Call(1); + + // If DocumentComplete hasn't been received for the previous navigation, and + // no other signals are found, consider the transition as JavaScript redirect. + FireNavigationEvents(web_browser_, url); + check.Call(2); + + web_progress_notifier_->mock_has_potential_javascript_redirect_ = true; + FireNavigationEvents(web_browser_, url); + // If JavaScript code to navigate the page is found on the previous page, and + // no other signals are found, consider the transition as JavaScript redirect. + FireNavigationEvents(web_browser_, url); + check.Call(3); + + // If currently we are in the onload event handler, consider the transition as + // onload redirect. + web_progress_notifier_->mock_has_potential_javascript_redirect_ = false; + web_progress_notifier_->mock_in_onload_event_ = true; + FireNavigationEvents(web_browser_, url); + check.Call(4); + + // If the previous page has <meta http-equiv="refresh"> tag, consider the + // transition as meta-refresh redirect. + web_progress_notifier_->mock_in_onload_event_ = false; + web_progress_notifier_->mock_is_meta_refresh_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionUserAction) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + const char* link = "link"; + const char* typed = "typed"; + const char* no_qualifier = ""; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(link), StrEq(no_qualifier), _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(typed), StrEq(no_qualifier), _)); + } + + // User actions override JavaScript redirect signals, so setting the following + // flag should have no effect. + web_progress_notifier_->mock_has_potential_javascript_redirect_ = true; + + // If there is user action in the content window, consider the transition as + // link. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + true; + FireNavigationEvents(web_browser_, url); + check.Call(1); + + // If there is user action in the browser UI, consider the transition as + // typed. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + false; + web_progress_notifier_->mock_is_possible_user_action_in_browser_ui_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestTransitionForwardBack) { + IgnoreCallsToEventsFunnelExceptOnCommitted(); + + CComBSTR url(L"http://www.google.com"); + const char* auto_bookmark = "auto_bookmark"; + const char* forward_back = "forward_back"; + + EXPECT_CALL(web_progress_notifier_->mock_webnavigation_events_funnel_, + OnCommitted(_, _, _, StrEq(auto_bookmark), StrEq(forward_back), + _)); + + // Forward/back overrides other signals, so setting the following flags + // should have no effect. + web_progress_notifier_->mock_is_possible_user_action_in_content_window_ = + true; + web_progress_notifier_->mock_is_possible_user_action_in_browser_ui_ = + true; + web_progress_notifier_->mock_in_onload_event_ = true; + web_progress_notifier_->mock_is_meta_refresh_ = true; + + // If the current navigation doesn't cause browsing history to change, + // consider the transition as forward/back. + web_progress_notifier_->mock_is_forward_back_ = true; + FireNavigationEvents(web_browser_, url); +} + +TEST_F(WebProgressNotifierTestFixture, TestDetectingForwardBack) { + TLENUMF back_fore = TLEF_RELATIVE_BACK | TLEF_RELATIVE_FORE | + TLEF_INCLUDE_UNINVOKEABLE; + TLENUMF fore = TLEF_RELATIVE_FORE | TLEF_INCLUDE_UNINVOKEABLE; + MockFunction<void(int check_point)> check; + { + InSequence sequence; + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(5), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(3), + Return(S_OK))); + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(*mock_travel_log_stg_, GetCount(back_fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(6), + Return(S_OK))); + EXPECT_CALL(*mock_travel_log_stg_, GetCount(fore, NotNull())) + .WillOnce(DoAll(SetArgumentPointee<1>(0), + Return(S_OK))); + } + + CComBSTR google_url(L"http://www.google.com/"); + CComBSTR gmail_url(L"http://mail.google.com/"); + + // Test recording of the previous travel log info. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + EXPECT_EQ(6, web_progress_notifier_->previous_travel_log_info().length); + EXPECT_EQ(0, web_progress_notifier_->previous_travel_log_info().position); + EXPECT_EQ(google_url, + web_progress_notifier_->previous_travel_log_info().newest_url); + check.Call(1); + + // If the length of the forward/back list has changed, the navigation is not + // forward/back. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + check.Call(2); + + // If the length of the forward/back list remains the same, and there are + // entries in the forward list, the navigation is forward/back. + EXPECT_TRUE(web_progress_notifier_->CallRealIsForwardBack(gmail_url)); + EXPECT_NE(gmail_url, + web_progress_notifier_->previous_travel_log_info().newest_url); + check.Call(3); + + // If the length of the forward/back list remains the same, and the URL of the + // last entry in the list remains the same, the navigation is forward/back. + EXPECT_TRUE(web_progress_notifier_->CallRealIsForwardBack(google_url)); + check.Call(4); + + // If the length of the forward/back list remains the same, but the URL of the + // last entry in the list has changed, the navigation is not forward/back. + EXPECT_FALSE(web_progress_notifier_->CallRealIsForwardBack(gmail_url)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel.cc b/ceee/ie/plugin/bho/webnavigation_events_funnel.cc new file mode 100644 index 0000000..2717131 --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from wherever through the Broker. + +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" + +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" + +namespace keys = extension_webnavigation_api_constants; + +namespace { + +double MilliSecondsFromTime(const base::Time& time) { + return base::Time::kMillisecondsPerSecond * time.ToDoubleT(); +} + +} // namespace + +HRESULT WebNavigationEventsFunnel::OnBeforeNavigate( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeNavigate, args); +} + +HRESULT WebNavigationEventsFunnel::OnBeforeRetarget( + CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kSourceTabIdKey, static_cast<int>(source_tab_handle)); + args.SetString(keys::kSourceUrlKey, source_url); + args.SetString(keys::kTargetUrlKey, target_url); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRetarget, args); +} + +HRESULT WebNavigationEventsFunnel::OnCommitted( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kTransitionTypeKey, transition_type); + args.SetString(keys::kTransitionQualifiersKey, transition_qualifiers); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCommitted, args); +} + +HRESULT WebNavigationEventsFunnel::OnCompleted( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCompleted, args); +} + +HRESULT WebNavigationEventsFunnel::OnDOMContentLoaded( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnDOMContentLoaded, args); +} + +HRESULT WebNavigationEventsFunnel::OnErrorOccurred( + CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnErrorOccurred, args); +} diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel.h b/ceee/ie/plugin/bho/webnavigation_events_funnel.h new file mode 100644 index 0000000..68243cc --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel.h @@ -0,0 +1,108 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Web Navigation Events. + +#ifndef CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ +#include <atlcomcli.h> + +#include "base/time.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "toolband.h" // NOLINT + +// Implements a set of methods to send web navigation related events to the +// Broker. +class WebNavigationEventsFunnel : public EventsFunnel { + public: + WebNavigationEventsFunnel() : EventsFunnel(false) {} + + // Sends the webNavigation.onBeforeNavigate event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation is + // about to occur. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param request_id The ID of the request to retrieve the document of this + // navigation. + // @param time_stamp The time when the browser was about to start the + // navigation. + virtual HRESULT OnBeforeNavigate(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onBeforeRetarget event to the Broker. + // @param source_tab_handle The window handle of the tab in which the + // navigation is triggered. + // @param source_url The URL of the document that is opening the new window. + // @param target_url The URL to be opened in the new window. + // @param time_stamp The time when the browser was about to create a new view. + virtual HRESULT OnBeforeRetarget(CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp); + + // Sends the webNavigation.onCommitted event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param transition_type Cause of the navigation. + // @param transition_qualifiers Zero or more transition qualifiers delimited + // by "|". + // @param time_stamp The time when the navigation was committed. + virtual HRESULT OnCommitted(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp); + + // Sends the webNavigation.onCompleted event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param time_stamp The time when the document finished loading. + virtual HRESULT OnCompleted(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onDOMContentLoaded event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param time_stamp The time when the page's DOM was fully constructed. + virtual HRESULT OnDOMContentLoaded(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp); + + // Sends the webNavigation.onErrorOccurred event to the Broker. + // @param tab_handle The window handle of the tab in which the navigation + // occurs. + // @param url The URL of the navigation. + // @param frame_id 0 indicates the navigation happens in the tab content + // window; positive value indicates navigation in a subframe. + // @param error The error description. + // @param time_stamp The time when the error occurred. + virtual HRESULT OnErrorOccurred(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp); + + private: + DISALLOW_COPY_AND_ASSIGN(WebNavigationEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBNAVIGATION_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc b/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc new file mode 100644 index 0000000..bc7d7f9b --- /dev/null +++ b/ceee/ie/plugin/bho/webnavigation_events_funnel_unittest.cc @@ -0,0 +1,180 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WebNavigationEventsFunnel. + +#include <atlcomcli.h> + +#include "base/time.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "chrome/browser/extensions/extension_webnavigation_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +namespace keys = extension_webnavigation_api_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWebNavigationEventsFunnel : public WebNavigationEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(WebNavigationEventsFunnelTest, OnBeforeNavigate) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://www.google.com/"); + int frame_id = 512; + int request_id = 1024; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnBeforeNavigate), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnBeforeNavigate( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, request_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnBeforeRetarget) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int source_tab_handle = 256; + std::string source_url("http://docs.google.com/"); + std::string target_url("http://calendar.google.com/"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kSourceTabIdKey, source_tab_handle); + args.SetString(keys::kSourceUrlKey, source_url); + args.SetString(keys::kTargetUrlKey, target_url); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRetarget), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnBeforeRetarget( + static_cast<CeeeWindowHandle>(source_tab_handle), + CComBSTR(source_url.c_str()), CComBSTR(target_url.c_str()), time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnCommitted) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + std::string transition_type("link"); + std::string transition_qualifiers("client_redirect"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kTransitionTypeKey, transition_type); + args.SetString(keys::kTransitionQualifiersKey, transition_qualifiers); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnCommitted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnCommitted( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, transition_type.c_str(), transition_qualifiers.c_str(), + time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnCompleted) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://groups.google.com/"); + int frame_id = 512; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnCompleted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnCompleted( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnDOMContentLoaded) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnDOMContentLoaded), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnDOMContentLoaded( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, time_stamp)); +} + +TEST(WebNavigationEventsFunnelTest, OnErrorOccurred) { + TestWebNavigationEventsFunnel webnavigation_events_funnel; + + int tab_handle = 256; + std::string url("http://mail.google.com/"); + int frame_id = 512; + std::string error("not a valid URL"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kFrameIdKey, frame_id); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webnavigation_events_funnel, + SendEvent(StrEq(keys::kOnErrorOccurred), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webnavigation_events_funnel.OnErrorOccurred( + static_cast<CeeeWindowHandle>(tab_handle), CComBSTR(url.c_str()), + frame_id, CComBSTR(error.c_str()), time_stamp)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel.cc b/ceee/ie/plugin/bho/webrequest_events_funnel.cc new file mode 100644 index 0000000..c0d5b1e --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel.cc @@ -0,0 +1,106 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Events from whereever through the Broker. + +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" + +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" + +namespace keys = extension_webrequest_api_constants; + +namespace { + +double MilliSecondsFromTime(const base::Time& time) { + return base::Time::kMillisecondsPerSecond * time.ToDoubleT(); +} + +} // namespace + +HRESULT WebRequestEventsFunnel::OnBeforeRedirect(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetString(keys::kRedirectUrlKey, redirect_url); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRedirect, args); +} + +HRESULT WebRequestEventsFunnel::OnBeforeRequest(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kMethodKey, method); + args.SetInteger(keys::kTabIdKey, static_cast<int>(tab_handle)); + args.SetString(keys::kTypeKey, type); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnBeforeRequest, args); +} + +HRESULT WebRequestEventsFunnel::OnCompleted(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnCompleted, args); +} + +HRESULT WebRequestEventsFunnel::OnErrorOccurred(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnErrorOccurred, args); +} + +HRESULT WebRequestEventsFunnel::OnHeadersReceived( + int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnHeadersReceived, args); +} + +HRESULT WebRequestEventsFunnel::OnRequestSent(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp) { + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kIpKey, ip); + args.SetReal(keys::kTimeStampKey, MilliSecondsFromTime(time_stamp)); + + return SendEvent(keys::kOnRequestSent, args); +} diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel.h b/ceee/ie/plugin/bho/webrequest_events_funnel.h new file mode 100644 index 0000000..55879cc --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel.h @@ -0,0 +1,98 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Funnel of Chrome Extension Web Request Events. + +#ifndef CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ +#define CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ + +#include <atlcomcli.h> + +#include "base/time.h" +#include "ceee/ie/plugin/bho/events_funnel.h" + +#include "toolband.h" // NOLINT + +// Implements a set of methods to send web request related events to the +// Broker. +class WebRequestEventsFunnel : public EventsFunnel { + public: + WebRequestEventsFunnel() : EventsFunnel(false) {} + + // Sends the webRequest.onBeforeRedirect event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the current request. + // @param status_code Standard HTTP status code returned by the server. + // @param redirect_url The new URL. + // @param time_stamp The time when the browser was about to make the redirect. + virtual HRESULT OnBeforeRedirect(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp); + + // Sends the webRequest.onBeforeRequest event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param method Standard HTTP method, such as "GET" or "POST". + // @param tab_handle The window handle of the tab in which the request takes + // place. Set to INVALID_HANDLE_VALUE if the request isn't related to a + // tab. + // @param type How the requested resource will be used, such as "main_frame" + // or "sub_frame". Please find the complete list and explanation on + // http://www.chromium.org/developers/design-documents/extensions/notifications-of-web-request-and-navigation + // @param time_stamp The time when the browser was about to make the request. + virtual HRESULT OnBeforeRequest(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp); + + // Sends the webRequest.onCompleted event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param status_code Standard HTTP status code returned by the server. + // @param time_stamp The time when the response was received completely. + virtual HRESULT OnCompleted(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp); + + // Sends the webRequest.onErrorOccurred event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param error The error description. + // @param time_stamp The time when the error occurred. + virtual HRESULT OnErrorOccurred(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp); + + // Sends the webRequest.onHeadersReceived event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param status_code Standard HTTP status code returned by the server. + // @param time_stamp The time when the status line and response headers were + // received. + virtual HRESULT OnHeadersReceived(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp); + + // Sends the webRequest.onRequestSent event to the broker. + // @param request_id The ID of the request. + // @param url The URL of the request. + // @param ip The server IP address that is actually connected to. + // @param time_stamp The time when the browser finished sending the request. + virtual HRESULT OnRequestSent(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp); + + private: + DISALLOW_COPY_AND_ASSIGN(WebRequestEventsFunnel); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBREQUEST_EVENTS_FUNNEL_H_ diff --git a/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc b/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc new file mode 100644 index 0000000..b56a35c --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_events_funnel_unittest.cc @@ -0,0 +1,171 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for WebRequestEventsFunnel. + +#include <atlcomcli.h> + +#include "base/time.h" +#include "base/values.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "chrome/browser/extensions/extension_webrequest_api_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace keys = extension_webrequest_api_constants; + +namespace { + +using testing::Return; +using testing::StrEq; + +MATCHER_P(ValuesEqual, value, "") { + return arg.Equals(value); +} + +class TestWebRequestEventsFunnel : public WebRequestEventsFunnel { + public: + MOCK_METHOD2(SendEvent, HRESULT(const char*, const Value&)); +}; + +TEST(WebRequestEventsFunnelTest, OnBeforeRedirect) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://www.google.com/"); + DWORD status_code = 200; + std::wstring redirect_url(L"http://mail.google.com/"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetString(keys::kRedirectUrlKey, redirect_url); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRedirect), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnBeforeRedirect( + request_id, url.c_str(), status_code, redirect_url.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnBeforeRequest) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://calendar.google.com/"); + std::string method("GET"); + int tab_handle = 512; + std::string type("main_frame"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kMethodKey, method); + args.SetInteger(keys::kTabIdKey, tab_handle); + args.SetString(keys::kTypeKey, type); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnBeforeRequest), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnBeforeRequest( + request_id, url.c_str(), method.c_str(), + static_cast<CeeeWindowHandle>(tab_handle), type.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnCompleted) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://image.google.com/"); + DWORD status_code = 404; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnCompleted), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnCompleted( + request_id, url.c_str(), status_code, time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnErrorOccurred) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://docs.google.com/"); + std::wstring error(L"cannot resolve the host"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kErrorKey, error); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnErrorOccurred), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnErrorOccurred( + request_id, url.c_str(), error.c_str(), time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnHeadersReceived) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://news.google.com/"); + DWORD status_code = 200; + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetInteger(keys::kStatusCodeKey, status_code); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnHeadersReceived), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnHeadersReceived( + request_id, url.c_str(), status_code, time_stamp)); +} + +TEST(WebRequestEventsFunnelTest, OnRequestSent) { + TestWebRequestEventsFunnel webrequest_events_funnel; + + int request_id = 256; + std::wstring url(L"http://finance.google.com/"); + std::string ip("127.0.0.1"); + base::Time time_stamp = base::Time::FromDoubleT(2048.0); + + DictionaryValue args; + args.SetInteger(keys::kRequestIdKey, request_id); + args.SetString(keys::kUrlKey, url); + args.SetString(keys::kIpKey, ip); + args.SetReal(keys::kTimeStampKey, + base::Time::kMillisecondsPerSecond * time_stamp.ToDoubleT()); + + EXPECT_CALL(webrequest_events_funnel, + SendEvent(StrEq(keys::kOnRequestSent), ValuesEqual(&args))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(webrequest_events_funnel.OnRequestSent( + request_id, url.c_str(), ip.c_str(), time_stamp)); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/webrequest_notifier.cc b/ceee/ie/plugin/bho/webrequest_notifier.cc new file mode 100644 index 0000000..e0449e4 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier.cc @@ -0,0 +1,811 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web request notifier implementation. +#include "ceee/ie/plugin/bho/webrequest_notifier.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "chrome_frame/function_stub.h" +#include "chrome_frame/utils.h" + +namespace { + +const wchar_t kUrlMonModuleName[] = L"urlmon.dll"; +const char kWinINetModuleName[] = "wininet.dll"; +const char kInternetSetStatusCallbackAFunctionName[] = + "InternetSetStatusCallbackA"; +const char kInternetSetStatusCallbackWFunctionName[] = + "InternetSetStatusCallbackW"; +const char kInternetConnectAFunctionName[] = "InternetConnectA"; +const char kInternetConnectWFunctionName[] = "InternetConnectW"; +const char kHttpOpenRequestAFunctionName[] = "HttpOpenRequestA"; +const char kHttpOpenRequestWFunctionName[] = "HttpOpenRequestW"; +const char kHttpSendRequestAFunctionName[] = "HttpSendRequestA"; +const char kHttpSendRequestWFunctionName[] = "HttpSendRequestW"; +const char kInternetReadFileFunctionName[] = "InternetReadFile"; + +} // namespace + +WebRequestNotifier::WebRequestNotifier() + : internet_status_callback_stub_(NULL), + start_count_(0), + initialize_state_(NOT_INITIALIZED) { +} + +WebRequestNotifier::~WebRequestNotifier() { + DCHECK_EQ(start_count_, 0); +} + +bool WebRequestNotifier::RequestToStart() { + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + start_count_++; + + if (initialize_state_ != NOT_INITIALIZED) + return initialize_state_ != FAILED_TO_INITIALIZE; + initialize_state_ = INITIALIZING; + } + + bool success = false; + do { + // We are not going to unpatch any of the patched WinINet functions or the + // status callback function. Instead, we pin our DLL in memory so that all + // the patched functions can be accessed until the process goes away. + PinModule(); + + PatchWinINetFunction(kInternetSetStatusCallbackAFunctionName, + &internet_set_status_callback_a_patch_, + InternetSetStatusCallbackAPatch); + PatchWinINetFunction(kInternetSetStatusCallbackWFunctionName, + &internet_set_status_callback_w_patch_, + InternetSetStatusCallbackWPatch); + if (!HasPatchedOneVersion(internet_set_status_callback_a_patch_, + internet_set_status_callback_w_patch_)) { + break; + } + + PatchWinINetFunction(kInternetConnectAFunctionName, + &internet_connect_a_patch_, + InternetConnectAPatch); + PatchWinINetFunction(kInternetConnectWFunctionName, + &internet_connect_w_patch_, + InternetConnectWPatch); + if (!HasPatchedOneVersion(internet_connect_a_patch_, + internet_connect_w_patch_)) { + break; + } + + PatchWinINetFunction(kHttpOpenRequestAFunctionName, + &http_open_request_a_patch_, + HttpOpenRequestAPatch); + PatchWinINetFunction(kHttpOpenRequestWFunctionName, + &http_open_request_w_patch_, + HttpOpenRequestWPatch); + if (!HasPatchedOneVersion(http_open_request_a_patch_, + http_open_request_w_patch_)) { + break; + } + + PatchWinINetFunction(kHttpSendRequestAFunctionName, + &http_send_request_a_patch_, + HttpSendRequestAPatch); + PatchWinINetFunction(kHttpSendRequestWFunctionName, + &http_send_request_w_patch_, + HttpSendRequestWPatch); + if (!HasPatchedOneVersion(http_send_request_a_patch_, + http_send_request_w_patch_)) { + break; + } + + PatchWinINetFunction(kInternetReadFileFunctionName, + &internet_read_file_patch_, + InternetReadFilePatch); + if (!internet_read_file_patch_.is_patched()) + break; + + success = true; + } while (false); + + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + initialize_state_ = success ? SUCCEEDED_TO_INITIALIZE : + FAILED_TO_INITIALIZE; + } + return success; +} + +void WebRequestNotifier::RequestToStop() { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (start_count_ <= 0) { + NOTREACHED(); + return; + } + + start_count_--; + if (start_count_ == 0) { + // It is supposed that every handle must be closed using + // InternetCloseHandle(). However, IE seems to leak handles in some (rare) + // cases. For example, when the current page is a JPG or GIF image and the + // user refreshes the page. + // If that happens, the server_map_ and request_map_ won't be empty. + LOG_IF(WARNING, !server_map_.empty() || !request_map_.empty()) + << "There are Internet handles that haven't been closed when " + << "WebRequestNotifier stops."; + + for (RequestMap::iterator iter = request_map_.begin(); + iter != request_map_.end(); ++iter) { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &iter->second); + } + + server_map_.clear(); + request_map_.clear(); + } +} + +void WebRequestNotifier::PatchWinINetFunction( + const char* name, + app::win::IATPatchFunction* patch_function, + void* handler) { + DWORD error = patch_function->Patch(kUrlMonModuleName, kWinINetModuleName, + name, handler); + // The patching operation is either successful, or failed cleanly. + DCHECK(error == NO_ERROR || !patch_function->is_patched()); +} + +INTERNET_STATUS_CALLBACK STDAPICALLTYPE + WebRequestNotifier::InternetSetStatusCallbackAPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + INTERNET_STATUS_CALLBACK new_callback = + instance->HandleBeforeInternetSetStatusCallback(internet, callback); + return ::InternetSetStatusCallbackA(internet, new_callback); +} + +INTERNET_STATUS_CALLBACK STDAPICALLTYPE + WebRequestNotifier::InternetSetStatusCallbackWPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + INTERNET_STATUS_CALLBACK new_callback = + instance->HandleBeforeInternetSetStatusCallback(internet, callback); + return ::InternetSetStatusCallbackW(internet, new_callback); +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::InternetConnectAPatch( + HINTERNET internet, + LPCSTR server_name, + INTERNET_PORT server_port, + LPCSTR user_name, + LPCSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetConnect(internet); + + HINTERNET server = ::InternetConnectA(internet, server_name, server_port, + user_name, password, service, flags, + context); + + instance->HandleAfterInternetConnect(server, CA2W(server_name), server_port, + service); + return server; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::InternetConnectWPatch( + HINTERNET internet, + LPCWSTR server_name, + INTERNET_PORT server_port, + LPCWSTR user_name, + LPCWSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetConnect(internet); + + HINTERNET server = ::InternetConnectW(internet, server_name, server_port, + user_name, password, service, flags, + context); + + instance->HandleAfterInternetConnect(server, server_name, server_port, + service); + return server; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::HttpOpenRequestAPatch( + HINTERNET connect, + LPCSTR verb, + LPCSTR object_name, + LPCSTR version, + LPCSTR referrer, + LPCSTR* accept_types, + DWORD flags, + DWORD_PTR context) { + HINTERNET request = ::HttpOpenRequestA(connect, verb, object_name, version, + referrer, accept_types, flags, + context); + + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterHttpOpenRequest(connect, request, verb, + CA2W(object_name), flags); + return request; +} + +HINTERNET STDAPICALLTYPE WebRequestNotifier::HttpOpenRequestWPatch( + HINTERNET connect, + LPCWSTR verb, + LPCWSTR object_name, + LPCWSTR version, + LPCWSTR referrer, + LPCWSTR* accept_types, + DWORD flags, + DWORD_PTR context) { + HINTERNET request = ::HttpOpenRequestW(connect, verb, object_name, version, + referrer, accept_types, flags, + context); + + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterHttpOpenRequest(connect, request, CW2A(verb), + object_name, flags); + return request; +} + +BOOL STDAPICALLTYPE WebRequestNotifier::HttpSendRequestAPatch( + HINTERNET request, + LPCSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeHttpSendRequest(request); + return ::HttpSendRequestA(request, headers, headers_length, optional, + optional_length); +} + +BOOL STDAPICALLTYPE WebRequestNotifier::HttpSendRequestWPatch( + HINTERNET request, + LPCWSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeHttpSendRequest(request); + return ::HttpSendRequestW(request, headers, headers_length, optional, + optional_length); +} + +void CALLBACK WebRequestNotifier::InternetStatusCallbackPatch( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length) { + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleBeforeInternetStatusCallback(original, internet, context, + internet_status, + status_information, + status_information_length); + original(internet, context, internet_status, status_information, + status_information_length); +} + +BOOL STDAPICALLTYPE WebRequestNotifier::InternetReadFilePatch( + HINTERNET file, + LPVOID buffer, + DWORD number_of_bytes_to_read, + LPDWORD number_of_bytes_read) { + BOOL result = ::InternetReadFile(file, buffer, number_of_bytes_to_read, + number_of_bytes_read); + WebRequestNotifier* instance = ProductionWebRequestNotifier::get(); + instance->HandleAfterInternetReadFile(file, result, number_of_bytes_read); + + return result; +} + +INTERNET_STATUS_CALLBACK + WebRequestNotifier::HandleBeforeInternetSetStatusCallback( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return internet_callback; + + if (internet_callback == NULL) + return NULL; + + if (internet_status_callback_stub_ == NULL) { + return CreateInternetStatusCallbackStub(internet_callback); + } else { + if (internet_status_callback_stub_->argument() != + reinterpret_cast<uintptr_t>(internet_callback)) { + NOTREACHED(); + return CreateInternetStatusCallbackStub(internet_callback); + } else { + return reinterpret_cast<INTERNET_STATUS_CALLBACK>( + internet_status_callback_stub_->code()); + } + } +} + +void WebRequestNotifier::HandleBeforeInternetConnect(HINTERNET internet) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + if (internet_status_callback_stub_ == NULL) { + INTERNET_STATUS_CALLBACK original_callback = + ::InternetSetStatusCallbackA(internet, NULL); + if (original_callback != NULL) { + INTERNET_STATUS_CALLBACK new_callback = CreateInternetStatusCallbackStub( + original_callback); + ::InternetSetStatusCallbackA(internet, new_callback); + } + } +} + +// NOTE: this method must be called within a lock. +INTERNET_STATUS_CALLBACK WebRequestNotifier::CreateInternetStatusCallbackStub( + INTERNET_STATUS_CALLBACK original_callback) { + DCHECK(original_callback != NULL); + + internet_status_callback_stub_ = FunctionStub::Create( + reinterpret_cast<uintptr_t>(original_callback), + InternetStatusCallbackPatch); + // internet_status_callback_stub_ is not NULL if the function stub is + // successfully created. + if (internet_status_callback_stub_ != NULL) { + return reinterpret_cast<INTERNET_STATUS_CALLBACK>( + internet_status_callback_stub_->code()); + } else { + NOTREACHED(); + return original_callback; + } +} + +void WebRequestNotifier::HandleAfterInternetConnect(HINTERNET server, + const wchar_t* server_name, + INTERNET_PORT server_port, + DWORD service) { + if (service != INTERNET_SERVICE_HTTP || server == NULL || + IS_INTRESOURCE(server_name) || wcslen(server_name) == 0) { + return; + } + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // It is not possible that the same connection handle is opened more than + // once. + DCHECK(server_map_.find(server) == server_map_.end()); + + ServerInfo server_info; + server_info.server_name = server_name; + server_info.server_port = server_port; + + server_map_.insert( + std::make_pair<HINTERNET, ServerInfo>(server, server_info)); +} + +void WebRequestNotifier::HandleAfterHttpOpenRequest(HINTERNET server, + HINTERNET request, + const char* method, + const wchar_t* path, + DWORD flags) { + if (server == NULL || request == NULL) + return; + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // It is not possible that the same request handle is opened more than once. + DCHECK(request_map_.find(request) == request_map_.end()); + + ServerMap::iterator server_iter = server_map_.find(server); + // It is possible to find that we haven't recorded the connection handle to + // the server, if we patch WinINet functions after the InternetConnect call. + // In that case, we will ignore all events related to requests happening on + // that connection. + if (server_iter == server_map_.end()) + return; + + RequestInfo request_info; + // TODO(yzshen@google.com): create the request ID. + request_info.server_handle = server; + request_info.method = method == NULL ? "GET" : method; + if (!ConstructUrl((flags & INTERNET_FLAG_SECURE) != 0, + server_iter->second.server_name.c_str(), + server_iter->second.server_port, + path, + &request_info.url)) { + NOTREACHED(); + return; + } + + request_map_.insert( + std::make_pair<HINTERNET, RequestInfo>(request, request_info)); +} + +void WebRequestNotifier::HandleBeforeHttpSendRequest(HINTERNET request) { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(request); + if (request_iter != request_map_.end()) { + request_iter->second.before_request_time = base::Time::Now(); + TransitRequestToNextState(RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, + &request_iter->second); + } +} + +void WebRequestNotifier::HandleBeforeInternetStatusCallback( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length) { + switch (internet_status) { + case INTERNET_STATUS_HANDLE_CLOSING: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + // We don't know whether we are closing a server or a request handle. As a + // result, we have to test both server_map_ and request_map_. + ServerMap::iterator server_iter = server_map_.find(internet); + if (server_iter != server_map_.end()) { + server_map_.erase(server_iter); + } else { + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + // TODO(yzshen@google.com): For now, we don't bother + // checking whether the content of the response has + // completed downloading in this case. Have to make + // improvement if the requirement for more accurate + // webRequest.onCompleted notifications emerges. + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, + &request_iter->second); + request_map_.erase(request_iter); + } + } + break; + } + case INTERNET_STATUS_REQUEST_SENT: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + TransitRequestToNextState(RequestInfo::NOTIFIED_REQUEST_SENT, + &request_iter->second); + } + break; + } + case INTERNET_STATUS_REDIRECT: { + DWORD status_code = 0; + bool result = QueryHttpInfoNumber(internet, HTTP_QUERY_STATUS_CODE, + &status_code); + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end()) { + RequestInfo& info = request_iter->second; + if (result) { + info.status_code = status_code; + TransitRequestToNextState(RequestInfo::NOTIFIED_HEADERS_RECEIVED, + &info); + + info.original_url = info.url; + info.url = CA2W(reinterpret_cast<PCSTR>(status_information)); + TransitRequestToNextState(RequestInfo::NOTIFIED_BEFORE_REDIRECT, + &info); + } else { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &info); + } + } + } + break; + } + case INTERNET_STATUS_REQUEST_COMPLETE: { + DWORD status_code = 0; + DWORD content_length = 0; + RequestInfo::MessageLengthType length_type = + RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + + bool result = QueryHttpInfoNumber(internet, HTTP_QUERY_STATUS_CODE, + &status_code) && + DetermineMessageLength(internet, status_code, + &content_length, &length_type); + { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end() && + request_iter->second.state == RequestInfo::NOTIFIED_REQUEST_SENT) { + RequestInfo& info = request_iter->second; + if (result) { + info.status_code = status_code; + info.content_length = content_length; + info.length_type = length_type; + TransitRequestToNextState(RequestInfo::NOTIFIED_HEADERS_RECEIVED, + &info); + if (info.length_type == RequestInfo::NO_MESSAGE_BODY) + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, &info); + } else { + TransitRequestToNextState(RequestInfo::ERROR_OCCURRED, &info); + } + } + } + break; + } + case INTERNET_STATUS_CONNECTED_TO_SERVER: { + // TODO(yzshen@google.com): get IP information. + break; + } + case INTERNET_STATUS_SENDING_REQUEST: { + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + // Some HttpSendRequest() calls don't actually make HTTP requests, and the + // corresponding request handles are closed right after the calls. + // In that case, we ignore them totally, and don't send + // webRequest.onBeforeRequest notifications. + RequestMap::iterator request_iter = request_map_.find(internet); + if (request_iter != request_map_.end() && + request_iter->second.state == + RequestInfo::WILL_NOTIFY_BEFORE_REQUEST) { + TransitRequestToNextState(RequestInfo::NOTIFIED_BEFORE_REQUEST, + &request_iter->second); + } + break; + } + default: { + break; + } + } +} + +void WebRequestNotifier::HandleAfterInternetReadFile( + HINTERNET request, + BOOL result, + LPDWORD number_of_bytes_read) { + if (!result || number_of_bytes_read == NULL) + return; + + CComCritSecLock<CComAutoCriticalSection> lock(critical_section_); + if (IsNotRunning()) + return; + + RequestMap::iterator iter = request_map_.find(request); + if (iter != request_map_.end()) { + RequestInfo& info = iter->second; + // We don't update the length_type field until we reach the last request + // of the redirection chain. As a result, the check below also prevents us + // from firing webRequest.onCompleted before the last request in the + // chain. + if (info.length_type == RequestInfo::CONTENT_LENGTH_HEADER) { + info.read_progress += *number_of_bytes_read; + if (info.read_progress >= info.content_length && + info.state == RequestInfo::NOTIFIED_HEADERS_RECEIVED) { + DCHECK(info.read_progress == info.content_length); + TransitRequestToNextState(RequestInfo::NOTIFIED_COMPLETED, &info); + } + } + } +} + +// Currently this method is always called within a lock. +bool WebRequestNotifier::ConstructUrl(bool https, + const wchar_t* server_name, + INTERNET_PORT server_port, + const wchar_t* path, + std::wstring* url) { + if (url == NULL || server_name == NULL || wcslen(server_name) == 0) + return false; + + url->clear(); + url->append(https ? L"https://" : L"http://"); + url->append(server_name); + + bool need_port = server_port != INTERNET_INVALID_PORT_NUMBER && + (https ? server_port != INTERNET_DEFAULT_HTTPS_PORT : + server_port != INTERNET_DEFAULT_HTTP_PORT); + if (need_port) { + static const int kMaxPortLength = 10; + wchar_t buffer[kMaxPortLength]; + if (swprintf(buffer, kMaxPortLength, L":%d", server_port) == -1) + return false; + url->append(buffer); + } + + url->append(path); + return true; +} + +bool WebRequestNotifier::QueryHttpInfoNumber(HINTERNET request, + DWORD info_flag, + DWORD* value) { + DCHECK(value != NULL); + *value = 0; + + DWORD size = sizeof(info_flag); + return ::HttpQueryInfo(request, info_flag | HTTP_QUERY_FLAG_NUMBER, value, + &size, NULL) ? true : false; +} + +bool WebRequestNotifier::DetermineMessageLength( + HINTERNET request, + DWORD status_code, + DWORD* length, + RequestInfo::MessageLengthType* type) { + // Please see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + // for how the length of a message is determined. + + DCHECK(length != NULL && type != NULL); + *length = 0; + *type = RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + + std::wstring method; + // Request methods are case-sensitive. + if ((status_code >= 100 && status_code < 200) || + status_code == 204 || + status_code == 304 || + (QueryHttpInfoString(request, HTTP_QUERY_REQUEST_METHOD, &method) && + method == L"HEAD")) { + *type = RequestInfo::NO_MESSAGE_BODY; + return true; + } + + std::wstring transfer_encoding; + // All transfer-coding values are case-insensitive. + if (QueryHttpInfoString(request, HTTP_QUERY_TRANSFER_ENCODING, + &transfer_encoding) && + _wcsicmp(transfer_encoding.c_str(), L"entity") != 0) { + *type = RequestInfo::VARIABLE_MESSAGE_LENGTH; + return true; + } + + DWORD content_length = 0; + if (QueryHttpInfoNumber(request, HTTP_QUERY_CONTENT_LENGTH, + &content_length)) { + *type = RequestInfo::CONTENT_LENGTH_HEADER; + *length = content_length; + return true; + } + + *type = RequestInfo::VARIABLE_MESSAGE_LENGTH; + return true; +} + +bool WebRequestNotifier::QueryHttpInfoString(HINTERNET request, + DWORD info_flag, + std::wstring* value) { + DCHECK(value != NULL); + value->clear(); + + DWORD size = 20; + scoped_array<wchar_t> buffer(new wchar_t[size]); + + BOOL result = ::HttpQueryInfo(request, info_flag, + reinterpret_cast<LPVOID>(buffer.get()), &size, + NULL); + if (!result && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + buffer.reset(new wchar_t[size]); + result = ::HttpQueryInfo(request, info_flag, + reinterpret_cast<LPVOID>(buffer.get()), &size, + NULL); + } + if (!result) + return false; + + *value = buffer.get(); + return true; +} + +// NOTE: this method must be called within a lock. +void WebRequestNotifier::TransitRequestToNextState( + RequestInfo::State next_state, + RequestInfo* info) { + // TODO(yzshen@google.com): generate and fill in missing parameters + // for notifications. + DCHECK(info != NULL); + + bool fire_on_error_occurred = false; + switch (info->state) { + case RequestInfo::BEGIN: + if (next_state != RequestInfo::WILL_NOTIFY_BEFORE_REQUEST && + next_state != RequestInfo::ERROR_OCCURRED) { + next_state = RequestInfo::ERROR_OCCURRED; + // We don't fire webRequest.onErrorOccurred in this case, since the + // first event for any request has to be webRequest.onBeforeRequest. + } + break; + case RequestInfo::WILL_NOTIFY_BEFORE_REQUEST: + if (next_state == RequestInfo::NOTIFIED_BEFORE_REQUEST) { + webrequest_events_funnel().OnBeforeRequest( + info->id, info->url.c_str(), info->method.c_str(), info->tab_handle, + "other", info->before_request_time); + } else if (next_state != RequestInfo::ERROR_OCCURRED) { + next_state = RequestInfo::ERROR_OCCURRED; + // We don't fire webRequest.onErrorOccurred in this case, since the + // first event for any request has to be webRequest.onBeforeRequest. + } + break; + case RequestInfo::NOTIFIED_BEFORE_REQUEST: + case RequestInfo::NOTIFIED_BEFORE_REDIRECT: + if (next_state == RequestInfo::NOTIFIED_REQUEST_SENT) { + webrequest_events_funnel().OnRequestSent( + info->id, info->url.c_str(), info->ip.c_str(), base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_REQUEST_SENT: + if (next_state == RequestInfo::NOTIFIED_HEADERS_RECEIVED) { + webrequest_events_funnel().OnHeadersReceived( + info->id, info->url.c_str(), info->status_code, base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_HEADERS_RECEIVED: + if (next_state == RequestInfo::NOTIFIED_BEFORE_REDIRECT) { + webrequest_events_funnel().OnBeforeRedirect( + info->id, info->original_url.c_str(), info->status_code, + info->url.c_str(), base::Time::Now()); + } else if (next_state == RequestInfo::NOTIFIED_COMPLETED) { + webrequest_events_funnel().OnCompleted( + info->id, info->url.c_str(), info->status_code, base::Time::Now()); + } else { + if (next_state != RequestInfo::ERROR_OCCURRED) + next_state = RequestInfo::ERROR_OCCURRED; + + fire_on_error_occurred = true; + } + break; + case RequestInfo::NOTIFIED_COMPLETED: + // The webRequest.onCompleted notification is supposed to be the last + // event sent for a given request. As a result, if the request is already + // in the NOTIFIED_COMPLETED state, we just keep it in that state without + // sending any further notification. + // + // When a request handle is closed, we consider transiting the state to + // NOTIFIED_COMPLETED. If there is no response body or we have completed + // reading the response body, the request has already been in this state. + // In that case, we will hit this code path. + next_state = RequestInfo::NOTIFIED_COMPLETED; + break; + case RequestInfo::ERROR_OCCURRED: + next_state = RequestInfo::ERROR_OCCURRED; + break; + default: + NOTREACHED(); + break; + } + + if (fire_on_error_occurred) { + webrequest_events_funnel().OnErrorOccurred( + info->id, info->url.c_str(), L"", base::Time::Now()); + } + info->state = next_state; +} diff --git a/ceee/ie/plugin/bho/webrequest_notifier.h b/ceee/ie/plugin/bho/webrequest_notifier.h new file mode 100644 index 0000000..2529c42 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier.h @@ -0,0 +1,453 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Web request notifier implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ +#define CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ + +#include <atlbase.h> +#include <wininet.h> + +#include <map> +#include <string> + +#include "app/win/iat_patch_function.h" +#include "base/singleton.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "toolband.h" + +struct FunctionStub; + +// WebRequestNotifier monitors HTTP request/response events via WinINet hooks, +// and sends the events to the broker. +class WebRequestNotifier { + public: + // Starts the service if it hasn't been started. + // @return Returns true if the service is being initialized or has been + // successfully initialized. + bool RequestToStart(); + // Stops the service if it is currently running and nobody is interested in + // the service any more. Every call to RequestToStart (even if it failed) + // should be paired with a call to RequestToStop. + void RequestToStop(); + + protected: + // Information related to an Internet connection. + struct ServerInfo { + ServerInfo() : server_port(INTERNET_INVALID_PORT_NUMBER) {} + + // The host name of the server. + std::wstring server_name; + // The port number on the server. + INTERNET_PORT server_port; + }; + + // Information related to a HTTP request. + struct RequestInfo { + RequestInfo() + : server_handle(NULL), + id(-1), + tab_handle(reinterpret_cast<CeeeWindowHandle>(INVALID_HANDLE_VALUE)), + status_code(0), + state(BEGIN), + content_length(0), + read_progress(0), + length_type(UNKNOWN_MESSAGE_LENGTH_TYPE) { + } + + enum State { + // The start state. + // Possible next state: WILL_NOTIFY_BEFORE_REQUEST; + // ERROR_OCCURRED. + BEGIN = 0, + // We are about to fire webRequest.onBeforeRequest. + // Possible next state: NOTIFIED_BEFORE_REQUEST; + // ERROR_OCCURRED. + WILL_NOTIFY_BEFORE_REQUEST = 1, + // The last event fired is webRequest.onBeforeRequest. + // Possible next state: NOTIFIED_REQUEST_SENT; + // ERROR_OCCURRED. + NOTIFIED_BEFORE_REQUEST = 2, + // The last event fired is webRequest.onRequestSent. + // Possible next state: NOTIFIED_HEADERS_RECEIVED; + // ERROR_OCCURRED. + NOTIFIED_REQUEST_SENT = 3, + // The last event fired is webRequest.onHeadersReceived. + // Possible next state: NOTIFIED_BEFORE_REDIRECT; + // NOTIFIED_COMPLETED; + // ERROR_OCCURRED. + NOTIFIED_HEADERS_RECEIVED = 4, + // The last event fired is webRequest.onBeforeRedirect. + // Possible next state: NOTIFIED_REQUEST_SENT; + // ERROR_OCCURRED. + NOTIFIED_BEFORE_REDIRECT = 5, + // One of the two stop states. + // The last event fired is webRequest.onCompleted. + // Possible next state: NOTIFIED_COMPLETED. + NOTIFIED_COMPLETED = 6, + // One of the two stop states. + // Some error has occurred. + // Possible next state: ERROR_OCCURRED. + ERROR_OCCURRED = 7 + }; + + // How the length of a message body is decided. + enum MessageLengthType { + UNKNOWN_MESSAGE_LENGTH_TYPE = 0, + // There is no message body. + NO_MESSAGE_BODY = 1, + // The length is decided by Content-Length header. + CONTENT_LENGTH_HEADER = 2, + // Including: + // (1) a Transfer-Encoding header field is present and has any value + // other than "identity"; + // (2) the message uses the media type "multipart/byteranges"; + // (3) the length should be decided by the server closing the connection. + VARIABLE_MESSAGE_LENGTH = 3, + }; + + // The handle of the connection to the server. + HINTERNET server_handle; + // The request ID, which is unique within a browser session. + int id; + // The window handle of the tab which sent the request. + CeeeWindowHandle tab_handle; + // The standard HTTP method, such as "GET" or "POST". + std::string method; + // The URL to retrieve. + std::wstring url; + // The URL before redirection. + std::wstring original_url; + // The server IP. + std::string ip; + // The standard HTTP status code returned by the server. + DWORD status_code; + // The request state to help decide what event should be sent next. + State state; + // When the browser was about to make the request. + base::Time before_request_time; + // The length of the response body. It is meaningful only when length_type + // is set to CONTENT_LENGTH_HEADER. + DWORD content_length; + // The progress of reading the response body. + DWORD read_progress; + // How the length of the response body is decided. + MessageLengthType length_type; + }; + + WebRequestNotifier(); + virtual ~WebRequestNotifier(); + + // Accessor so that we can mock it in unit tests. + // Currently this method is always called within a lock. + virtual WebRequestEventsFunnel& webrequest_events_funnel() { + return webrequest_events_funnel_; + } + + // Gets called before calling InternetSetStatusCallback. + // @param internet The handle for which the callback is set. + // @param callback The real callback function. + // @return A patch for the callback function. + INTERNET_STATUS_CALLBACK HandleBeforeInternetSetStatusCallback( + HINTERNET internet, + INTERNET_STATUS_CALLBACK callback); + + // Gets called before calling InternetConnect. + // @param internet Handle returned by InternetOpen. + void HandleBeforeInternetConnect(HINTERNET internet); + + // Gets called after calling InternetConnect. + // @param server The sever handle returned by InternetConnect. + // @param server_name The host name of the server. + // @param server_port The port number on the server. + // @param service Type of service to access. + void HandleAfterInternetConnect(HINTERNET server, + const wchar_t* server_name, + INTERNET_PORT server_port, + DWORD service); + + // Gets called after calling HttpOpenRequest. + // @param server The server handle. + // @param request The request handle. + // @param method Standard HTTP method, such as "GET" or "POST". + // @param path The path to the target object. + // @param flags Internet options that are passed into HttpOpenRequest. + void HandleAfterHttpOpenRequest(HINTERNET server, + HINTERNET request, + const char* method, + const wchar_t* path, + DWORD flags); + + // Gets called before calling HttpSendRequest. + // @param request The request handle. + void HandleBeforeHttpSendRequest(HINTERNET request); + + // Gets called before calling InternetStatusCallback. + // @param original The original status callback function. + // @param internet The handle for which the callback function is called. + // @param context The application-defined context value associated with + // the internet parameter. + // @param internet_status A status code that indicates why the callback + // function is called. + // @param status_information A pointer to additional status information. + // @param status_information_length The size, in bytes, of the additional + // status information. + void HandleBeforeInternetStatusCallback(INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length); + + // Gets called after calling InternetReadFile. + // @param request The request handle. + // @param result Whether the read operation is successful or not. + // @param number_of_bytes_read How many bytes have been read. + void HandleAfterInternetReadFile(HINTERNET request, + BOOL result, + LPDWORD number_of_bytes_read); + + // InternetSetStatusCallback function patches. + // InternetSetStatusCallback documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385120(VS.85).aspx + static INTERNET_STATUS_CALLBACK STDAPICALLTYPE + InternetSetStatusCallbackAPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback); + static INTERNET_STATUS_CALLBACK STDAPICALLTYPE + InternetSetStatusCallbackWPatch( + HINTERNET internet, + INTERNET_STATUS_CALLBACK internet_callback); + + // InternetConnect function patches. + // InternetConnect documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384363(VS.85).aspx + static HINTERNET STDAPICALLTYPE InternetConnectAPatch( + HINTERNET internet, + LPCSTR server_name, + INTERNET_PORT server_port, + LPCSTR user_name, + LPCSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context); + static HINTERNET STDAPICALLTYPE InternetConnectWPatch( + HINTERNET internet, + LPCWSTR server_name, + INTERNET_PORT server_port, + LPCWSTR user_name, + LPCWSTR password, + DWORD service, + DWORD flags, + DWORD_PTR context); + + // HttpOpenRequest function patches. + // HttpOpenRequest documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384233(v=VS.85).aspx + static HINTERNET STDAPICALLTYPE HttpOpenRequestAPatch(HINTERNET connect, + LPCSTR verb, + LPCSTR object_name, + LPCSTR version, + LPCSTR referrer, + LPCSTR* accept_types, + DWORD flags, + DWORD_PTR context); + static HINTERNET STDAPICALLTYPE HttpOpenRequestWPatch(HINTERNET connect, + LPCWSTR verb, + LPCWSTR object_name, + LPCWSTR version, + LPCWSTR referrer, + LPCWSTR* accept_types, + DWORD flags, + DWORD_PTR context); + + // HttpSendRequest function patches. + // HttpSendRequest documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa384247(v=VS.85).aspx + static BOOL STDAPICALLTYPE HttpSendRequestAPatch(HINTERNET request, + LPCSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length); + static BOOL STDAPICALLTYPE HttpSendRequestWPatch(HINTERNET request, + LPCWSTR headers, + DWORD headers_length, + LPVOID optional, + DWORD optional_length); + + // InternetStatusCallback function patch. + // InternetStatusCallback function documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385121(v=VS.85).aspx + static void CALLBACK InternetStatusCallbackPatch( + INTERNET_STATUS_CALLBACK original, + HINTERNET internet, + DWORD_PTR context, + DWORD internet_status, + LPVOID status_information, + DWORD status_information_length); + + // InternetReadFile function patch. + // InternetReadFile documentation can be found at: + // http://msdn.microsoft.com/en-us/library/aa385103(v=VS.85).aspx + static BOOL STDAPICALLTYPE InternetReadFilePatch( + HINTERNET file, + LPVOID buffer, + DWORD number_of_bytes_to_read, + LPDWORD number_of_bytes_read); + + // Patches a WinINet function. + // @param name The name of the function to be intercepted. + // @param patch_function The patching helper. You could check the is_patched + // member of this object to see whether the patching operation is + // successful or not. + // @param handler The new function implementation. + void PatchWinINetFunction(const char* name, + app::win::IATPatchFunction* patch_function, + void* handler); + + // Constructs a URL. The method omits the port number if it is the default + // number for the protocol, or it is INTERNET_INVALID_PORT_NUMBER. + // Currently this method is always called within a lock. + // @param https Is it http or https? + // @param server_name The host name of the server. + // @param server_port The port number on the server. + // @param path The path to the target object. + // @param url The returned URL. + // @return Whether the operation is successful or not. + bool ConstructUrl(bool https, + const wchar_t* server_name, + INTERNET_PORT server_port, + const wchar_t* path, + std::wstring* url); + + // Retrieves header information as a number. + // Make the method virtual so that we could mock it for unit tests. + // @param request The request handle. + // @param info_flag Query info flags could be found on: + // http://msdn.microsoft.com/en-us/library/aa385351(v=VS.85).aspx + // @param value The returned value. + // @return Whether the operation is successful or not. + virtual bool QueryHttpInfoNumber(HINTERNET request, + DWORD info_flag, + DWORD* value); + // Retrieves header information as a string. + // Make the method virtual so that we could mock it for unit tests. + // @param request The request handle. + // @param info_flag Query info flags could be found on: + // http://msdn.microsoft.com/en-us/library/aa385351(v=VS.85).aspx + // @param value The returned value. + // @return Whether the operation is successful or not. + virtual bool QueryHttpInfoString(HINTERNET request, + DWORD info_flag, + std::wstring* value); + + // Determines the length of the response body. + // @param request The request handle. + // @param status_code Standard HTTP status code. + // @param length Returns the length of the response body. It is meaningful + // only when type is set to CONTENT_LENGTH_HEADER. + // @param type Returns how the length of the response body is decided. + // @return Whether the operation is successful or not. + bool DetermineMessageLength(HINTERNET request, + DWORD status_code, + DWORD* length, + RequestInfo::MessageLengthType* type); + + // Performs state transition on a request. + // NOTE: this method must be called within a lock. + // @param state The target state. Please note that if it is not a valid + // transition, the request may end up with a state other than the + // target state. + // @param info Information about the request. + void TransitRequestToNextState(RequestInfo::State state, RequestInfo* info); + + // Creates a function stub for the status callback function. + // NOTE: this method must be called within a lock. + // @param original_callback The original callback function. + // @return A patch for the callback function. + INTERNET_STATUS_CALLBACK CreateInternetStatusCallbackStub( + INTERNET_STATUS_CALLBACK original_callback); + + // NOTE: this method must be called within a lock. + // @return Returns true if the service is not functioning, either because + // nobody is interested in the service or because the initialization + // has failed. + bool IsNotRunning() const { + return start_count_ == 0 || initialize_state_ != SUCCEEDED_TO_INITIALIZE; + } + + // Returns true if exactly one (but not both) of the patches has been + // successfully applied. + // @param patch_function_1 A function patch. + // @param patch_function_2 Another function patch. + // @return Returns true if exactly one of them has been successfully applied. + bool HasPatchedOneVersion( + const app::win::IATPatchFunction& patch_function_1, + const app::win::IATPatchFunction& patch_function_2) const { + return (patch_function_1.is_patched() && !patch_function_2.is_patched()) || + (!patch_function_1.is_patched() && patch_function_2.is_patched()); + } + + // Function patches that allow us to intercept WinINet functions. + app::win::IATPatchFunction internet_set_status_callback_a_patch_; + app::win::IATPatchFunction internet_set_status_callback_w_patch_; + app::win::IATPatchFunction internet_connect_a_patch_; + app::win::IATPatchFunction internet_connect_w_patch_; + app::win::IATPatchFunction http_open_request_a_patch_; + app::win::IATPatchFunction http_open_request_w_patch_; + app::win::IATPatchFunction http_send_request_a_patch_; + app::win::IATPatchFunction http_send_request_w_patch_; + app::win::IATPatchFunction internet_read_file_patch_; + + // The funnel for sending webRequest events to the broker. + WebRequestEventsFunnel webrequest_events_funnel_; + + // Used to protect the access to all the following data members. + CComAutoCriticalSection critical_section_; + + // Used to intercept InternetStatusCallback function, which is defined by a + // WinINet client to observe status changes. + FunctionStub* internet_status_callback_stub_; + + // Maps Internet connection handles to ServerInfo instances. + typedef std::map<HINTERNET, ServerInfo> ServerMap; + ServerMap server_map_; + + // Maps HTTP request handles to RequestInfo instances. + typedef std::map<HINTERNET, RequestInfo> RequestMap; + RequestMap request_map_; + + // The number of RequestToStart calls minus the number of RequestToStop calls. + // If the number drops to 0, then the service will be stopped. + int start_count_; + + // Indicates the progress of initialization. + enum InitializeState { + // Initialization hasn't been started. + NOT_INITIALIZED, + // Initialization is happening. + INITIALIZING, + // Initialization has failed. + FAILED_TO_INITIALIZE, + // Initialization has succeeded. + SUCCEEDED_TO_INITIALIZE + }; + InitializeState initialize_state_; + + private: + DISALLOW_COPY_AND_ASSIGN(WebRequestNotifier); +}; + +// A singleton that keeps the WebRequestNotifier used by production code. +class ProductionWebRequestNotifier + : public WebRequestNotifier, + public Singleton<ProductionWebRequestNotifier> { + private: + ProductionWebRequestNotifier() {} + + friend struct DefaultSingletonTraits<ProductionWebRequestNotifier>; + DISALLOW_COPY_AND_ASSIGN(ProductionWebRequestNotifier); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WEBREQUEST_NOTIFIER_H_ diff --git a/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc b/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc new file mode 100644 index 0000000..81ee4d0 --- /dev/null +++ b/ceee/ie/plugin/bho/webrequest_notifier_unittest.cc @@ -0,0 +1,340 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit test for WebRequestNotifier class. +#include "ceee/ie/plugin/bho/webrequest_notifier.h" + +#include "ceee/ie/testing/mock_broker_and_friends.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::InSequence; +using testing::MockFunction; +using testing::StrictMock; + +class TestWebRequestNotifier : public WebRequestNotifier { + public: + TestWebRequestNotifier() + : mock_method_(L"GET"), + mock_status_code_(200), + mock_transfer_encoding_present_(false), + mock_content_length_(0), + mock_content_length_present_(false) { + } + virtual ~TestWebRequestNotifier() {} + + virtual WebRequestEventsFunnel& webrequest_events_funnel() { + return mock_webrequest_events_funnel_; + } + + virtual bool QueryHttpInfoString(HINTERNET /*request*/, + DWORD info_flag, + std::wstring* value) { + if (value == NULL) + return false; + if (info_flag == HTTP_QUERY_REQUEST_METHOD) { + *value = mock_method_; + return true; + } else if (mock_transfer_encoding_present_ && + info_flag == HTTP_QUERY_TRANSFER_ENCODING) { + *value = mock_transfer_encoding_; + return true; + } else { + return false; + } + } + + virtual bool QueryHttpInfoNumber(HINTERNET /*request*/, + DWORD info_flag, + DWORD* value) { + if (value == NULL) + return false; + if (info_flag == HTTP_QUERY_STATUS_CODE) { + *value = mock_status_code_; + return true; + } else if (mock_content_length_present_ && + info_flag == HTTP_QUERY_CONTENT_LENGTH) { + *value = mock_content_length_; + return true; + } else { + return false; + } + } + + StrictMock<testing::MockWebRequestEventsFunnel> + mock_webrequest_events_funnel_; + + std::wstring mock_method_; + DWORD mock_status_code_; + std::wstring mock_transfer_encoding_; + bool mock_transfer_encoding_present_; + DWORD mock_content_length_; + bool mock_content_length_present_; + + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestConstructUrl); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestDetermineMessageLength); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestTransitRequestToNextState); + friend class GTEST_TEST_CLASS_NAME_(WebRequestNotifierTestFixture, + TestWinINetPatchHandlers); +}; + +class WebRequestNotifierTestFixture : public testing::Test { + protected: + virtual void SetUp() { + webrequest_notifier_.reset(new TestWebRequestNotifier()); + ASSERT_TRUE(webrequest_notifier_->RequestToStart()); + } + + virtual void TearDown() { + webrequest_notifier_->RequestToStop(); + webrequest_notifier_.reset(NULL); + } + + scoped_ptr<TestWebRequestNotifier> webrequest_notifier_; +}; + +TEST_F(WebRequestNotifierTestFixture, TestConstructUrl) { + std::wstring output; + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + true, L"www.google.com", INTERNET_INVALID_PORT_NUMBER, L"/foobar", + &output)); + EXPECT_STREQ(L"https://www.google.com/foobar", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + false, L"mail.google.com", INTERNET_DEFAULT_HTTP_PORT, L"/index.html", + &output)); + EXPECT_STREQ(L"http://mail.google.com/index.html", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + false, L"docs.google.com", INTERNET_DEFAULT_HTTPS_PORT, L"/login", + &output)); + EXPECT_STREQ(L"http://docs.google.com:443/login", output.c_str()); + + EXPECT_TRUE(webrequest_notifier_->ConstructUrl( + true, L"image.google.com", 123, L"/helloworld", &output)); + EXPECT_STREQ(L"https://image.google.com:123/helloworld", output.c_str()); +} + +TEST_F(WebRequestNotifierTestFixture, TestDetermineMessageLength) { + DWORD length = 0; + WebRequestNotifier::RequestInfo::MessageLengthType type = + WebRequestNotifier::RequestInfo::UNKNOWN_MESSAGE_LENGTH_TYPE; + HINTERNET request = reinterpret_cast<HINTERNET>(1024); + + // Requests with "HEAD" method don't have a message body in the response. + webrequest_notifier_->mock_method_ = L"HEAD"; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::NO_MESSAGE_BODY, type); + + // Requests with status code 1XX, 204 or 304 don't have a message body in the + // response. + webrequest_notifier_->mock_method_ = L"GET"; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 100, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::NO_MESSAGE_BODY, type); + + // If a Transfer-Encoding header field is present and has any value other than + // "identity", the response body length is variable. + // The Content-Length field is ignored in this case. + webrequest_notifier_->mock_transfer_encoding_present_ = true; + webrequest_notifier_->mock_transfer_encoding_ = L"chunked"; + webrequest_notifier_->mock_content_length_present_ = true; + webrequest_notifier_->mock_content_length_ = 256; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::VARIABLE_MESSAGE_LENGTH, type); + + // If a Content-Length header field is present, the response body length is + // the same as specified in the Content-Length header. + webrequest_notifier_->mock_transfer_encoding_present_ = false; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(256, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::CONTENT_LENGTH_HEADER, type); + + // Otherwise, consider the response body length is variable. + webrequest_notifier_->mock_content_length_present_ = false; + EXPECT_TRUE(webrequest_notifier_->DetermineMessageLength( + request, 200, &length, &type)); + EXPECT_EQ(0, length); + EXPECT_EQ(WebRequestNotifier::RequestInfo::VARIABLE_MESSAGE_LENGTH, type); +} + +TEST_F(WebRequestNotifierTestFixture, TestTransitRequestToNextState) { + scoped_ptr<WebRequestNotifier::RequestInfo> info( + new WebRequestNotifier::RequestInfo()); + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRedirect(_, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(1)); + + EXPECT_CALL(check, Call(2)); + + EXPECT_CALL(check, Call(3)); + + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnErrorOccurred(_, _, _, _)); + EXPECT_CALL(check, Call(4)); + + EXPECT_CALL(check, Call(5)); + } + + // The normal state transition sequence. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REDIRECT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_COMPLETED, info.get()); + check.Call(1); + + // No event is fired after webRequest.onCompleted for any request. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::ERROR_OCCURRED, info.get()); + check.Call(2); + + // No webRequest.onErrorOccurred is fired since the first event for any + // request has to be webRequest.onBeforeRequest. + info.reset(new WebRequestNotifier::RequestInfo()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::ERROR_OCCURRED, info.get()); + check.Call(3); + + // Unexpected next-state will result in webRequest.onErrorOccurred to be + // fired. + info.reset(new WebRequestNotifier::RequestInfo()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::WILL_NOTIFY_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_BEFORE_REQUEST, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_REQUEST_SENT, info.get()); + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_COMPLETED, info.get()); + check.Call(4); + + // No event is fired after webRequest.onErrorOccurred for any request. + webrequest_notifier_->TransitRequestToNextState( + WebRequestNotifier::RequestInfo::NOTIFIED_HEADERS_RECEIVED, info.get()); + check.Call(5); +} + +TEST_F(WebRequestNotifierTestFixture, TestWinINetPatchHandlers) { + MockFunction<void(int check_point)> check; + { + InSequence sequence; + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRequest(_, _, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnBeforeRedirect(_, _, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnRequestSent(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnHeadersReceived(_, _, _, _)); + EXPECT_CALL(webrequest_notifier_->mock_webrequest_events_funnel_, + OnCompleted(_, _, _, _)); + EXPECT_CALL(check, Call(1)); + } + + HINTERNET internet = reinterpret_cast<HINTERNET>(512); + HINTERNET server = reinterpret_cast<HINTERNET>(1024); + HINTERNET request = reinterpret_cast<HINTERNET>(2048); + + webrequest_notifier_->mock_method_ = L"GET"; + webrequest_notifier_->mock_status_code_ = 200; + webrequest_notifier_->mock_content_length_ = 256; + webrequest_notifier_->mock_content_length_present_ = true; + + webrequest_notifier_->HandleBeforeInternetConnect(internet); + webrequest_notifier_->HandleAfterInternetConnect( + server, L"www.google.com", INTERNET_DEFAULT_HTTP_PORT, + INTERNET_SERVICE_HTTP); + + webrequest_notifier_->HandleAfterHttpOpenRequest(server, request, "GET", + L"/", 0); + + webrequest_notifier_->HandleBeforeHttpSendRequest(request); + + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_SENDING_REQUEST, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_SENT, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REDIRECT, + "http://www.google.com/index.html", 32); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_SENDING_REQUEST, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_SENT, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_REQUEST_COMPLETE, NULL, 0); + + DWORD number_of_bytes_read = 64; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + number_of_bytes_read = 128; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + number_of_bytes_read = 64; + webrequest_notifier_->HandleAfterInternetReadFile(request, TRUE, + &number_of_bytes_read); + + // Since we have read the whole response body, webRequest.onCompleted has been + // sent at this point. + check.Call(1); + + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, request, NULL, INTERNET_STATUS_HANDLE_CLOSING, NULL, 0); + webrequest_notifier_->HandleBeforeInternetStatusCallback( + NULL, server, NULL, INTERNET_STATUS_HANDLE_CLOSING, NULL, 0); +} + +} // namespace diff --git a/ceee/ie/plugin/bho/window_message_source.cc b/ceee/ie/plugin/bho/window_message_source.cc new file mode 100644 index 0000000..ec6b0ea --- /dev/null +++ b/ceee/ie/plugin/bho/window_message_source.cc @@ -0,0 +1,216 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window message source implementation. +#include "ceee/ie/plugin/bho/window_message_source.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/win_util.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" + +WindowMessageSource::MessageSourceMap WindowMessageSource::message_source_map_; +Lock WindowMessageSource::lock_; + +WindowMessageSource::WindowMessageSource() + : create_thread_id_(::GetCurrentThreadId()), + get_message_hook_(NULL), + call_wnd_proc_ret_hook_(NULL) { +} + +WindowMessageSource::~WindowMessageSource() { + DCHECK(get_message_hook_ == NULL); + DCHECK(call_wnd_proc_ret_hook_ == NULL); + DCHECK(GetEntryFromMap(create_thread_id_) == NULL); +} + +bool WindowMessageSource::Initialize() { + if (!AddEntryToMap(create_thread_id_, this)) + return false; + + get_message_hook_ = ::SetWindowsHookEx(WH_GETMESSAGE, GetMessageHookProc, + NULL, create_thread_id_); + if (get_message_hook_ == NULL) { + TearDown(); + return false; + } + + call_wnd_proc_ret_hook_ = ::SetWindowsHookEx(WH_CALLWNDPROCRET, + CallWndProcRetHookProc, NULL, + create_thread_id_); + if (call_wnd_proc_ret_hook_ == NULL) { + TearDown(); + return false; + } + return true; +} + +void WindowMessageSource::TearDown() { + if (get_message_hook_ != NULL) { + ::UnhookWindowsHookEx(get_message_hook_); + get_message_hook_ = NULL; + } + + if (call_wnd_proc_ret_hook_ != NULL) { + ::UnhookWindowsHookEx(call_wnd_proc_ret_hook_); + call_wnd_proc_ret_hook_ = NULL; + } + + RemoveEntryFromMap(create_thread_id_); +} + +void WindowMessageSource::RegisterSink(Sink* sink) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter == sinks_.end()) + sinks_.push_back(sink); +} + +void WindowMessageSource::UnregisterSink(Sink* sink) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (sink == NULL) + return; + + std::vector<Sink*>::iterator iter = std::find(sinks_.begin(), sinks_.end(), + sink); + if (iter != sinks_.end()) + sinks_.erase(iter); +} + +// static +LRESULT CALLBACK WindowMessageSource::GetMessageHookProc(int code, + WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION && wparam == PM_REMOVE) { + MSG* message_info = reinterpret_cast<MSG*>(lparam); + if (message_info != NULL) { + if ((message_info->message >= WM_MOUSEFIRST && + message_info->message <= WM_MOUSELAST) || + (message_info->message >= WM_KEYFIRST && + message_info->message <= WM_KEYLAST)) { + WindowMessageSource* source = GetEntryFromMap(::GetCurrentThreadId()); + if (source != NULL) + source->OnHandleMessage(message_info); + } + } + } + + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +void WindowMessageSource::OnHandleMessage(const MSG* message_info) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + DCHECK(message_info != NULL); + MessageType type = IsWithinTabContentWindow(message_info->hwnd) ? + TAB_CONTENT_WINDOW : BROWSER_UI_SAME_THREAD; + + for (std::vector<Sink*>::iterator iter = sinks_.begin(); iter != sinks_.end(); + ++iter) { + (*iter)->OnHandleMessage(type, message_info); + } +} + +// static +LRESULT CALLBACK WindowMessageSource::CallWndProcRetHookProc(int code, + WPARAM wparam, + LPARAM lparam) { + if (code == HC_ACTION) { + CWPRETSTRUCT* message_info = reinterpret_cast<CWPRETSTRUCT*>(lparam); + if (message_info != NULL && message_info->message == WM_NCDESTROY) { + WindowMessageSource* source = GetEntryFromMap(::GetCurrentThreadId()); + if (source != NULL) + source->OnWindowNcDestroy(message_info->hwnd); + } + } + + return ::CallNextHookEx(NULL, code, wparam, lparam); +} + +void WindowMessageSource::OnWindowNcDestroy(HWND window) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + tab_content_window_map_.erase(window); +} + +bool WindowMessageSource::IsWithinTabContentWindow(HWND window) { + DCHECK(create_thread_id_ == ::GetCurrentThreadId()); + + if (window == NULL) + return false; + + // Look up the cache to see whether we have already examined this window + // handle and got the answer. + TabContentWindowMap::const_iterator iter = + tab_content_window_map_.find(window); + if (iter != tab_content_window_map_.end()) + return iter->second; + + // Examine whether the window or one of its ancestors is the tab content + // window. + std::vector<HWND> self_and_ancestors; + bool is_within_tab_content_window = false; + do { + self_and_ancestors.push_back(window); + + if (window_utils::IsWindowClass(window, + windows::kIeTabContentWindowClass)) { + is_within_tab_content_window = true; + break; + } + + window = ::GetAncestor(window, GA_PARENT); + if (window == NULL || !window_utils::IsWindowThread(window)) + break; + + TabContentWindowMap::const_iterator iter = + tab_content_window_map_.find(window); + if (iter != tab_content_window_map_.end()) { + is_within_tab_content_window = iter->second; + break; + } + } while (true); + + // Add the windows that we have examined into the cache. + for (std::vector<HWND>::const_iterator iter = self_and_ancestors.begin(); + iter != self_and_ancestors.end(); ++iter) { + tab_content_window_map_.insert( + std::make_pair<HWND, bool>(*iter, is_within_tab_content_window)); + } + return is_within_tab_content_window; +} + +// static +bool WindowMessageSource::AddEntryToMap(DWORD thread_id, + WindowMessageSource* source) { + DCHECK(source != NULL); + + AutoLock auto_lock(lock_); + MessageSourceMap::const_iterator iter = message_source_map_.find(thread_id); + if (iter != message_source_map_.end()) + return false; + + message_source_map_.insert( + std::make_pair<DWORD, WindowMessageSource*>(thread_id, source)); + return true; +} + +// static +WindowMessageSource* WindowMessageSource::GetEntryFromMap(DWORD thread_id) { + AutoLock auto_lock(lock_); + MessageSourceMap::const_iterator iter = message_source_map_.find(thread_id); + return iter == message_source_map_.end() ? NULL : iter->second; +} + +// static +void WindowMessageSource::RemoveEntryFromMap(DWORD thread_id) { + AutoLock auto_lock(lock_); + message_source_map_.erase(thread_id); +} diff --git a/ceee/ie/plugin/bho/window_message_source.h b/ceee/ie/plugin/bho/window_message_source.h new file mode 100644 index 0000000..baceafa --- /dev/null +++ b/ceee/ie/plugin/bho/window_message_source.h @@ -0,0 +1,103 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Window message source implementation. +#ifndef CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ +#define CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ + +#include <map> +#include <vector> + +#include "base/basictypes.h" +#include "base/lock.h" + +// A WindowMessageSource instance monitors keyboard and mouse messages on the +// same thread as the one that creates the instance, and fires events to those +// registered sinks. +// +// NOTE: (1) All the non-static methods are supposed to be called on the thread +// that creates the instance. +// (2) Multiple WindowMessageSource instances cannot live in the same +// thread. Only the first one will be successfully initialized. +class WindowMessageSource { + public: + enum MessageType { + // The destination of the message is the content window (or its descendants) + // of a tab. + TAB_CONTENT_WINDOW, + // Otherwise. + BROWSER_UI_SAME_THREAD + }; + + // The interface that event consumers have to implement. + // NOTE: All the callback methods will be called on the thread that creates + // the WindowMessageSource instance. + class Sink { + public: + virtual ~Sink() {} + // Called before a message is handled. + virtual void OnHandleMessage(MessageType type, const MSG* message_info) {} + }; + + WindowMessageSource(); + virtual ~WindowMessageSource(); + + bool Initialize(); + void TearDown(); + + virtual void RegisterSink(Sink* sink); + virtual void UnregisterSink(Sink* sink); + + private: + // Hook procedure for WH_GETMESSAGE. + static LRESULT CALLBACK GetMessageHookProc(int code, + WPARAM wparam, + LPARAM lparam); + void OnHandleMessage(const MSG* message_info); + + // Hook procedure for WH_CALLWNDPROCRET. + static LRESULT CALLBACK CallWndProcRetHookProc(int code, + WPARAM wparam, + LPARAM lparam); + void OnWindowNcDestroy(HWND window); + + // Returns true if the specified window is the tab content window or one of + // its descendants. + bool IsWithinTabContentWindow(HWND window); + + // Adds an entry to the message_source_map_. Returns false if the item was + // already present. + static bool AddEntryToMap(DWORD thread_id, WindowMessageSource* source); + // Retrieves an entry from the message_source_map_. + static WindowMessageSource* GetEntryFromMap(DWORD thread_id); + // Removes an entry from the message_source_map_. + static void RemoveEntryFromMap(DWORD thread_id); + + // The thread that creates this object. + const DWORD create_thread_id_; + // Event consumers. + std::vector<Sink*> sinks_; + + // The handle to the hook procedure of WH_GETMESSAGE. + HHOOK get_message_hook_; + // The handle to the hook procedure of WH_CALLWNDPROCRET. + HHOOK call_wnd_proc_ret_hook_; + + // Caches the information about whether a given window is within the tab + // content window or not. + typedef std::map<HWND, bool> TabContentWindowMap; + TabContentWindowMap tab_content_window_map_; + + // Maintains a map from thread IDs to their corresponding + // WindowMessageSource instances. + typedef std::map<DWORD, WindowMessageSource*> MessageSourceMap; + static MessageSourceMap message_source_map_; + + // Used to protect access to the message_source_map_. + static Lock lock_; + + DISALLOW_COPY_AND_ASSIGN(WindowMessageSource); +}; + +#endif // CEEE_IE_PLUGIN_BHO_WINDOW_MESSAGE_SOURCE_H_ diff --git a/ceee/ie/plugin/scripting/base.js b/ceee/ie/plugin/scripting/base.js new file mode 100644 index 0000000..58401de --- /dev/null +++ b/ceee/ie/plugin/scripting/base.js @@ -0,0 +1,1291 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview Bootstrap for the Google JS Library (Closure) Also includes + * stuff taken from //depot/google3/javascript/lang.js. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/base.js + * + * LOCAL CHANGES: Tagged "CEEE changes" below. + * + * This file has been changed from its original so that the deps.js file is + * not automatically inserted into the page. In the context of CEEE, there + * is no deps.js file. Furthermore, because this file is being injected into + * the page using the Firefox sandbox mechanism, it does not have the security + * rights to write to the document using the write() method. + * + * The reason for injecting base.js into the file is to use the closure-based + * json.js file. If we no longer need json.js, then it would be possible to + * remove this file too. Firefox does not have native json library that can + * be used by unprivileged js code, but it will in version 3.5. + */ + +/** + * @define {boolean} Overridden to true by the compiler when --closure_pass + * or --mark_as_compiled is specified. + */ +var COMPILED = false; + + +/** + * Base namespace for the Closure library. Checks to see goog is + * already defined in the current scope before assigning to prevent + * clobbering if base.js is loaded more than once. + */ +var goog = goog || {}; // Check to see if already defined in current scope + + +/** + * Reference to the global context. In most cases this will be 'window'. + */ +goog.global = this; + + +/** + * @define {boolean} DEBUG is provided as a convenience so that debugging code + * that should not be included in a production js_binary can be easily stripped + * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most + * toString() methods should be declared inside an "if (goog.DEBUG)" conditional + * because they are generally used for debugging purposes and it is difficult + * for the JSCompiler to statically determine whether they are used. + */ +goog.DEBUG = true; + + +/** + * @define {string} LOCALE defines the locale being used for compilation. It is + * used to select locale specific data to be compiled in js binary. BUILD rule + * can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler + * option. + * + * Take into account that the locale code format is important. You should use + * the canonical Unicode format with hyphen as a delimiter. Language must be + * lowercase, Language Script - Capitalized, Region - UPPERCASE. + * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. + * + * See more info about locale codes here: + * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers + * + * For language codes you should use values defined by ISO 693-1. See it here + * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from + * this rule: the Hebrew language. For legacy reasons the old code (iw) should + * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. + */ +// CEEE changes: Changed from 'en' to 'en_US' +goog.LOCALE = 'en_US'; // default to en_US + + +/** + * Indicates whether or not we can call 'eval' directly to eval code in the + * global scope. Set to a Boolean by the first call to goog.globalEval (which + * empirically tests whether eval works for globals). @see goog.globalEval + * @type {boolean?} + * @private + */ +goog.evalWorksForGlobals_ = null; + + +/** + * Creates object stubs for a namespace. When present in a file, goog.provide + * also indicates that the file defines the indicated object. Calls to + * goog.provide are resolved by the compiler if --closure_pass is set. + * @param {string} name name of the object that this file defines. + */ +goog.provide = function(name) { + if (!COMPILED) { + // Ensure that the same namespace isn't provided twice. This is intended + // to teach new developers that 'goog.provide' is effectively a variable + // declaration. And when JSCompiler transforms goog.provide into a real + // variable declaration, the compiled JS should work the same as the raw + // JS--even when the raw JS uses goog.provide incorrectly. + if (goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) { + throw Error('Namespace "' + name + '" already declared.'); + } + + var namespace = name; + while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { + goog.implicitNamespaces_[namespace] = true; + } + } + + goog.exportPath_(name); +}; + + +if (!COMPILED) { + /** + * Namespaces implicitly defined by goog.provide. For example, + * goog.provide('goog.events.Event') implicitly declares + * that 'goog' and 'goog.events' must be namespaces. + * + * @type {Object} + * @private + */ + goog.implicitNamespaces_ = {}; +} + + +/** + * Builds an object structure for the provided namespace path, + * ensuring that names that already exist are not overwritten. For + * example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * Used by goog.provide and goog.exportSymbol. + * @param {string} name name of the object that this file defines. + * @param {Object} opt_object the object to expose at the end of the path. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + * @private + */ +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || goog.global; + var part; + + // Internet Explorer exhibits strange behavior when throwing errors from + // methods externed in this manner. See the testExportSymbolExceptions in + // base_test.html for an example. + if (!(parts[0] in cur) && cur.execScript) { + cur.execScript('var ' + parts[0]); + } + + // Parentheses added to eliminate strict JS warning in Firefox. + while (parts.length && (part = parts.shift())) { + if (!parts.length && goog.isDef(opt_object)) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (cur[part]) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } +}; + + +/** + * Returns an object based on its fully qualified external name. If you are + * using a compilation pass that renames property names beware that using this + * function will not find renamed properties. + * + * @param {string} name The fully qualified name. + * @param {Object} opt_obj The object within which to look; default is + * |goog.global|. + * @return {Object?} The object or, if not found, null. + */ +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split('.'); + var cur = opt_obj || goog.global; + for (var part; part = parts.shift(); ) { + if (cur[part]) { + cur = cur[part]; + } else { + return null; + } + } + return cur; +}; + + +/** + * Globalizes a whole namespace, such as goog or goog.lang. + * + * @param {Object} obj The namespace to globalize. + * @param {Object} opt_global The object to add the properties to. + * @deprecated Properties may be explicitly exported to the global scope, but + * this should no longer be done in bulk. + */ +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for (var x in obj) { + global[x] = obj[x]; + } +}; + + +/** + * Adds a dependency from a file to the files it requires. + * @param {string} relPath The path to the js file. + * @param {Array} provides An array of strings with the names of the objects + * this file provides. + * @param {Array} requires An array of strings with the names of the objects + * this file requires. + */ +goog.addDependency = function(relPath, provides, requires) { + if (!COMPILED) { + var provide, require; + var path = relPath.replace(/\\/g, '/'); + var deps = goog.dependencies_; + for (var i = 0; provide = provides[i]; i++) { + deps.nameToPath[provide] = path; + if (!(path in deps.pathToNames)) { + deps.pathToNames[path] = {}; + } + deps.pathToNames[path][provide] = true; + } + for (var j = 0; require = requires[j]; j++) { + if (!(path in deps.requires)) { + deps.requires[path] = {}; + } + deps.requires[path][require] = true; + } + } +}; + + +/** + * Implements a system for the dynamic resolution of dependencies + * that works in parallel with the BUILD system. Note that all calls + * to goog.require will be stripped by the JSCompiler when the + * --closure_pass option is used. + * @param {string} rule Rule to include, in the form goog.package.part. + */ +goog.require = function(rule) { + + // if the object already exists we do not need do do anything + if (!COMPILED) { + if (goog.getObjectByName(rule)) { + return; + } + var path = goog.getPathFromDeps_(rule); + if (path) { + goog.included_[path] = true; + goog.writeScripts_(); + } else { + // NOTE(nicksantos): We could always throw an error, but this would break + // legacy users that depended on this failing silently. Instead, the + // compiler should warn us when there are invalid goog.require calls. + // For now, we simply give clients a way to turn strict mode on. + if (goog.useStrictRequires) { + throw new Error('goog.require could not find: ' + rule); + } + } + } +}; + + +/** + * Whether goog.require should throw an exception if it fails. + * @type {boolean} + */ +goog.useStrictRequires = false; + + +/** + * Path for included scripts + * @type {string} + */ +goog.basePath = ''; + + +/** + * Null function used for default values of callbacks, etc. + * @type {!Function} + */ +goog.nullFunction = function() {}; + + +/** + * The identity function. Returns its first argument. + * + * @param {*} var_args The arguments of the function. + * @return {*} The first argument. + * @deprecated Use goog.functions.identity instead. + */ +goog.identityFunction = function(var_args) { + return arguments[0]; +}; + + +/** + * When defining a class Foo with an abstract method bar(), you can do: + * + * Foo.prototype.bar = goog.abstractMethod + * + * Now if a subclass of Foo fails to override bar(), an error + * will be thrown when bar() is invoked. + * + * Note: This does not take the name of the function to override as + * an argument because that would make it more difficult to obfuscate + * our JavaScript code. + * + * @throws {Error} when invoked to indicate the method should be + * overridden. + */ +goog.abstractMethod = function() { + throw Error('unimplemented abstract method'); +}; + + +/** + * Adds a {@code getInstance} static method that always return the same instance + * object. + * @param {!Function} ctor The constructor for the class to add the static + * method to. + */ +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor()); + }; +}; + + +if (!COMPILED) { + /** + * Object used to keep track of urls that have already been added. This + * record allows the prevention of circular dependencies. + * @type {Object} + * @private + */ + goog.included_ = {}; + + + /** + * This object is used to keep track of dependencies and other data that is + * used for loading scripts + * @private + * @type {Object} + */ + goog.dependencies_ = { + pathToNames: {}, // 1 to many + nameToPath: {}, // 1 to 1 + requires: {}, // 1 to many + visited: {}, // used when resolving dependencies to prevent us from + // visiting the file twice + written: {} // used to keep track of script files we have written + }; + + + /** + * Tries to detect the base path of the base.js script that bootstraps Closure + * @private + */ + goog.findBasePath_ = function() { + var doc = goog.global.document; + if (typeof doc == 'undefined') { + return; + } + if (goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return; + } else { + // HACKHACK to hide compiler warnings :( + goog.global.CLOSURE_BASE_PATH = null; + } + var scripts = doc.getElementsByTagName('script'); + for (var script, i = 0; script = scripts[i]; i++) { + var src = script.src; + var l = src.length; + if (src.substr(l - 7) == 'base.js') { + goog.basePath = src.substr(0, l - 7); + return; + } + } + }; + + + /** + * Writes a script tag if, and only if, that script hasn't already been added + * to the document. (Must be called at execution time) + * @param {string} src Script source. + * @private + */ + goog.writeScriptTag_ = function(src) { + var doc = goog.global.document; + if (typeof doc != 'undefined' && + !goog.dependencies_.written[src]) { + goog.dependencies_.written[src] = true; + doc.write('<script type="text/javascript" src="' + + src + '"></' + 'script>'); + } + }; + + + /** + * Resolves dependencies based on the dependencies added using addDependency + * and calls writeScriptTag_ in the correct order. + * @private + */ + goog.writeScripts_ = function() { + // the scripts we need to write this time + var scripts = []; + var seenScript = {}; + var deps = goog.dependencies_; + + function visitNode(path) { + if (path in deps.written) { + return; + } + + // we have already visited this one. We can get here if we have cyclic + // dependencies + if (path in deps.visited) { + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + return; + } + + deps.visited[path] = true; + + if (path in deps.requires) { + for (var requireName in deps.requires[path]) { + if (requireName in deps.nameToPath) { + visitNode(deps.nameToPath[requireName]); + } else { + throw Error('Undefined nameToPath for ' + requireName); + } + } + } + + if (!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path); + } + } + + for (var path in goog.included_) { + if (!deps.written[path]) { + visitNode(path); + } + } + + for (var i = 0; i < scripts.length; i++) { + if (scripts[i]) { + goog.writeScriptTag_(goog.basePath + scripts[i]); + } else { + throw Error('Undefined script input'); + } + } + }; + + + /** + * Looks at the dependency rules and tries to determine the script file that + * fulfills a particular rule. + * @param {string} rule In the form goog.namespace.Class or project.script. + * @return {string?} Url corresponding to the rule, or null. + * @private + */ + goog.getPathFromDeps_ = function(rule) { + if (rule in goog.dependencies_.nameToPath) { + return goog.dependencies_.nameToPath[rule]; + } else { + return null; + } + }; + + goog.findBasePath_(); + // start CEEE changes {{ + // This file is injected into the page using the Firefox sandbox mechanism. + // There is no generated deps.js file to inject with it. + //goog.writeScriptTag_(goog.basePath + 'deps.js'); + // }} end CEEE changes +} + + + +//============================================================================== +// Language Enhancements +//============================================================================== + + +/** + * This is a "fixed" version of the typeof operator. It differs from the typeof + * operator in such a way that null returns 'null' and arrays return 'array'. + * @param {*} value The value to get the type of. + * @return {string} The name of the type. + */ +goog.typeOf = function(value) { + var s = typeof value; + if (s == 'object') { + if (value) { + // We cannot use constructor == Array or instanceof Array because + // different frames have different Array objects. In IE6, if the iframe + // where the array was created is destroyed, the array loses its + // prototype. Then dereferencing val.splice here throws an exception, so + // we can't use goog.isFunction. Calling typeof directly returns 'unknown' + // so that will work. In this case, this function will return false and + // most array functions will still work because the array is still + // array-like (supports length and []) even though it has lost its + // prototype. + // Mark Miller noticed that Object.prototype.toString + // allows access to the unforgeable [[Class]] property. + // 15.2.4.2 Object.prototype.toString ( ) + // When the toString method is called, the following steps are taken: + // 1. Get the [[Class]] property of this object. + // 2. Compute a string value by concatenating the three strings + // "[object ", Result(1), and "]". + // 3. Return Result(2). + // and this behavior survives the destruction of the execution context. + if (value instanceof Array || // Works quickly in same execution context. + // If value is from a different execution context then + // !(value instanceof Object), which lets us early out in the common + // case when value is from the same context but not an array. + // The {if (value)} check above means we don't have to worry about + // undefined behavior of Object.prototype.toString on null/undefined. + // + // HACK: In order to use an Object prototype method on the arbitrary + // value, the compiler requires the value be cast to type Object, + // even though the ECMA spec explicitly allows it. + (!(value instanceof Object) && + Object.prototype.toString.call( + /** @type {Object} */(value)) == '[object Array]')) { + return 'array'; + } + // HACK: There is still an array case that fails. + // function ArrayImpostor() {} + // ArrayImpostor.prototype = []; + // var impostor = new ArrayImpostor; + // this can be fixed by getting rid of the fast path + // (value instanceof Array) and solely relying on + // (value && Object.prototype.toString.vall(value) === '[object Array]') + // but that would require many more function calls and is not warranted + // unless closure code is receiving objects from untrusted sources. + + // IE in cross-window calls does not correctly marshal the function type + // (it appears just as an object) so we cannot use just typeof val == + // 'function'. However, if the object has a call property, it is a + // function. + if (typeof value.call != 'undefined') { + return 'function'; + } + } else { + return 'null'; + } + + // In Safari typeof nodeList returns 'function', and on Firefox + // typeof behaves similarly for HTML{Applet,Embed,Object}Elements + // and RegExps. We would like to return object for those and we can + // detect an invalid function by making sure that the function + // object has a call method. + } else if (s == 'function' && typeof value.call == 'undefined') { + return 'object'; + } + return s; +}; + + +/** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). Does not use browser native + * Object.propertyIsEnumerable. + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ +goog.propertyIsEnumerableCustom_ = function(object, propName) { + // KJS in Safari 2 is not ECMAScript compatible and lacks crucial methods + // such as propertyIsEnumerable. We therefore use a workaround. + // Does anyone know a more efficient work around? + if (propName in object) { + for (var key in object) { + if (key == propName && + Object.prototype.hasOwnProperty.call(object, propName)) { + return true; + } + } + } + return false; +}; + + +if (Object.prototype.propertyIsEnumerable) { + /** + * Safe way to test whether a property is enumarable. It allows testing + * for enumerable on objects where 'propertyIsEnumerable' is overridden or + * does not exist (like DOM nodes in IE). + * @param {Object} object The object to test if the property is enumerable. + * @param {string} propName The property name to check for. + * @return {boolean} True if the property is enumarable. + * @private + */ + goog.propertyIsEnumerable_ = function(object, propName) { + // In IE if object is from another window, cannot use propertyIsEnumerable + // from this window's Object. Will raise a 'JScript object expected' error. + if (object instanceof Object) { + return Object.prototype.propertyIsEnumerable.call(object, propName); + } else { + return goog.propertyIsEnumerableCustom_(object, propName); + } + }; +} else { + // CEEE changes: Added the conditional above and this case as a bugfix. + goog.propertyIsEnumerable_ = goog.propertyIsEnumerableCustom_; +} + +/** + * Returns true if the specified value is not |undefined|. + * WARNING: Do not use this to test if an object has a property. Use the in + * operator instead. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined. + */ +goog.isDef = function(val) { + return typeof val != 'undefined'; +}; + + +/** + * Returns true if the specified value is |null| + * @param {*} val Variable to test. + * @return {boolean} Whether variable is null. + */ +goog.isNull = function(val) { + return val === null; +}; + + +/** + * Returns true if the specified value is defined and not null + * @param {*} val Variable to test. + * @return {boolean} Whether variable is defined and not null. + */ +goog.isDefAndNotNull = function(val) { + return goog.isDef(val) && !goog.isNull(val); +}; + + +/** + * Returns true if the specified value is an array + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArray = function(val) { + return goog.typeOf(val) == 'array'; +}; + + +/** + * Returns true if the object looks like an array. To qualify as array like + * the value needs to be either a NodeList or an object with a Number length + * property. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an array. + */ +goog.isArrayLike = function(val) { + var type = goog.typeOf(val); + return type == 'array' || type == 'object' && typeof val.length == 'number'; +}; + + +/** + * Returns true if the object looks like a Date. To qualify as Date-like + * the value needs to be an object and have a getFullYear() function. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a like a Date. + */ +goog.isDateLike = function(val) { + return goog.isObject(val) && typeof val.getFullYear == 'function'; +}; + + +/** + * Returns true if the specified value is a string + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a string. + */ +goog.isString = function(val) { + return typeof val == 'string'; +}; + + +/** + * Returns true if the specified value is a boolean + * @param {*} val Variable to test. + * @return {boolean} Whether variable is boolean. + */ +goog.isBoolean = function(val) { + return typeof val == 'boolean'; +}; + + +/** + * Returns true if the specified value is a number + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a number. + */ +goog.isNumber = function(val) { + return typeof val == 'number'; +}; + + +/** + * Returns true if the specified value is a function + * @param {*} val Variable to test. + * @return {boolean} Whether variable is a function. + */ +goog.isFunction = function(val) { + return goog.typeOf(val) == 'function'; +}; + + +/** + * Returns true if the specified value is an object. This includes arrays + * and functions. + * @param {*} val Variable to test. + * @return {boolean} Whether variable is an object. + */ +goog.isObject = function(val) { + var type = goog.typeOf(val); + return type == 'object' || type == 'array' || type == 'function'; +}; + + +/** + * Adds a hash code field to an object. The hash code is unique for the + * given object. + * @param {Object} obj The object to get the hash code for. + * @return {number} The hash code for the object. + */ +goog.getHashCode = function(obj) { + // In IE, DOM nodes do not extend Object so they do not have this method. + // we need to check hasOwnProperty because the proto might have this set. + + if (obj.hasOwnProperty && obj.hasOwnProperty(goog.HASH_CODE_PROPERTY_)) { + var hashCode = obj[goog.HASH_CODE_PROPERTY_]; + // CEEE changes: workaround for Chrome bug 1252508. + if (hashCode) { + return hashCode; + } + } + if (!obj[goog.HASH_CODE_PROPERTY_]) { + obj[goog.HASH_CODE_PROPERTY_] = ++goog.hashCodeCounter_; + } + return obj[goog.HASH_CODE_PROPERTY_]; +}; + + +/** + * Removes the hash code field from an object. + * @param {Object} obj The object to remove the field from. + */ +goog.removeHashCode = function(obj) { + // DOM nodes in IE are not instance of Object and throws exception + // for delete. Instead we try to use removeAttribute + if ('removeAttribute' in obj) { + obj.removeAttribute(goog.HASH_CODE_PROPERTY_); + } + /** @preserveTry */ + try { + delete obj[goog.HASH_CODE_PROPERTY_]; + } catch (ex) { + } +}; + + +/** + * {String} Name for hash code property + * @private + */ +goog.HASH_CODE_PROPERTY_ = 'closure_hashCode_'; + + +/** + * @type {number} Counter for hash codes. + * @private + */ +goog.hashCodeCounter_ = 0; + + +/** + * Clone an object/array (recursively) + * @param {Object} proto Object to clone. + * @return {Object} Clone of x;. + */ +goog.cloneObject = function(proto) { + var type = goog.typeOf(proto); + if (type == 'object' || type == 'array') { + if (proto.clone) { + return proto.clone.call(proto); + } + var clone = type == 'array' ? [] : {}; + for (var key in proto) { + clone[key] = goog.cloneObject(proto[key]); + } + return clone; + } + + return proto; +}; + + +/** + * Forward declaration for the clone method. This is necessary until the + * compiler can better support duck-typing constructs as used in + * goog.cloneObject. + * + * @type {Function} + */ +Object.prototype.clone; + + +/** + * Partially applies this function to a particular 'this object' and zero or + * more arguments. The result is a new function with some arguments of the first + * function pre-filled and the value of |this| 'pre-specified'.<br><br> + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones.<br><br> + * + * Also see: {@link #partial}.<br><br> + * + * Note that bind and partial are optimized such that repeated calls to it do + * not create more than one function object, so there is no additional cost for + * something like:<br> + * + * <pre>var g = bind(f, obj); + * var h = partial(g, 1, 2, 3); + * var k = partial(h, a, b, c);</pre> + * + * Usage: + * <pre>var barMethBound = bind(myFunction, myObj, 'arg1', 'arg2'); + * barMethBound('arg3', 'arg4');</pre> + * + * @param {Function} fn A function to partially apply. + * @param {Object} selfObj Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to the function. + * + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.bind = function(fn, selfObj, var_args) { + var boundArgs = fn.boundArgs_; + + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + if (boundArgs) { + args.unshift.apply(args, boundArgs); + } + boundArgs = args; + } + + selfObj = fn.boundSelf_ || selfObj; + fn = fn.boundFn_ || fn; + + var newfn; + var context = selfObj || goog.global; + + if (boundArgs) { + newfn = function() { + // Combine the static args and the new args into one big array + var args = Array.prototype.slice.call(arguments); + args.unshift.apply(args, boundArgs); + return fn.apply(context, args); + }; + } else { + newfn = function() { + return fn.apply(context, arguments); + }; + } + + newfn.boundArgs_ = boundArgs; + newfn.boundSelf_ = selfObj; + newfn.boundFn_ = fn; + + return newfn; +}; + + +/** + * Like bind(), except that a 'this object' is not required. Useful when the + * target function is already bound. + * + * Usage: + * var g = partial(f, arg1, arg2); + * g(arg3, arg4); + * + * @param {Function} fn A function to partially apply. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +goog.partial = function(fn, var_args) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(fn, null); + return goog.bind.apply(null, args); +}; + + +/** + * Copies all the members of a source object to a target object. + * @param {Object} target Target. + * @param {Object} source Source. + * @deprecated Use goog.object.extend instead. + */ +goog.mixin = function(target, source) { + for (var x in source) { + target[x] = source[x]; + } + + // For IE the for-in-loop does not contain any properties that are not + // enumerable on the prototype object (for example, isPrototypeOf from + // Object.prototype) but also it will not include 'replace' on objects that + // extend String and change 'replace' (not that it is common for anyone to + // extend anything except Object). +}; + + +/** + * A simple wrapper for new Date().getTime(). + * + * @return {number} An integer value representing the number of milliseconds + * between midnight, January 1, 1970 and the current time. + */ +goog.now = Date.now || (function() { + return new Date().getTime(); +}); + + +/** + * Evals javascript in the global scope. In IE this uses execScript, other + * browsers use goog.global.eval. If goog.global.eval does not evaluate in the + * global scope (for example, in Safari), appends a script tag instead. + * Throws an exception if neither execScript or eval is defined. + * @param {string} script JavaScript string. + */ +goog.globalEval = function(script) { + if (goog.global.execScript) { + goog.global.execScript(script, 'JavaScript'); + } else if (goog.global.eval) { + // Test to see if eval works + if (goog.evalWorksForGlobals_ == null) { + goog.global.eval('var _et_ = 1;'); + if (typeof goog.global['_et_'] != 'undefined') { + delete goog.global['_et_']; + goog.evalWorksForGlobals_ = true; + } else { + goog.evalWorksForGlobals_ = false; + } + } + + if (goog.evalWorksForGlobals_) { + goog.global.eval(script); + } else { + var doc = goog.global.document; + var scriptElt = doc.createElement('script'); + scriptElt.type = 'text/javascript'; + scriptElt.defer = false; + // Note(pupius): can't use .innerHTML since "t('<test>')" will fail and + // .text doesn't work in Safari 2. Therefore we append a text node. + scriptElt.appendChild(doc.createTextNode(script)); + doc.body.appendChild(scriptElt); + doc.body.removeChild(scriptElt); + } + } else { + throw Error('goog.globalEval not available'); + } +}; + + +/** + * Forward declaration of a type name. + * + * A call of the form + * goog.declareType('goog.MyClass'); + * tells JSCompiler "goog.MyClass is not a hard dependency of this file. + * But it may appear in the type annotations here. This is to assure + * you that the class does indeed exist, even if it's not declared in the + * final binary." + * + * In uncompiled code, does nothing. + * @param {string} typeName The name of the type. + */ +goog.declareType = function(typeName) {}; + + +/** + * A macro for defining composite types. + * + * By assigning goog.typedef to a name, this tells JSCompiler that this is not + * the name of a class, but rather it's the name of a composite type. + * + * For example, + * /** @type {Array|NodeList} / goog.ArrayLike = goog.typedef; + * will tell JSCompiler to replace all appearances of goog.ArrayLike in type + * definitions with the union of Array and NodeList. + * + * Does nothing in uncompiled code. + */ +goog.typedef = true; + + +/** + * Handles strings that are intended to be used as CSS class names. + * + * Without JS Compiler the arguments are simple joined with a hyphen and passed + * through unaltered. + * + * With the JS Compiler the arguments are inlined, e.g: + * var x = goog.getCssName('foo'); + * var y = goog.getCssName(this.baseClass, 'active'); + * becomes: + * var x= 'foo'; + * var y = this.baseClass + '-active'; + * + * If a CSS renaming map is passed to the compiler it will replace symbols in + * the classname. If one argument is passed it will be processed, if two are + * passed only the modifier will be processed, as it is assumed the first + * argument was generated as a result of calling goog.getCssName. + * + * Names are split on 'hyphen' and processed in parts such that the following + * are equivalent: + * var base = goog.getCssName('baseclass'); + * goog.getCssName(base, 'modifier'); + * goog.getCSsName('baseclass-modifier'); + * + * If any part does not appear in the renaming map a warning is logged and the + * original, unobfuscated class name is inlined. + * + * @param {string} className The class name. + * @param {string} opt_modifier A modifier to be appended to the class name. + * @return {string} The class name or the concatenation of the class name and + * the modifier. + */ +goog.getCssName = function(className, opt_modifier) { + return className + (opt_modifier ? '-' + opt_modifier : ''); +}; + + +/** + * Abstract implementation of goog.getMsg for use with localized messages. + * @param {string} str Translatable string, places holders in the form {$foo}. + * @param {Object} opt_values Map of place holder name to value. + * @return {string} message with placeholders filled. + */ +goog.getMsg = function(str, opt_values) { + var values = opt_values || {}; + for (var key in values) { + str = str.replace(new RegExp('\\{\\$' + key + '\\}', 'gi'), values[key]); + } + return str; +}; + + +/** + * Exposes an unobfuscated global namespace path for the given object. + * Note that fields of the exported object *will* be obfuscated, + * unless they are exported in turn via this function or + * goog.exportProperty + * + * <p>Also handy for making public items that are defined in anonymous + * closures. + * + * ex. goog.exportSymbol('Foo', Foo); + * + * ex. goog.exportSymbol('public.path.Foo.staticFunction', + * Foo.staticFunction); + * public.path.Foo.staticFunction(); + * + * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod', + * Foo.prototype.myMethod); + * new public.path.Foo().myMethod(); + * + * @param {string} publicPath Unobfuscated name to export. + * @param {Object} object Object the name should point to. + * @param {Object} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + */ +goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) { + goog.exportPath_(publicPath, object, opt_objectToExportTo); +}; + + +/** + * Exports a property unobfuscated into the object's namespace. + * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction); + * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod); + * @param {Object} object Object whose static property is being exported. + * @param {string} publicName Unobfuscated name to export. + * @param {Object} symbol Object the name should point to. + */ +goog.exportProperty = function(object, publicName, symbol) { + object[publicName] = symbol; +}; + + +/** + * Inherit the prototype methods from one constructor into another. + * + * Usage: + * <pre> + * function ParentClass(a, b) { } + * ParentClass.prototype.foo = function(a) { } + * + * function ChildClass(a, b, c) { + * ParentClass.call(this, a, b); + * } + * + * goog.inherits(ChildClass, ParentClass); + * + * var child = new ChildClass('a', 'b', 'see'); + * child.foo(); // works + * </pre> + * + * In addition, a superclass' implementation of a method can be invoked + * as follows: + * + * <pre> + * ChildClass.prototype.foo = function(a) { + * ChildClass.superClass_.foo.call(this, a); + * // other code + * }; + * </pre> + * + * @param {Function} childCtor Child class. + * @param {Function} parentCtor Parent class. + */ +goog.inherits = function(childCtor, parentCtor) { + /** @constructor */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; + + +//============================================================================== +// Extending Function +//============================================================================== + + +/** + * @define {boolean} Whether to extend Function.prototype. + * Use --define='goog.MODIFY_FUNCTION_PROTOTYPES=false' to change. + */ +goog.MODIFY_FUNCTION_PROTOTYPES = true; + +if (goog.MODIFY_FUNCTION_PROTOTYPES) { + + + /** + * An alias to the {@link goog.bind()} global function. + * + * Usage: + * var g = f.bind(obj, arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} selfObj Specifies the object to which |this| should point + * when the function is run. If the value is null or undefined, it will + * default to the global object. + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the Function on which + * bind() was invoked as a method. + * @deprecated Use the static function goog.bind instead. + */ + Function.prototype.bind = function(selfObj, var_args) { + if (arguments.length > 1) { + var args = Array.prototype.slice.call(arguments, 1); + args.unshift(this, selfObj); + return goog.bind.apply(null, args); + } else { + return goog.bind(this, selfObj); + } + }; + + + /** + * An alias to the {@link goog.partial()} static function. + * + * Usage: + * var g = f.partial(arg1, arg2); + * g(arg3, arg4); + * + * @param {Object} var_args Additional arguments that are partially + * applied to fn. + * @return {!Function} A partially-applied form of the function partial() was + * invoked as a method of. + * @deprecated Use the static function goog.partial instead. + */ + Function.prototype.partial = function(var_args) { + var args = Array.prototype.slice.call(arguments); + args.unshift(this, null); + return goog.bind.apply(null, args); + }; + + + /** + * Inherit the prototype methods from one constructor into another. + * @param {Function} parentCtor Parent class. + * @see goog.inherits + * @deprecated Use the static function goog.inherits instead. + */ + Function.prototype.inherits = function(parentCtor) { + goog.inherits(this, parentCtor); + }; + + + /** + * Mixes in an object's properties and methods into the callee's prototype. + * Basically mixin based inheritance, thus providing an alternative method for + * adding properties and methods to a class' prototype. + * + * <pre> + * function X() {} + * X.mixin({ + * one: 1, + * two: 2, + * three: 3, + * doit: function() { return this.one + this.two + this.three; } + * }); + * + * function Y() { } + * Y.mixin(X.prototype); + * Y.prototype.four = 15; + * Y.prototype.doit2 = function() { return this.doit() + this.four; } + * }); + * + * // or + * + * function Y() { } + * Y.inherits(X); + * Y.mixin({ + * one: 10, + * four: 15, + * doit2: function() { return this.doit() + this.four; } + * }); + * </pre> + * + * @param {Object} source from which to copy properties. + * @see goog.mixin + * @deprecated Use the static function goog.object.extend instead. + */ + Function.prototype.mixin = function(source) { + goog.mixin(this.prototype, source); + }; +}
\ No newline at end of file diff --git a/ceee/ie/plugin/scripting/ceee_bootstrap.js b/ceee/ie/plugin/scripting/ceee_bootstrap.js new file mode 100644 index 0000000..28a0a7d --- /dev/null +++ b/ceee/ie/plugin/scripting/ceee_bootstrap.js @@ -0,0 +1,145 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview this file provides the bootstrap interface between the + * Chrome event and extension bindings JavaScript files, and the CEEE + * native IE interface, as well as initialization hooks for the native + * interface. + */ + + +// Console is diverted to nativeContentScriptApi.Log method. +var console = console || {}; + +// Any function declared native in the Chrome extension bindings files +// is diverted to the ceee namespace to allow the IE JS engine +// to grok the code. +var ceee = ceee || {}; + +(function () { +// Keep a reference to the global environment in +// effect during boostrap script parsing. +var global = this; + +var chromeHidden = {}; +var nativeContentScriptApi = null; + +// Supply a JSON implementation by leeching off closure. +global.JSON = goog.json; +global.JSON.stringify = JSON.serialize; + +ceee.AttachEvent = function (eventName) { + nativeContentScriptApi.AttachEvent(eventName); +}; + +ceee.DetachEvent = function (eventName) { + nativeContentScriptApi.DetachEvent(eventName); +}; + +ceee.OpenChannelToExtension = function (sourceId, targetId, name) { + return nativeContentScriptApi.OpenChannelToExtension(sourceId, + targetId, + name); +}; + +ceee.CloseChannel = function (portId) { + return nativeContentScriptApi.CloseChannel(portId); +}; + +ceee.PortAddRef = function (portId) { + return nativeContentScriptApi.PortAddRef(portId); +}; + +ceee.PortRelease = function (portId) { + return nativeContentScriptApi.PortRelease(portId); +}; + +ceee.PostMessage = function (portId, msg) { + return nativeContentScriptApi.PostMessage(portId, msg); +}; + +ceee.GetChromeHidden = function () { + return chromeHidden; +}; + +// This function is invoked from the native CEEE implementation by name +// to pass in the native CEEE interface implementation at the start of +// script engine initialization. This allows us to provide logging +// and any other required or convenient services during the initialization +// of other boostrap scripts. +ceee.startInit_ = function (nativenativeContentScriptApi, extensionId) { + nativeContentScriptApi = nativenativeContentScriptApi; +}; + +// Last uninitialization callback. +ceee.onUnload_ = function () { + // Dispatch the onUnload event. + chromeHidden.dispatchOnUnload(); + + // Release the native API as very last act. + nativeContentScriptApi = null; +}; + +// This function is invoked from the native CEEE implementation by name +// to pass in the extension ID, and to allow any final initialization of +// the script environment before the content scripts themselves are loaded. +ceee.endInit_ = function (nativenativeContentScriptApi, extensionId) { + chrome.initExtension(extensionId); + + // Provide the native implementation with the the the + // event notification dispatchers. + nativeContentScriptApi.onLoad = chromeHidden.dispatchOnLoad; + nativeContentScriptApi.onUnload = ceee.onUnload_; + + // And the port notification dispatchers. + // function(portId, channelName, tab, extensionId) + nativeContentScriptApi.onPortConnect = chromeHidden.Port.dispatchOnConnect; + + // function(portId) + nativeContentScriptApi.onPortDisconnect = + chromeHidden.Port.dispatchOnDisconnect; + // function(msg, portId) + nativeContentScriptApi.onPortMessage = + chromeHidden.Port.dispatchOnMessage; + + // TODO(siggi@chromium.org): If there is a different global + // environment at this point (i.e. we have cloned the scripting + // engine for a new window) this is where we can restore goog, + // JSON and chrome. + + // Delete the ceee namespace from globals. + delete ceee; +} + +console.log = console.log || function (msg) { + if (nativeContentScriptApi) + nativeContentScriptApi.Log("info", msg); +}; + +console.error = console.error || function (msg) { + if (nativeContentScriptApi) + nativeContentScriptApi.Log("error", msg); +} + +// Provide an indexOf member for arrays if it's not already there +// to satisfy the Chrome extension bindings expectations. +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(elt /*, from*/) { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) { + if (from in this && this[from] === elt) + return from; + } + return -1; + } +}; + +})(); diff --git a/ceee/ie/plugin/scripting/content_script_manager.cc b/ceee/ie/plugin/scripting/content_script_manager.cc new file mode 100644 index 0000000..0080159 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.cc @@ -0,0 +1,384 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager implementation. +#include "ceee/ie/plugin/scripting/content_script_manager.h" + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/dom_utils.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "base/logging.h" +#include "base/resource_util.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" + +#include "toolband.h" // NOLINT + +namespace { +// The list of bootstrap scripts we need to parse in a new scripting engine. +// We store our content scripts by name, in RT_HTML resources. This allows +// referring them by res: URLs, which makes debugging easier. +struct BootstrapScript { + const wchar_t* name; + // A named function to be called after the script is executed. + const wchar_t* function_name; + std::wstring url; + std::wstring content; +}; + +BootstrapScript bootstrap_scripts[] = { + { L"base.js", NULL }, + { L"json.js", NULL }, + { L"ceee_bootstrap.js", L"ceee.startInit_" }, + { L"event_bindings.js", NULL }, + { L"renderer_extension_bindings.js", L"ceee.endInit_" } +}; + +bool bootstrap_scripts_loaded = false; + +// Load the bootstrap javascript resources to our cache. +bool EnsureBoostrapScriptsLoaded() { + if (bootstrap_scripts_loaded) + return true; + + ceee_module_util::AutoLock lock; + if (bootstrap_scripts_loaded) + return true; + + HMODULE module = _AtlBaseModule.GetResourceInstance(); + + // And construct the base URL. + std::wstring base_url(L"ceee-content://bootstrap/"); + + // Retrieve the resources one by one and convert them to Unicode. + for (int i = 0; i < arraysize(bootstrap_scripts); ++i) { + const wchar_t* name = bootstrap_scripts[i].name; + HRSRC hres_info = ::FindResource(module, name, MAKEINTRESOURCE(RT_HTML)); + if (hres_info == NULL) + return false; + + DWORD data_size = ::SizeofResource(module, hres_info); + HGLOBAL hres = ::LoadResource(module, hres_info); + if (!hres) + return false; + + void* resource = ::LockResource(hres); + if (!resource) + return false; + bool converted = UTF8ToWide(reinterpret_cast<const char*>(resource), + data_size, + &bootstrap_scripts[i].content); + if (!converted) + return false; + + bootstrap_scripts[i].url = StringPrintf(L"%ls%ls", base_url.c_str(), name); + } + + bootstrap_scripts_loaded = true; + return true; +} + +HRESULT InvokeNamedFunction(IScriptHost* script_host, + const wchar_t* function_name, + VARIANT* args, + size_t num_args) { + // Get the named function. + CComVariant function_var; + HRESULT hr = script_host->RunExpression(function_name, &function_var); + if (FAILED(hr)) + return hr; + + // And invoke it with the the params. + if (V_VT(&function_var) != VT_DISPATCH) + return E_UNEXPECTED; + + // Take over the IDispatch pointer. + CComDispatchDriver function_disp; + function_disp.Attach(V_DISPATCH(&function_var)); + V_VT(&function_var) = VT_EMPTY; + V_DISPATCH(&function_var) = NULL; + + return function_disp.InvokeN(static_cast<DISPID>(DISPID_VALUE), + args, + num_args); +} + +} // namespace + +ContentScriptManager::ContentScriptManager() : require_all_frames_(false) { +} + +ContentScriptManager::~ContentScriptManager() { + // TODO(siggi@chromium.org): This mandates teardown prior to + // deletion, is that necessary? + DCHECK(script_host_ == NULL); +} + +HRESULT ContentScriptManager::GetOrCreateScriptHost( + IHTMLDocument2* document, IScriptHost** host) { + if (script_host_ == NULL) { + HRESULT hr = CreateScriptHost(&script_host_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create script host " << com::LogHr(hr); + return hr; + } + + hr = InitializeScriptHost(document, script_host_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to initialize script host " << com::LogHr(hr); + script_host_.Release(); + return hr; + } + + CComQIPtr<IObjectWithSite> script_host_with_site(script_host_); + // Our implementation of script host must always implement IObjectWithSite. + DCHECK(script_host_with_site != NULL); + hr = script_host_with_site->SetSite(document); + DCHECK(SUCCEEDED(hr)); + } + + DCHECK(script_host_ != NULL); + + return script_host_.CopyTo(host); +} + +HRESULT ContentScriptManager::CreateScriptHost(IScriptHost** script_host) { + return ScriptHost::CreateInitializedIID(IID_IScriptHost, script_host); +} + +HRESULT ContentScriptManager::InitializeScriptHost( + IHTMLDocument2* document, IScriptHost* script_host) { + DCHECK(document != NULL); + DCHECK(script_host != NULL); + + CComPtr<IExtensionPortMessagingProvider> messaging_provider; + HRESULT hr = host_->GetExtensionPortMessagingProvider(&messaging_provider); + hr = ContentScriptNativeApi::CreateInitialized(messaging_provider, + &native_api_); + if (FAILED(hr)) + return hr; + + std::wstring extension_id; + host_->GetExtensionId(&extension_id); + DCHECK(extension_id.size()) << + "Need to revisit async loading of enabled extension list."; + + // Execute the bootstrap scripts. + hr = BootstrapScriptHost(script_host, native_api_, extension_id.c_str()); + + CComPtr<IHTMLWindow2> window; + hr = document->get_parentWindow(&window); + if (FAILED(hr)) + return hr; + + // Register the window object and make its members global. + hr = script_host->RegisterScriptObject(L"window", window, true); + + return hr; +} + +HRESULT ContentScriptManager::BootstrapScriptHost(IScriptHost* script_host, + IDispatch* native_api, + const wchar_t* extension_id) { + bool loaded = EnsureBoostrapScriptsLoaded(); + if (!loaded) { + NOTREACHED() << "Unable to load bootstrap scripts"; + return E_UNEXPECTED; + } + + // Note args go in reverse order. + CComVariant args[] = { + extension_id, + native_api + }; + + // Run the bootstrap scripts. + for (int i = 0; i < arraysize(bootstrap_scripts); ++i) { + const wchar_t* url = bootstrap_scripts[i].url.c_str(); + HRESULT hr = script_host->RunScript(url, + bootstrap_scripts[i].content.c_str()); + if (FAILED(hr)) { + NOTREACHED() << "Bootstrap script \"" << url << "\" failed to load"; + return hr; + } + + // Execute the script's named function if it exists. + const wchar_t* function_name = bootstrap_scripts[i].function_name; + if (function_name) { + hr = InvokeNamedFunction(script_host, function_name, args, + arraysize(args)); + if (FAILED(hr)) { + NOTREACHED() << "Named function \"" << function_name << "\" not called"; + return hr; + } + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::LoadCss(const GURL& match_url, + IHTMLDocument2* document) { + // Get the CSS content for all matching user scripts and inject it. + std::string css_content; + HRESULT hr = host_->GetMatchingUserScriptsCssContent(match_url, + require_all_frames_, + &css_content); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get script content " << com::LogHr(hr); + return hr; + } + + if (!css_content.empty()) + return InsertCss(CA2W(css_content.c_str()), document); + + return S_OK; +} + +HRESULT ContentScriptManager::LoadStartScripts(const GURL& match_url, + IHTMLDocument2* document) { + // Run the document end scripts. + return LoadScriptsImpl(match_url, document, UserScript::DOCUMENT_START); +} + +HRESULT ContentScriptManager::LoadEndScripts(const GURL& match_url, + IHTMLDocument2* document) { + // Run the document end scripts. + return LoadScriptsImpl(match_url, document, UserScript::DOCUMENT_END); +} + +HRESULT ContentScriptManager::LoadScriptsImpl(const GURL& match_url, + IHTMLDocument2* document, + UserScript::RunLocation when) { + // Run the document start scripts. + UserScriptsLibrarian::JsFileList js_file_list; + HRESULT hr = host_->GetMatchingUserScriptsJsContent(match_url, + when, + require_all_frames_, + &js_file_list); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get script content " << com::LogHr(hr); + return hr; + } + + // Early out to avoid initializing scripting if we don't need it. + if (js_file_list.size() == 0) + return S_OK; + + for (size_t i = 0; i < js_file_list.size(); ++i) { + hr = ExecuteScript(CA2W(js_file_list[i].content.c_str()), + js_file_list[i].file_path.c_str(), + document); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to inject JS content into page " << com::LogHr(hr); + return hr; + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::ExecuteScript(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document) { + CComPtr<IScriptHost> script_host; + HRESULT hr = GetOrCreateScriptHost(document, &script_host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to retrieve script host " << com::LogHr(hr); + return hr; + } + + hr = script_host->RunScript(file_path, code); + if (FAILED(hr)) { + if (hr == OLESCRIPT_E_SYNTAX) { + // This function is used to execute scripts from extensions. We log + // syntax and runtime errors but we don't return a failing HR as we are + // executing third party code. A syntax or runtime error already causes + // the script host to prompt the user to debug. + LOG(ERROR) << "A syntax or runtime error occured while executing " << + "script " << com::LogHr(hr); + } else { + LOG(ERROR) << "Failed to execute script " << com::LogHr(hr); + return hr; + } + } + + return S_OK; +} + +HRESULT ContentScriptManager::InsertCss(const wchar_t* code, + IHTMLDocument2* document) { + CComPtr<IHTMLDOMNode> head_node; + HRESULT hr = GetHeadNode(document, &head_node); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to retrieve document head node " << com::LogHr(hr); + return hr; + } + + hr = InjectStyleTag(document, head_node, code); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to inject CSS content into page " + << com::LogHr(hr); + return hr; + } + + return S_OK; +} + +HRESULT ContentScriptManager::GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** dom_head) { + return DomUtils::GetHeadNode(document, dom_head); +} + +HRESULT ContentScriptManager::InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code) { + return DomUtils::InjectStyleTag(document, head_node, code); +} + +HRESULT ContentScriptManager::Initialize(IFrameEventHandlerHost* host, + bool require_all_frames) { + DCHECK(host != NULL); + DCHECK(host_ == NULL); + host_ = host; + + require_all_frames_ = require_all_frames; + + return S_OK; +} + +HRESULT ContentScriptManager::TearDown() { + if (native_api_ != NULL) { + CComPtr<ICeeeContentScriptNativeApi> native_api; + native_api_.QueryInterface(&native_api); + if (native_api != NULL) { + ContentScriptNativeApi* implementation = + static_cast<ContentScriptNativeApi*>(native_api.p); + // Teardown will release references from ContentScriptNativeApi to + // objects blocking release of BHO. Somehow ContentScriptNativeApi is + // alive after IScriptHost::Close(). + implementation->TearDown(); + } + native_api_.Release(); + } + HRESULT hr = S_OK; + if (script_host_ != NULL) { + hr = script_host_->Close(); + LOG_IF(ERROR, FAILED(hr)) << "ScriptHost::Close failed " << com::LogHr(hr); + + CComQIPtr<IObjectWithSite> script_host_with_site(script_host_); + DCHECK(script_host_with_site != NULL); + hr = script_host_with_site->SetSite(NULL); + DCHECK(SUCCEEDED(hr)); + } + + // TODO(siggi@chromium.org): Kill off open extension ports. + + script_host_.Release(); + + return hr; +} diff --git a/ceee/ie/plugin/scripting/content_script_manager.h b/ceee/ie/plugin/scripting/content_script_manager.h new file mode 100644 index 0000000..f5b64cb --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.h @@ -0,0 +1,112 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager declaration. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ + +#include <mshtml.h> +#include <string> +#include "base/basictypes.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + +// Forward declaration. +class IFrameEventHandlerHost; + +// The content script manager implements CSS and content script injection +// for an extension in a frame. +// It also manages the ScriptHost, its bootstrapping and injecting the +// native API instance to the bootstrap JavaScript code. +class ContentScriptManager { + public: + ContentScriptManager(); + virtual ~ContentScriptManager(); + + // Initialize before first use. + // @param host the frame event handler host we delegate requests to. + // @param require_all_frames Whether to require the all_frames property of the + // matched user scripts to be true. + HRESULT Initialize(IFrameEventHandlerHost* host, bool require_all_frames); + + // Inject CSS for @p match_url into @p document + // Needs to be invoked on READYSTATE_LOADED transition for @p document. + virtual HRESULT LoadCss(const GURL& match_url, IHTMLDocument2* document); + + // Inject start scripts for @p match_url into @p document + // Needs to be invoked on READYSTATE_LOADED transition for @p document. + virtual HRESULT LoadStartScripts(const GURL& match_url, + IHTMLDocument2* document); + + // Inject end scripts for @p match_url into @p document. + // Needs to be invoked on READYSTATE_COMPLETE transition for @p document. + virtual HRESULT LoadEndScripts(const GURL& match_url, + IHTMLDocument2* document); + + // Run the given script code in the document. The @p file_path argument is + // more debugging information, providing context for where the code came + // from. If the given script code has a syntax or runtime error, this + // function will still return S_OK since it is used to run third party code. + virtual HRESULT ExecuteScript(const wchar_t* code, + const wchar_t* file_path, + IHTMLDocument2* document); + + // Inject the given CSS code into the document. + virtual HRESULT InsertCss(const wchar_t* code, IHTMLDocument2* document); + + // Release any resources we've acquired or created. + virtual HRESULT TearDown(); + + protected: + // Implementation of script loading Load{Start|End}Scripts. + virtual HRESULT LoadScriptsImpl(const GURL& match_url, + IHTMLDocument2* document, + UserScript::RunLocation when); + + // Retrieves the script host, creating it if does not already exist. + virtual HRESULT GetOrCreateScriptHost(IHTMLDocument2* document, + IScriptHost** host); + + // Create a script host, virtual to make a unittest seam. + virtual HRESULT CreateScriptHost(IScriptHost** host); + + // Load and initialize bootstrap scripts into host. + virtual HRESULT BootstrapScriptHost(IScriptHost* host, + IDispatch* api, + const wchar_t* extension_id); + + // Initializes a newly-created script host. + virtual HRESULT InitializeScriptHost(IHTMLDocument2* document, + IScriptHost* host); + + // Testing seam. + virtual HRESULT GetHeadNode(IHTMLDocument* document, + IHTMLDOMNode** head_node); + virtual HRESULT InjectStyleTag(IHTMLDocument2* document, + IHTMLDOMNode* head_node, + const wchar_t* code); + + // The script engine that hosts the content scripts. + CComPtr<IScriptHost> script_host_; + + // TODO(siggi@chromium.org): Stash the PageApi instance here for teardown. + + // This is where we get scripts and CSS to inject. + CComPtr<IFrameEventHandlerHost> host_; + + // Whether to require all frames to be true when matching users scripts. + bool require_all_frames_; + + private: + // API accessible from javascript. This reference is required to release + // some resources from ContentScriptManager::Teardown. + // Must be instance of ContentScriptNativeApi. + CComPtr<IDispatch> native_api_; + DISALLOW_COPY_AND_ASSIGN(ContentScriptManager); +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_MANAGER_H_ diff --git a/ceee/ie/plugin/scripting/content_script_manager.rc b/ceee/ie/plugin/scripting/content_script_manager.rc new file mode 100644 index 0000000..7551e5ef --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager.rc @@ -0,0 +1,19 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script manager resources. +#include <winres.h> + +#ifdef APSTUDIO_INVOKED +#error Please edit as text. +#endif + +// These are included as HTML resources to allow referring them +// by res: URLs, which helps debugging. +base.js HTML "base.js" +json.js HTML "json.js" +ceee_bootstrap.js HTML "ceee_bootstrap.js" +event_bindings.js HTML "event_bindings.js" +renderer_extension_bindings.js HTML "renderer_extension_bindings.js" diff --git a/ceee/ie/plugin/scripting/content_script_manager_unittest.cc b/ceee/ie/plugin/scripting/content_script_manager_unittest.cc new file mode 100644 index 0000000..0384875 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_manager_unittest.cc @@ -0,0 +1,489 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Content script manager implementation unit tests. +#include "ceee/ie/plugin/scripting/content_script_manager.h" + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/ie/testing/mock_frame_event_handler_host.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mshtml_mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::CopyInterfaceToArgument; +using testing::CopyVariantToArgument; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; + +typedef UserScriptsLibrarian::JsFileList JsFileList; +typedef UserScriptsLibrarian::JsFile JsFile; + +using testing::InstanceCountMixin; +using testing::IActiveScriptSiteMockImpl; +using testing::IActiveScriptSiteDebugMockImpl; + +// An arbitrary valid extension ID. +const wchar_t kExtensionId[] = L"fepbkochiplomghbdfgekenppangbiap"; + +class TestingContentScriptManager: public ContentScriptManager { + public: + // Expose public for testing. + using ContentScriptManager::LoadScriptsImpl; + using ContentScriptManager::GetOrCreateScriptHost; + using ContentScriptManager::InitializeScriptHost; + + MOCK_METHOD1(CreateScriptHost, HRESULT(IScriptHost** host)); + MOCK_METHOD2(GetHeadNode, + HRESULT(IHTMLDocument* document, IHTMLDOMNode** dom_head)); + MOCK_METHOD3(InjectStyleTag, + HRESULT(IHTMLDocument2* document, IHTMLDOMNode* dom_head, + const wchar_t* code)); + MOCK_METHOD2(InsertCss, + HRESULT(const wchar_t* code, IHTMLDocument2* document)); +}; + + +class IScriptHostMockImpl: public IScriptHost { + public: + MOCK_METHOD3(RegisterScriptObject, + HRESULT(const wchar_t* name, IDispatch* disp_obj, bool global)); + MOCK_METHOD2(RunScript, + HRESULT(const wchar_t* file_path, const wchar_t* code)); + MOCK_METHOD3(RunScript, HRESULT(const wchar_t* file_path, + size_t char_offset, + const wchar_t* code)); + MOCK_METHOD2(RunExpression, + HRESULT(const wchar_t* code, VARIANT* result)); + MOCK_METHOD0(Close, HRESULT()); +}; + +class MockDomNode + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDomNode>, + public StrictMock<IHTMLDOMNodeMockImpl> { + BEGIN_COM_MAP(MockDomNode) + COM_INTERFACE_ENTRY(IHTMLDOMNode) + END_COM_MAP() + + HRESULT Initialize() { + return S_OK; + } +}; + +class MockScriptHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockScriptHost>, + public InstanceCountMixin<MockScriptHost>, + public StrictMock<IActiveScriptSiteMockImpl>, + public StrictMock<IActiveScriptSiteDebugMockImpl>, + public IObjectWithSiteImpl<MockScriptHost>, + public StrictMock<IScriptHostMockImpl> { + public: + BEGIN_COM_MAP(MockScriptHost) + COM_INTERFACE_ENTRY(IActiveScriptSite) + COM_INTERFACE_ENTRY(IActiveScriptSiteDebug) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IScriptHost, IScriptHost) + END_COM_MAP() + + HRESULT Initialize(MockScriptHost** script_host) { + *script_host = this; + return S_OK; + } +}; + +class MockWindow + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockWindow>, + public InstanceCountMixin<MockWindow>, + public StrictMock<IHTMLWindow2MockImpl> { + public: + BEGIN_COM_MAP(MockWindow) + COM_INTERFACE_ENTRY(IHTMLWindow2) + END_COM_MAP() + + HRESULT Initialize(MockWindow** self) { + *self = this; + return S_OK; + } +}; + +class MockDispatch + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDispatch>, + public InstanceCountMixin<MockDispatch>, + public StrictMock<testing::IDispatchExMockImpl> { + public: + BEGIN_COM_MAP(MockDispatch) + COM_INTERFACE_ENTRY(IDispatch) + END_COM_MAP() + + HRESULT Initialize(MockDispatch** self) { + *self = this; + return S_OK; + } +}; + +class MockDocument + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockDocument>, + public InstanceCountMixin<MockDocument>, + public StrictMock<IHTMLDocument2MockImpl> { + public: + BEGIN_COM_MAP(MockDocument) + COM_INTERFACE_ENTRY(IHTMLDocument2) + END_COM_MAP() + + HRESULT Initialize(MockDocument** self) { + *self = this; + return S_OK; + } +}; + +class MockFrameEventHandlerAsApiHost + : public testing::MockFrameEventHandlerHostBase< + MockFrameEventHandlerAsApiHost> { + public: + HRESULT Initialize(MockFrameEventHandlerAsApiHost** self) { + *self = this; + return S_OK; + } + + // Always supply ourselves as native API host. + HRESULT GetExtensionPortMessagingProvider( + IExtensionPortMessagingProvider** messaging_provider) { + GetUnknown()->AddRef(); + *messaging_provider = this; + return S_OK; + } +}; + +class ContentScriptManagerTest: public testing::Test { + public: + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + MockFrameEventHandlerAsApiHost::CreateInitializedIID( + &frame_host_, IID_IFrameEventHandlerHost, &frame_host_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockScriptHost::CreateInitializedIID( + &script_host_, IID_IScriptHost, &script_host_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockWindow::CreateInitialized(&window_, &window_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockDocument::CreateInitialized(&document_, &document_keeper_)); + + ASSERT_HRESULT_SUCCEEDED( + MockDispatch::CreateInitialized(&function_, &function_keeper_)); + + // Set the document up to return the content window if queried. + EXPECT_CALL(*document_, get_parentWindow(_)) + .WillRepeatedly( + DoAll( + CopyInterfaceToArgument<0>(window_keeper_), + Return(S_OK))); + } + + void TearDown() { + document_ = NULL; + document_keeper_.Release(); + + window_ = NULL; + window_keeper_.Release(); + + script_host_ = NULL; + script_host_keeper_.Release(); + + frame_host_ = NULL; + frame_host_keeper_.Release(); + + function_ = NULL; + function_keeper_.Release(); + + // Test for leakage. + EXPECT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + // Set up to expect scripting initialization. + void ExpectScriptInitialization() { + EXPECT_CALL(*script_host_, + RegisterScriptObject(StrEq(L"window"), _, true)) + .WillOnce(Return(S_OK)); + + EXPECT_CALL(*script_host_, + RunScript(testing::StartsWith(L"ceee-content://"), _)) + .Times(5) + .WillRepeatedly(Return(S_OK)); + + // Return the mock function for start/end init. + EXPECT_CALL(*script_host_, RunExpression(StrEq(L"ceee.startInit_"), _)) + .WillOnce( + DoAll( + CopyVariantToArgument<1>(CComVariant(function_keeper_)), + Return(S_OK))); + EXPECT_CALL(*script_host_, RunExpression(StrEq(L"ceee.endInit_"), _)) + .WillOnce( + DoAll( + CopyVariantToArgument<1>(CComVariant(function_keeper_)), + Return(S_OK))); + + EXPECT_CALL(*frame_host_, GetExtensionId(_)).WillOnce(DoAll( + SetArgumentPointee<0>(std::wstring(kExtensionId)), + Return(S_OK))); + + // And expect two invocations. + // TODO(siggi@chromium.org): be more specific? + EXPECT_CALL(*function_, Invoke(_, _, _, _, _, _, _, _)) + .Times(2) + .WillRepeatedly(Return(S_OK)); + } + + void ExpectCreateScriptHost(TestingContentScriptManager* manager) { + EXPECT_CALL(*manager, CreateScriptHost(_)) + .WillOnce( + DoAll( + CopyInterfaceToArgument<0>(script_host_keeper_), + Return(S_OK))); + } + + // Set up to expect NO scripting initialization. + void ExpectNoScriptInitialization() { + EXPECT_CALL(*script_host_, RegisterScriptObject(_, _, _)) + .Times(0); + } + + // Set up to expect a query CSS content. + void ExpectCSSQuery(const GURL& url) { + // Expect CSS query. + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(url, false, _)). + WillOnce(Return(S_OK)); + } + + void SetScriptQueryResults(const GURL& url, + UserScript::RunLocation location, + const JsFileList& js_file_list) { + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsJsContent(url, location, false, _)) + .WillOnce( + DoAll( + SetArgumentPointee<3>(js_file_list), + Return(S_OK))); + } + + protected: + TestingContentScriptManager manager; + + MockFrameEventHandlerAsApiHost* frame_host_; + CComPtr<IFrameEventHandlerHost> frame_host_keeper_; + + MockScriptHost* script_host_; + CComPtr<IScriptHost> script_host_keeper_; + + MockWindow* window_; + CComPtr<IHTMLWindow2> window_keeper_; + + MockDocument* document_; + CComPtr<IHTMLDocument2> document_keeper_; + + // Standin for JS functions. + MockDispatch *function_; + CComPtr<IDispatch> function_keeper_; +}; + +TEST_F(ContentScriptManagerTest, InitializationAndTearDownSucceed) { + ContentScriptManager manager; + + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + manager.TearDown(); +} + +TEST_F(ContentScriptManagerTest, InitializeScripting) { + TestingContentScriptManager manager; + + EXPECT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + ExpectScriptInitialization(); + ASSERT_HRESULT_SUCCEEDED( + manager.InitializeScriptHost(document_, script_host_)); +} + +const GURL kTestUrl( + L"http://www.google.com/search?q=Google+Buys+Iceland"); + +// Verify that we don't initialize scripting when there's nothing to inject. +TEST_F(ContentScriptManagerTest, NoScriptInitializationOnEmptyScripts) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + // No script host creation. + EXPECT_CALL(manager, CreateScriptHost(_)).Times(0); + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_START, JsFileList()); + ASSERT_HRESULT_SUCCEEDED( + manager.LoadStartScripts(kTestUrl, document_)); + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_END, JsFileList()); + ASSERT_HRESULT_SUCCEEDED( + manager.LoadEndScripts(kTestUrl, document_)); + + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +const wchar_t kJsFilePath1[] = L"foo.js"; +const char kJsFileContent1[] = "window.alert('XSS!!');"; +const wchar_t kJsFilePath2[] = L"bar.js"; +const char kJsFileContent2[] = + "window.alert = function () { console.log('gotcha'); }"; + +// Verify that we initialize scripting and inject when there's a URL match. +TEST_F(ContentScriptManagerTest, ScriptInitializationOnUrlMatch) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + JsFileList list; + list.push_back(JsFile()); + JsFile& file1 = list.back(); + file1.file_path = kJsFilePath1; + file1.content = kJsFileContent1; + list.push_back(JsFile()); + JsFile& file2 = list.back(); + file2.file_path = kJsFilePath2; + file2.content = kJsFileContent2; + + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_START, list); + + ExpectScriptInitialization(); + ExpectCreateScriptHost(&manager); + const std::wstring content1(UTF8ToWide(kJsFileContent1)); + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath1), StrEq(content1.c_str()))) + .WillOnce(Return(S_OK)); + + const std::wstring content2(UTF8ToWide(kJsFileContent2)); + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath2), StrEq(content2.c_str()))) + .WillOnce(Return(S_OK)); + + // This should initialize scripting and evaluate our script. + ASSERT_HRESULT_SUCCEEDED( + manager.LoadStartScripts(kTestUrl, document_)); + + // Drop the second script. + list.pop_back(); + SetScriptQueryResults(kTestUrl, UserScript::DOCUMENT_END, list); + + EXPECT_CALL(*script_host_, + RunScript(StrEq(kJsFilePath1), StrEq(content1.c_str()))) + .WillOnce(Return(S_OK)); + + // This should only evaluate the script. + ASSERT_HRESULT_SUCCEEDED( + manager.LoadEndScripts(kTestUrl, document_)); + + // The script host needs to be shut down on teardown. + EXPECT_CALL(*script_host_, Close()).Times(1); + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +const wchar_t kCssContent[] = L".foo {};"; +// Verify that we inject CSS into the document. +TEST_F(ContentScriptManagerTest, CssInjectionOnUrlMatch) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(kTestUrl, false, _)) + .WillOnce(Return(S_OK)); + + // This should not cause any CSS injection. + ASSERT_HRESULT_SUCCEEDED(manager.LoadCss(kTestUrl, document_)); + + EXPECT_CALL(*frame_host_, + GetMatchingUserScriptsCssContent(kTestUrl, false, _)) + .WillOnce( + DoAll( + SetArgumentPointee<2>(std::string(CW2A(kCssContent))), + Return(S_OK))); + + EXPECT_CALL(manager, + InsertCss(StrEq(kCssContent), document_)) + .WillOnce(Return(S_OK)); + + // We now expect to see the document injected. + ASSERT_HRESULT_SUCCEEDED(manager.LoadCss(kTestUrl, document_)); +} + +const wchar_t kTestCode[] = L"function foo {};"; +const wchar_t kTestFilePath[] = L"TestFilePath"; +TEST_F(ContentScriptManagerTest, ExecuteScript) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_SUCCEEDED(MockDomNode::CreateInitialized(&head_node)); + + ExpectCreateScriptHost(&manager); + ExpectScriptInitialization(); + + EXPECT_CALL(*script_host_, + RunScript(kTestFilePath, kTestCode)) + .WillOnce(Return(S_OK)); + + // We now expect to see the document injected. + ASSERT_HRESULT_SUCCEEDED(manager.ExecuteScript(kTestCode, + kTestFilePath, + document_)); + + // The script host needs to be shut down on teardown. + EXPECT_CALL(*script_host_, Close()).Times(1); + ASSERT_HRESULT_SUCCEEDED(manager.TearDown()); +} + +TEST_F(ContentScriptManagerTest, InsertCss) { + TestingContentScriptManager manager; + ASSERT_HRESULT_SUCCEEDED( + manager.Initialize(frame_host_keeper_, false)); + + CComPtr<IHTMLDOMNode> head_node; + ASSERT_HRESULT_SUCCEEDED(MockDomNode::CreateInitialized(&head_node)); + + EXPECT_CALL(manager, GetHeadNode(document_, _)) + .WillOnce( + DoAll( + CopyInterfaceToArgument<1>(head_node), + Return(S_OK))); + + EXPECT_CALL(manager, + InjectStyleTag(document_, head_node.p, StrEq(kCssContent))) + .WillOnce(Return(S_OK)); + + ASSERT_HRESULT_SUCCEEDED( + manager.ContentScriptManager::InsertCss(kCssContent, document_)); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/content_script_native_api.cc b/ceee/ie/plugin/scripting/content_script_native_api.cc new file mode 100644 index 0000000..a74d077 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api.cc @@ -0,0 +1,276 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API implementation. +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ceee/common/com_utils.h" + +namespace { + +// DISPID_VALUE is a macro defined to 0, which confuses overloaded Invoke-en. +DISPID kDispidValue = DISPID_VALUE; + +} // namespace + +ContentScriptNativeApi::ContentScriptNativeApi() + : next_local_port_id_(kFirstPortId) { +} + +ContentScriptNativeApi::LocalPort::LocalPort(LocalPortId id) + : state(PORT_UNINITIALIZED), local_id(id), remote_id(kInvalidPortId) { +} + +HRESULT ContentScriptNativeApi::Initialize( + IExtensionPortMessagingProvider *messaging_provider) { + DCHECK(messaging_provider != NULL); + messaging_provider_ = messaging_provider; + return S_OK; +} + +void ContentScriptNativeApi::FinalRelease() { + DCHECK(on_load_ == NULL); + DCHECK(on_unload_ == NULL); + DCHECK(on_port_connect_ == NULL); + DCHECK(on_port_disconnect_ == NULL); + DCHECK(on_port_message_ == NULL); +} + +HRESULT ContentScriptNativeApi::TearDown() { + on_load_.Release(); + on_unload_.Release(); + on_port_connect_.Release(); + on_port_disconnect_.Release(); + on_port_message_.Release(); + messaging_provider_.Release(); + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::Log(BSTR level, BSTR message) { + const wchar_t* str_level = com::ToString(level); + size_t len = ::SysStringLen(level); + if (LowerCaseEqualsASCII(str_level, str_level + len, "info")) { + LOG(INFO) << com::ToString(message); + } else if (LowerCaseEqualsASCII(str_level, str_level + len, "error")) { + LOG(ERROR) << com::ToString(message); + } else { + LOG(WARNING) << com::ToString(message); + } + + return S_OK; +} + + +STDMETHODIMP ContentScriptNativeApi::OpenChannelToExtension(BSTR source_id, + BSTR target_id, + BSTR name, + long* port_id) { + // TODO(siggi@chromium.org): handle connecting to other extensions. + // TODO(siggi@chromium.org): check for the correct source_id here. + if (0 != wcscmp(com::ToString(source_id), com::ToString(target_id))) + return E_UNEXPECTED; + + LocalPortId id = GetNextLocalPortId(); + std::pair<LocalPortMap::iterator, bool> inserted = + local_ports_.insert(std::make_pair(id, LocalPort(id))); + DCHECK(inserted.second && inserted.first != local_ports_.end()); + // Get the port we just inserted. + LocalPort& port = inserted.first->second; + DCHECK_EQ(id, port.local_id); + + std::string extension_id; + bool converted = WideToUTF8(com::ToString(source_id), + ::SysStringLen(source_id), + &extension_id); + DCHECK(converted); + std::string channel_name; + converted = WideToUTF8(com::ToString(name), + ::SysStringLen(name), + &channel_name); + DCHECK(converted); + + // Send off the connection request with our local port ID as cookie. + HRESULT hr = messaging_provider_->OpenChannelToExtension(this, + extension_id, + channel_name, + port.local_id); + DCHECK(SUCCEEDED(hr)); + + port.state = PORT_CONNECTING; + + // TODO(siggi@chromium.org): Clean up on failure. + + *port_id = id; + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::CloseChannel(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "CloseChannel(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PortAddRef(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "PortAddRef(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PortRelease(long port_id) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "PortRelease(" << port_id << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::PostMessage(long port_id, BSTR msg) { + LocalPortMap::iterator it(local_ports_.find(port_id)); + // TODO(siggi@chromium.org): should I expect to get messages to + // defunct port ids? + DCHECK(it != local_ports_.end()); + if (it == local_ports_.end()) + return E_UNEXPECTED; + LocalPort& port = it->second; + + std::string msg_str(WideToUTF8(com::ToString(msg))); + if (port.state == PORT_CONNECTED) { + messaging_provider_->PostMessage(port.remote_id, msg_str); + } else if (port.state == PORT_CONNECTING) { + port.pending_messages.push_back(msg_str); + } else { + LOG(ERROR) << "Unexpected PostMessage for port in state " << port.state; + } + + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::AttachEvent(BSTR event_name) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "AttachEvent(" << com::ToString(event_name) << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::DetachEvent(BSTR event_name) { + // TODO(siggi@chromium.org): Writeme. + LOG(INFO) << "DetachEvent(" << com::ToString(event_name) << ")"; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onLoad(IDispatch* callback) { + on_load_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onUnload(IDispatch* callback) { + on_unload_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortConnect(IDispatch* callback) { + on_port_connect_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortDisconnect(IDispatch* callback) { + on_port_disconnect_ = callback; + return S_OK; +} + +STDMETHODIMP ContentScriptNativeApi::put_onPortMessage(IDispatch* callback) { + on_port_message_ = callback; + return S_OK; +} + +void ContentScriptNativeApi::OnChannelOpened(int cookie, + int port_id) { + // The cookie is our local port ID. + LocalPortId local_id = cookie; + LocalPortMap::iterator it(local_ports_.find(local_id)); + DCHECK(it != local_ports_.end()); + + LocalPort& port = it->second; + DCHECK_EQ(local_id, port.local_id); + port.remote_id = port_id; + port.state = PORT_CONNECTED; + + // Remember the mapping so that we can find this port by remote_id. + remote_to_local_port_id_[port.remote_id] = port.local_id; + + // Flush pending messages on this port. + if (port.pending_messages.size() > 0) { + MessageList::iterator it(port.pending_messages.begin()); + MessageList::iterator end(port.pending_messages.end()); + + for (; it != end; ++it) { + messaging_provider_->PostMessage(port.remote_id, *it); + } + } +} + +void ContentScriptNativeApi::OnPostMessage(int port_id, + const std::string& message) { + // Translate the remote port id to a local port id. + RemoteToLocalPortIdMap::iterator it(remote_to_local_port_id_.find(port_id)); + DCHECK(it != remote_to_local_port_id_.end()); + + LocalPortId local_id = it->second; + + // And push the message to the script. + std::wstring message_wide(UTF8ToWide(message)); + CallOnPortMessage(message_wide.c_str(), local_id); +} + +HRESULT ContentScriptNativeApi::CallOnLoad(const wchar_t* extension_id) { + if (on_load_ == NULL) + return E_UNEXPECTED; + + return on_load_.Invoke1(kDispidValue, &CComVariant(extension_id)); +} + +HRESULT ContentScriptNativeApi::CallOnUnload() { + if (on_unload_ == NULL) + return E_UNEXPECTED; + + return on_unload_.Invoke0(kDispidValue); +} + +HRESULT ContentScriptNativeApi::CallOnPortConnect( + long port_id, const wchar_t* channel_name, const wchar_t* tab, + const wchar_t* source_extension_id, const wchar_t* target_extension_id) { + if (on_port_connect_ == NULL) + return E_UNEXPECTED; + + // Note args go in reverse order of declaration for Invoke. + CComVariant args[] = { + target_extension_id, + source_extension_id, + tab, + channel_name, + port_id}; + + return on_port_connect_.InvokeN(kDispidValue, args, arraysize(args)); +} + +HRESULT ContentScriptNativeApi::CallOnPortDisconnect(long port_id) { + if (on_port_disconnect_ == NULL) + return E_UNEXPECTED; + + return on_port_disconnect_.Invoke1(kDispidValue, &CComVariant(port_id)); +} + +HRESULT ContentScriptNativeApi::CallOnPortMessage(const wchar_t* msg, + long port_id) { + if (on_port_message_ == NULL) + return E_UNEXPECTED; + + // Note args go in reverse order of declaration for Invoke. + CComVariant args[] = { port_id, msg }; + + return on_port_message_.InvokeN(kDispidValue, args, arraysize(args)); +} diff --git a/ceee/ie/plugin/scripting/content_script_native_api.h b/ceee/ie/plugin/scripting/content_script_native_api.h new file mode 100644 index 0000000..5c97184 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api.h @@ -0,0 +1,183 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API class declaration. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ + +#include <map> +#include <string> +#include <vector> + +#include "ceee/common/initializing_coclass.h" +#include "toolband.h" // NOLINT + +// Fwd. +class IContentScriptNativeApi; + +class IExtensionPortMessagingProvider: public IUnknown { + public: + // Close all ports opening and opened for this instance. + virtual void CloseAll(IContentScriptNativeApi* instance) = 0; + + // Initiates opening a channel to an extension. + // @param instance the PageApi instance requesting the channel. + // @param extension the id of the extension to open the channel to. + // @param cookie a caller-provided cookie associated with this request. + // @note in the fullness of time, the manager should call + // ChannelOpened, supplying the callback and the assigned port_id. + virtual HRESULT OpenChannelToExtension(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie) = 0; + + // Posts a message from the page to the previously assigned channel + // corresponding to port_id. + virtual HRESULT PostMessage(int port_id, const std::string& message) = 0; +}; + +class IContentScriptNativeApi: public IUnknown { + public: + // Called by host to complete a channel open request. + // @param cookie the cookie previously passed to the host on an + // OpenChannelToExtension invocation. + // @param port_id the port ID assigned to the port by the host. + virtual void OnChannelOpened(int cookie, int port_id) = 0; + + // Called by host on an incoming postMessage. + // @param port_id the host port ID of the destination port. + // @param message the message. + virtual void OnPostMessage(int port_id, const std::string& message) = 0; +}; + +// This class implements the native API provided to content scripts through +// the ceee_bootstrap.js script. The functionality provided here has to be +// safe for any page content to invoke. +// For safety's sake, do not expose any IDispatch-derived interfaces on this +// object other than ICeeeContentScriptNativeApi. +class ContentScriptNativeApi + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ContentScriptNativeApi>, + public IContentScriptNativeApi, + public IDispatchImpl<ICeeeContentScriptNativeApi, + &IID_ICeeeContentScriptNativeApi, + &LIBID_ToolbandLib, + 0xFFFF, // Magic ATL incantation to load + 0xFFFF> { // typelib from our resource. + public: + BEGIN_COM_MAP(ContentScriptNativeApi) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(ICeeeContentScriptNativeApi) + END_COM_MAP() + + ContentScriptNativeApi(); + + HRESULT Initialize(IExtensionPortMessagingProvider* messaging_provider); + void FinalRelease(); + + // @name ICeeeContentScriptNativeApi implementation + // This is the interface presented to the JavaScript + // code in ceee_bootstrap.js. + // @{ + STDMETHOD(Log)(BSTR level, BSTR message); + STDMETHOD(OpenChannelToExtension)(BSTR source_id, + BSTR target_id, + BSTR name, + long* port_id); + STDMETHOD(CloseChannel)(long port_id); + STDMETHOD(PortAddRef)(long port_id); + STDMETHOD(PortRelease)(long port_id); + STDMETHOD(PostMessage)(long port_id, BSTR msg); + STDMETHOD(AttachEvent)(BSTR event_name); + STDMETHOD(DetachEvent)(BSTR event_name); + STDMETHOD(put_onLoad)(IDispatch* callback); + STDMETHOD(put_onUnload)(IDispatch* callback); + STDMETHOD(put_onPortConnect)(IDispatch* callback); + STDMETHOD(put_onPortDisconnect)(IDispatch* callback); + STDMETHOD(put_onPortMessage)(IDispatch* callback); + // @} + + + // @name IContentScriptNativeApi implementation + // @{ + virtual void OnChannelOpened(int cookie, int port_id); + virtual void OnPostMessage(int port_id, const std::string& message); + // @} + + // Typed wrapper functions to call on the respective callbacks. + // @{ + HRESULT CallOnLoad(const wchar_t* extension_id); + HRESULT CallOnUnload(); + HRESULT CallOnPortConnect(long port_id, + const wchar_t* channel_name, + const wchar_t* tab, + const wchar_t* source_extension_id, + const wchar_t* target_extension_id); + HRESULT CallOnPortDisconnect(long port_id); + HRESULT CallOnPortMessage(const wchar_t* msg, long port_id); + // @} + + // Release all resources. + HRESULT TearDown(); + + private: + // Storage for our callback properties. + CComDispatchDriver on_load_; + CComDispatchDriver on_unload_; + CComDispatchDriver on_port_connect_; + CComDispatchDriver on_port_disconnect_; + CComDispatchDriver on_port_message_; + + // The messaging provider takes care of communication transport for us. + CComPtr<IExtensionPortMessagingProvider> messaging_provider_; + + typedef int PortId; + typedef PortId LocalPortId; + typedef PortId RemotePortId; + static const int kInvalidPortId = -1; + static const int kFirstPortId = 2; + + enum LocalPortState { + PORT_UNINITIALIZED, + PORT_CONNECTING, + PORT_CONNECTED, + PORT_CLOSING, + PORT_CLOSED, + }; + + typedef std::vector<std::string> MessageList; + + // State we maintain per port. + class LocalPort { + public: + explicit LocalPort(LocalPortId id); + + LocalPortState state; + LocalPortId local_id; + RemotePortId remote_id; + + // Messages waiting to be posted. + MessageList pending_messages; + }; + + LocalPortId GetNextLocalPortId() { + LocalPortId id = next_local_port_id_; + next_local_port_id_ += 2; + return id; + } + + typedef std::map<LocalPortId, LocalPort> LocalPortMap; + typedef std::map<RemotePortId, LocalPortId> RemoteToLocalPortIdMap; + + // Local state for the ports we handle. + LocalPortMap local_ports_; + // Maps + RemoteToLocalPortIdMap remote_to_local_port_id_; + + LocalPortId next_local_port_id_; +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_CONTENT_SCRIPT_NATIVE_API_H_ diff --git a/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc b/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc new file mode 100644 index 0000000..48b6213 --- /dev/null +++ b/ceee/ie/plugin/scripting/content_script_native_api_unittest.cc @@ -0,0 +1,209 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Content script native API implementation. +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace { + +using testing::IDispatchExMockImpl; +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; +using testing::_; +using testing::AllOf; +using testing::DispParamArgEq; +using testing::Field; +using testing::Eq; +using testing::Return; +using testing::StrictMock; +using testing::MockDispatchEx; + +class IExtensionPortMessagingProviderMockImpl + : public IExtensionPortMessagingProvider { + public: + MOCK_METHOD1(CloseAll, void(IContentScriptNativeApi* instance)); + MOCK_METHOD4(OpenChannelToExtension, HRESULT( + IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie)); + MOCK_METHOD2(PostMessage, HRESULT(int port_id, const std::string& message)); +}; + +class MockIExtensionPortMessagingProvider + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockIExtensionPortMessagingProvider>, + public InstanceCountMixin<MockIExtensionPortMessagingProvider>, + public StrictMock<IExtensionPortMessagingProviderMockImpl> { + BEGIN_COM_MAP(MockIExtensionPortMessagingProvider) + END_COM_MAP() + + HRESULT Initialize() { return S_OK; } +}; + +class TestingContentScriptNativeApi + : public ContentScriptNativeApi, + public InitializingCoClass<TestingContentScriptNativeApi> { + public: + // Disambiguate. + using InitializingCoClass<TestingContentScriptNativeApi>::CreateInitialized; + using ContentScriptNativeApi::Initialize; + + HRESULT Initialize(TestingContentScriptNativeApi** self) { + *self = this; + return S_OK; + } +}; + +class ContentScriptNativeApiTest: public testing::Test { + public: + ContentScriptNativeApiTest() : api_(NULL), function_(NULL) { + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + TestingContentScriptNativeApi::CreateInitialized(&api_, &api_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + MockDispatchEx::CreateInitialized(&function_, &function_keeper_)); + } + + void TearDown() { + if (api_ != NULL) + api_->TearDown(); + + api_ = NULL; + api_keeper_.Release(); + + function_ = NULL; + function_keeper_.Release(); + + ASSERT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + protected: + TestingContentScriptNativeApi* api_; + CComPtr<ICeeeContentScriptNativeApi> api_keeper_; + + MockDispatchEx* function_; + CComPtr<IDispatch> function_keeper_; +}; + +TEST_F(ContentScriptNativeApiTest, ImplementsInterfaces) { + CComPtr<IDispatch> disp; + ASSERT_HRESULT_SUCCEEDED( + api_keeper_->QueryInterface(&disp)); +} + +int kPortId = 42; +const wchar_t* kChannelName = L"Q92FM"; +const wchar_t* kTab = NULL; +const wchar_t* kSourceExtensionId = L"fepbkochiplomghbdfgekenppangbiap"; +const wchar_t* kTargetExtensionId = L"kgeddobpkdopccblmihponcjlbdpmbod"; +const wchar_t* kWideMsg = L"\"JSONified string\""; +const char* kMsg = "\"JSONified string\""; + +const wchar_t* kWideMsg2 = L"\"Other JSONified string\""; +const char* kMsg2 = "\"Other JSONified string\""; + +TEST_F(ContentScriptNativeApiTest, CallUnsetCallbacks) { + ASSERT_HRESULT_FAILED(api_->CallOnLoad(kSourceExtensionId)); + ASSERT_HRESULT_FAILED(api_->CallOnUnload()); + ASSERT_HRESULT_FAILED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); + ASSERT_HRESULT_FAILED(api_->CallOnPortDisconnect(kPortId)); + ASSERT_HRESULT_FAILED(api_->CallOnPortMessage(kWideMsg, kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnLoad) { + ASSERT_HRESULT_FAILED(api_->CallOnLoad(kSourceExtensionId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onLoad(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kSourceExtensionId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnLoad(kSourceExtensionId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnUnload) { + ASSERT_HRESULT_FAILED(api_->CallOnUnload()); + ASSERT_HRESULT_SUCCEEDED(api_->put_onUnload(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnUnload()); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortConnect) { + ASSERT_HRESULT_FAILED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortConnect(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, + kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortConnect(kPortId, + kChannelName, + kTab, + kSourceExtensionId, + kTargetExtensionId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortDisconnect) { + ASSERT_HRESULT_FAILED(api_->CallOnPortDisconnect(kPortId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortDisconnect(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kPortId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortDisconnect(kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, CallOnPortMessage) { + ASSERT_HRESULT_FAILED(api_->CallOnPortMessage(kWideMsg, kPortId)); + ASSERT_HRESULT_SUCCEEDED(api_->put_onPortMessage(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kWideMsg, kPortId); + ASSERT_HRESULT_SUCCEEDED(api_->CallOnPortMessage(kWideMsg, kPortId)); +} + +TEST_F(ContentScriptNativeApiTest, OnPostMessage) { + MockIExtensionPortMessagingProvider* mock_provider; + ASSERT_HRESULT_SUCCEEDED(MockIExtensionPortMessagingProvider:: + CreateInstance(&mock_provider)); + CComPtr<IExtensionPortMessagingProvider> mock_provider_holder(mock_provider); + EXPECT_HRESULT_SUCCEEDED(api_->Initialize(mock_provider_holder.p)); + // TODO(siggi@chromium.org): Expect the appropriate argument values. + EXPECT_CALL(*mock_provider, OpenChannelToExtension(api_, _, _, _)) + .WillRepeatedly(Return(S_OK)); + CComBSTR source_id(L"SourceId"); + CComBSTR name(L"name"); + long local_port_id1 = 0; + EXPECT_HRESULT_SUCCEEDED(api_->OpenChannelToExtension( + source_id, source_id, name, &local_port_id1)); + long local_port_id2 = 0; + EXPECT_HRESULT_SUCCEEDED(api_->OpenChannelToExtension( + source_id, source_id, name, &local_port_id2)); + + // TODO(siggi@chromium.org): Test pending messages code. + static const int kRemotePortId1 = 42; + static const int kRemotePortId2 = 84; + api_->OnChannelOpened((int)local_port_id2, kRemotePortId2); + api_->OnChannelOpened((int)local_port_id1, kRemotePortId1); + + EXPECT_HRESULT_SUCCEEDED(api_->put_onPortMessage(function_keeper_)); + function_->ExpectInvoke(DISPID_VALUE, kWideMsg, local_port_id1); + api_->OnPostMessage(kRemotePortId1, kMsg); + + function_->ExpectInvoke(DISPID_VALUE, kWideMsg2, local_port_id2); + api_->OnPostMessage(kRemotePortId2, kMsg2); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/json.js b/ceee/ie/plugin/scripting/json.js new file mode 100644 index 0000000..0ef04e4 --- /dev/null +++ b/ceee/ie/plugin/scripting/json.js @@ -0,0 +1,318 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in +// the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +/** + * @fileoverview JSON utility functions. + */ + +/* + * ORIGINAL VERSION: http://doctype.googlecode.com/svn/trunk/goog/json/json.js + * + * LOCAL CHANGES: None. + */ + +goog.provide('goog.json'); +goog.provide('goog.json.Serializer'); + + + +/** + * Tests if a string is an invalid JSON string. This only ensures that we are + * not using any invalid characters + * @param {string} s The string to test. + * @return {boolean} True if the input is a valid JSON string. + * @private + */ +goog.json.isValid_ = function(s) { + // All empty whitespace is not valid. + if (/^\s*$/.test(s)) { + return false; + } + + // This is taken from http://www.json.org/json2.js which is released to the + // public domain. + // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator + // inside strings. We also treat \u2028 and \u2029 as whitespace which they + // are in the RFC but IE and Safari does not match \s to these so we need to + // include them in the reg exps in all places where whitespace is allowed. + // We allowed \x7f inside strings because some tools don't escape it, + // e.g. http://www.json.org/java/org/json/JSONObject.java + + // Parsing happens in three stages. In the first stage, we run the text + // against regular expressions that look for non-JSON patterns. We are + // especially concerned with '()' and 'new' because they can cause invocation, + // and '=' because it can cause mutation. But just to be safe, we want to + // reject all unexpected forms. + + // We split the first stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace all backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + // Don't make these static since they have the global flag. + var backslashesRe = /\\["\\\/bfnrtu]/g; + var simpleValuesRe = + /"[^"\\\n\r\u2028\u2029\x00-\x1f\x80-\x9f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g; + var remainderRe = /^[\],:{}\s\u2028\u2029]*$/; + + return remainderRe.test(s.replace(backslashesRe, '@'). + replace(simpleValuesRe, ']'). + replace(openBracketsRe, '')); +}; + + +/** + * Parses a JSON string and returns the result. This throws an exception if + * the string is an invalid JSON string. + * + * Note that this is very slow on large strings. If you trust the source of + * the string then you should use unsafeParse instead. + * + * @param {*} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.parse = function(s) { + var o = String(s); + if (goog.json.isValid_(o)) { + /** @preserveTry */ + try { + return eval('(' + o + ')'); + } catch (ex) { + } + } + throw Error('Invalid JSON string: ' + o); +}; + + +/** + * Parses a JSON string and returns the result. This uses eval so it is open + * to security issues and it should only be used if you trust the source. + * + * @param {string} s The JSON string to parse. + * @return {Object} The object generated from the JSON string. + */ +goog.json.unsafeParse = function(s) { + return eval('(' + s + ')'); +}; + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.serialize = function(object) { + return new goog.json.Serializer().serialize(object); +}; + + + +/** + * Class that is used to serialize JSON objects to a string. + * @constructor + */ +goog.json.Serializer = function() { +}; + + +/** + * Serializes an object or a value to a JSON string. + * + * @param {Object?} object The object to serialize. + * @throws Error if there are loops in the object graph. + * @return {string} A JSON string representation of the input. + */ +goog.json.Serializer.prototype.serialize = function(object) { + var sb = []; + this.serialize_(object, sb); + return sb.join(''); +}; + + +/** + * Serializes a generic value to a JSON string + * @private + * @param {string|number|boolean|undefined|Object|Array} object The object to + * serialize. + * @param {Array} sb Array used as a string builder. + * @throws Error if there are loops in the object graph. + */ +goog.json.Serializer.prototype.serialize_ = function(object, sb) { + switch (typeof object) { + case 'string': + this.serializeString_((/** @type {string} */ object), sb); + break; + case 'number': + this.serializeNumber_((/** @type {number} */ object), sb); + break; + case 'boolean': + sb.push(object); + break; + case 'undefined': + sb.push('null'); + break; + case 'object': + if (object == null) { + sb.push('null'); + break; + } + if (goog.isArray(object)) { + this.serializeArray_(object, sb); + break; + } + // should we allow new String, new Number and new Boolean to be treated + // as string, number and boolean? Most implementations do not and the + // need is not very big + this.serializeObject_(object, sb); + break; + case 'function': + // Skip functions. + break; + default: + throw Error('Unknown type: ' + typeof object); + } +}; + + +/** + * Character mappings used internally for goog.string.quote + * @private + * @type {Object} + */ +goog.json.Serializer.charToJsonCharCache_ = { + '\"': '\\"', + '\\': '\\\\', + '/': '\\/', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + + '\x0B': '\\u000b' // '\v' is not supported in JScript +}; + + +/** + * Regular expression used to match characters that need to be replaced. + * The S60 browser has a bug where unicode characters are not matched by + * regular expressions. The condition below detects such behaviour and + * adjusts the regular expression accordingly. + * @private + * @type {RegExp} + */ +goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ? + /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g; + + +/** + * Serializes a string to a JSON string + * @private + * @param {string} s The string to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeString_ = function(s, sb) { + // The official JSON implementation does not work with international + // characters. + sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) { + // caching the result improves performance by a factor 2-3 + if (c in goog.json.Serializer.charToJsonCharCache_) { + return goog.json.Serializer.charToJsonCharCache_[c]; + } + + var cc = c.charCodeAt(0); + var rv = '\\u'; + if (cc < 16) { + rv += '000'; + } else if (cc < 256) { + rv += '00'; + } else if (cc < 4096) { // \u1000 + rv += '0'; + } + return goog.json.Serializer.charToJsonCharCache_[c] = rv + cc.toString(16); + }), '"'); +}; + + +/** + * Serializes a number to a JSON string + * @private + * @param {number} n The number to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) { + sb.push(isFinite(n) && !isNaN(n) ? n : 'null'); +}; + + +/** + * Serializes an array to a JSON string + * @private + * @param {Array} arr The array to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeArray_ = function(arr, sb) { + var l = arr.length; + sb.push('['); + var sep = ''; + for (var i = 0; i < l; i++) { + sb.push(sep) + this.serialize_(arr[i], sb); + sep = ','; + } + sb.push(']'); +}; + + +/** + * Serializes an object to a JSON string + * @private + * @param {Object} obj The object to serialize. + * @param {Array} sb Array used as a string builder. + */ +goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) { + sb.push('{'); + var sep = ''; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + var value = obj[key]; + // Skip functions. + if (typeof value != 'function') { + sb.push(sep); + this.serializeString_(key, sb); + sb.push(':'); + this.serialize_(value, sb); + sep = ','; + } + } + } + sb.push('}'); +};
\ No newline at end of file diff --git a/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc new file mode 100644 index 0000000..4f441a9 --- /dev/null +++ b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.cc @@ -0,0 +1,479 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// JS unittests for our extension API bindings. +#include <iostream> + +#include "base/path_service.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/string_util.h" + +#include "ceee/ie/plugin/scripting/content_script_manager.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include <initguid.h> // NOLINT + +namespace { + +using testing::_; +using testing::DoAll; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrEq; +using testing::StrictMock; +using testing::InstanceCountMixin; +using testing::InstanceCountMixinBase; +using testing::IDispatchExMockImpl; + +// This is the chromium buildbot extension id. +const wchar_t kExtensionId[] = L"fepbkochiplomghbdfgekenppangbiap"; +// And the gmail checker. +const wchar_t kAnotherExtensionId[] = L"kgeddobpkdopccblmihponcjlbdpmbod"; +const wchar_t kFileName[] = TEXT(__FILE__); + +// The class and macros belwo make it possible to execute and debug JavaScript +// snippets interspersed with C++ code, which is a humane way to write, but +// particularly to read and debug the unittests below. +// To use this, declare a JavaScript block as follows: +// <code> +// BEGIN_SCRIPT_BLOCK(some_identifier) /* +// window.alert('here I am!' +// */ END_SCRIPT_BLOCK() +// +// HRESULT hr = some_identifier.Execute(script_host); +// </code> +#define BEGIN_SCRIPT_BLOCK(x) ScriptBlock x(TEXT(__FILE__), __LINE__); +#define END_SCRIPT_BLOCK() +class ScriptBlock { + public: + ScriptBlock(const wchar_t* file, size_t line) : file_(file), line_(line) { + } + + HRESULT Execute(IScriptHost* script_host) { + FilePath self_path(file_); + + if (!self_path.IsAbsolute()) { + // Construct the absolute path to this source file. + // The __FILE__ macro may expand to a solution-relative path to the file. + FilePath src_root; + EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &src_root)); + + self_path = src_root.Append(L"ceee") + .Append(L"ie") + .Append(file_); + } + + // Slurp the file. + std::string contents; + if (!file_util::ReadFileToString(self_path, &contents)) + return E_UNEXPECTED; + + // Walk the lines to ours. + std::string::size_type start_pos = 0; + for (size_t i = 0; i < line_; ++i) { + // Find the next newline. + start_pos = contents.find('\n', start_pos); + if (start_pos == contents.npos) + return E_UNEXPECTED; + + // Walk past the newline char. + start_pos++; + } + + // Now find the next occurrence of END_SCRIPT_BLOCK. + std::string::size_type end_pos = contents.find("END_SCRIPT_BLOCK", + start_pos); + if (end_pos == contents.npos) + return E_UNEXPECTED; + + // And walk back to the start of that line. + end_pos = contents.rfind('\n', end_pos); + if (end_pos == contents.npos) + return E_UNEXPECTED; + + CComPtr<IDebugDocumentHelper> doc; + ScriptHost* host = static_cast<ScriptHost*>(script_host); + host->AddDebugDocument(file_, CA2W(contents.c_str()), &doc); + + std::string script = contents.substr(start_pos, end_pos - start_pos); + return host->RunScriptSnippet(start_pos, CA2W(script.c_str()), doc); + } + + private: + const wchar_t* file_; + const size_t line_; +}; + + +class TestingContentScriptNativeApi + : public ContentScriptNativeApi, + public InstanceCountMixin<TestingContentScriptNativeApi>, + public InitializingCoClass<TestingContentScriptNativeApi> { + public: + // Disambiguate. + using InitializingCoClass<TestingContentScriptNativeApi>::CreateInitialized; + + HRESULT Initialize(TestingContentScriptNativeApi** self) { + *self = this; + return S_OK; + } + + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, Log, + HRESULT(BSTR level, BSTR message)); + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, OpenChannelToExtension, + HRESULT(BSTR source_id, BSTR target_id, BSTR name, LONG* port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, CloseChannel, + HRESULT(LONG port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PortAddRef, + HRESULT(LONG port_id)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PortRelease, + HRESULT(LONG port_id)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, PostMessage, + HRESULT(LONG port_id, BSTR msg)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, AttachEvent, + HRESULT(BSTR event_name)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, DetachEvent, + HRESULT(BSTR event_name)); +}; + +class TestingContentScriptManager + : public ContentScriptManager { + public: + // Make accessible for testing. + using ContentScriptManager::BootstrapScriptHost; +}; + +class TestingScriptHost + : public ScriptHost, + public InitializingCoClass<TestingScriptHost>, + public InstanceCountMixin<TestingScriptHost> { + public: + using InitializingCoClass<TestingScriptHost>::CreateInitializedIID; + + HRESULT Initialize(ScriptHost::DebugApplication* debug, + TestingScriptHost** self) { + *self = this; + return ScriptHost::Initialize(debug); + } +}; + +class RendererExtensionBindingsTest: public testing::Test { + public: + RendererExtensionBindingsTest() : api_(NULL), script_host_(NULL) { + } + + static void SetUpTestCase() { + EXPECT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + debug_.Initialize(); + } + + static void TearDownTestCase() { + debug_.Terminate(); + + ::CoUninitialize(); + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + + ASSERT_HRESULT_SUCCEEDED( + TestingScriptHost::CreateInitializedIID(&debug_, + &script_host_, + IID_IScriptHost, + &script_host_keeper_)); + ASSERT_HRESULT_SUCCEEDED( + TestingContentScriptNativeApi::CreateInitialized(&api_, &api_keeper_)); + } + + void TearDown() { + if (api_) { + EXPECT_HRESULT_SUCCEEDED(api_->TearDown()); + api_ = NULL; + api_keeper_.Release(); + } + + script_host_ = NULL; + if (script_host_keeper_ != NULL) + script_host_keeper_->Close(); + script_host_keeper_.Release(); + + EXPECT_EQ(0, InstanceCountMixinBase::all_instance_count()); + } + + void Initialize() { + ASSERT_HRESULT_SUCCEEDED( + manager_.BootstrapScriptHost(script_host_keeper_, + api_keeper_, + kExtensionId)); + } + + void AssertNameExists(const wchar_t* name) { + CComVariant result; + ASSERT_HRESULT_SUCCEEDED(script_host_->RunExpression(name, &result)); + ASSERT_NE(VT_EMPTY, V_VT(&result)); + } + + void ExpectFirstConnection() { + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + } + void ExpectConnection(const wchar_t* src_extension_id, + const wchar_t* dst_extension_id, + const wchar_t* port_name, + LONG port_id) { + EXPECT_CALL(*api_, + OpenChannelToExtension(StrEq(src_extension_id), + StrEq(dst_extension_id), + StrEq(port_name), + _)) + .WillOnce( + DoAll( + SetArgumentPointee<3>(port_id), + Return(S_OK))); + + EXPECT_CALL(*api_, PortAddRef(port_id)) + .WillOnce(Return(S_OK)); + } + + protected: + TestingContentScriptNativeApi* api_; + CComPtr<ICeeeContentScriptNativeApi> api_keeper_; + TestingContentScriptManager manager_; + + TestingScriptHost* script_host_; + CComPtr<IScriptHost> script_host_keeper_; + + static ScriptHost::DebugApplication debug_; +}; + +ScriptHost::DebugApplication + RendererExtensionBindingsTest::debug_(L"RendererExtensionBindingsTest"); + +TEST_F(RendererExtensionBindingsTest, TestNamespace) { + Initialize(); + + AssertNameExists(L"chrome"); + AssertNameExists(L"chrome.extension"); + AssertNameExists(L"chrome.extension.connect"); + + AssertNameExists(L"JSON"); + AssertNameExists(L"JSON.parse"); +} + +TEST_F(RendererExtensionBindingsTest, GetUrl) { + Initialize(); + + CComVariant result; + + ASSERT_HRESULT_SUCCEEDED( + script_host_->RunExpression( + L"chrome.extension.getURL('foo')", &result)); + + ASSERT_EQ(VT_BSTR, V_VT(&result)); + ASSERT_STREQ(StringPrintf(L"chrome-extension://%ls/foo", + kExtensionId).c_str(), + V_BSTR(&result)); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectDisconnect) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, L"port = chrome.extension.connect()")); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectWithName) { + Initialize(); + const LONG kPortId = 42; + const wchar_t* kPortName = L"A Port Name"; + ExpectConnection(kExtensionId, kExtensionId, kPortName, kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf( + L"port = chrome.extension.connect({name: \"%ls\"});", + kPortName).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, PortConnectToExtension) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kAnotherExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port = chrome.extension.connect(\"%ls\");", + kAnotherExtensionId).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, PostMessage) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kAnotherExtensionId, L"", kPortId); + ExpectFirstConnection(); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port = chrome.extension.connect(\"%ls\");", + kAnotherExtensionId).c_str())); + + const wchar_t* kMsg = L"Message in a bottle, yeah!"; + // Note the extra on-the-wire quotes, due to JSON encoding the input. + EXPECT_CALL(*api_, + PostMessage(kPortId, + StrEq(StringPrintf(L"\"%ls\"", kMsg).c_str()))); + + EXPECT_HRESULT_SUCCEEDED(script_host_->RunScript( + kFileName, StringPrintf(L"port.postMessage(\"%ls\");", kMsg).c_str())); +} + +TEST_F(RendererExtensionBindingsTest, OnConnect) { + Initialize(); + const LONG kPortId = 42; + const wchar_t kPortName[] = L"A port of call"; + BEGIN_SCRIPT_BLOCK(script) /* + function onConnect(port) { + if (port.name == 'A port of call') + console.log('SUCCESS'); + else + console.log(port.name); + }; + chrome.extension.onConnect.addListener(onConnect); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))). + WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + // A 'SUCCESS' log signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(L"SUCCESS"))).Times(1); + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))). + WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, PortAddRef(kPortId)). + WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED( + api_->CallOnPortConnect(kPortId, + kPortName, + L"", + kExtensionId, + kExtensionId)); +} + +TEST_F(RendererExtensionBindingsTest, OnDisconnect) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(script1) /* + var port = chrome.extension.connect() + */ END_SCRIPT_BLOCK() + EXPECT_HRESULT_SUCCEEDED(script1.Execute(script_host_)); + + BEGIN_SCRIPT_BLOCK(script2) /* + port.onDisconnect.addListener(function (port) { + console.log('SUCCESS'); + }); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(script2.Execute(script_host_)); + + // A 'SUCCESS' log signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(L"SUCCESS"))).Times(1); + + EXPECT_HRESULT_SUCCEEDED(api_->CallOnPortDisconnect(kPortId)); +} + +TEST_F(RendererExtensionBindingsTest, OnMessage) { + Initialize(); + const LONG kPortId = 42; + ExpectConnection(kExtensionId, kExtensionId, L"", kPortId); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(connect_script) /* + // Connect to our extension. + var port = chrome.extension.connect(); + */ END_SCRIPT_BLOCK() + EXPECT_HRESULT_SUCCEEDED(connect_script.Execute(script_host_)); + + BEGIN_SCRIPT_BLOCK(add_listener_script) /* + // Log the received message to console. + function onMessage(msg, port) { + console.log(msg); + }; + port.onMessage.addListener(onMessage); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))) + .WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(add_listener_script.Execute(script_host_)); + + const wchar_t kMessage[] = L"A message in a bottle, yeah!"; + // The message logged signals success. + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(kMessage))).Times(1); + + EXPECT_HRESULT_SUCCEEDED( + api_->CallOnPortMessage(StringPrintf(L"\"%ls\"", kMessage).c_str(), + kPortId)); +} + +TEST_F(RendererExtensionBindingsTest, OnLoad) { + Initialize(); + + BEGIN_SCRIPT_BLOCK(script) /* + var chromeHidden = ceee.GetChromeHidden(); + function onLoad(extension_id) { + console.log(extension_id); + } + chromeHidden.onLoad.addListener(onLoad); + */ END_SCRIPT_BLOCK() + + EXPECT_CALL(*api_, AttachEvent(StrEq(L""))); + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + EXPECT_CALL(*api_, Log(StrEq(L"info"), StrEq(kExtensionId))) + .WillOnce(Return(S_OK)); + EXPECT_HRESULT_SUCCEEDED(api_->CallOnLoad(kExtensionId)); +} + +TEST_F(RendererExtensionBindingsTest, OnUnload) { + Initialize(); + const LONG kPort1Id = 42; + const LONG kPort2Id = 57; + ExpectConnection(kExtensionId, kExtensionId, L"port1", kPort1Id); + ExpectConnection(kExtensionId, kExtensionId, L"port2", kPort2Id); + ExpectFirstConnection(); + + BEGIN_SCRIPT_BLOCK(script) /* + var port1 = chrome.extension.connect({name: 'port1'}); + var port2 = chrome.extension.connect({name: 'port2'}); + */ END_SCRIPT_BLOCK() + + EXPECT_HRESULT_SUCCEEDED(script.Execute(script_host_)); + + EXPECT_CALL(*api_, PortRelease(kPort1Id)).WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, PortRelease(kPort2Id)).WillOnce(Return(S_OK)); + EXPECT_CALL(*api_, DetachEvent(StrEq(L""))).WillOnce(Return(S_OK)); + + EXPECT_HRESULT_SUCCEEDED(api_->CallOnUnload()); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc new file mode 100644 index 0000000..1be0ca5 --- /dev/null +++ b/ceee/ie/plugin/scripting/renderer_extension_bindings_unittest.rc @@ -0,0 +1,12 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#include <winres.h> + +#ifdef APSTUDIO_INVOKED +#error Please edit as text. +#endif + +// The test needs the CEEE TLB. +1 TYPELIB "toolband.tlb" diff --git a/ceee/ie/plugin/scripting/script_host.cc b/ceee/ie/plugin/scripting/script_host.cc new file mode 100644 index 0000000..e2faf42 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host.cc @@ -0,0 +1,886 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implements our scripting host. + +#include "ceee/ie/plugin/scripting/script_host.h" + +#include <dispex.h> +#include <mshtml.h> +#include <mshtmhst.h> +#include <objsafe.h> + +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" + +#ifndef CMDID_SCRIPTSITE_URL + +// These are documented in MSDN, but not declared in the platform SDK. +// See [http://msdn.microsoft.com/en-us/library/aa769871(VS.85).aspx]. +#define CMDID_SCRIPTSITE_URL 0 +#define CMDID_SCRIPTSITE_HTMLDLGTRUST 1 +#define CMDID_SCRIPTSITE_SECSTATE 2 +#define CMDID_SCRIPTSITE_SID 3 +#define CMDID_SCRIPTSITE_TRUSTEDDOC 4 +#define CMDID_SCRIPTSITE_SECURITY_WINDOW 5 +#define CMDID_SCRIPTSITE_NAMESPACE 6 +#define CMDID_SCRIPTSITE_IURI 7 + +const GUID CGID_ScriptSite = { + 0x3050F3F1, 0x98B5, 0x11CF, 0xBB, 0x82, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x0B }; +#endif // CMDID_SCRIPTSITE_URL + +namespace { +// This class is a necessary wrapper around a text string in +// IDebugDocumentHelper, as one can really only use the deferred +// text mode of the helper if one wants to satisfy the timing +// requirements on notifications imposed by script debuggers. +// +// The timing requirement is this: +// +// It appears that for a debugger to successfully set a breakpoint in +// an ActiveScript engine, the code in question has to have been parsed. +// The VisualStudio and the IE8 debugger both appear to use the events +// IDebugDocumentTextEvents as a trigger point to apply any pending +// breakpoints to the engine. The problem here is that with naive usage of +// the debug document helper, one would simply provide it with the +// text contents of the document at creation, before ParseScriptText +// is performed. This fires the text events too early, which means +// the debugger will not find any code to set breakpoints on. +// To ensure that the debugger finds something to grab on, +// one needs to set or modify the debug document text after +// ParseScriptText, but before execution of the text, such as e.g. +// on the OnEnterScript event to the ActiveScript site. +class DocHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<DocHost>, + public IDebugDocumentHost { + public: + BEGIN_COM_MAP(DocHost) + COM_INTERFACE_ENTRY(IDebugDocumentHost) + END_COM_MAP() + + HRESULT Initialize(const wchar_t* code) { + code_ = code; + return S_OK; + } + + STDMETHOD(GetDeferredText)(DWORD cookie, + WCHAR* text, + SOURCE_TEXT_ATTR* text_attr, + ULONG* num_chars_returned, + ULONG max_chars) { + size_t num_chars = std::min(static_cast<size_t>(max_chars), code_.length()); + *num_chars_returned = num_chars; + memcpy(text, code_.c_str(), num_chars * sizeof(wchar_t)); + + return S_OK; + } + + STDMETHOD(GetScriptTextAttributes)(LPCOLESTR code, + ULONG num_code_chars, + LPCOLESTR delimiter, + DWORD flags, + SOURCE_TEXT_ATTR* attr) { + return E_NOTIMPL; + } + + STDMETHOD(OnCreateDocumentContext)(IUnknown** outer) { + return E_NOTIMPL; + } + + STDMETHOD(GetPathName)(BSTR *long_name, BOOL *is_original_file) { + return E_NOTIMPL; + } + + STDMETHOD(GetFileName)(BSTR *short_name) { + return E_NOTIMPL; + } + STDMETHOD(NotifyChanged)(void) { + return E_NOTIMPL; + } + private: + std::wstring code_; +}; + +HRESULT GetUrlForDocument(IUnknown* unknown, VARIANT* url_out) { + CComQIPtr<IHTMLDocument2> document(unknown); + if (document == NULL) + return E_NOINTERFACE; + + CComBSTR url; + HRESULT hr = document->get_URL(&url); + if (SUCCEEDED(hr)) { + url_out->vt = VT_BSTR; + url_out->bstrVal = url.Detach(); + } else { + DLOG(ERROR) << "Failed to get security url " << com::LogHr(hr); + } + + return hr; +} + +HRESULT GetSIDForDocument(IUnknown* unknown, VARIANT* sid_out) { + CComQIPtr<IServiceProvider> sp(unknown); + if (sp == NULL) + return E_NOINTERFACE; + + CComPtr<IInternetHostSecurityManager> security_manager; + HRESULT hr = sp->QueryService(SID_SInternetHostSecurityManager, + &security_manager); + if (FAILED(hr)) + return hr; + + // This is exactly mimicking observed behavior in IE. + CComBSTR security_id(MAX_SIZE_SECURITY_ID); + DWORD size = MAX_SIZE_SECURITY_ID; + hr = security_manager->GetSecurityId( + reinterpret_cast<BYTE*>(security_id.m_str), &size, 0); + if (SUCCEEDED(hr)) { + sid_out->vt = VT_BSTR; + sid_out->bstrVal = security_id.Detach(); + } else { + DLOG(ERROR) << "Failed to get security manager " << com::LogHr(hr); + } + + return hr; +} + +HRESULT GetWindowForDocument(IUnknown* unknown, VARIANT* window_out) { + CComQIPtr<IServiceProvider> sp(unknown); + if (sp == NULL) + return E_NOINTERFACE; + + CComPtr<IDispatch> window; + HRESULT hr = sp->QueryService(SID_SHTMLWindow, &window); + if (SUCCEEDED(hr)) { + window_out->vt = VT_DISPATCH; + window_out->pdispVal = window.Detach(); + } else { + DLOG(ERROR) << "Failed to get window " << com::LogHr(hr); + } + + return hr; +} + +} // namespace + +// {58E6D2A5-4868-4E49-B3E9-072C845A014A} +const GUID IID_IScriptHost = + { 0x58ECD2A5, 0x4868, 0x4E49, + { 0xB3, 0xE9, 0x07, 0x2C, 0x84, 0x5A, 0x01, 0x4A } }; + +// {f414c260-6ac0-11cf-b6d1-00aa00bbbb58} +const GUID CLSID_JS = + { 0xF414C260, 0x6AC0, 0x11CF, + { 0xB6, 0xD1, 0x00, 0xAA, 0x00, 0xBB, 0xBB, 0x58 } }; + + +ScriptHost::DebugApplication* ScriptHost::default_debug_application_; + + +ScriptHost::ScriptHost() : debug_application_(NULL) { +} + +HRESULT ScriptHost::Initialize(DebugApplication* debug_application) { + debug_application_ = debug_application; + + HRESULT hr = CreateScriptEngine(&script_); + if (FAILED(hr)) { + NOTREACHED(); + return hr; + } + + if (FAILED(hr = script_->SetScriptSite(this))) { + NOTREACHED(); + return hr; + } + + // Get engine's IActiveScriptParse interface, initialize it + script_parse_ = script_; + if (!script_parse_) { + NOTREACHED(); + return E_NOINTERFACE; + } + + if (FAILED(hr = script_parse_->InitNew())) { + NOTREACHED(); + return hr; + } + + // Set the security options of the script engine so it + // queries us for IInternetHostSecurityManager, which + // we delegate to our site. + CComQIPtr<IObjectSafety> script_safety(script_); + if (script_safety == NULL) { + NOTREACHED() << "Script engine does not implement IObjectSafety"; + return E_NOINTERFACE; + } + + hr = script_safety->SetInterfaceSafetyOptions( + IID_IDispatch, INTERFACE_USES_SECURITY_MANAGER, + INTERFACE_USES_SECURITY_MANAGER); + + // Set the script engine into a running state. + hr = script_->SetScriptState(SCRIPTSTATE_CONNECTED); + DCHECK(SUCCEEDED(hr)); + + return hr; +} + +HRESULT ScriptHost::Initialize(DebugApplication* debug_application, + ScriptHost** self) { + *self = this; + return Initialize(debug_application); +} + +HRESULT ScriptHost::Initialize() { + return Initialize(default_debug_application_); +} + +void ScriptHost::FinalRelease() { + DCHECK(script_ == NULL); + debug_application_ = NULL; +} + +HRESULT ScriptHost::RegisterScriptObject(const wchar_t* name, + IDispatch* disp_obj, + bool make_members_global) { + DCHECK(name); + DCHECK(disp_obj); + std::wstring wname = name; + + // Check if the name already exists. + ScriptObjectMap::iterator iter = script_objects_.find(wname); + if (iter != script_objects_.end()) { + return E_ACCESSDENIED; + } + + // Add to the script object map. + CComPtr<IDispatch> disp_obj_ptr(disp_obj); + CAdapt<CComPtr<IDispatch>> disp_obj_adapt(disp_obj_ptr); + script_objects_.insert(std::make_pair(wname, disp_obj_adapt)); + + // Add to the script engine. + DWORD flags = SCRIPTITEM_ISSOURCE | SCRIPTITEM_ISVISIBLE; + if (make_members_global) { + flags |= SCRIPTITEM_GLOBALMEMBERS; + } + script_->AddNamedItem(name, flags); + + return S_OK; +} + +HRESULT ScriptHost::RunScript(const wchar_t* file_path, + const wchar_t* code) { + DCHECK(file_path); + DCHECK(code); + if (!file_path || !code) + return E_POINTER; + + DWORD source_context = 0; + HRESULT hr = GetSourceContext(file_path, code, &source_context); + if (FAILED(hr)) + return hr; + + ScopedExcepInfo ei; + hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, source_context, 0, + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE, NULL, &ei); + // A syntax error is not a CEEE error, so we don't log an error and we return + // it normally so that the caller knows what happened. + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +HRESULT ScriptHost::RunExpression(const wchar_t* code, VARIANT* result) { + DCHECK(code); + if (!code) + return E_POINTER; + + ScopedExcepInfo ei; + HRESULT hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, 0, 0, SCRIPTTEXT_ISEXPRESSION, result, &ei); + // Ignore compilation and runtime errors in the script + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +HRESULT ScriptHost::Close() { + // Close our script host. + HRESULT hr = S_OK; + if (script_) { + // Try to force garbage collection at this time so any objects holding + // reference to native components will be released immediately and dlls + // loaded can be unloaded quickly. + CComQIPtr<IActiveScriptGarbageCollector> script_gc(script_); + if (script_gc != NULL) + script_gc->CollectGarbage(SCRIPTGCTYPE_EXHAUSTIVE); + + hr = script_->Close(); + } + + // Detach all debug documents. + DebugDocMap::iterator iter; + for (iter = debug_docs_.begin(); iter != debug_docs_.end(); iter++) { + // Note that this is IDebugDocumentHelper::Detach and not + // CComPtr::Detach + iter->second.document->Detach(); + } + debug_docs_.clear(); + + script_.Release(); + + return hr; +} + +STDMETHODIMP ScriptHost::GetItemInfo(LPCOLESTR item_name, DWORD return_mask, + IUnknown** item_unknown, + ITypeInfo** item_itypeinfo) { + DCHECK(!(return_mask & SCRIPTINFO_IUNKNOWN) || item_unknown); + DCHECK(!(return_mask & SCRIPTINFO_ITYPEINFO) || item_itypeinfo); + + HRESULT hr = S_OK; + + std::wstring wname = item_name; + ScriptObjectMap::iterator iter = script_objects_.find(wname); + if (iter != script_objects_.end()) { + CComPtr<IDispatch> disp_obj = iter->second.m_T; + + CComPtr<IUnknown> unknown; + if (return_mask & SCRIPTINFO_IUNKNOWN) { + DCHECK(item_unknown); + hr = disp_obj.QueryInterface(&unknown); + } + + CComPtr<ITypeInfo> typeinfo; + if (SUCCEEDED(hr) && return_mask & SCRIPTINFO_ITYPEINFO) { + DCHECK(item_itypeinfo); + hr = disp_obj->GetTypeInfo(0, LANG_NEUTRAL, &typeinfo); + } + + // We have everything ready, return the out args on success. + if (SUCCEEDED(hr)) { + if (return_mask & SCRIPTINFO_IUNKNOWN) { + hr = unknown.CopyTo(item_unknown); + DCHECK(SUCCEEDED(hr)); + } + if (return_mask & SCRIPTINFO_ITYPEINFO) { + hr = typeinfo.CopyTo(item_itypeinfo); + DCHECK(SUCCEEDED(hr)); + } + } + } else { + hr = TYPE_E_ELEMENTNOTFOUND; + } + + return hr; +} + +STDMETHODIMP ScriptHost::GetLCID(LCID *plcid) { + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::GetDocVersionString(BSTR* version) { + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::OnEnterScript() { + DebugDocMap::iterator it(debug_docs_.begin()); + DebugDocMap::iterator end(debug_docs_.end()); + + // It is necessary to defer the document size notifications + // below until after defining all relevant script blocks, + // in order to tickle the debugger at just the right time + // so it can turn around and set any pending breakpoints + // prior to first execution. + for (; it != end; ++it) { + DebugDocInfo& info = it->second; + + if (info.is_new) { + info.is_new = false; + info.document->AddDeferredText(info.len, 0); + } + } + + return S_OK; +} + +STDMETHODIMP ScriptHost::OnLeaveScript() { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnStateChange(SCRIPTSTATE state) { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnScriptTerminate(const VARIANT* result, + const EXCEPINFO* excep_info) { + return S_OK; +} + +STDMETHODIMP ScriptHost::OnScriptError(IActiveScriptError* script_error) { + ScopedExcepInfo ei; + HRESULT hr = script_error->GetExceptionInfo(&ei); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetExceptionInfo. " << + com::LogHr(hr); + + CComBSTR source_line; + hr = script_error->GetSourceLineText(&source_line); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetSourceLineText. " << + com::LogHr(hr); + + DWORD context = 0; + ULONG line_number = 0; + LONG char_pos = 0; + hr = script_error->GetSourcePosition(&context, &line_number, &char_pos); + LOG_IF(ERROR, FAILED(hr)) << "Failed to GetSourcePosition. " << + com::LogHr(hr); + + LOG(ERROR) << "Script error occurred: " << + com::ToString(ei.bstrDescription) << ". Source Text: " << + com::ToString(source_line) << ". Context: "<< context << ", line: " << + line_number << ", char pos: " << char_pos; + return S_OK; +} + +STDMETHODIMP ScriptHost::GetDocumentContextFromPosition( + DWORD source_context, ULONG char_offset, ULONG num_chars, + IDebugDocumentContext** debug_doc_context) { + LOG(INFO) << "GetDocumentContextFromPosition(" << source_context << ", " + << char_offset << ", " << num_chars << ")"; + + DebugDocMap::iterator iter; + iter = debug_docs_.find(source_context); + if (iter != debug_docs_.end()) { + DebugDocInfo& info = iter->second; + ULONG start_position = 0; + HRESULT hr = info.document->GetScriptBlockInfo(source_context, + NULL, + &start_position, + NULL); + if (FAILED(hr)) + LOG(ERROR) << "GetScriptBlockInfo failed " << com::LogHr(hr); + + if (SUCCEEDED(hr)) { + hr = info.document->CreateDebugDocumentContext( + start_position + char_offset, num_chars, debug_doc_context); + if (FAILED(hr)) + LOG(ERROR) << "GetScriptBlockInfo failed " << com::LogHr(hr); + } + + return hr; + } + + LOG(ERROR) << "No debug document for context " << source_context; + + return E_FAIL; +} + +STDMETHODIMP ScriptHost::GetApplication(IDebugApplication** debug_app) { + if (debug_application_) + return debug_application_->GetDebugApplication(debug_app); + + return E_NOTIMPL; +} + +STDMETHODIMP ScriptHost::GetRootApplicationNode( + IDebugApplicationNode** debug_app_node) { + DCHECK(debug_app_node); + if (!debug_app_node) + return E_POINTER; + + if (debug_application_ != NULL) { + return debug_application_->GetRootApplicationNode(debug_app_node); + } else { + *debug_app_node = NULL; + return S_OK; + } + + NOTREACHED(); +} + +STDMETHODIMP ScriptHost::OnScriptErrorDebug(IActiveScriptErrorDebug* err, + BOOL* enter_debugger, BOOL* call_on_script_err_when_continuing) { + DCHECK(err); + DCHECK(enter_debugger); + DCHECK(call_on_script_err_when_continuing); + if (!err || !enter_debugger || !call_on_script_err_when_continuing) + return E_POINTER; + + // TODO(ericdingle@chromium.org): internationalization + int ret = ::MessageBox( + NULL, L"A script error occured. Do you want to debug?", + L"Google Chrome Extensions Execution Environment", + MB_ICONERROR | MB_SETFOREGROUND | MB_TASKMODAL | MB_YESNO); + *enter_debugger = (ret == IDYES); + *call_on_script_err_when_continuing = FALSE; + + return S_OK; +} + +STDMETHODIMP ScriptHost::QueryStatus(const GUID* cmd_group, ULONG num_cmds, + OLECMD cmds[], OLECMDTEXT *cmd_text) { + LOG(WARNING) << "QueryStatus " << + CComBSTR(cmd_group ? *cmd_group : GUID_NULL) << ", " << num_cmds; + // We're practically unimplemented. + DLOG(INFO) << "ScriptHost::QueryStatus called"; + return OLECMDERR_E_UNKNOWNGROUP; +}; + +STDMETHODIMP ScriptHost::Exec(const GUID* cmd_group, DWORD cmd_id, + DWORD cmd_exec_opt, VARIANT *arg_in, VARIANT *arg_out) { + LOG(WARNING) << "Exec " << CComBSTR(cmd_group ? *cmd_group : GUID_NULL) << + ", " << cmd_id; + + if (cmd_group && *cmd_group == CGID_ScriptSite) { + switch (cmd_id) { + case CMDID_SCRIPTSITE_URL: + return GetUrlForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_HTMLDLGTRUST: + DLOG(INFO) << "CMDID_SCRIPTSITE_HTMLDLGTRUST"; + break; + case CMDID_SCRIPTSITE_SECSTATE: + DLOG(INFO) << "CMDID_SCRIPTSITE_SECSTATE"; + break; + case CMDID_SCRIPTSITE_SID: + return GetSIDForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_TRUSTEDDOC: + DLOG(INFO) << "CMDID_SCRIPTSITE_TRUSTEDDOC"; + break; + case CMDID_SCRIPTSITE_SECURITY_WINDOW: + return GetWindowForDocument(m_spUnkSite, arg_out); + break; + case CMDID_SCRIPTSITE_NAMESPACE: + DLOG(INFO) << "CMDID_SCRIPTSITE_NAMESPACE"; + break; + case CMDID_SCRIPTSITE_IURI: + DLOG(INFO) << "CMDID_SCRIPTSITE_IURI"; + break; + default: + DLOG(INFO) << "ScriptHost::Exec unknown command " << cmd_id; + break; + } + } + + return OLECMDERR_E_UNKNOWNGROUP; +} + +HRESULT ScriptHost::CreateScriptEngine(IActiveScript** script) { + return script_.CoCreateInstance(CLSID_JS, NULL, CLSCTX_INPROC_SERVER); +} + +HRESULT ScriptHost::GetSourceContext(const wchar_t* file_path, + const wchar_t* code, + DWORD* source_context) { + DCHECK(debug_application_ != NULL); + HRESULT hr = S_OK; + CComPtr<IDebugDocumentHelper> helper; + hr = debug_application_->CreateDebugDocumentHelper(file_path, + code, + 0, + &helper); + size_t len = lstrlenW(code); + if (SUCCEEDED(hr) && helper != NULL) { + hr = helper->DefineScriptBlock(0, len, script_, FALSE, source_context); + DCHECK(SUCCEEDED(hr)); + + DebugDocInfo info; + info.is_new = true; + info.len = len; + info.document = helper; + debug_docs_.insert(std::make_pair(*source_context, info)); + } + + return hr; +} + +HRESULT ScriptHost::AddDebugDocument(const wchar_t* file_path, + const wchar_t* code, + IDebugDocumentHelper** doc) { + DCHECK(debug_application_ != NULL); + return debug_application_->CreateDebugDocumentHelper(file_path, + code, + TEXT_DOC_ATTR_READONLY, + doc); +} + +HRESULT ScriptHost::RunScriptSnippet(size_t start_offset, + const wchar_t* code, + IDebugDocumentHelper* doc) { + DWORD source_context = 0; + if (doc) { + size_t len = lstrlenW(code); + HRESULT hr = doc->DefineScriptBlock(start_offset, + len, + script_, + FALSE, + &source_context); + + if (SUCCEEDED(hr)) { + DebugDocInfo info; + info.document = doc; + info.len = start_offset + len; + info.is_new = true; + + debug_docs_.insert(std::make_pair(source_context, info)); + } else { + LOG(ERROR) << "Failed to define a script block " << com::LogHr(hr); + LOG(ERROR) << "Script: " << code; + } + } + + ScopedExcepInfo ei; + HRESULT hr = script_parse_->ParseScriptText( + code, NULL, NULL, NULL, source_context, 0, + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE, NULL, &ei); + + // Ignore compilation and runtime errors in the script + if (FAILED(hr) && hr != OLESCRIPT_E_SYNTAX) { + LOG(ERROR) << "Non-script error occurred while parsing script. " + << com::LogHr(hr); + NOTREACHED(); + } + + return hr; +} + +ScriptHost::DebugApplication::DebugApplication(const wchar_t* application_name) + : application_name_(application_name), + debug_app_cookie_(kInvalidDebugAppCookie), + initialization_count_(0) { +} + +ScriptHost::DebugApplication::~DebugApplication() { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK_EQ(0U, initialization_count_); +} + +void ScriptHost::DebugApplication::RegisterDebugApplication() { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK(debug_app_cookie_ == kInvalidDebugAppCookie); + + // Don't need to lock as this MUST be single-threaded and first use. + CComPtr<IProcessDebugManager> manager; + HRESULT hr = CreateProcessDebugManager(&manager); + + CComPtr<IDebugApplication> application; + if (SUCCEEDED(hr)) + hr = manager->CreateApplication(&application); + + if (SUCCEEDED(hr)) + hr = application->SetName(application_name_); + + DWORD cookie = 0; + if (SUCCEEDED(hr)) + hr = manager->AddApplication(application, &cookie); + + if (FAILED(hr)) { + LOG(INFO) << "ScriptHost debug initialization failed: " << com::LogHr(hr); + return; + } + + debug_manager_ = manager; + debug_application_ = application; + debug_app_cookie_ = cookie; +} + +void ScriptHost::DebugApplication::Initialize() { + AutoLock lock(lock_); + + ++initialization_count_; + + if (initialization_count_ == 1) + RegisterDebugApplication(); +} + +void ScriptHost::DebugApplication::Initialize( + IUnknown* debug_application_provider) { + AutoLock lock(lock_); + + ++initialization_count_; + + if (initialization_count_ == 1) { + DCHECK(debug_manager_ == NULL); + DCHECK(debug_application_ == NULL); + DCHECK(debug_app_cookie_ == kInvalidDebugAppCookie); + + CComPtr<IDebugApplication> debug_app; + HRESULT hr = debug_application_provider->QueryInterface(&debug_app); + if (FAILED(hr) || debug_app == NULL) { + CComPtr<IServiceProvider> sp; + hr = debug_application_provider->QueryInterface(&sp); + if (SUCCEEDED(hr) && sp != NULL) + hr = sp->QueryService(IID_IDebugApplication, &debug_app); + } + + if (debug_app != NULL) + debug_application_ = debug_app; + else + RegisterDebugApplication(); + } +} + +void ScriptHost::DebugApplication::Initialize( + IProcessDebugManager* manager, IDebugApplication* app) { + AutoLock lock(lock_); + // This function is exposed for testing only. + DCHECK_EQ(0U, initialization_count_); + + DCHECK_EQ(static_cast<IUnknown*>(NULL), debug_manager_); + DCHECK_EQ(static_cast<IUnknown*>(NULL), debug_application_); + DCHECK_EQ(kInvalidDebugAppCookie, debug_app_cookie_); + + ++initialization_count_; + + debug_manager_ = manager; + debug_application_ = app; +} + + +void ScriptHost::DebugApplication::Terminate() { + AutoLock lock(lock_); + DCHECK_GT(initialization_count_, (size_t)0); + --initialization_count_; + + if (initialization_count_ == 0) { + if (debug_manager_ != NULL) { + if (debug_app_cookie_ != kInvalidDebugAppCookie) { + HRESULT hr = debug_manager_->RemoveApplication(debug_app_cookie_); + DCHECK(SUCCEEDED(hr)); + } + } + + debug_manager_.Release(); + debug_application_.Release(); + debug_app_cookie_ = kInvalidDebugAppCookie; + } +} + +HRESULT ScriptHost::DebugApplication::GetDebugApplication( + IDebugApplication** app) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) + return E_NOTIMPL; + + return debug_application_.CopyTo(app); +} + +HRESULT ScriptHost::DebugApplication::GetRootApplicationNode( + IDebugApplicationNode** debug_app_node) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) { + *debug_app_node = NULL; + return S_OK; + } + + return debug_application_->GetRootNode(debug_app_node); +} + +HRESULT ScriptHost::DebugApplication::CreateDebugDocumentHelper( + const wchar_t* long_name, const wchar_t* code, TEXT_DOC_ATTR attributes, + IDebugDocumentHelper** helper) { + AutoLock lock(lock_); + + if (debug_application_ == NULL) + return S_OK; + + // Find the last forward or backward slash in the long name, + // and construct a short name from the rest - the base name. + std::wstring name(long_name); + size_t pos = name.find_last_of(L"\\/"); + const wchar_t* short_name = long_name; + if (pos != name.npos && long_name[pos + 1] != '\0') + short_name = long_name + pos + 1; + + CComPtr<IDebugDocumentHelper> doc; + HRESULT hr = CreateDebugDocumentHelper(&doc); + if (SUCCEEDED(hr)) + hr = doc->Init(debug_application_, short_name, long_name, attributes); + + // Wrap the text in a document host. + CComPtr<IDebugDocumentHost> host; + if (SUCCEEDED(hr)) + hr = DocHost::CreateInitialized(code, &host); + if (SUCCEEDED(hr)) + hr = doc->SetDebugDocumentHost(host); + if (SUCCEEDED(hr)) + hr = doc->Attach(NULL); + + if (SUCCEEDED(hr)) { + DCHECK(doc != NULL); + + *helper = doc.Detach(); + } + + return hr; +} + +HRESULT ScriptHost::DebugApplication::CreateDebugDocumentHelper( + IDebugDocumentHelper** helper) { + // Create the debug document. + if (debug_manager_ != NULL) + return debug_manager_->CreateDebugDocumentHelper(NULL, helper); + + // As it turns out, it's better to create a debug document + // from the same DLL server as issued the debug manager, so let's + // try and accomodate. Get the class object function from the + // in-process instance. + HMODULE pdm = ::GetModuleHandle(L"pdm.dll"); + LPFNGETCLASSOBJECT pdm_get_class_object = NULL; + if (pdm != NULL) + pdm_get_class_object = reinterpret_cast<LPFNGETCLASSOBJECT>( + ::GetProcAddress(pdm, "DllGetClassObject")); + + // Fallback to plain CoCreateInstance if we didn't get the function. + if (!pdm_get_class_object) { + LOG(WARNING) << "CreateDebugDocumentHelper falling back to " + "CoCreateInstance"; + return ::CoCreateInstance(CLSID_CDebugDocumentHelper, + NULL, + CLSCTX_INPROC_SERVER, + IID_IDebugDocumentHelper, + reinterpret_cast<void**>(helper)); + } + + // Create a debug helper. + CComPtr<IClassFactory> factory; + HRESULT hr = pdm_get_class_object(CLSID_CDebugDocumentHelper, + IID_IClassFactory, + reinterpret_cast<void**>(&factory)); + if (SUCCEEDED(hr)) { + DCHECK(factory != NULL); + hr = factory->CreateInstance(NULL, + IID_IDebugDocumentHelper, + reinterpret_cast<void**>(helper)); + } + + return hr; +} + +HRESULT ScriptHost::DebugApplication::CreateProcessDebugManager( + IProcessDebugManager** manager) { + return ::CoCreateInstance(CLSID_ProcessDebugManager, + NULL, + CLSCTX_INPROC_SERVER, + IID_IProcessDebugManager, + reinterpret_cast<void**>(manager)); +} diff --git a/ceee/ie/plugin/scripting/script_host.h b/ceee/ie/plugin/scripting/script_host.h new file mode 100644 index 0000000..553d142 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host.h @@ -0,0 +1,317 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A host for Microsoft's JScript engine that we use to create a separate +// engine for our content scripts so that they do not run in the same +// context/namespace as the scripts that are part of the page. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ + +// The Platform SDK version of this header is newer than the +// Active Script SDK, and we use some newer interfaces. +#include <activscp.h> +#include <atlbase.h> +#include <atlcom.h> +#include <dispex.h> +#include <docobj.h> +#include <map> +#include <string> + +#include "base/lock.h" +#include "base/logging.h" +#include "third_party/activscp/activdbg.h" +#include "ceee/common/initializing_coclass.h" + +#ifndef OLESCRIPT_E_SYNTAX +#define OLESCRIPT_E_SYNTAX 0x80020101 +#endif + +extern const GUID IID_IScriptHost; + +// Interface for ScriptHost needed for unit testing. +class IScriptHost : public IUnknown { + public: + // Registers an IDispatch script object with the script host. + // @param name The name the object will have inside the script host. + // @param disp_obj The IDispatch object to register. + // @param make_members_global Whether or not to make the object's members + // global. + virtual HRESULT RegisterScriptObject(const wchar_t* name, + IDispatch* disp_obj, + bool make_members_global) = 0; + + // Run the specified script in the script host. + // @param file_path The name/path to the file for debugging. + // @param code The code to be executed. + virtual HRESULT RunScript(const wchar_t* file_path, const wchar_t* code) = 0; + + // Run the specified expression in the script host and get its return value. + // @param code The code to be executed. + // @param result A variant to write the result to. + virtual HRESULT RunExpression(const wchar_t* code, VARIANT* result) = 0; + + // Close the script host and release resources. + virtual HRESULT Close() = 0; +}; + +// Implementation of ScriptHost class. +// +// This implements the requisite IActiveScript{Debug} interfaces necessary +// to host a active script engine and to integrate with script debugging. +// It also exposes IServiceProvider and IOleCommandTarget, which serve the +// purpose of declaring the script code origin to the IE DOM. When our scripts +// invoke on the IE DOM through IDispatchEx::InvokeEx, the last parameter is +// a service provider that leads back to this host. The IE DOM implementation +// will query this service provider for SID_GetScriptSite, and acquire an +// IOleCommandTarget on it, which in turn gets interrogated about the scripts +// origin and security properties. +class ATL_NO_VTABLE ScriptHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<ScriptHost>, + public IActiveScriptSite, + public IActiveScriptSiteDebug, + public IServiceProviderImpl<ScriptHost>, + public IObjectWithSiteImpl<ScriptHost>, + public IOleCommandTarget, + public IScriptHost { + public: + BEGIN_COM_MAP(ScriptHost) + COM_INTERFACE_ENTRY(IActiveScriptSite) + COM_INTERFACE_ENTRY(IActiveScriptSiteDebug) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IOleCommandTarget) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY_IID(IID_IScriptHost, IScriptHost) + END_COM_MAP() + BEGIN_SERVICE_MAP(ScriptHost) + SERVICE_ENTRY(SID_GetScriptSite) + // We delegate to our site object. This allows the site to provide + // SID_SInternetHostSecurityManager, which can govern ActiveX control + // creation and the like. + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + ScriptHost(); + + // Fwd. + class DebugApplication; + + // Sets the default debug application script host instaces + // will use unless provided with another specific instance. + // Note: a DebugApplication instance set here must outlive all + // ScriptHost instances in this process. + static void set_default_debug_application(DebugApplication* debug) { + default_debug_application_ = debug; + } + static DebugApplication* default_debug_application() { + return default_debug_application_; + } + + // Initialize with a debug application and return a pointer to self. + // @param debug_application the debug application on whose behalf we're + // running. + // @param self returns a referenceless pointer to new instance. + HRESULT Initialize(DebugApplication* debug_application, ScriptHost** self); + + // Initialize with a debug application. + // @param debug_application the debug application on whose behalf we're + // running. + HRESULT Initialize(DebugApplication* debug_application); + + HRESULT Initialize(); + + void FinalRelease(); + + // IScriptHost methods. + HRESULT RegisterScriptObject(const wchar_t* name, IDispatch* disp_obj, + bool make_members_global); + HRESULT RunScript(const wchar_t* file_path, const wchar_t* code); + HRESULT RunExpression(const wchar_t* code, VARIANT* result); + HRESULT Close(); + + // IActiveScriptSite methods. + STDMETHOD(GetItemInfo)(LPCOLESTR item_name, DWORD return_mask, + IUnknown** item_iunknown, ITypeInfo** item_itypeinfo); + STDMETHOD(GetLCID)(LCID *plcid); + STDMETHOD(GetDocVersionString)(BSTR* version); + STDMETHOD(OnEnterScript)(); + STDMETHOD(OnLeaveScript)(); + STDMETHOD(OnStateChange)(SCRIPTSTATE state); + STDMETHOD(OnScriptTerminate)(const VARIANT* result, + const EXCEPINFO* excep_info); + STDMETHOD(OnScriptError)(IActiveScriptError* script_error); + + // IActiveScriptSiteDebug methods. + STDMETHOD(GetDocumentContextFromPosition)(DWORD source_context, + ULONG char_offset, ULONG num_chars, + IDebugDocumentContext** debug_doc_context); + STDMETHOD(GetApplication)(IDebugApplication** debug_app); + STDMETHOD(GetRootApplicationNode)(IDebugApplicationNode** debug_app_node); + STDMETHOD(OnScriptErrorDebug)(IActiveScriptErrorDebug* error_debug, + BOOL* enter_debugger, BOOL* call_on_script_err_when_continuing); + + + // @name IOleCommandTarget methods + // @{ + STDMETHOD(QueryStatus)(const GUID* cmd_group, + ULONG num_cmds, + OLECMD cmds[], + OLECMDTEXT *cmd_text); + STDMETHOD(Exec)(const GUID* cmd_group, + DWORD cmd_id, + DWORD cmd_exec_opt, + VARIANT* arg_in, + VARIANT* arg_out); + // @} + + // @name Debug-only functionality. + // @( + // Create a debug document for @p code, which originates from @p file + // and return it in @doc. + // @param file_path a file path or URL to code. + // @param code document code containing JavaScript snippets. + // @param doc on success returns the new debug document. + HRESULT AddDebugDocument(const wchar_t* file_path, + const wchar_t* code, + IDebugDocumentHelper** doc); + + // Run a JavaScript snippet from a previously declared document. + // @param start_offset character offset from start of document to + // @p code. + // @param code a code snippet from a document previously declared + // to AddDebugDocument. + // @param doc a debug document previously received from AddDebugDocument. + HRESULT RunScriptSnippet(size_t start_offset, + const wchar_t* code, + IDebugDocumentHelper* doc); + // @} + + protected: + // Virtual methods to inject dependencies for unit testing. + virtual HRESULT CreateScriptEngine(IActiveScript** script); + + private: + struct ScopedExcepInfo : public EXCEPINFO { + public: + ScopedExcepInfo() { + bstrSource = NULL; + bstrDescription = NULL; + bstrHelpFile = NULL; + } + ~ScopedExcepInfo() { + if (bstrSource) { + ::SysFreeString(bstrSource); + bstrSource = NULL; + } + if (bstrDescription) { + ::SysFreeString(bstrDescription); + bstrDescription = NULL; + } + if (bstrHelpFile) { + ::SysFreeString(bstrHelpFile); + bstrHelpFile = NULL; + } + } + }; + + HRESULT GetSourceContext(const wchar_t* file_path, + const wchar_t* code, + DWORD* source_context); + + // The JScript script engine. NULL until Initialize(). + CComPtr<IActiveScript> script_; + + // The JScript parser. NULL until Initialize(). + CComQIPtr<IActiveScriptParse> script_parse_; + + // A map of wstring to IDispatch pointers to hold registered script objects. + // Empty on Initialize(). Filled using calls to RegisterScriptObject(). + typedef std::map<std::wstring, CAdapt<CComPtr<IDispatch> > > ScriptObjectMap; + ScriptObjectMap script_objects_; + + // A map of source contexts to debug document helpers used for debugging. + // Empty on Initialize(). Filled using calls to CreateDebugDoc(). + // Resources released and emptied on Close(). + struct DebugDocInfo { + bool is_new; + size_t len; + CComPtr<IDebugDocumentHelper> document; + }; + typedef std::map<DWORD, DebugDocInfo> DebugDocMap; + DebugDocMap debug_docs_; + + // Our debug application state. + DebugApplication* debug_application_; + + // Default debug application state, which is used if no other state is + // provided at initialization time. + static DebugApplication* default_debug_application_; +}; + +class ScriptHost::DebugApplication { + public: + DebugApplication(const wchar_t* application_name); + ~DebugApplication(); + + // Best-effort initialize process script debugging. + // Every call to this function must be matched with a call to Terminate(). + void Initialize(); + + // Best-effort initialize process script debugging. + // @param manager_or_provider either implements IDebugApplication or + // IServiceProvider. Both methods will be tried in turn to aquire + // an IDebugApplication. If both methods fail, this will fall back + // to initialization by creating a new debug application. + void Initialize(IUnknown* debug_application_provider); + + // Exposed for testing only, needs a corresponding call to Terminate. + void Initialize(IProcessDebugManager* manager, IDebugApplication* app); + + // Terminate script debugging, call once for every call to Initialize(). + void Terminate(); + + // Creates a debug document helper with the given long name, containing code. + HRESULT CreateDebugDocumentHelper(const wchar_t* long_name, + const wchar_t* code, + TEXT_DOC_ATTR attributes, + IDebugDocumentHelper** helper); + // Retrieve the debug application. + HRESULT GetDebugApplication(IDebugApplication** application); + // Retrieve root application node. + HRESULT GetRootApplicationNode(IDebugApplicationNode** debug_app_node); + + private: + // Virtual for testing. + virtual HRESULT CreateProcessDebugManager(IProcessDebugManager** manager); + virtual HRESULT CreateDebugDocumentHelper(IDebugDocumentHelper** helper); + + // Creates and registers a new debug application for us. + void RegisterDebugApplication(); // Under lock_. + + // Protects all members below. + ::Lock lock_; // Our containing class has a Lock method. + + // Number of initialization calls. + size_t initialization_count_; + + // The debug manager, non-NULL only if we register + // our own debug application. + CComPtr<IProcessDebugManager> debug_manager_; + + // Registration cookie for debug_manager_ registration of debug_application_. + DWORD debug_app_cookie_; + + // The debug application to be used for debugging. NULL until Initialize(). + CComPtr<IDebugApplication> debug_application_; + + static const DWORD kInvalidDebugAppCookie = 0; + + // The application name we register. + const wchar_t* application_name_; +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_SCRIPT_HOST_H_ diff --git a/ceee/ie/plugin/scripting/script_host_unittest.cc b/ceee/ie/plugin/scripting/script_host_unittest.cc new file mode 100644 index 0000000..4e52874 --- /dev/null +++ b/ceee/ie/plugin/scripting/script_host_unittest.cc @@ -0,0 +1,519 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Script host implementation unit tests. + +#include "ceee/ie/plugin/scripting/script_host.h" + +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/mshtml_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::InstanceCountMixin; +using testing::MockDispatchEx; +using testing::MockIServiceProvider; + +using testing::_; +using testing::AddRef; +using testing::AllOf; +using testing::CopyInterfaceToArgument; +using testing::DispParamArgEq; +using testing::DoAll; +using testing::Eq; +using testing::Return; +using testing::SetArgumentPointee; +using testing::StrictMock; +using testing::StrEq; + +class MockIHTMLWindow2 + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<IHTMLWindow2MockImpl>, + public InstanceCountMixin<MockIHTMLWindow2> { + public: + BEGIN_COM_MAP(MockIHTMLWindow2) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IHTMLWindow2) + COM_INTERFACE_ENTRY(IHTMLFramesCollection2) + END_COM_MAP() + + HRESULT Initialize(MockIHTMLWindow2** self) { + *self = this; + return S_OK; + } +}; + +class MockProcessDebugManager + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IProcessDebugManagerMockImpl>, + public InstanceCountMixin<MockProcessDebugManager> { + public: + BEGIN_COM_MAP(MockProcessDebugManager) + COM_INTERFACE_ENTRY(IProcessDebugManager) + END_COM_MAP() + + HRESULT Initialize(MockProcessDebugManager** debug_manager) { + *debug_manager = this; + return S_OK; + } +}; + +class MockDebugApplication + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IDebugApplicationMockImpl>, + public InstanceCountMixin<MockDebugApplication> { + public: + BEGIN_COM_MAP(MockDebugApplication) + COM_INTERFACE_ENTRY(IDebugApplication) + END_COM_MAP() + + HRESULT Initialize(MockDebugApplication** debug_application) { + *debug_application = this; + return S_OK; + } +}; + +class MockDebugDocumentHelper + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IDebugDocumentHelperMockImpl>, + public InstanceCountMixin<MockDebugDocumentHelper> { + public: + BEGIN_COM_MAP(MockDebugDocumentHelper) + COM_INTERFACE_ENTRY(IDebugDocumentHelper) + END_COM_MAP() + + HRESULT Initialize(MockDebugDocumentHelper** debug_document) { + *debug_document = this; + return S_OK; + } +}; + +const wchar_t* kDebugApplicationName = L"ScriptHostTest"; + +class TestingDebugApplication : public ScriptHost::DebugApplication { + public: + TestingDebugApplication() + : ScriptHost::DebugApplication(kDebugApplicationName) { + } + + MOCK_METHOD1(CreateProcessDebugManager, + HRESULT(IProcessDebugManager** manager)); +}; + +class MockIActiveScriptAndParse + : public CComObjectRootEx<CComSingleThreadModel>, + public StrictMock<testing::IActiveScriptMockImpl>, + public StrictMock<testing::IActiveScriptParseMockImpl>, + public StrictMock<testing::IObjectSafetyMockImpl>, + public InstanceCountMixin<MockIActiveScriptAndParse> { + public: + BEGIN_COM_MAP(MockIActiveScriptAndParse) + COM_INTERFACE_ENTRY(IActiveScript) + COM_INTERFACE_ENTRY(IActiveScriptParse) + COM_INTERFACE_ENTRY(IObjectSafety) + END_COM_MAP() + + HRESULT Initialize(MockIActiveScriptAndParse** script) { + *script = this; + return S_OK; + } +}; + +class TestingScriptHost + : public ScriptHost, + public InstanceCountMixin<TestingScriptHost> { + public: + HRESULT Initialize(ScriptHost::DebugApplication* debug, + IActiveScript* script) { + my_script_ = script; + ScriptHost::Initialize(debug); + return S_OK; + } + + private: + HRESULT CreateDebugManager(IProcessDebugManager** debug_manager) { + return my_debug_manager_.CopyTo(debug_manager); + } + HRESULT CreateScriptEngine(IActiveScript** script) { + return my_script_.CopyTo(script); + } + + CComPtr<IProcessDebugManager> my_debug_manager_; + CComPtr<IActiveScript> my_script_; +}; + +const DISPID kDispId = 5; +const wchar_t* kDispObjName = L"tpain"; +const wchar_t* kDispObjName2 = L"akon"; +const wchar_t* kJsFilePath = L"liljon.js"; +const wchar_t* kJsCode = L"alert('WWHHHATTT? OOOKKAAYY.')"; +const DWORD kAddFlags = SCRIPTITEM_ISSOURCE | SCRIPTITEM_ISVISIBLE; +const DWORD kAddGlobalFlags = kAddFlags | SCRIPTITEM_GLOBALMEMBERS; +const DWORD kSourceContext = 123456; +const DWORD kUnknownSourceContext = 654321; +const DWORD kNoSourceContext = 0; +const DWORD kScriptFlags = + SCRIPTTEXT_HOSTMANAGESSOURCE | SCRIPTTEXT_ISVISIBLE; +const DWORD kExpressionFlags = SCRIPTTEXT_ISEXPRESSION; +const ULONG kCharOffset = 1234; +const ULONG kNumChars = 4321; + +class ScriptHostTest : public testing::Test { + public: + ScriptHostTest() : mock_debug_manager_(NULL), mock_debug_application_(NULL), + mock_script_(NULL), debug_(kDebugApplicationName) { + } + + void SetUp() { + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockProcessDebugManager>:: + CreateInitializedIID(&mock_debug_manager_, + IID_IProcessDebugManager, + &debug_manager_)); + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDebugApplication>:: + CreateInitializedIID(&mock_debug_application_, + IID_IDebugApplication, + &debug_application_)); + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIActiveScriptAndParse>::CreateInitializedIID( + &mock_script_, IID_IActiveScript, &script_)); + + debug_.Initialize(debug_manager_, debug_application_); + } + + void ExpectEngineInitialization() { + EXPECT_CALL(*mock_script_, SetScriptSite(_)).WillOnce( + Return(S_OK)); + EXPECT_CALL(*mock_script_, InitNew()).WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_script_, SetScriptState(SCRIPTSTATE_CONNECTED)).WillOnce( + Return(S_OK)); + EXPECT_CALL(*mock_script_, SetInterfaceSafetyOptions( + IID_IDispatch, INTERFACE_USES_SECURITY_MANAGER, + INTERFACE_USES_SECURITY_MANAGER)).WillOnce(Return(S_OK)); + } + + void TearDown() { + if (script_host_) { + EXPECT_CALL(*mock_script_, Close()).WillOnce(Return(S_OK)); + + script_host_->Close(); + } + + mock_debug_manager_ = NULL; + debug_manager_.Release(); + + mock_debug_application_ = NULL; + debug_application_.Release(); + + mock_script_ = NULL; + script_.Release(); + + script_host_.Release(); + + debug_.Terminate(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateScriptHost() { + ExpectEngineInitialization(); + + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<TestingScriptHost>::CreateInitializedIID( + &debug_, script_, IID_IScriptHost, &script_host_)); + } + + void ExpectCreateDebugDocumentHelper( + MockDebugDocumentHelper** mock_debug_document) { + CComPtr<IDebugDocumentHelper> debug_document; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDebugDocumentHelper>::CreateInitialized( + mock_debug_document, &debug_document)); + + EXPECT_CALL(*mock_debug_manager_, CreateDebugDocumentHelper(NULL, _)) + .WillOnce(DoAll(CopyInterfaceToArgument<1>(debug_document), + Return(S_OK))); + + EXPECT_CALL(**mock_debug_document, + Init(Eq(debug_application_), StrEq(kJsFilePath), StrEq(kJsFilePath), _)) + .WillOnce(Return(S_OK)); + EXPECT_CALL(**mock_debug_document, Attach(NULL)).WillOnce(Return(S_OK)); + EXPECT_CALL(**mock_debug_document, SetDebugDocumentHost(_)).WillOnce( + Return(S_OK)); + EXPECT_CALL(**mock_debug_document, + DefineScriptBlock(_, _, Eq(script_), _, _)) + .WillOnce(DoAll(SetArgumentPointee<4>(kSourceContext), Return(S_OK))); + + // Detach will be called on this document when the script host is closed + EXPECT_CALL(**mock_debug_document, Detach()).WillOnce(Return(S_OK)); + } + + void ExpectParseScriptText(DWORD source_context, DWORD flags) { + EXPECT_CALL(*mock_script_, ParseScriptText(StrEq(kJsCode), NULL, NULL, NULL, + source_context, 0, flags, _, _)) + .WillOnce(Return(S_OK)); + } + + MockProcessDebugManager* mock_debug_manager_; + CComPtr<IProcessDebugManager> debug_manager_; + MockDebugApplication* mock_debug_application_; + CComPtr<IDebugApplication> debug_application_; + MockIActiveScriptAndParse* mock_script_; + CComPtr<IActiveScript> script_; + CComPtr<IScriptHost> script_host_; + + ScriptHost::DebugApplication debug_; +}; + +TEST_F(ScriptHostTest, DebugApplicationDefaultInitialize) { + TestingDebugApplication debug; + + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_manager_), + Return(S_OK))); + + EXPECT_CALL(*mock_debug_manager_, CreateApplication(_)).WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_application_), Return(S_OK))); + EXPECT_CALL(*mock_debug_application_, SetName(StrEq(kDebugApplicationName))) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_debug_manager_, AddApplication( + static_cast<IDebugApplication*>(debug_application_), _)) + .WillOnce(DoAll(SetArgumentPointee<1>(42), Return(S_OK))); + + debug.Initialize(); + + // We expect the script host to undo debug app registration. + EXPECT_CALL(*mock_debug_manager_, RemoveApplication(42)) + .WillOnce(Return(S_OK)); + debug.Terminate(); +} + +// Test initializing debugging with a service provider. +TEST_F(ScriptHostTest, DebugApplicationInitializeWithServiceProvider) { + TestingDebugApplication debug; + + MockIServiceProvider* sp_keeper; + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIServiceProvider>::CreateInitialized( + &sp_keeper, &sp)); + + + EXPECT_CALL(*sp_keeper, + QueryService(IID_IDebugApplication, IID_IDebugApplication, _)) + .WillOnce(DoAll( + AddRef(debug_manager_.p), + SetArgumentPointee<2>(static_cast<void*>(debug_manager_)), + Return(S_OK))); + + // Initialization with a service provider that yields + // a debug application should only query service, and + // not try to create a debug manager. + EXPECT_CALL(debug, CreateProcessDebugManager(_)).Times(0); + + debug.Initialize(sp); + + // And there should be no app unregistration. + debug.Terminate(); +} + +TEST_F(ScriptHostTest, DebugApplicationInitializeWithEmptyServiceProvider) { + TestingDebugApplication debug; + + MockIServiceProvider* sp_keeper; + CComPtr<IServiceProvider> sp; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIServiceProvider>::CreateInitialized( + &sp_keeper, &sp)); + + + EXPECT_CALL(*sp_keeper, + QueryService(IID_IDebugApplication, IID_IDebugApplication, _)) + .WillOnce(Return(E_NOINTERFACE)); + + // Initialization with a service provider that yields + // no debug application should go the default route. + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_manager_), + Return(S_OK))); + + EXPECT_CALL(*mock_debug_manager_, CreateApplication(_)).WillOnce( + DoAll(CopyInterfaceToArgument<0>(debug_application_), Return(S_OK))); + EXPECT_CALL(*mock_debug_application_, SetName(StrEq(kDebugApplicationName))) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*mock_debug_manager_, AddApplication( + static_cast<IDebugApplication*>(debug_application_), _)) + .WillOnce(DoAll(SetArgumentPointee<1>(42), Return(S_OK))); + + debug.Initialize(sp); + + // And the registration should be undone. + EXPECT_CALL(*mock_debug_manager_, RemoveApplication(42)) + .WillOnce(Return(S_OK)); + debug.Terminate(); +} + +TEST_F(ScriptHostTest, DebugApplicationInitFailure) { + TestingDebugApplication debug; + + EXPECT_CALL(debug, CreateProcessDebugManager(_)) + .WillOnce(Return(REGDB_E_CLASSNOTREG)); + + debug.Initialize(); + + CComPtr<IDebugDocumentHelper> helper; + EXPECT_HRESULT_SUCCEEDED( + debug.CreateDebugDocumentHelper(kJsFilePath, + kJsCode, + 0, + &helper)); + EXPECT_TRUE(helper == NULL); + + // This must fail when debugging is not present. + CComPtr<IDebugApplication> debug_app; + EXPECT_HRESULT_FAILED(debug.GetDebugApplication(&debug_app)); + ASSERT_TRUE(debug_app == NULL); + + // Whereas this should succeed, but return a NULL node. + CComPtr<IDebugApplicationNode> root_node; + EXPECT_HRESULT_SUCCEEDED(debug.GetRootApplicationNode(&root_node)); + ASSERT_TRUE(root_node == NULL); + + debug.Terminate(); +} + +TEST_F(ScriptHostTest, QueryInterface) { + CreateScriptHost(); + + CComQIPtr<IActiveScriptSite> script_site(script_host_); + ASSERT_TRUE(script_site != NULL); + + CComQIPtr<IActiveScriptSiteDebug> script_site_debug(script_host_); + ASSERT_TRUE(script_site_debug != NULL); +} + +TEST_F(ScriptHostTest, RegisterScriptObject) { + CreateScriptHost(); + + MockIHTMLWindow2* mock_dispatch_obj; + CComPtr<IHTMLWindow2> dispatch_obj; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockIHTMLWindow2>::CreateInitialized( + &mock_dispatch_obj, &dispatch_obj)); + + // Add a non global script object. + EXPECT_CALL(*mock_script_, AddNamedItem(StrEq(kDispObjName), kAddFlags)) + .WillOnce(Return(S_OK)); + HRESULT hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, + false); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Add a global script object. + EXPECT_CALL(*mock_script_, + AddNamedItem(StrEq(kDispObjName2), kAddGlobalFlags)) + .WillOnce(Return(S_OK)); + hr = script_host_->RegisterScriptObject(kDispObjName2, dispatch_obj, + true); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Add a duplicate named object. + hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, false); + EXPECT_EQ(hr, E_ACCESSDENIED); +} + +TEST_F(ScriptHostTest, RegisterScriptObjectAndGetItemInfo) { + CreateScriptHost(); + + MockDispatchEx* mock_dispatch_obj; + CComPtr<IDispatchEx> dispatch_obj; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized( + &mock_dispatch_obj, &dispatch_obj)); + + // Add a non global script object. + EXPECT_CALL(*mock_script_, AddNamedItem(StrEq(kDispObjName), kAddFlags)) + .WillOnce(Return(S_OK)); + HRESULT hr = script_host_->RegisterScriptObject(kDispObjName, dispatch_obj, + false); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Make sure we return the object when it's called for. + EXPECT_CALL(*mock_dispatch_obj, GetTypeInfo(0, LANG_NEUTRAL, _)) + .WillOnce(Return(S_OK)); + CComQIPtr<IActiveScriptSite> script_site(script_host_); + ASSERT_TRUE(script_site != NULL); + + CComPtr<IUnknown> item_iunknown; + CComPtr<ITypeInfo> item_itypeinfo; + hr = script_site->GetItemInfo(kDispObjName, + SCRIPTINFO_IUNKNOWN | SCRIPTINFO_ITYPEINFO, + &item_iunknown, &item_itypeinfo); + ASSERT_HRESULT_SUCCEEDED(hr); + EXPECT_EQ(dispatch_obj, item_iunknown); +} + +TEST_F(ScriptHostTest, RunScript) { + CreateScriptHost(); + + MockDebugDocumentHelper* mock_debug_document; + ExpectCreateDebugDocumentHelper(&mock_debug_document); + ExpectParseScriptText(kSourceContext, kScriptFlags); + + HRESULT hr = script_host_->RunScript(kJsFilePath, kJsCode); + ASSERT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(ScriptHostTest, RunExpression) { + CreateScriptHost(); + + ExpectParseScriptText(kNoSourceContext, kExpressionFlags); + + CComVariant dummy; + HRESULT hr = script_host_->RunExpression(kJsCode, &dummy); + ASSERT_HRESULT_SUCCEEDED(hr); +} + +TEST_F(ScriptHostTest, RunScriptAndGetDocumentContextFromPosition) { + CreateScriptHost(); + + MockDebugDocumentHelper* mock_debug_document; + ExpectCreateDebugDocumentHelper(&mock_debug_document); + ExpectParseScriptText(kSourceContext, kScriptFlags); + + script_host_->RunScript(kJsFilePath, kJsCode); + + CComQIPtr<IActiveScriptSiteDebug> script_site_debug(script_host_); + ASSERT_TRUE(script_site_debug != NULL); + + EXPECT_CALL(*mock_debug_document, + GetScriptBlockInfo(kSourceContext, NULL, _, NULL)) + .WillOnce(DoAll(SetArgumentPointee<2>(kCharOffset), Return(S_OK))); + EXPECT_CALL(*mock_debug_document, + CreateDebugDocumentContext(2 * kCharOffset, kNumChars, _)) + .WillOnce(Return(S_OK)); + + + // Call with a known source context. + CComPtr<IDebugDocumentContext> dummy_debug_document_context; + HRESULT hr = script_site_debug->GetDocumentContextFromPosition( + kSourceContext, kCharOffset, kNumChars, &dummy_debug_document_context); + ASSERT_HRESULT_SUCCEEDED(hr); + + // Call with an unknown source context. + hr = script_site_debug->GetDocumentContextFromPosition( + kUnknownSourceContext, kCharOffset, kNumChars, + &dummy_debug_document_context); + ASSERT_TRUE(hr == E_FAIL); +} + +} // namespace diff --git a/ceee/ie/plugin/scripting/scripting.gyp b/ceee/ie/plugin/scripting/scripting.gyp new file mode 100644 index 0000000..d7593e7 --- /dev/null +++ b/ceee/ie/plugin/scripting/scripting.gyp @@ -0,0 +1,94 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + '../../../../ceee/common.gypi', + ], + 'targets': [ + { + 'target_name': 'scripting', + 'type': 'static_library', + 'dependencies': [ + 'javascript_bindings', + '../../common/common.gyp:ie_common', + '../../common/common.gyp:ie_common_settings', + '../toolband/toolband.gyp:toolband_idl', + '../../../../base/base.gyp:base', + '../../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + 'base.js', + 'ceee_bootstrap.js', + 'json.js', + 'content_script_manager.cc', + 'content_script_manager.h', + 'content_script_manager.rc', + 'content_script_native_api.cc', + 'content_script_native_api.h', + '../../common/precompile.cc', + '../../common/precompile.h', + 'script_host.cc', + 'script_host.h', + 'userscripts_librarian.cc', + 'userscripts_librarian.h', + 'userscripts_docs.h', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'javascript_bindings', + 'type': 'none', + 'variables': { + 'chrome_renderer_path' : '../../../../chrome/renderer', + 'input_js_files': [ + '<(chrome_renderer_path)/resources/event_bindings.js', + '<(chrome_renderer_path)/resources/renderer_extension_bindings.js', + ], + 'output_js_files': [ + '<(SHARED_INTERMEDIATE_DIR)/event_bindings.js', + '<(SHARED_INTERMEDIATE_DIR)/renderer_extension_bindings.js', + ], + }, + 'sources': [ + 'transform_native_js.py', + '<@(input_js_files)', + ], + 'actions': [ + { + 'action_name': 'transform_native_js', + 'msvs_cygwin_shell': 0, + 'msvs_quote_cmd': 0, + 'inputs': [ + '<@(_sources)', + ], + 'outputs': [ + '<@(output_js_files)', + ], + 'action': [ + '<@(python)', + 'transform_native_js.py', + '<@(input_js_files)', + '-o', + '<(SHARED_INTERMEDIATE_DIR)', + ], + }, + ], + # Make sure our dependents can refer to the transformed + # files from their .rc file(s). + 'direct_dependent_settings': { + 'resource_include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + ] +} diff --git a/ceee/ie/plugin/scripting/transform_native_js.py b/ceee/ie/plugin/scripting/transform_native_js.py new file mode 100644 index 0000000..b77ffc7 --- /dev/null +++ b/ceee/ie/plugin/scripting/transform_native_js.py @@ -0,0 +1,53 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +'''A GYP script action file to transform native JS function declarations. + +Any line in the input script of the form + "native function <NAME>(<ARGS>);" +will be transformed to + "var <NAME> = ceee.<NAME>; // function(<ARGS>)" +''' + +import optparse +import os.path +import re +import sys + +_NATIVE_FUNCTION_RE = re.compile( + 'native\s+function\s+(?P<name>\w+)\((?P<args>[^)]*)\)\s*;') + + +def TransformFile(input_file, output_file): + '''Transform native functions in input_file, write to output_file''' + # Slurp the input file. + contents = open(input_file, "r").read() + + repl = 'var \g<name> = ceee.\g<name>; // function(\g<args>)' + contents = _NATIVE_FUNCTION_RE.sub(repl, contents,) + + # Write the output file. + open(output_file, "w").write(contents) + + +def GetOptionParser(): + parser = optparse.OptionParser(description=__doc__) + parser.add_option('-o', dest='output_dir', + help='Output directory') + return parser + +def Main(): + parser = GetOptionParser() + (opts, args) = parser.parse_args() + if not opts.output_dir: + parser.error('You must provide an output directory') + + for input_file in args: + output_file = os.path.join(opts.output_dir, os.path.basename(input_file)) + TransformFile(input_file, output_file) + + return 0 + + +if __name__ == '__main__': + sys.exit(Main()) diff --git a/ceee/ie/plugin/scripting/userscripts_docs.h b/ceee/ie/plugin/scripting/userscripts_docs.h new file mode 100644 index 0000000..c9aded9 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_docs.h @@ -0,0 +1,66 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +#ifndef CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ // Mainly for lint +#define CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ + +/** @page UserScriptsDoc Detailed documentation of the UserScriptsLibrarian. + +@section UserScriptsLibrarianIntro Introduction + +We need to be able to load the user scripts information (including url pattern +matching info) from an extension manifest file and make it available for +insertion in a WEB page as needed. + +@section IE + +We already have a class (ExtensionManifest) that reads information from a +manifest file that was first needed to load the toolstrip info. So we need to +add code in there to load the information related to user scripts. + +Since this class has a one to one relationship with a manifest file, we need +another one to hold on all of the scripts from all the extensions that we will +eventually read from and make them accessible to the page API to load the ones +that match the current URL. + +So this other class (UserScriptsLibrarian) has a method to add UserScripts and +to retrieve the CSS and JavaScript content of the ones that match a given URL. +It reuses the UserScript object. + +TODO(siggi@chromium.org): Unbranch this code; a lot of it was taken +from (and adapted) the UserScriptSlave class which is used to apply +user scripts to a given WebKit frame. + +Our version simply returns the JavaScript code and CSS as text and let the +PageApi class take care of the insertion in the browser script engine. + +The PageAPI code must react to the earliest event after the creation of the +HTML document so that we can inject the CSS content before the page is rendered. +For the user script, we have not yet found a way to inject the scripts that +specify the start location. + +@section Firefox + +TODO: The implementation has changed, so we should update this doc. + +@section IEFF For both IE and Firefox + +The JavaScript code returned by the functions extracting them from the user +scripts contain the proper heading to emulate the Greasemonkey API +(taken from greasemonkey_api.js) for running scripts marked as stand alone +(i.e., an extension without a public key), and it also wraps them +individually within an anonymous function. + +The code taking care of injecting the code in chromium (in UserScriptSlave) +concatenate all the scripts of a dictionary entry in the +manifest file, and executes them in a separate context. +@note However, this is being changed: http://crbug.com/22110. + +We don't have this capacity yet but the code must be written in a way that it +will be easy to add this later on, so the methods returning the script code +should only concatenate together the scripts of a single dictionary entry at a +time. The CSS styles can all be bundled up in a single string though. +**/ + +#endif // CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_DOCS_H_ diff --git a/ceee/ie/plugin/scripting/userscripts_librarian.cc b/ceee/ie/plugin/scripting/userscripts_librarian.cc new file mode 100644 index 0000000..adeb4e3 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian.cc @@ -0,0 +1,119 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to manage user scripts. + +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/string_util.h" + +UserScriptsLibrarian::UserScriptsLibrarian() { +} + +UserScriptsLibrarian::~UserScriptsLibrarian() { +} + +HRESULT UserScriptsLibrarian::AddUserScripts( + const UserScriptList& user_scripts) { + // Note that we read the userscript files from disk every time that a frame + // loads (one BHO and one librarian per frame). This offers us the advantage + // of not having to reload scripts into memory when an extension autoupdates + // in Chrome. If we decide to cache these scripts in memory, then we will need + // a Chrome extension autoupdate automation notification. + + size_t i = user_scripts_.size(); + user_scripts_.insert(user_scripts_.end(), user_scripts.begin(), + user_scripts.end()); + for (; i < user_scripts_.size(); ++i) { + UserScript& user_script = user_scripts_[i]; + UserScript::FileList& js_scripts = user_script.js_scripts(); + LoadFiles(&js_scripts); + UserScript::FileList& css_scripts = user_script.css_scripts(); + LoadFiles(&css_scripts); + } + + return S_OK; +} + +bool UserScriptsLibrarian::HasMatchingUserScripts(const GURL& url) const { + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + if (user_script.MatchesUrl(url)) + return true; + } + return false; +} + +HRESULT UserScriptsLibrarian::GetMatchingUserScriptsCssContent( + const GURL& url, bool require_all_frames, std::string* css_content) const { + DCHECK(css_content); + if (!css_content) + return E_POINTER; + + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + if (!user_script.MatchesUrl(url) || + (require_all_frames && !user_script.match_all_frames())) + continue; + + for (size_t j = 0; j < user_script.css_scripts().size(); ++j) { + const UserScript::File& file = user_script.css_scripts()[j]; + *css_content += file.GetContent().as_string(); + } + } + + return S_OK; +} + +HRESULT UserScriptsLibrarian::GetMatchingUserScriptsJsContent( + const GURL& url, UserScript::RunLocation location, bool require_all_frames, + JsFileList* js_file_list) { + DCHECK(js_file_list); + if (!js_file_list) + return E_POINTER; + + if (!user_scripts_.empty()) { + for (size_t i = 0; i < user_scripts_.size(); ++i) { + const UserScript& user_script = user_scripts_[i]; + + // TODO(ericdingle@chromium.org): Remove the fourth and fifth + // conditions once DOCUMENT_IDLE is supported. + if (!user_script.MatchesUrl(url) || + (require_all_frames && !user_script.match_all_frames()) || + user_script.run_location() != location && + (user_script.run_location() != UserScript::DOCUMENT_IDLE || + location != UserScript::DOCUMENT_END)) + continue; + + for (size_t j = 0; j < user_script.js_scripts().size(); ++j) { + const UserScript::File& file = user_script.js_scripts()[j]; + + js_file_list->push_back(JsFile()); + JsFile& js_file = (*js_file_list)[js_file_list->size()-1]; + js_file.file_path = + file.extension_root().Append(file.relative_path()).value(); + js_file.content = file.GetContent().as_string(); + } + } + } + + return S_OK; +} + +void UserScriptsLibrarian::LoadFiles(UserScript::FileList* file_list) { + for (size_t i = 0; i < file_list->size(); ++i) { + UserScript::File& script_file = (*file_list)[i]; + // The content may have been set manually (e.g., for unittests). + // So we first check if they need to be loaded from the file, or not. + if (script_file.GetContent().empty()) { + std::string content; + file_util::ReadFileToString( + script_file.extension_root().Append( + script_file.relative_path()), &content); + script_file.set_content(content); + } + } +} diff --git a/ceee/ie/plugin/scripting/userscripts_librarian.h b/ceee/ie/plugin/scripting/userscripts_librarian.h new file mode 100644 index 0000000..e554a08 --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian.h @@ -0,0 +1,73 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Utility class to maintain user scripts and their matching URLs. + +#ifndef CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ +#define CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ + +#include <windows.h> + +#include <string> +#include <vector> + +#include "chrome/common/extensions/user_script.h" +#include "googleurl/src/gurl.h" + + +// The manager of UserScripts responsible for loading them from an extension +// and then returning the ones that match a given URL. +// TODO(mad@chromium.org): Find a way to reuse code from +// chrome/renderer/user_script_slave. +class UserScriptsLibrarian { + public: + typedef struct { + std::wstring file_path; + std::string content; + } JsFile; + typedef std::vector<JsFile> JsFileList; + + UserScriptsLibrarian(); + ~UserScriptsLibrarian(); + + // Adds a list of users scripts to our current list. + HRESULT AddUserScripts(const UserScriptList& user_scripts); + + // Identifies if we have any userscript that would match the given URL. + bool HasMatchingUserScripts(const GURL& url) const; + + // Retrieve the CSS content from user scripts that match the given URL. + // @param url The URL to match. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param css_content The single stream of CSS content. + HRESULT GetMatchingUserScriptsCssContent( + const GURL& url, + bool require_all_frames, + std::string* css_content) const; + + // Retrieve the JS content from user scripts that match the given URL. + // @param url The URL to match. + // @param location The location where the scripts will be run at. + // @param require_all_frames Whether to require the all_frames property of the + // user script to be true. + // @param js_file_list A map of file names to JavaScript content to allow the + // caller to apply them individually (e.g., each with their own script + // engine). + HRESULT GetMatchingUserScriptsJsContent( + const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + JsFileList* js_file_list); + + private: + // A helper function to load the content of a script file if it has not + // already been done, or explicitly set. + void LoadFiles(UserScript::FileList* file_list); + + UserScriptList user_scripts_; + DISALLOW_COPY_AND_ASSIGN(UserScriptsLibrarian); +}; + +#endif // CEEE_IE_PLUGIN_SCRIPTING_USERSCRIPTS_LIBRARIAN_H_ diff --git a/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc b/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc new file mode 100644 index 0000000..0a3a22b --- /dev/null +++ b/ceee/ie/plugin/scripting/userscripts_librarian_unittest.cc @@ -0,0 +1,332 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit tests for ExtensionManifest. + +#include <atlconv.h> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "ceee/ie/plugin/scripting/userscripts_librarian.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kExtensionId[] = "abcdefghijklmnopqabcdefghijklmno"; +const char kCssContent[] = "body:after {content: \"The End\";}"; +const char kJsContent[] = "alert('red');"; +const char kUrlPattern1[] = "http://madlymad.com/*"; +const char kUrlPattern2[] = "https://superdave.com/*"; +const char kUrl1[] = "http://madlymad.com/index.html"; +const char kUrl2[] = "https://superdave.com/here.blue"; +const char kUrl3[] = "http://not.here.com/there.where"; +const wchar_t kJsPath1[] = L"script1.js"; +const wchar_t kJsPath2[] = L"script2.js"; +const wchar_t kCssFileName[] = L"CssFile.css"; + +TEST(UserScriptsLibrarianTest, Empty) { + testing::LogDisabler no_dchecks; + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(UserScriptList()); + + std::string css; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(), true, &css)); + EXPECT_TRUE(css.empty()); + + UserScriptsLibrarian::JsFileList js; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(), UserScript::DOCUMENT_START, true, &js)); + EXPECT_TRUE(js.empty()); +} + +TEST(UserScriptsLibrarianTest, SingleScript) { + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + // Set up the librarian. + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Matching URL + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Non matching URL + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl2), false, &css_content)); + EXPECT_TRUE(css_content.empty()); + + // Matching URL and run location. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Matching URL and non matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_END, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Non matching URL and matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Non matching URL and non matching run location. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_END, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); +} + +TEST(UserScriptsLibrarianTest, MultipleScript) { + testing::LogDisabler no_dchecks; + + // Set up one UserScript object + URLPattern url_pattern1(UserScript::kValidUserScriptSchemes); + url_pattern1.Parse(kUrlPattern1); + + UserScript user_script1; + user_script1.add_url_pattern(url_pattern1); + user_script1.set_extension_id(kExtensionId); + + user_script1.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script1.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script1.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script1.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list1; + user_script_list1.push_back(user_script1); + + // Set up a second UserScript object + URLPattern url_pattern2(UserScript::kValidUserScriptSchemes); + url_pattern2.Parse(kUrlPattern2); + + UserScript user_script2; + user_script2.add_url_pattern(url_pattern2); + user_script2.set_extension_id(kExtensionId); + + user_script2.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file2 = user_script2.js_scripts().back(); + js_file2.set_content(kJsContent); + + user_script_list1.push_back(user_script2); + + // Set up the librarian. + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list1); + + // Matching URL + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(css_content.c_str(), kCssContent); + + // Matching URL and non matching location + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); + + // Matching URL and location + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_END, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Matching URL and location, shouldn't have extension init + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl2), UserScript::DOCUMENT_END, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); +} + +TEST(UserScriptsLibrarianTest, SingleScriptFromFile) { + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + FilePath extension_folder; + EXPECT_TRUE(file_util::GetTempDir(&extension_folder)); + FilePath css_path(extension_folder.Append(kCssFileName)); + + user_script.css_scripts().push_back(UserScript::File( + extension_folder, FilePath(kCssFileName), GURL())); + + FILE* temp_file = file_util::OpenFile(css_path, "w"); + EXPECT_TRUE(temp_file != NULL); + + fwrite(kCssContent, ::strlen(kCssContent), 1, temp_file); + file_util::CloseFile(temp_file); + temp_file = NULL; + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + EXPECT_TRUE(file_util::Delete(css_path, false)); + + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + EXPECT_EQ(0, js_content_list.size()); +} + +TEST(UserScriptsLibrarianTest, MatchAllFramesFalse) { + // all_frames is set to false. We should only get user scripts back + // when we pass false for require_all_frames to GetMatching*. + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Get CSS with require_all_frames set to false. + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get CSS with require_all_frames set to true. + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), true, &css_content)); + EXPECT_TRUE(css_content.empty()); + + // Get JS with require_all_frames set to false. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Get JS with require_all_frames set to true. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, true, &js_content_list)); + EXPECT_TRUE(js_content_list.empty()); +} + +TEST(UserScriptsLibrarianTest, MatchAllFramesTrue) { + // all_frames is set to true. We should get user scripts back + // when we pass any value for require_all_frames to GetMatching*. + testing::LogDisabler no_dchecks; + + URLPattern url_pattern(UserScript::kValidUserScriptSchemes); + url_pattern.Parse(kUrlPattern1); + + UserScript user_script; + user_script.add_url_pattern(url_pattern); + user_script.set_extension_id(kExtensionId); + user_script.set_run_location(UserScript::DOCUMENT_START); + user_script.set_match_all_frames(true); + + user_script.css_scripts().push_back(UserScript::File()); + UserScript::File& css_file = user_script.css_scripts().back(); + css_file.set_content(kCssContent); + + user_script.js_scripts().push_back(UserScript::File()); + UserScript::File& js_file = user_script.js_scripts().back(); + js_file.set_content(kJsContent); + + UserScriptList user_script_list; + user_script_list.push_back(user_script); + + UserScriptsLibrarian librarian; + librarian.AddUserScripts(user_script_list); + + // Get CSS with require_all_frames set to false. + std::string css_content; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), false, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get CSS with require_all_frames set to true. + css_content.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsCssContent( + GURL(kUrl1), true, &css_content)); + EXPECT_STREQ(kCssContent, css_content.c_str()); + + // Get JS with require_all_frames set to false. + UserScriptsLibrarian::JsFileList js_content_list; + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, false, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); + + // Get JS with require_all_frames set to true. + js_content_list.clear(); + EXPECT_HRESULT_SUCCEEDED(librarian.GetMatchingUserScriptsJsContent( + GURL(kUrl1), UserScript::DOCUMENT_START, true, &js_content_list)); + ASSERT_EQ(1, js_content_list.size()); + EXPECT_STREQ(kJsContent, js_content_list[0].content.c_str()); +} + +} // namespace diff --git a/ceee/ie/plugin/toolband/resource.h b/ceee/ie/plugin/toolband/resource.h new file mode 100644 index 0000000..d090e40 --- /dev/null +++ b/ceee/ie/plugin/toolband/resource.h @@ -0,0 +1,35 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Resource constants for CEEE toolband. +#ifndef CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ + + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by toolband.rc +// +#define IDS_PROJNAME 100 +#define IDR_IE 101 +#define IDR_BROWSERHELPEROBJECT 102 +#define IDR_TOOL_BAND 103 +#define IDR_GREASEMONKEY_API_JS 105 +#define IDR_EXECUTOR 106 +#define IDR_EXECUTOR_CREATOR 107 +#define IDR_NO_EXTENSION 108 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 109 +#define _APS_NEXT_COMMAND_VALUE 32768 +#define _APS_NEXT_CONTROL_VALUE 201 +#define _APS_NEXT_SYMED_VALUE 109 +#endif +#endif + + +#endif // CEEE_IE_PLUGIN_TOOLBAND_RESOURCE_H_ diff --git a/ceee/ie/plugin/toolband/tool_band.cc b/ceee/ie/plugin/toolband/tool_band.cc new file mode 100644 index 0000000..b8dfcea --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.cc @@ -0,0 +1,628 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband implementation. +#include "ceee/ie/plugin/toolband/tool_band.h" + +#include <atlsafe.h> +#include <atlstr.h> +#include <shlguid.h> + +#include "base/debug/trace_event.h" +#include "base/file_path.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/window_utils.h" +#include "ceee/common/windows_constants.h" +#include "ceee/ie/common/extension_manifest.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/tool_band_visibility.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/automation/automation_constants.h" +#include "chrome_frame/com_message_event.h" + +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO ToolBand::handler_type_long_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_I4 } }; +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_bstr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BSTR } }; +_ATL_FUNC_INFO ToolBand::handler_type_bstr_i4_= + { CC_STDCALL, VT_EMPTY, 2, { VT_BSTR, VT_I4 } }; +_ATL_FUNC_INFO ToolBand::handler_type_bstrarray_= + { CC_STDCALL, VT_EMPTY, 1, { VT_ARRAY | VT_BSTR } }; +_ATL_FUNC_INFO ToolBand::handler_type_idispatch_variantref_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_VARIANT | VT_BYREF } }; + +ToolBand::ToolBand() + : already_tried_installing_(false), + own_line_flag_(false), + already_checked_own_line_flag_(false), + listening_to_browser_events_(false), + band_id_(0), + is_quitting_(false), + current_width_(64), + current_height_(25) { + TRACE_EVENT_BEGIN("ceee.toolband", this, ""); +} + +ToolBand::~ToolBand() { + TRACE_EVENT_END("ceee.toolband", this, ""); +} + +HRESULT ToolBand::FinalConstruct() { + return S_OK; +} + +void ToolBand::FinalRelease() { +} + +STDMETHODIMP ToolBand::SetSite(IUnknown* site) { + typedef IObjectWithSiteImpl<ToolBand> SuperSite; + + // From experience we know the site may be set multiple times. + // Let's ignore second and subsequent set or unset. + if (NULL != site && NULL != m_spUnkSite.p || + NULL == site && NULL == m_spUnkSite.p) { + // TODO(siggi@chromium.org) log this. + return S_OK; + } + + if (NULL == site) { + // We're being torn down. + Teardown(); + } + + HRESULT hr = SuperSite::SetSite(site); + if (FAILED(hr)) + return hr; + + if (NULL != site) { + // We're being initialized. + hr = Initialize(site); + + // Release the site in case of failure. + if (FAILED(hr)) + SuperSite::SetSite(NULL); + } + + return hr; +} + +STDMETHODIMP ToolBand::ShowDW(BOOL show) { + ShowWindow(show ? SW_SHOW : SW_HIDE); + if (show) { + // Report that the toolband is being shown, so that the BHO + // knows it doesn't need to explicitly make it visible. + ToolBandVisibility::ReportToolBandVisible(web_browser_); + } + // Unless ShowDW changes are explicitly being ignored (e.g. if the + // BHO is forcing the toolband to be visible via + // ShowBrowserBar), or unless the toolband is closing on quit, then we assume + // a call to ShowDW reflects the user's toolband visibility choice, modifiable + // through the View -> Toolbars menu in IE. We track this choice here. + if (!ceee_module_util::GetIgnoreShowDWChanges() && !is_quitting_) { + ceee_module_util::SetOptionToolbandIsHidden(show == FALSE); + } + return S_OK; +} + +STDMETHODIMP ToolBand::CloseDW(DWORD reserved) { + // Indicates to ShowDW() that the tool band is being closed, as opposed to + // being explicitly hidden by the user. + is_quitting_ = true; + return ShowDW(FALSE); +} + +STDMETHODIMP ToolBand::ResizeBorderDW(LPCRECT border, + IUnknown* toolband_site, + BOOL reserved) { + DCHECK(FALSE); // Not used for toolbands. + return E_NOTIMPL; +} + +STDMETHODIMP ToolBand::GetBandInfo(DWORD band_id, + DWORD view_mode, + DESKBANDINFO* deskband_info) { + band_id_ = band_id; + + // We're only registered as a horizontal band. + DCHECK(view_mode == DBIF_VIEWMODE_NORMAL); + + if (!deskband_info) + return E_POINTER; + + if (deskband_info->dwMask & DBIM_MINSIZE) { + deskband_info->ptMinSize.x = current_width_; + deskband_info->ptMinSize.y = current_height_; + } + + if (deskband_info->dwMask & DBIM_MAXSIZE) { + deskband_info->ptMaxSize.x = -1; + deskband_info->ptMaxSize.y = -1; + } + + if (deskband_info->dwMask & DBIM_INTEGRAL) { + deskband_info->ptIntegral.x = 1; + deskband_info->ptIntegral.y = 1; + } + + if (deskband_info->dwMask & DBIM_ACTUAL) { + // By not setting, we just use the default. + // deskband_info->ptActual.x = 7000; + deskband_info->ptActual.y = current_height_; + } + + if (deskband_info->dwMask & DBIM_TITLE) { + // Title is empty. + deskband_info->wszTitle[0] = 0; + } + + if (deskband_info->dwMask & DBIM_MODEFLAGS) { + deskband_info->dwModeFlags = DBIMF_NORMAL /* | DBIMF_TOPALIGN */; + + if (ShouldForceOwnLine()) { + deskband_info->dwModeFlags |= DBIMF_BREAK; + } + } + + if (deskband_info->dwMask & DBIM_BKCOLOR) { + // Use the default background color by removing this flag. + deskband_info->dwMask &= ~DBIM_BKCOLOR; + } + return S_OK; +} + +STDMETHODIMP ToolBand::GetWindow(HWND* window) { + *window = m_hWnd; + return S_OK; +} + +STDMETHODIMP ToolBand::ContextSensitiveHelp(BOOL enter_mode) { + LOG(INFO) << "ContextSensitiveHelp"; + return E_NOTIMPL; +} + +STDMETHODIMP ToolBand::GetClassID(CLSID* clsid) { + *clsid = GetObjectCLSID(); + return S_OK; +} + +STDMETHODIMP ToolBand::IsDirty() { + return S_FALSE; // Never dirty for now. +} + +STDMETHODIMP ToolBand::Load(IStream* stream) { + return S_OK; // Loading is no-op. +} + +STDMETHODIMP ToolBand::Save(IStream* stream, BOOL clear_dirty) { + return S_OK; // Saving is no-op. +} + +STDMETHODIMP ToolBand::GetSizeMax(ULARGE_INTEGER* size) { + size->QuadPart = 0ULL; // We're frugal. + return S_OK; +} + +STDMETHODIMP ToolBand::GetWantsPrivileged(boolean* wants_privileged) { + *wants_privileged = true; + return S_OK; +} + +STDMETHODIMP ToolBand::GetChromeExtraArguments(BSTR* args) { + DCHECK(args); + + // Extra arguments are passed on verbatim, so we add the -- prefix. + CComBSTR str = "--"; + str.Append(switches::kEnableExperimentalExtensionApis); + + *args = str.Detach(); + return S_OK; +} + +STDMETHODIMP ToolBand::GetChromeProfileName(BSTR* profile_name) { + *profile_name = ::SysAllocString( + ceee_module_util::GetBrokerProfileNameForIe()); + return S_OK; +} + +STDMETHODIMP ToolBand::GetExtensionApisToAutomate(BSTR* functions_enabled) { + *functions_enabled = NULL; + return S_FALSE; +} + +HRESULT ToolBand::Initialize(IUnknown* site) { + TRACE_EVENT_INSTANT("ceee.toolband.initialize", this, ""); + + CComQIPtr<IServiceProvider> service_provider = site; + DCHECK(service_provider); + if (service_provider == NULL) { + return E_FAIL; + } + + HRESULT hr = InitializeAndShowWindow(site); + + if (FAILED(hr)) { + LOG(ERROR) << "Toolband failed to initalize its site window: " << + com::LogHr(hr); + return hr; + } + + // Store the web browser, used to report toolband visibility to + // the BHO. Also required to get navigate2 notification. + hr = service_provider->QueryService( + SID_SWebBrowserApp, IID_IWebBrowser2, + reinterpret_cast<void**>(&web_browser_)); + + DCHECK(SUCCEEDED(hr)); + + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get web browser: 0x" << std::hex << hr; + return hr; + } else if (ShouldForceOwnLine()) { + // This may seem odd, but event subscription is required + // only to clear 'own line' flag later (see OnIeNavigateComplete2) + hr = HostingBrowserEvents::DispEventAdvise(web_browser_, + &DIID_DWebBrowserEvents2); + listening_to_browser_events_ = SUCCEEDED(hr); + DCHECK(SUCCEEDED(hr)) << + "DispEventAdvise on web browser failed. Error: " << hr; + // Non-critical functionality. If fails in the field, just move on. + } + + return S_OK; +} + +HRESULT ToolBand::InitializeAndShowWindow(IUnknown* site) { + CComPtr<IOleWindow> site_window; + HRESULT hr = site->QueryInterface(&site_window); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get site window: " << com::LogHr(hr); + return hr; + } + + DCHECK(NULL != site_window.p); + hr = site_window->GetWindow(&parent_window_.m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get parent window handle: " << com::LogHr(hr); + return hr; + } + DCHECK(parent_window_); + if (!parent_window_) + return E_FAIL; + + if (NULL == Create(parent_window_)) + return E_FAIL; + + BOOL shown = ShowWindow(SW_SHOW); + DCHECK(shown); + + return hr; +} + +HRESULT ToolBand::Teardown() { + TRACE_EVENT_INSTANT("ceee.toolband.teardown", this, ""); + + if (IsWindow()) { + // Teardown the ActiveX host window. + CAxWindow host(m_hWnd); + CComPtr<IObjectWithSite> host_with_site; + HRESULT hr = host.QueryHost(&host_with_site); + if (SUCCEEDED(hr)) + host_with_site->SetSite(NULL); + + DestroyWindow(); + } + + if (chrome_frame_) { + ChromeFrameEvents::DispEventUnadvise(chrome_frame_); + } + + if (web_browser_ && listening_to_browser_events_) { + HostingBrowserEvents::DispEventUnadvise(web_browser_, + &DIID_DWebBrowserEvents2); + } + listening_to_browser_events_ = false; + + return S_OK; +} + +void ToolBand::OnFinalMessage(HWND window) { + GetUnknown()->Release(); +} + +LRESULT ToolBand::OnCreate(LPCREATESTRUCT lpCreateStruct) { + // Grab a self-reference. + GetUnknown()->AddRef(); + + // Create a host window instance. + CComPtr<IAxWinHostWindow> host; + HRESULT hr = CAxHostWindow::CreateInstance(&host); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create ActiveX host window. " << com::LogHr(hr); + return 1; + } + + // We're the site for the host window, this needs to be in place + // before we attach ChromeFrame to the ActiveX control window, so + // as to allow it to probe our service provider. + hr = SetChildSite(host); + DCHECK(SUCCEEDED(hr)); + + // Create the chrome frame instance. + hr = chrome_frame_.CoCreateInstance(L"ChromeTab.ChromeFrame"); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to create the Chrome Frame instance. " << + com::LogHr(hr); + return 1; + } + + // And attach it to our window. + hr = host->AttachControl(chrome_frame_, m_hWnd); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to attach Chrome Frame to the host. " << + com::LogHr(hr); + return 1; + } + + // Hook up the chrome frame event listener. + hr = ChromeFrameEvents::DispEventAdvise(chrome_frame_); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to hook up event sink. " << com::LogHr(hr); + } + + return 0; +} + +void ToolBand::OnPaint(CDCHandle dc) { + RECT rc = {}; + if (GetUpdateRect(&rc, FALSE)) { + PAINTSTRUCT ps = {}; + BeginPaint(&ps); + + BOOL ret = GetClientRect(&rc); + DCHECK(ret); + CString text; + text.Format(L"Google CEEE. No Chrome Frame found. Instance: 0x%p. ID: %d!)", + this, band_id_); + ::DrawText(ps.hdc, text, -1, &rc, DT_SINGLELINE | DT_BOTTOM | DT_CENTER); + + EndPaint(&ps); + } +} + +void ToolBand::OnSize(UINT type, CSize size) { + LOG(INFO) << "ToolBand::OnSize(" << type << ", " + << size.cx << "x" << size.cy << ")"; + CWindow chrome_window = ::GetWindow(m_hWnd, GW_CHILD); + if (!chrome_window) { + LOG(ERROR) << "Failed to retrieve Chrome Frame window"; + return; + } + + BOOL resized = chrome_window.ResizeClient(size.cx, size.cy); + DCHECK(resized); +} + +STDMETHODIMP_(void) ToolBand::OnCfReadyStateChanged(LONG state) { + DLOG(INFO) << "OnCfReadyStateChanged(" << state << ")"; + + if (state == READYSTATE_COMPLETE) { + extension_path_ = ceee_module_util::GetExtensionPath(); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_) && + ceee_module_util::NeedToInstallExtension()) { + LOG(INFO) << "Installing extension: \"" << extension_path_ << "\""; + chrome_frame_->installExtension(CComBSTR(extension_path_.c_str())); + } else { + // In the case where we don't have a CRX (or we don't need to install it), + // we must ask for the currently enabled extension before we can decide + // what we need to do. + chrome_frame_->getEnabledExtensions(); + } + } +} + +STDMETHODIMP_(void) ToolBand::OnCfMessage(IDispatch* event) { + VARIANT origin = {VT_NULL}; + HRESULT hr = event->Invoke(ComMessageEvent::DISPID_MESSAGE_EVENT_ORIGIN, + IID_NULL, 0, DISPATCH_PROPERTYGET, 0, &origin, 0, 0); + if (FAILED(hr) || origin.vt != VT_BSTR) { + DLOG(WARNING) << __FUNCTION__ << ": unable to discern message origin."; + return; + } + + VARIANT data_bstr = {VT_NULL}; + hr = event->Invoke(ComMessageEvent::DISPID_MESSAGE_EVENT_DATA, + IID_NULL, 0, DISPATCH_PROPERTYGET, 0, &data_bstr, 0, 0); + if (FAILED(hr) || data_bstr.vt != VT_BSTR) { + DLOG(INFO) << __FUNCTION__ << ": no message data. Origin:" + << origin.bstrVal; + return; + } + DLOG(INFO) << __FUNCTION__ << ": Origin: " << origin.bstrVal + << ", Data: " << data_bstr.bstrVal; + CString data(data_bstr); + + // Handle CEEE-specific messages. + // TODO(skare@google.com): If we will need this for more than one + // message, consider making responses proper JSON. + + // ceee_getCurrentWindowId: chrome.windows.getCurrent workaround. + CString message; + if (data == L"ceee_getCurrentWindowId") { + HWND browser_window = 0; + web_browser_->get_HWND(reinterpret_cast<long*>(&browser_window)); + bool is_ieframe = window_utils::IsWindowClass(browser_window, + windows::kIeFrameWindowClass); + if (is_ieframe) { + message.Format(L"ceee_getCurrentWindowId %d", browser_window); + } else { + DCHECK(is_ieframe); + LOG(WARNING) << "Could not find IE Frame window."; + message = L"ceee_getCurrentWindowId -1"; + } + } + + if (!message.IsEmpty()) { + chrome_frame_->postMessage(CComBSTR(message), origin); + } +} + +void ToolBand::StartExtension(const wchar_t* base_dir) { + if (!LoadManifestFile(base_dir, &extension_url_)) { + LOG(ERROR) << "No extension found"; + } else { + HRESULT hr = chrome_frame_->put_src(CComBSTR(extension_url_.c_str())); + DCHECK(SUCCEEDED(hr)); + LOG_IF(WARNING, FAILED(hr)) << "IChromeFrame::put_src returned: " << + com::LogHr(hr); + } +} + +STDMETHODIMP_(void) ToolBand::OnCfExtensionReady(BSTR path, int response) { + TRACE_EVENT_INSTANT("ceee.toolband.oncfextensionready", this, ""); + + if (ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // If we get here, it's because we just did the first-time + // install, so save the installation path+time for future comparison. + ceee_module_util::SetInstalledExtensionPath( + FilePath(extension_path_)); + } + + // Now list enabled extensions so that we can properly start it whether + // it's a CRX file or an exploded folder. + // + // Note that we do this even if Chrome says installation failed, + // as that is the error code it uses when we try to install an + // older version of the extension than it already has, which happens + // on overinstall when Chrome has already auto-updated. + // + // If it turns out no extension is installed, we will handle that + // error in the OnCfGetEnabledExtensionsComplete callback. + chrome_frame_->getEnabledExtensions(); +} + +STDMETHODIMP_(void) ToolBand::OnCfGetEnabledExtensionsComplete( + SAFEARRAY* extension_directories) { + CComSafeArray<BSTR> directories; + directories.Attach(extension_directories); // MUST DETACH BEFORE RETURNING + + // TODO(joi@chromium.org) Handle multiple extensions. + if (directories.GetCount() > 0) { + // If our extension_path is not a CRX, it MUST be the same as the installed + // extension path which would be an exploded extension. + // If you get this DCHECK, you may have changed your registry settings to + // debug with an exploded extension, but you didn't uninstall the previous + // extension, either via the Chrome UI or by simply wiping out your + // profile folder. + DCHECK(ceee_module_util::IsCrxOrEmpty(extension_path_) || + extension_path_ == std::wstring(directories.GetAt(0))); + StartExtension(directories.GetAt(0)); + } else if (!ceee_module_util::IsCrxOrEmpty(extension_path_)) { + // We have an extension path that isn't a CRX and we don't have any + // enabled extension, so we must load the exploded extension from this + // given path. WE MUST DO THIS BEFORE THE NEXT ELSE IF because it assumes + // a CRX file. + chrome_frame_->loadExtension(CComBSTR(extension_path_.c_str())); + } else if (!already_tried_installing_ && !extension_path_.empty()) { + // We attempt to install the .crx file from the CEEE folder; in the + // default case this will happen only once after installation. + // It may seem redundant with OnCfReadyStateChanged; this is in case the + // user deleted the extension but the registry stayed the same. + already_tried_installing_ = true; + chrome_frame_->installExtension(CComBSTR(extension_path_.c_str())); + } else { + // Hide the browser bar as fast as we can. + // Set the current height of the bar to 0, so that if the user manually + // shows the bar, it will not be visible on screen. + current_height_ = 0; + + // Ask IE to reload all info for this toolband. + CComPtr<IOleCommandTarget> cmd_target; + HRESULT hr = GetSite(IID_IOleCommandTarget, + reinterpret_cast<void**>(&cmd_target)); + if (SUCCEEDED(hr)) { + CComVariant band_id(static_cast<int>(band_id_)); + hr = cmd_target->Exec(&CGID_DeskBand, DBID_BANDINFOCHANGED, + OLECMDEXECOPT_DODEFAULT, &band_id, NULL); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to Execute DBID_BANDINFOCHANGED. Error Code: " + << com::LogHr(hr); + } + } else { + LOG(ERROR) << "Failed to obtain OleCommandTarget. Error Code: " + << com::LogHr(hr); + } + } + directories.Detach(); +} + +STDMETHODIMP_(void) ToolBand::OnIeNavigateComplete2(IDispatch* dispatch, + VARIANT* url) { + // The flag is cleared on navigation complete since at this point we are + // certain the process of placing the toolband has been completed. + // Doing it in GetBandInfo proved premature as many queries are expeced. + ClearForceOwnLineFlag(); + + // We need to clear that flag just once. Now that's done, unadvise. + DCHECK(web_browser_ != NULL); + if (web_browser_ && listening_to_browser_events_) { + HostingBrowserEvents::DispEventUnadvise(web_browser_, + &DIID_DWebBrowserEvents2); + listening_to_browser_events_ = false; + } +} + +bool ToolBand::LoadManifestFile(const std::wstring& base_dir, + std::string* toolband_url) { + DCHECK(toolband_url); + FilePath toolband_extension_path; + toolband_extension_path = FilePath(base_dir); + + if (toolband_extension_path.empty()) { + // Expected case if no extensions registered/found. + return false; + } + + ExtensionManifest manifest; + HRESULT hr = manifest.ReadManifestFile(toolband_extension_path, true); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to read manifest at \"" << + toolband_extension_path.value() << "\", error " << com::LogHr(hr); + return false; + } + + const std::vector<std::string>& toolstrip_names( + manifest.GetToolstripFileNames()); + if (!toolstrip_names.empty()) { + *toolband_url = "chrome-extension://"; + *toolband_url += manifest.extension_id(); + *toolband_url += "/"; + // TODO(mad@chromium.org): For now we only load the first one we + // find, we may want to stack them at one point... + *toolband_url += toolstrip_names[0]; + } + + return true; +} + +bool ToolBand::ShouldForceOwnLine() { + if (!already_checked_own_line_flag_) { + own_line_flag_ = ceee_module_util::GetOptionToolbandForceReposition(); + already_checked_own_line_flag_ = true; + } + + return own_line_flag_; +} + +void ToolBand::ClearForceOwnLineFlag() { + if (own_line_flag_ || !already_checked_own_line_flag_) { + own_line_flag_ = false; + already_checked_own_line_flag_ = true; + ceee_module_util::SetOptionToolbandForceReposition(false); + } +} diff --git a/ceee/ie/plugin/toolband/tool_band.h b/ceee/ie/plugin/toolband/tool_band.h new file mode 100644 index 0000000..33ad61c --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.h @@ -0,0 +1,247 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband implementation. +#ifndef CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ + +#include <atlbase.h> +#include <atlapp.h> // Must be included AFTER base. +#include <atlcom.h> +#include <atlcrack.h> +#include <atlgdi.h> +#include <atlwin.h> +#include <atlmisc.h> +#include <exdispid.h> +#include <shobjidl.h> +#include <list> +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "ceee/ie/plugin/toolband/resource.h" + +#include "chrome_tab.h" // NOLINT +#include "toolband.h" // NOLINT + +class DictionaryValue; +class PageApi; +class ToolBand; +class DictionaryValue; +class Value; + +typedef IDispEventSimpleImpl<0, ToolBand, &DIID_DIChromeFrameEvents> + ChromeFrameEvents; + +typedef IDispEventSimpleImpl<1, ToolBand, &DIID_DWebBrowserEvents2> + HostingBrowserEvents; + +// Implements an IE toolband which gets instantiated for every IE browser tab +// and renders by hosting chrome frame as an ActiveX control. +class ATL_NO_VTABLE ToolBand : public CComObjectRootEx<CComSingleThreadModel>, + public CComCoClass<ToolBand, &CLSID_ToolBand>, + public IObjectWithSiteImpl<ToolBand>, + public IServiceProviderImpl<ToolBand>, + public IChromeFramePrivileged, + public IDeskBand, + public IPersistStream, + public ChromeFrameEvents, + public HostingBrowserEvents, + public CWindowImpl<ToolBand> { + public: + ToolBand(); + ~ToolBand(); + + DECLARE_REGISTRY_RESOURCEID(IDR_TOOL_BAND) + + BEGIN_COM_MAP(ToolBand) + COM_INTERFACE_ENTRY(IDeskBand) + COM_INTERFACE_ENTRY(IDockingWindow) + COM_INTERFACE_ENTRY(IOleWindow) + COM_INTERFACE_ENTRY(IPersist) + COM_INTERFACE_ENTRY(IPersistStream) + COM_INTERFACE_ENTRY(IObjectWithSite) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IChromeFramePrivileged) + END_COM_MAP() + + BEGIN_SERVICE_MAP(ToolBand) + SERVICE_ENTRY(SID_ChromeFramePrivileged) + SERVICE_ENTRY_CHAIN(m_spUnkSite) + END_SERVICE_MAP() + + + BEGIN_SINK_MAP(ToolBand) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONREADYSTATECHANGED, + OnCfReadyStateChanged, &handler_type_long_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONEXTENSIONREADY, + OnCfExtensionReady, &handler_type_bstr_i4_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONGETENABLEDEXTENSIONSCOMPLETE, + OnCfGetEnabledExtensionsComplete, &handler_type_bstrarray_) + SINK_ENTRY_INFO(0, DIID_DIChromeFrameEvents, + CF_EVENT_DISPID_ONMESSAGE, + OnCfMessage, &handler_type_idispatch_) + SINK_ENTRY_INFO(1, DIID_DWebBrowserEvents2, + DISPID_NAVIGATECOMPLETE2, + OnIeNavigateComplete2, &handler_type_idispatch_variantref_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + HRESULT FinalConstruct(); + void FinalRelease(); + + BEGIN_MSG_MAP(ToolBand) + MSG_WM_CREATE(OnCreate) + MSG_WM_PAINT(OnPaint) + MSG_WM_SIZE(OnSize) + END_MSG_MAP() + + // @name IObjectWithSite overrides. + STDMETHOD(SetSite)(IUnknown *site); + + // @name IDockingWindow implementation. + // @{ + STDMETHOD(ShowDW)(BOOL show); + STDMETHOD(CloseDW)(DWORD reserved); + STDMETHOD(ResizeBorderDW)(LPCRECT border, IUnknown *toolband_site, + BOOL reserved); + // @} + + // @name IDeskBand implementation. + STDMETHOD(GetBandInfo)(DWORD band_id, DWORD view_mode, + DESKBANDINFO *deskband_info); + + // @name IOleWindow implementation. + // @{ + STDMETHOD(GetWindow)(HWND *window); + STDMETHOD(ContextSensitiveHelp)(BOOL enter_mode); + // @} + + // @name IPersist implementation. + STDMETHOD(GetClassID)(CLSID *clsid); + + // @name IPersistStream implementation. + // @{ + STDMETHOD(IsDirty)(); + STDMETHOD(Load)(IStream *stream); + STDMETHOD(Save)(IStream *stream, BOOL clear_dirty); + STDMETHOD(GetSizeMax)(ULARGE_INTEGER *size); + // @} + + + // @name IChromeFramePrivileged implementation. + // @{ + STDMETHOD(GetWantsPrivileged)(boolean *wants_privileged); + STDMETHOD(GetChromeExtraArguments)(BSTR *args); + STDMETHOD(GetChromeProfileName)(BSTR *args); + STDMETHOD(GetExtensionApisToAutomate)(BSTR *args); + // @} + + + // @name ChromeFrame event handlers + // @{ + STDMETHOD_(void, OnCfReadyStateChanged)(LONG state); + STDMETHOD_(void, OnCfExtensionReady)(BSTR path, int response); + STDMETHOD_(void, OnCfGetEnabledExtensionsComplete)( + SAFEARRAY* extension_directories); + STDMETHOD_(void, OnCfMessage)(IDispatch* event); + STDMETHOD_(void, OnIeNavigateComplete2)(IDispatch* dispatch, VARIANT* url); + // @} + + protected: + // Our window maintains a refcount on us for the duration of its lifetime. + // The self-reference is managed with those two methods. + virtual void OnFinalMessage(HWND window); + LRESULT OnCreate(LPCREATESTRUCT lpCreateStruct); + + // Loads the manifest from file and retrieves the URL to the extension. + // @returns true on success, false on failure to read the manifest or URL. + bool LoadManifestFile(const std::wstring& base_dir, + std::string* toolband_url); + + // @name Message handlers. + // @{ + void OnPaint(CDCHandle dc); + void OnSize(UINT type, CSize size); + // @} + + private: + // Initializes the toolband to the given site. + // Called from SetSite. + HRESULT Initialize(IUnknown *site); + // Tears down an initialized toolband. + // Called from SetSite. + HRESULT Teardown(); + + // Handles the dispatching of command received from the User/UI context. + HRESULT DispatchUserCommand(const DictionaryValue& dict, + scoped_ptr<Value>* return_value); + + // Parses the manifest and navigates CF to the toolband URL. + void StartExtension(const wchar_t* base_dir); + + // Subroutine of general initialization. Extracted to make testable. + virtual HRESULT InitializeAndShowWindow(IUnknown* site); + + // The OwnLine flag indicates that the toolband should request from the IE + // host to put it in its own space (and not behind whatever toolband might + // have been installed first). + bool ShouldForceOwnLine(); + void ClearForceOwnLineFlag(); + + // The web browser that initialized this toolband. + CComPtr<IWebBrowser2> web_browser_; + // Our parent window, yielded by our site's IOleWindow. + CWindow parent_window_; + // Our band id, provided by GetBandInfo. + DWORD band_id_; + + // The minimum size the toolband should take. + LONG current_width_; + LONG current_height_; + + // The URL to our extension. + std::string extension_url_; + + // Our Chrome frame instance. + CComPtr<IChromeFrame> chrome_frame_; + + // Indicates whether CloseDW() is being called on this tool band. + bool is_quitting_; + + // True if we noticed that no extensions are enabled and requested + // to install one. + bool already_tried_installing_; + + // Flag purpose: see comments to ShouldForceOwnLine + // for efficiency we read only once (thus the second flag). + bool own_line_flag_; + bool already_checked_own_line_flag_; + + // Listening to DIID_DWebBrowserEvents2 is optional. For registration + // purposes we have to know if we are listening, though. + bool listening_to_browser_events_; + + // Filesystem path to the .crx we will install, or the empty string, or + // (if not ending in .crx) the path to an exploded extension directory to + // load. + std::wstring extension_path_; + + // Function info objects describing our message handlers. + // Effectively const but can't make const because of silly ATL macro problem. + static _ATL_FUNC_INFO handler_type_idispatch_; + static _ATL_FUNC_INFO handler_type_long_; + static _ATL_FUNC_INFO handler_type_idispatch_bstr_; + static _ATL_FUNC_INFO handler_type_bstr_i4_; + static _ATL_FUNC_INFO handler_type_bstrarray_; + static _ATL_FUNC_INFO handler_type_idispatch_variantref_; + + DISALLOW_COPY_AND_ASSIGN(ToolBand); +}; + +#endif // CEEE_IE_PLUGIN_TOOLBAND_TOOL_BAND_H_ diff --git a/ceee/ie/plugin/toolband/tool_band.rgs b/ceee/ie/plugin/toolband/tool_band.rgs new file mode 100644 index 0000000..0a7ec34 --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band.rgs @@ -0,0 +1,20 @@ +HKCR { + NoRemove CLSID { + ForceRemove '{2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B}' = s 'Google Chrome Extensions Execution Environment' { + InprocServer32 = s '%MODULE%' { + val ThreadingModel = s 'Apartment' + } + } + } +} +HKLM { + NoRemove Software { + NoRemove Microsoft { + NoRemove 'Internet Explorer' { + NoRemove Toolbar { + val '{2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B}' = s 'Google Chrome Extensions Execution Environment' + } + } + } + } +} diff --git a/ceee/ie/plugin/toolband/tool_band_unittest.cc b/ceee/ie/plugin/toolband/tool_band_unittest.cc new file mode 100644 index 0000000..cc8c3ca --- /dev/null +++ b/ceee/ie/plugin/toolband/tool_band_unittest.cc @@ -0,0 +1,363 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband unit tests. +#include "ceee/ie/plugin/toolband/tool_band.h" + +#include <exdisp.h> +#include <shlguid.h> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/common/mock_ceee_module_util.h" +#include "ceee/ie/testing/mock_browser_and_friends.h" +#include "ceee/testing/utils/dispex_mocks.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/test_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "broker_lib.h" // NOLINT + +namespace { + +using testing::GetConnectionCount; +using testing::InstanceCountMixin; +using testing::MockDispatchEx; +using testing::Return; +using testing::StrictMock; +using testing::TestBrowser; +using testing::TestBrowserSite; + +// Makes ToolBand testable - circumvents InitializeAndShowWindow. +class TestingToolBand + : public ToolBand, + public InstanceCountMixin<TestingToolBand>, + public InitializingCoClass<TestingToolBand> { + public: + HRESULT Initialize(TestingToolBand** self) { + *self = this; + return S_OK; + } + private: + virtual HRESULT InitializeAndShowWindow(IUnknown* site) { + return S_OK; // This aspect is not tested. + } +}; + +class ToolBandTest: public testing::Test { + public: + ToolBandTest() : tool_band_(NULL), site_(NULL), browser_(NULL) { + } + + ~ToolBandTest() { + } + + virtual void SetUp() { + // Create the instance to test. + ASSERT_HRESULT_SUCCEEDED( + TestingToolBand::CreateInitialized(&tool_band_, &tool_band_with_site_)); + tool_band_with_site_ = tool_band_; + + ASSERT_TRUE(tool_band_with_site_ != NULL); + } + + virtual void TearDown() { + tool_band_ = NULL; + tool_band_with_site_.Release(); + + site_ = NULL; + site_keeper_.Release(); + + browser_ = NULL; + browser_keeper_.Release(); + + // Everything should have been relinquished. + ASSERT_EQ(0, testing::InstanceCountMixinBase::all_instance_count()); + } + + void CreateSite() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowserSite::CreateInitialized(&site_, &site_keeper_)); + } + + void CreateBrowser() { + ASSERT_HRESULT_SUCCEEDED( + TestBrowser::CreateInitialized(&browser_, &browser_keeper_)); + + if (site_) + site_->browser_ = browser_keeper_; + } + + bool ToolbandHasSite() { + // Check whether ToolBand has a site set. + CComPtr<IUnknown> site; + if (SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&site)))) { + return true; + } + + // If GetSite failed and site != NULL, we are seeing things. + DCHECK(site == NULL); + return false; + } + + static void PrepareDeskBandInfo(DESKBANDINFO* pdinfo_for_test) { + memset(pdinfo_for_test, 0, sizeof(*pdinfo_for_test)); + + // What I really care in this test is DBIM_MODEFLAGS, but if there + // are weird interactions here, we want to be warned. + pdinfo_for_test->dwMask = DBIM_MODEFLAGS | DBIM_MAXSIZE | DBIM_MINSIZE | + DBIM_TITLE | DBIM_INTEGRAL; + } + + static const wchar_t* kUrl1; + + testing::TestBrowserSite* site_; + CComPtr<IUnknown> site_keeper_; + + TestBrowser* browser_; + CComPtr<IWebBrowser2> browser_keeper_; + + TestingToolBand* tool_band_; + CComPtr<IObjectWithSite> tool_band_with_site_; + + // the purpose of this mock is to redirect registry calls + StrictMock<testing::MockCeeeModuleUtils> ceee_module_utils_; +}; + +const wchar_t* ToolBandTest::kUrl1 = L"http://www.google.com"; + + +// Setting the ToolBand site with a non-service provider fails. +TEST_F(ToolBandTest, SetSiteWithNoServiceProviderFails) { + testing::LogDisabler no_dchecks; + + // Create an object that doesn't implement IServiceProvider. + MockDispatchEx* site = NULL; + CComPtr<IUnknown> site_keeper; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&site, + &site_keeper)); + // Setting a site that doesn't implement IServiceProvider fails. + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with no browser fails. +TEST_F(ToolBandTest, SetSiteWithNullBrowserFails) { + testing::LogDisabler no_dchecks; + + CreateSite(); + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with a non-browser fails. +TEST_F(ToolBandTest, SetSiteWithNonBrowserFails) { + testing::LogDisabler no_dchecks; + + CreateSite(); + // Endow the site with a non-browser service. + MockDispatchEx* mock_non_browser = NULL; + ASSERT_HRESULT_SUCCEEDED( + InitializingCoClass<MockDispatchEx>::CreateInitialized(&mock_non_browser, + &site_->browser_)); + ASSERT_HRESULT_FAILED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_FALSE(ToolbandHasSite()); +} + +// Setting the ToolBand site with a browser that doesn't implement the +// DIID_DWebBrowserEvents2 still works. +TEST_F(ToolBandTest, SetSiteWithNoEventsWorksAnyway) { + // We need to quash dcheck here, too (see: ToolBand::Initialize). + testing::LogDisabler no_dchecks; + CreateSite(); + CreateBrowser(); + + // Disable the connection point. + browser_->no_events_ = true; + + // Successful SetSite always calls GetOptionToolbandForceReposition + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + ASSERT_TRUE(ToolbandHasSite()); +} + +TEST_F(ToolBandTest, SetSiteWithBrowserSucceeds) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site.IsEqualObject(site_keeper_)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, SetSiteEstablishesConnectionWhenRequired) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + // Check the site's retained. + CComPtr<IUnknown> set_site; + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->GetSite( + IID_IUnknown, reinterpret_cast<void**>(&set_site))); + ASSERT_TRUE(set_site.IsEqualObject(site_keeper_)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); + + // And check that the connection was severed. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); +} + +TEST_F(ToolBandTest, NavigationCompleteResetsFlagAndUnadvises) { + CreateSite(); + CreateBrowser(); + + size_t num_connections = 0; + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + // Check that the we have not set the connection if not strictly required. + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(1, num_connections); + + EXPECT_CALL(ceee_module_utils_, + SetOptionToolbandForceReposition(false)).Times(1); + + // First navigation triggers (single) registry check and unadivising. + // After that things stay quiet. + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ASSERT_HRESULT_SUCCEEDED(GetConnectionCount(browser_keeper_, + DIID_DWebBrowserEvents2, + &num_connections)); + ASSERT_EQ(0, num_connections); + + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, NormalRunDoesntTriggerLineBreak) { + CreateSite(); + CreateBrowser(); + + // Expected sequence of actions: + // 1) initialization will trigger registry check + // 2) invocations if GetBandInfo do not trigger registry check + // 3) since spoofed registry says 'do not reposition', there should be no + // DBIMF_BREAK flag set in the structure. + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(false)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + DESKBANDINFO dinfo_for_test; + PrepareDeskBandInfo(&dinfo_for_test); + + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + // Take another pass and result should be the same. + PrepareDeskBandInfo(&dinfo_for_test); + + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +TEST_F(ToolBandTest, NewInstallationTriggersLineBreak) { + CreateSite(); + CreateBrowser(); + + EXPECT_CALL(ceee_module_utils_, GetOptionToolbandForceReposition()) + .WillOnce(Return(true)); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(site_keeper_)); + + DESKBANDINFO dinfo_for_test; + + // Expected sequence of actions: + // 1) invocation of 'GetBandInfo' will trigger registry check. + // 2) subsequent invocations do not trigger registry check, but the answer + // should also be 'line break' until navigation is completed; + // navigation completed is emulated by a call to FireOnNavigateComplete + // after that, the break flag is not returned. + + PrepareDeskBandInfo(&dinfo_for_test); + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + + EXPECT_CALL(ceee_module_utils_, + SetOptionToolbandForceReposition(false)).Times(1); + + ASSERT_TRUE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + browser_->FireOnNavigateComplete(browser_, &CComVariant(kUrl1)); + + PrepareDeskBandInfo(&dinfo_for_test); + ASSERT_HRESULT_SUCCEEDED(tool_band_->GetBandInfo(42, DBIF_VIEWMODE_NORMAL, + &dinfo_for_test)); + ASSERT_FALSE(dinfo_for_test.dwModeFlags & DBIMF_BREAK); + + ASSERT_HRESULT_SUCCEEDED(tool_band_with_site_->SetSite(NULL)); +} + +} // namespace diff --git a/ceee/ie/plugin/toolband/toolband.def b/ceee/ie/plugin/toolband/toolband.def new file mode 100644 index 0000000..9c9cc5b --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.def @@ -0,0 +1,13 @@ +; Copyright (c) 2010 The Chromium Authors. All rights reserved. +; Use of this source code is governed by a BSD-style license that can be +; found in the LICENSE file. +; +; ie.def : Declares the module exports. + +LIBRARY "ceee_ie.DLL" + +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE diff --git a/ceee/ie/plugin/toolband/toolband.gyp b/ceee/ie/plugin/toolband/toolband.gyp new file mode 100644 index 0000000..551092d --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.gyp @@ -0,0 +1,140 @@ +# Copyright (c) 2010 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'includes': [ + '../../../../build/common.gypi', + ], + 'targets': [ + { + # This target builds Chrome Frame's IDL file to our + # shared intermediate directory + 'target_name': 'chrome_tab_idl', + 'type': 'none', + 'msvs_settings': { + 'VCMIDLTool': { + 'OutputDirectory': '<(SHARED_INTERMEDIATE_DIR)', + }, + }, + 'sources': [ + '../../../../chrome_frame/chrome_tab.idl', + ], + # Add the output dir for those who depend on us. + 'direct_dependent_settings': { + 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + { + 'target_name': 'ceee_ie_lib', + 'type': 'static_library', + 'dependencies': [ + 'chrome_tab_idl', + '../../common/common.gyp:ie_common_settings', + '../../../../base/base.gyp:base', + '../../../../ceee/common/common.gyp:ceee_common', + ], + 'sources': [ + '../../common/precompile.cc', + '../../common/precompile.h', + 'tool_band.cc', + 'tool_band.h', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'ceee_ie', + 'type': 'shared_library', + 'dependencies': [ + 'ceee_ie_lib', + 'ie_toolband_common', + 'toolband_idl', + '../bho/bho.gyp:bho', + '../scripting/scripting.gyp:scripting', + '../../common/common.gyp:ie_common_settings', + '../../common/common.gyp:ie_guids', + '../../../../base/base.gyp:base', + '../../../../breakpad/breakpad.gyp:breakpad_handler', + '../../../../ceee/common/common.gyp:ceee_common', + '<(DEPTH)/chrome/chrome.gyp:chrome_version_header', + ], + 'sources': [ + '../../common/precompile.cc', + '../../common/precompile.h', + 'resource.h', + 'tool_band.rgs', + 'toolband.def', + 'toolband.rc', + 'toolband_module.cc', + '../bho/browser_helper_object.rgs', + '../executor.rgs', + '../executor_creator.rgs', + '../scripting/content_script_manager.rc', + ], + 'libraries': [ + 'oleacc.lib', + 'iepmapi.lib', + ], + 'include_dirs': [ + # Allows us to include .tlb and .h files generated + # from our .idl without undue trouble + '$(IntDir)', + ], + 'msvs_settings': { + 'VCLinkerTool': { + 'OutputFile': '$(OutDir)/servers/$(ProjectName).dll', + }, + }, + 'configurations': { + 'Debug': { + 'msvs_precompiled_source': '../../common/precompile.cc', + 'msvs_precompiled_header': '../../common/precompile.h', + }, + }, + }, + { + 'target_name': 'ie_toolband_common', + 'type': 'static_library', + 'dependencies': [ + '../../../../chrome_frame/crash_reporting/' + 'crash_reporting.gyp:crash_report', + '../../../../base/base.gyp:base', + '../../../../breakpad/breakpad.gyp:breakpad_handler', + ], + 'sources': [ + 'toolband_module_reporting.cc', + 'toolband_module_reporting.h', + ], + }, + { + 'target_name': 'toolband_idl', + 'type': 'none', + 'sources': [ + '../../broker/broker_lib.idl', + 'toolband.idl', + ], + 'msvs_settings': { + 'VCMIDLTool': { + 'OutputDirectory': '<(SHARED_INTERMEDIATE_DIR)', + 'DLLDataFileName': '$(InputName)_dlldata.c', + }, + }, + # Add the output dir for those who depend on us. + 'direct_dependent_settings': { + 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)'], + }, + }, + ] +} diff --git a/ceee/ie/plugin/toolband/toolband.idl b/ceee/ie/plugin/toolband/toolband.idl new file mode 100644 index 0000000..4164945 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.idl @@ -0,0 +1,370 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// @file +// Interface and object declarations for CEEE IE toolband. +import "oaidl.idl"; +import "ocidl.idl"; + +[ + object, + uuid(07995791-0923-4c4e-B861-AA3B933A31CC), + dual, + local, // no marshaling for this interface + nonextensible, + pointer_default(unique) +] +// Native interface to content scripts. This interface is exposed to +// ceee_boostrap.js, and is used to satisfy the same contract as +// Chrome's native functions declared in event_bindings.js and +// renderer_extension_bindings.js. Additionally this interface exposes +// properties for the dispatch functions declared in the same files. +// At any given time, this interface should to be synonymous with what you'd +// see by searching the two above files for the strings "dispatchOn" and +// "native function". +interface ICeeeContentScriptNativeApi : IDispatch { + // Supports JS console.log and console.error. + HRESULT Log([in] BSTR level, [in] BSTR message); + + // Port-related functions. + HRESULT OpenChannelToExtension([in] BSTR source_id, + [in] BSTR target_id, + [in] BSTR name, + [out, retval] long* port_id); + HRESULT CloseChannel([in] long port_id); + HRESULT PortAddRef([in] long port_id); + HRESULT PortRelease([in] long port_id); + HRESULT PostMessage([in] long port_id, [in] BSTR msg); + + // Event-related functions. + HRESULT AttachEvent([in] BSTR event_name); + HRESULT DetachEvent([in] BSTR event_name); + + // Event notification callbacks to script. + [propput] HRESULT onLoad(IDispatch* callback); + [propput] HRESULT onUnload(IDispatch* callback); + + // Port notification callbacks. + [propput] HRESULT onPortConnect(IDispatch* callback); + [propput] HRESULT onPortDisconnect(IDispatch* callback); + [propput] HRESULT onPortMessage(IDispatch* callback); +}; + +// An invalid tab Id. This is declared here so that both the BHO and the broker +// knows about it. +const int kInvalidChromeSessionId = -1; + +typedef long CeeeWindowHandle; + +typedef enum tagCeeeTabStatus { + kCeeeTabStatusLoading = 0, + kCeeeTabStatusComplete = 1, +} CeeeTabStatus; + +// Information about a tab. +typedef struct tagCeeeTabInfo { + BSTR url; + BSTR title; + CeeeTabStatus status; + BSTR fav_icon_url; +} CeeeTabInfo; + +typedef enum tagCeeeTabCodeType { + kCeeeTabCodeTypeCss = 0, + kCeeeTabCodeTypeJs = 1, +} CeeeTabCodeType; + +// Information about a window. +typedef struct { + BOOL focused; + RECT rect; + // We use a BSTR to dynamically allocate a list of couple (HWND, index). + // These are stored as a JSON list of longs, the even indexes are for ids + // and their associated odd numbers are the tab index. + // This value can be NULL if the caller didn't request to populate tabs. + BSTR tab_list; +} CeeeWindowInfo; + +// Information about an HTTP cookie. +typedef struct { + BSTR name; + BSTR value; + BSTR domain; + BOOL host_only; + BSTR path; + BOOL secure; + BOOL http_only; + BOOL session; + double expiration_date; + BSTR store_id; +} CeeeCookieInfo; + +[ + object, + uuid(8DEEECC5-7B49-482d-99F8-2109EA5F2618), + nonextensible, + helpstring("ICeeeWindowExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeWindowExecutor : IUnknown { + // Initializes the executor to work with the given CeeeWindowHandle. + // + // @param hwnd The HWND of the window the executor represents. + HRESULT Initialize([in] CeeeWindowHandle hwnd); + + // Returns information about the window represented by this executor. + // + // @param populate_tabs Specifies whether we want to receive the list of tabs. + // @param window_info Where to return the info about the window. The + // @p tab_list field are only set if @p populate_tabs is + // true. + HRESULT GetWindow([in] BOOL populate_tabs, + [out, ref] CeeeWindowInfo* window_info); + + // Returns the list of tabs of this window. We return both the tab HWND and + // the tab index (encoded in the @p tab_list BSTR) so that our callers don't + // need an extra IPC to get the tab index later on. + // + // @param tab_list Where to return the tab identifiers in the same format as + // described for tagCeeeWindowInfo::tab_ids. + HRESULT GetTabs([out, retval] BSTR* tab_list); + + // Updates the window with the given set of parameters. + // + // @param left The new left position of the window. -1 to leave unchanged. + // @param top The new top position of the window. -1 to leave unchanged. + // @param width The new width of the window. -1 to leave unchanged. + // @param height The new height of the window. -1 to leave unchanged. + // @param window_info Where to return the new info about the updated window. + HRESULT UpdateWindow( + [in] long left, [in] long top, [in] long width, [in] long height, + [out, ref] CeeeWindowInfo* window_info); + + // Close the window represented by our FrameExecutor. + // + // @retval S_OK We could successfully and silently removed the window. + // @retval S_FALSE We failed to <b>silently</b> remove the window, + // so the caller should try other alternatives (e.g., + // posting WM_CLOSE to the window). + HRESULT RemoveWindow(); + + // Returns the index of the given tab. + // + // @param tab The window handle (HWND) of the tab we want the index of. + // @param index Where to return the index. + HRESULT GetTabIndex([in] CeeeWindowHandle tab, [out, ref] long* index); + + // Moves the tab specified by @p tab to the index identified by @p index. + // + // @param tab The window handle (HWND) of the tab to move. + // @param index Where to move the tab. + HRESULT MoveTab([in] CeeeWindowHandle tab, [in] long index); + + // Removes the specified tab. + // + // @param tab The window handle (HWND) of the tab to be removed. + HRESULT RemoveTab([in] CeeeWindowHandle tab); + + // Selects the specified tab. + // + // @param tab The window handle (HWND) of the tab to be selected. + HRESULT SelectTab([in] CeeeWindowHandle tab); +}; + +[ + object, + uuid(C7FF41BA-72D5-4086-8B5A-2EF4FD12E0FE), + nonextensible, + helpstring("ICeeeTabExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeTabExecutor : IUnknown { + // Initializes the executor to work with the given CeeeWindowHandle. + // + // @param hwnd The HWND of the tab the executor represents. + HRESULT Initialize([in] CeeeWindowHandle hwnd); + + // Returns information about the tab represented by the TabExecutor in + // @p tab_info structure used to return the information. + // + // @param tab_info Where to return the tab information. + // + // @rvalue S_OK Success + // @return Other failure HRESULTs may also be returned in case of cascading + // errors. + HRESULT GetTabInfo([out, ref] CeeeTabInfo* tab_info); + + // Navigate to the given url from the given properties. + // + // @param url The URL where to navigate the tab to. Can NOT be NULL. + // @param flags Specifies the type of navigation based on the + // BrowserNavConstants enum values. + // @param target Specifies the navigation target (e.g., _top or _blank). + // + // @rvalue S_OK Success + // S_FALSE Nothing needed to be done since the URL was already set. + // @return Other failure HRESULTs may also be returned in case of cascading + // errors. + HRESULT Navigate([in] BSTR url, [in] long flags, [in] BSTR target); + + // Execute or insert code inside a tab. + // + // @param code The code to execute or insert. + // @param file A path to the file that contains the script to execute. + // This path is relative to the extension root. + // @param all_frames If true, applies to the top level frame as well as + // contained iframes. Otherwise, applies onlt to the + // top level frame. + HRESULT InsertCode([in] BSTR code, + [in] BSTR file, + [in] BOOL all_frames, + [in] CeeeTabCodeType type); +}; + +[ + object, + uuid(07630967-D7FB-4745-992F-28614930D9A3), + nonextensible, + helpstring("ICeeeCookieExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeCookieExecutor : IUnknown { + // Returns information about the cookie identified by the @c name field of + // the @p cookie_info structure used to return the information. + // + // @param url The URL with which the cookie to retrieve is associated. + // @param name The name of the cookie to retrieve. + // @param cookie_info Where to return the cookie information. + HRESULT GetCookie([in] BSTR url, [in] BSTR name, + [out, ref] CeeeCookieInfo* cookie_info); + + // Registers the executor's process as a known cookie store; used to indicate + // that the cookie store ID has been issued for this process and may be used + // in other cookie APIs. + // This API is used to ensure that stale cookie store IDs don't inadvertently + // match new cookie store processes. This may happen because IE derives the + // cookie store ID from the IE process ID, which may be recycled by Windows. + // The first time a cookie store ID is issued for an IE process, this + // RegisterCookieStore function should be called to indicate that the IE + // process may now be selected by a user-provided store ID. All cookie APIs + // should verify that CookieStoreIsRegistered() returns S_OK before matching + // a user-provided cookie store ID to an IE process. + HRESULT RegisterCookieStore(); + + // Returns S_OK if the executor's process has been registered as a cookie + // store, S_FALSE if not. All cookie API implementations must ensure this + // call returns S_OK before accessing a cookie store via a user-provided + // cookie store ID; if it doesn't, the store ID is stale. + HRESULT CookieStoreIsRegistered(); +}; + +[ + object, + uuid(276D47E8-1692-4a21-907D-948D170E4330), + nonextensible, + helpstring("ICeeeInfobarExecutor Interface"), + pointer_default(unique), + oleautomation +] +// Object provided to the broker to execute code in a given window's thread. +interface ICeeeInfobarExecutor : IUnknown { + // Stores the id of our extension. + // @param extension_id The id of the extension. + HRESULT SetExtensionId([in] BSTR extension_id); + + // Creates infobar and opens @p url in it. Translates relative path to the + // absolute path using "chrome-extension://extension_id" prefix where + // extension_id is the id set by SetExtensionId() call. + // @param url The URL the infobar window should be navigated to. + // @param window_handle Where to return the handle of the window in which + // this infobar was created. + HRESULT ShowInfobar([in] BSTR url, + [out, ref] CeeeWindowHandle* window_handle); + + // Notifies infobar about OnBeforeNavigate2 event for the browser top frame. + // @param url The URL the top frame is about to navigate to. + HRESULT OnTopFrameBeforeNavigate([in] BSTR url); +}; + +[ + object, + uuid(BBB10A7B-DB0D-4f1a-8669-65378DAD0C99), + nonextensible, + helpstring("ICeeeExecutorCreator Interface"), + pointer_default(unique), + local +] +// Creates an executor in a destination thread, and registers it in the +// CeeeBroker. + +// Used to instantiate a CeeeExecutor. +interface ICeeeExecutorCreator : IUnknown { + // Creates a CeeeExecutor for the given @p thread_id. + // + // @param thread_id The identifier of the destination thread where we want + // an executor to be creared. + // @param window The window handle (HWND) the new executor represents. + HRESULT CreateWindowExecutor([in] long thread_id, + [in] CeeeWindowHandle window); + + // Teardown what was left hanging while waiting for the + // new executor to be registered for the given @p thread_id. + // + // @param thread_id The identifier of the destination thread for which we want + // to tear down our infrastructure. + HRESULT Teardown([in] long thread_id); +}; + +[ + uuid(7C09079D-F9CB-4E9E-9293-D224B071D8BA), + version(1.0), + helpstring("Google CEEE 1.0 Type Library") +] +library ToolbandLib { + importlib("stdole2.tlb"); + + // include type info in .tlb + interface ICEEEContentScriptNativeApi; + interface ICeeeTabExecutor; + interface ICeeeWindowExecutor; + + [ + uuid(E49EBDB7-CEC9-4014-A5F5-8D3C8F5997DC), + helpstring("BrowserHelperObject Class") + ] + coclass BrowserHelperObject { + [default] interface IUnknown; + }; + [ + uuid(2F1A2D6B-55F6-4B63-8C37-F698D28FDC2B), + helpstring("ToolBand Class") + ] + coclass ToolBand { + [default] interface IUnknown; + }; + [ + uuid(4A562910-2D54-4e98-B87F-D4A7F5F5D0B9), + helpstring("CEEE Executor Creator Class") + ] + coclass CeeeExecutorCreator { + [default] interface IUnknown; + }; + [ + uuid(057FCFE3-F872-483d-86B0-0430E375E41F), + helpstring("CEEE Executor Class") + ] + coclass CeeeExecutor { + [default] interface IUnknown; + interface ICeeeTabExecutor; + interface ICeeeWindowExecutor; + interface ICeeeCookieExecutor; + interface ICeeeInfobarExecutor; + }; +}; diff --git a/ceee/ie/plugin/toolband/toolband.rc b/ceee/ie/plugin/toolband/toolband.rc new file mode 100644 index 0000000..d1319c2 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband.rc @@ -0,0 +1,116 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Microsoft Visual C++ generated resource script. +// +#include "ceee/ie/plugin/toolband/resource.h" +#include "version.h" + +// See winuser.h. +#define RT_HTML 23 + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +# error Don't open this in the GUI, it'll be massacred on save. +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION CHROME_VERSION + PRODUCTVERSION CHROME_VERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Google Inc." + VALUE "FileDescription", "Google Chrome Extensions Execution Environment for IE." + VALUE "FileVersion", CHROME_VERSION_STRING + VALUE "LegalCopyright", COPYRIGHT_STRING + VALUE "InternalName", "ceee_ie.dll" + VALUE "OriginalFilename", "ceee_ie.dll" + VALUE "ProductName", "Google Chrome Extensions Execution Environment" + VALUE "ProductVersion", CHROME_VERSION_STRING + + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +IDR_BROWSERHELPEROBJECT REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\browser_helper_object.rgs" +IDR_EXECUTOR REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\executor.rgs" +IDR_EXECUTOR_CREATOR REGISTRY "..\\..\\ceee\\ie\\plugin\\bho\\executor_creator.rgs" +IDR_TOOL_BAND REGISTRY "tool_band.rgs" + +///////////////////////////////////////////////////////////////////////////// +// +// BINDATA +// + +IDR_GREASEMONKEY_API_JS BINDATA "..\\..\\chrome\\renderer\\resources\\greasemonkey_api.js" + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PROJNAME "IE" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +1 TYPELIB "toolband.tlb" + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/ceee/ie/plugin/toolband/toolband_module.cc b/ceee/ie/plugin/toolband/toolband_module.cc new file mode 100644 index 0000000..2d2c735 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module.cc @@ -0,0 +1,408 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Declaration of ATL module object and DLL exports. + +#include "base/at_exit.h" +#include "base/atomic_ref_count.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/logging_win.h" +#include "base/thread.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/install_utils.h" +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/ie/plugin/bho/browser_helper_object.h" +#include "ceee/ie/plugin/bho/executor.h" +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" +#include "ceee/ie/plugin/toolband/tool_band.h" +#include "ceee/ie/plugin/scripting/script_host.h" +#include "ceee/common/windows_constants.h" +#include "chrome/common/url_constants.h" + +#include "toolband.h" // NOLINT + +namespace { + +const wchar_t kLogFileName[] = L"ceee.log"; + +// {73213C1A-C369-4740-A75C-FA849E6CE540} +static const GUID kCeeeIeLogProviderName = + { 0x73213c1a, 0xc369, 0x4740, + { 0xa7, 0x5c, 0xfa, 0x84, 0x9e, 0x6c, 0xe5, 0x40 } }; + +// This is the Script Debugging state for all script engines we instantiate. +ScriptHost::DebugApplication debug_application(L"CEEE"); + +} // namespace + +// Object entries go here instead of with each object, so that we can move +// the objects in a lib, and also to decrease the amount of magic. +OBJECT_ENTRY_AUTO(CLSID_BrowserHelperObject, BrowserHelperObject) +OBJECT_ENTRY_AUTO(CLSID_ToolBand, ToolBand) +OBJECT_ENTRY_AUTO(CLSID_CeeeExecutorCreator, CeeeExecutorCreator) +OBJECT_ENTRY_AUTO(CLSID_CeeeExecutor, CeeeExecutor) + +class ToolbandModule : public CAtlDllModuleT<ToolbandModule> { + public: + ToolbandModule(); + ~ToolbandModule(); + + DECLARE_LIBID(LIBID_ToolbandLib) + + // Needed to make sure we call Init/Term outside the loader lock. + HRESULT DllCanUnloadNow(); + HRESULT DllGetClassObject(REFCLSID clsid, REFIID iid, void** object); + void Init(); + void Term(); + bool module_initialized() const { + return module_initialized_; + } + + // Fires an event to the broker, so that the call can be made with an + // instance of a broker proxy that was CoCreated in the worker thread. + void FireEventToBroker(const std::string& event_name, + const std::string& event_args); + + private: + class ComWorkerThread : public base::Thread { + public: + ComWorkerThread(); + + // Called just prior to starting the message loop + virtual void Init(); + + // Called just after the message loop ends + virtual void CleanUp(); + + // Called by FireEventTask so that the broker we instantiate in the + // worker thread can be used. + void FireEventToBroker(BSTR event_name, BSTR event_args); + protected: + CComPtr<ICeeeBroker> broker_; + static const int kMaxNumberOfRetries = 5; + static const int64 kRetryDelayMs = 10; + int current_number_of_retries_; + }; + + class FireEventTask : public Task { + public: + FireEventTask(ComWorkerThread* worker_thread, + const std::string& event_name, + const std::string& event_args) + : worker_thread_(worker_thread), + event_name_(event_name.c_str()), + event_args_(event_args.c_str()) { + } + FireEventTask(ComWorkerThread* worker_thread, + const BSTR event_name, + const BSTR event_args) + : worker_thread_(worker_thread), + event_name_(event_name), + event_args_(event_args) { + } + virtual void Run() { + worker_thread_->FireEventToBroker(event_name_, event_args_); + } + private: + ComWorkerThread* worker_thread_; + CComBSTR event_name_; + CComBSTR event_args_; + }; + // We only start the thread on first use. If we would start it on + // initialization, when our DLL is loaded into the broker process, + // it would try to start this thread which tries to CoCreate a Broker + // and this could cause a complex deadlock... + void EnsureThreadStarted(); + + // We use a pointer so that we can make sure we only destroy the object + // when the thread is properly stopped. Otherwise, we would get a DCHECK + // if the thread is killed before we get to Stop it when DllCanUnloadNow + // returns S_OK, which happens when the application quits with live objects, + // this causes the destructor to DCHECK. + ComWorkerThread* worker_thread_; + base::AtExitManager at_exit_; + bool module_initialized_; + bool crash_reporting_initialized_; + + int worker_thread_ref_count_; + + friend void ceee_module_util::AddRefModuleWorkerThread(); + friend void ceee_module_util::ReleaseModuleWorkerThread(); + + void IncThreadRefCount(); + void DecThreadRefCount(); +}; + +ToolbandModule::ToolbandModule() + : crash_reporting_initialized_(false), + module_initialized_(false), + worker_thread_(NULL) { + wchar_t logfile_path[MAX_PATH]; + DWORD len = ::GetTempPath(arraysize(logfile_path), logfile_path); + ::PathAppend(logfile_path, kLogFileName); + + // It seems we're obliged to initialize the current command line + // before initializing logging. This feels a little strange for + // a plugin. + CommandLine::Init(0, NULL); + + logging::InitLogging( + logfile_path, + logging::LOG_TO_BOTH_FILE_AND_SYSTEM_DEBUG_LOG, + logging::LOCK_LOG_FILE, + logging::APPEND_TO_OLD_LOG_FILE); + + // Initialize ETW logging. + logging::LogEventProvider::Initialize(kCeeeIeLogProviderName); + + // Initialize control hosting. + BOOL initialized = AtlAxWinInit(); + DCHECK(initialized); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + ScriptHost::set_default_debug_application(&debug_application); +} + +ToolbandModule::~ToolbandModule() { + ScriptHost::set_default_debug_application(NULL); + + // Just leave thread as is. Releasing interface from this thread may hang IE. + DCHECK(worker_thread_ref_count_ == 0); + DCHECK(worker_thread_ == NULL); + + // Uninitialize control hosting. + BOOL uninitialized = AtlAxWinTerm(); + DCHECK(uninitialized); + + logging::CloseLogFile(); +} + +HRESULT ToolbandModule::DllCanUnloadNow() { + HRESULT hr = CAtlDllModuleT<ToolbandModule>::DllCanUnloadNow(); + if (hr == S_OK) { + // We must protect our data member against concurrent calls to check if we + // can be unloaded. We must also making the call to Term within the lock + // to make sure we don't try to re-initialize in case a new + // DllGetClassObject would occur in the mean time, in another thread. + m_csStaticDataInitAndTypeInfo.Lock(); + if (module_initialized_) { + Term(); + } + m_csStaticDataInitAndTypeInfo.Unlock(); + } + return hr; +} + +HRESULT ToolbandModule::DllGetClassObject(REFCLSID clsid, REFIID iid, + void** object) { + // Same comment as above in ToolbandModule::DllCanUnloadNow(). + m_csStaticDataInitAndTypeInfo.Lock(); + if (!module_initialized_) { + Init(); + } + m_csStaticDataInitAndTypeInfo.Unlock(); + return CAtlDllModuleT<ToolbandModule>::DllGetClassObject(clsid, iid, object); +} + +void ToolbandModule::Init() { + crash_reporting_initialized_ = InitializeCrashReporting(); + module_initialized_ = true; +} + +void ToolbandModule::Term() { + if (worker_thread_ != NULL) { + // It is OK to call Stop on a thread even when it isn't running. + worker_thread_->Stop(); + delete worker_thread_; + worker_thread_ = NULL; + } + if (crash_reporting_initialized_) { + bool crash_reporting_deinitialized = ShutdownCrashReporting(); + DCHECK(crash_reporting_deinitialized); + crash_reporting_initialized_ = false; + } + module_initialized_ = false; +} + +void ToolbandModule::IncThreadRefCount() { + m_csStaticDataInitAndTypeInfo.Lock(); + DCHECK_GE(worker_thread_ref_count_, 0); + worker_thread_ref_count_++; + m_csStaticDataInitAndTypeInfo.Unlock(); +} + +void ToolbandModule::DecThreadRefCount() { + ComWorkerThread* thread = NULL; + + m_csStaticDataInitAndTypeInfo.Lock(); + // If we're already at 0, we have a problem, so we check if we're >=. + DCHECK_GT(worker_thread_ref_count_, 0); + + // If this was our last reference, we delete the thread. This is okay even if + // we increment the count again, because the thread is created on the "first" + // FireEventToBroker, thus it will be created again if needed. + if (--worker_thread_ref_count_ == 0) { + if (worker_thread_ != NULL) { + // Store the worker_thread to a temporary pointer. It will be freed later. + thread = worker_thread_; + worker_thread_ = NULL; + } + } + m_csStaticDataInitAndTypeInfo.Unlock(); + + // Clean the thread after the unlock to be certain we don't get a deadlock + // (the CriticalSection could be used in the worker thread). + if (thread) { + // It is OK to call Stop on a thread even when it isn't running. + thread->Stop(); + delete thread; + } +} + +void ToolbandModule::EnsureThreadStarted() { + m_csStaticDataInitAndTypeInfo.Lock(); + if (worker_thread_ == NULL) { + worker_thread_ = new ComWorkerThread; + // The COM worker thread must be a UI thread so that it can pump windows + // messages and allow COM to handle cross apartment calls. + worker_thread_->StartWithOptions(base::Thread::Options(MessageLoop::TYPE_UI, + 0)); // stack_size + } + m_csStaticDataInitAndTypeInfo.Unlock(); +} + +void ToolbandModule::FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + EnsureThreadStarted(); + DCHECK(worker_thread_ != NULL); + MessageLoop* message_loop = worker_thread_->message_loop(); + if (message_loop) { + message_loop->PostTask(FROM_HERE, + new FireEventTask(worker_thread_, event_name, event_args)); + } else { + LOG(ERROR) << "Trying to post a message before the COM worker thread is" + "completely initialized and ready."; + } +} + + +ToolbandModule::ComWorkerThread::ComWorkerThread() + : base::Thread("CEEE-COM Worker Thread"), + current_number_of_retries_(0) { +} + +void ToolbandModule::ComWorkerThread::Init() { + ::CoInitializeEx(0, COINIT_MULTITHREADED); + HRESULT hr = broker_.CoCreateInstance(CLSID_CeeeBroker); + DCHECK(SUCCEEDED(hr)) << "Failed to create broker. " << com::LogHr(hr); +} + +void ToolbandModule::ComWorkerThread::CleanUp() { + broker_.Release(); + ::CoUninitialize(); +} + +void ToolbandModule::ComWorkerThread::FireEventToBroker(BSTR event_name, + BSTR event_args) { + DCHECK(broker_ != NULL); + if (broker_ != NULL) { + HRESULT hr = broker_->FireEvent(event_name, event_args); + if (SUCCEEDED(hr)) { + current_number_of_retries_ = 0; + return; + } + // If the server is busy (which can happen if it is calling in as we try to + // to call out to it), then we should retry a few times a little later. + if (current_number_of_retries_ < kMaxNumberOfRetries && message_loop()) { + ++current_number_of_retries_; + LOG(WARNING) << "Retrying Broker FireEvent Failure. " << com::LogHr(hr); + message_loop()->PostDelayedTask(FROM_HERE, + new FireEventTask(this, event_name, event_args), kRetryDelayMs); + } else { + current_number_of_retries_ = 0; + DCHECK(SUCCEEDED(hr)) << "Broker FireEvent Failed. " << com::LogHr(hr); + } + } +} + +ToolbandModule module; + +void ceee_module_util::AddRefModuleWorkerThread() { + module.IncThreadRefCount(); +} +void ceee_module_util::ReleaseModuleWorkerThread() { + module.DecThreadRefCount(); +} + +void ceee_module_util::FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + module.FireEventToBroker(event_name, event_args); +} + +void ceee_module_util::Lock() { + module.m_csStaticDataInitAndTypeInfo.Lock(); +} + +void ceee_module_util::Unlock() { + module.m_csStaticDataInitAndTypeInfo.Unlock(); +} + +LONG ceee_module_util::LockModule() { + return module.Lock(); +} + +LONG ceee_module_util::UnlockModule() { + return module.Unlock(); +} + + +// DLL Entry Point +extern "C" BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, + LPVOID reserved) { + // Prevent us from being loaded by older versions of the shell. + if (reason == DLL_PROCESS_ATTACH) { + wchar_t main_exe[MAX_PATH] = { 0 }; + ::GetModuleFileName(NULL, main_exe, arraysize(main_exe)); + + // We don't want to be loaded in the explorer process. + _wcslwr_s(main_exe, arraysize(main_exe)); + if (wcsstr(main_exe, windows::kExplorerModuleName)) + return FALSE; + } + + return module.DllMain(reason, reserved); +} + +// Used to determine whether the DLL can be unloaded by OLE +STDAPI DllCanUnloadNow(void) { + return module.DllCanUnloadNow(); +} + +// Returns a class factory to create an object of the requested type +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { + return module.DllGetClassObject(rclsid, riid, ppv); +} + +// DllRegisterServer - Adds entries to the system registry +// +// This is not the actual entrypoint; see the define right below this +// function, which keeps us safe from ever forgetting to check for +// the --enable-ceee flag. +STDAPI DllRegisterServerImpl(void) { + // registers object, typelib and all interfaces in typelib + HRESULT hr = module.DllRegisterServer(); + return hr; +} + +CEEE_DEFINE_DLL_REGISTER_SERVER() + +// DllUnregisterServer - Removes entries from the system registry +STDAPI DllUnregisterServer(void) { + // We always allow unregistration, even if no --enable-ceee install flag. + HRESULT hr = module.DllUnregisterServer(); + return hr; +} diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting.cc b/ceee/ie/plugin/toolband/toolband_module_reporting.cc new file mode 100644 index 0000000..c1d5f58 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of CEEE plugin's wrapper around common crash reporting. + +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "ceee/ie/common/ceee_module_util.h" + +// Well known SID for the system principal. +const wchar_t kSystemPrincipalSid[] = L"S-1-5-18"; + +// Returns the custom info structure based on the dll in parameter and the +// process type. +google_breakpad::CustomClientInfo* GetCustomInfo() { + // TODO(jeffbailey@google.com): Put in a real version. + // (bb3143594). + static google_breakpad::CustomInfoEntry ver_entry(L"ver", L"Ver.Goes.Here"); + static google_breakpad::CustomInfoEntry prod_entry(L"prod", L"CEEE_IE"); + static google_breakpad::CustomInfoEntry plat_entry(L"plat", L"Win32"); + static google_breakpad::CustomInfoEntry type_entry(L"ptype", L"ie_plugin"); + static google_breakpad::CustomInfoEntry entries[] = { + ver_entry, prod_entry, plat_entry, type_entry }; + static google_breakpad::CustomClientInfo custom_info = { + entries, arraysize(entries) }; + return &custom_info; +} + +bool InitializeCrashReporting() { + if (!ceee_module_util::GetCollectStatsConsent()) + return false; + + // Get the alternate dump directory. We use the temp path. + FilePath temp_directory; + if (!file_util::GetTempDir(&temp_directory) || temp_directory.empty()) { + return false; + } + + bool result = InitializeVectoredCrashReporting( + false, kSystemPrincipalSid, temp_directory.value(), GetCustomInfo()); + DCHECK(result) << "Failed initialize crashreporting."; + return result; +} + +bool ShutdownCrashReporting() { + return ShutdownVectoredCrashReporting(); +} diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting.h b/ceee/ie/plugin/toolband/toolband_module_reporting.h new file mode 100644 index 0000000..9b3aad9 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting.h @@ -0,0 +1,23 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A wrapper around common crash reporting code to manage reporting for CEEE +// plugin. + +#ifndef CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ +#define CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ + +#include "chrome_frame/crash_reporting/crash_report.h" + +extern const wchar_t kSystemPrincipalSid[]; + +// Intialize crash reporting for the Toolband Plugin. Specific parameters +// here include using the temp directory for dumps, using the system-wide +// install ID, and customized client info. +bool InitializeCrashReporting(); + +// Shut down crash reporting for CEEE plug-in. +bool ShutdownCrashReporting(); + +#endif // CEEE_IE_PLUGIN_TOOLBAND_TOOLBAND_MODULE_REPORTING_H_ diff --git a/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc b/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc new file mode 100644 index 0000000..dfbf904 --- /dev/null +++ b/ceee/ie/plugin/toolband/toolband_module_reporting_unittest.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// IE toolband module reporting unit tests. +#include "ceee/ie/plugin/toolband/toolband_module_reporting.h" + +#include "ceee/ie/common/ceee_module_util.h" +#include "ceee/testing/utils/mock_static.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using testing::_; +using testing::Return; +using testing::StrictMock; + +MOCK_STATIC_CLASS_BEGIN(MockToolbandModuleReporting) + MOCK_STATIC_INIT_BEGIN(MockToolbandModuleReporting) + MOCK_STATIC_INIT2(ceee_module_util::GetCollectStatsConsent, + GetCollectStatsConsent); + MOCK_STATIC_INIT(InitializeVectoredCrashReporting); + MOCK_STATIC_INIT_END() + + MOCK_STATIC0(bool, , GetCollectStatsConsent); + MOCK_STATIC4(bool, , InitializeVectoredCrashReporting, bool, + const wchar_t*, + const std::wstring&, + google_breakpad::CustomClientInfo*); +MOCK_STATIC_CLASS_END(MockToolbandModuleReporting) + +TEST(ToolbandModuleReportingTest, InitializeCrashReportingWithoutConsent) { + StrictMock<MockToolbandModuleReporting> mock; + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(mock, InitializeVectoredCrashReporting(_, _, _, _)) + .Times(0); + + InitializeCrashReporting(); +} + +TEST(ToolbandModuleReportingTest, InitializeCrashReportingWithConsent) { + StrictMock<MockToolbandModuleReporting> mock; + + EXPECT_CALL(mock, GetCollectStatsConsent()) + .Times(1) + .WillOnce(Return(true)); + + EXPECT_CALL(mock, InitializeVectoredCrashReporting(_, _, _, _)) + .Times(1) + .WillOnce(Return(true)); + + InitializeCrashReporting(); +} + +} // namespace diff --git a/ceee/ie/testing/ie_unittest_main.cc b/ceee/ie/testing/ie_unittest_main.cc new file mode 100644 index 0000000..2317472 --- /dev/null +++ b/ceee/ie/testing/ie_unittest_main.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Main function for common unittests. +#include <atlbase.h> +#include <atlcom.h> +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "ceee/testing/utils/gflag_utils.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "toolband.h" // NOLINT + +// Stub for unittesting. +namespace ceee_module_util { + +LONG LockModule() { + return 0; +} +LONG UnlockModule() { + return 0; +} +void Lock() { +} +void Unlock() { +} +void AddRefModuleWorkerThread() { +} +void ReleaseModuleWorkerThread() { +} + +// Fires an event to the broker, so that the call can be made with an +// instance of a broker proxy that was CoCreated in the worker thread. +void FireEventToBroker(const std::string& event_name, + const std::string& event_args) { + // Must get some work done so that the function can be mocked. + // Otherwise, it would be too short to be side stepped... + if (event_name == event_args) { + CHECK(event_name == event_args); + } else { + CHECK(event_name != event_args); + } +} + +} // namespace ceee_module_util + + +// We're testing ATL code that requires a module object. +class ObligatoryModule: public CAtlDllModuleT<ObligatoryModule> { + public: + DECLARE_LIBID(LIBID_ToolbandLib); +}; + +ObligatoryModule g_obligatory_atl_module; + +// Run these tests under page heap. +const DWORD kGFlags = FLG_USER_STACK_TRACE_DB | FLG_HEAP_PAGE_ALLOCS; + +int main(int argc, char **argv) { + // Disabled, bb2560934. + // if (!IsDebuggerPresent()) + // testing::RelaunchWithGFlags(kTestGFlags); + + base::AtExitManager at_exit; + CommandLine::Init(argc, argv); + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + testing::InitGoogleMock(&argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/ceee/ie/testing/mediumtest_ie_common.cc b/ceee/ie/testing/mediumtest_ie_common.cc new file mode 100644 index 0000000..1b2fd45 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_common.cc @@ -0,0 +1,267 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A common test fixture for testing against a captive shell browser +// control instance - which is as close to habitating the belly of the IE +// beast as can be done with a modicum of safety and sanitation. +#include "ceee/ie/testing/mediumtest_ie_common.h" + +#include <atlcrack.h> +#include <atlsync.h> +#include <atlwin.h> +#include <exdisp.h> +#include <exdispid.h> +#include <mshtmdid.h> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "base/utf_string_conversions.h" +#include "gtest/gtest.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_util.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + + +namespace testing { + +// A test resource that's a simple single-resource page. +const wchar_t* kSimplePage = L"simple_page.html"; +// A test resource that's a frameset referencing the two frames below. +const wchar_t* kTwoFramesPage = L"two_frames.html"; +const wchar_t* kFrameOne = L"frame_one.html"; +const wchar_t* kFrameTwo = L"frame_two.html"; +// Another test resource that's a frameset referencing the two frames below. +const wchar_t* kAnotherTwoFramesPage = L"another_two_frames.html"; +const wchar_t* kAnotherFrameOne = L"another_frame_one.html"; +const wchar_t* kAnotherFrameTwo = L"another_frame_two.html"; + +// A test resource that's a top-level frameset with two nested iframes +// the inner one of which refers frame_one.html. +const wchar_t* kDeepFramesPage = L"deep_frames.html"; +const wchar_t* kLevelOneFrame = L"level_one_frame.html"; +const wchar_t* kLevelTwoFrame = L"level_two_frame.html"; + +// A test resource that have javascript generate frames that look orphan. +const wchar_t* kOrphansPage = L"orphans.html"; + +std::set<READYSTATE> BrowserEventSinkBase::seen_states_; + +// Constructs a res: url to the test resource in our module. +std::wstring GetTestUrl(const wchar_t* resource_name) { + FilePath path; + EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &path)); + path = path.Append(FILE_PATH_LITERAL("ceee")) + .Append(FILE_PATH_LITERAL("ie")) + .Append(FILE_PATH_LITERAL("testing")) + .Append(FILE_PATH_LITERAL("test_data")) + .Append(resource_name); + + return UTF8ToWide(net::FilePathToFileURL(path).spec()); +} + +// Returns the path to our temp folder. +std::wstring GetTempPath() { + FilePath temp; + EXPECT_TRUE(PathService::Get(base::DIR_TEMP, &temp)); + + return temp.value(); +} + +_ATL_FUNC_INFO handler_type_idispatch_ = + { CC_STDCALL, VT_EMPTY, 1, { VT_DISPATCH } }; +_ATL_FUNC_INFO handler_type_idispatch_variantptr_ = + { CC_STDCALL, VT_EMPTY, 2, { VT_DISPATCH, VT_BYREF | VT_VARIANT } }; + + +void BrowserEventSinkBase::GetDescription(std::string* description) const { + description->clear(); + description->append("TestBrowserEventSink"); +} + +HRESULT BrowserEventSinkBase::Initialize(IWebBrowser2* browser) { + browser_ = browser; + HRESULT hr = DispEventAdvise(browser_); + if (SUCCEEDED(hr)) { + hr = AtlAdvise(browser_, + GetUnknown(), + IID_IPropertyNotifySink, + &prop_notify_cookie_); + } + + return hr; +} + +void BrowserEventSinkBase::TearDown() { + EXPECT_HRESULT_SUCCEEDED(DispEventUnadvise(browser_)); + EXPECT_HRESULT_SUCCEEDED(AtlUnadvise(browser_, + IID_IPropertyNotifySink, + prop_notify_cookie_)); +} + +STDMETHODIMP BrowserEventSinkBase::OnChanged(DISPID changed_property) { + ATLTRACE("OnChanged(%d)\n", changed_property); + + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + browser_->get_ReadyState(&ready_state); + ATLTRACE("READYSTATE: %d\n", ready_state); + seen_states_.insert(ready_state); + + return S_OK; +} + +STDMETHODIMP BrowserEventSinkBase::OnRequestEdit(DISPID changed_property) { + ATLTRACE("OnRequestEdit(%d)\n", changed_property); + return S_OK; +} + +STDMETHODIMP_(void) BrowserEventSinkBase::OnNavigateComplete( + IDispatch* browser_disp, VARIANT* url_var) { +} + +STDMETHODIMP_(void) BrowserEventSinkBase::OnDocumentComplete( + IDispatch* browser_disp, VARIANT* url) { +} + +void ShellBrowserTestBase::SetUpTestCase() { + // CoInitialize(NULL) serves two purposes here: + // 1. if we're running in a non-initialized apartment, it initializes it. + // 2. we need to be in an STA to use the shell browser, and if we're + // for whatever reason running in an MTA, it will fail. + ASSERT_HRESULT_SUCCEEDED(::CoInitialize(NULL)); + ASSERT_TRUE(AtlAxWinInit()); +} + +void ShellBrowserTestBase::TearDownTestCase() { + EXPECT_TRUE(AtlAxWinTerm()); + ::CoUninitialize(); +} + +void ShellBrowserTestBase::SetUp() { + ASSERT_TRUE(Create(HWND_MESSAGE) != NULL); + + // Create the webbrowser control and get the AX host. + ASSERT_HRESULT_SUCCEEDED( + AtlAxCreateControl(CComBSTR(CLSID_WebBrowser), + m_hWnd, + NULL, + &host_)); + + // Get the control's top-level IWebBrowser. + CComPtr<IUnknown> control; + ASSERT_HRESULT_SUCCEEDED(AtlAxGetControl(m_hWnd, &control)); + ASSERT_HRESULT_SUCCEEDED(control->QueryInterface(&browser_)); + + ASSERT_HRESULT_SUCCEEDED(CreateEventSink(browser_)); +} + +void ShellBrowserTestBase::TearDown() { + // Navigating the browser to a folder creates a non-webbrowser document, + // which shakes off all our frame handlers. + EXPECT_TRUE(NavigateBrowser(GetTempPath())); + + // Tear down the rest of our stuff. + if (event_sink_) + event_sink_->TearDown(); + + event_sink_keeper_.Release(); + host_.Release(); + browser_.Release(); + + // Should have retained no objects past this point. + EXPECT_EQ(0, InstanceCountMixinBase::all_instance_count()); + if (InstanceCountMixinBase::all_instance_count() > 0) { + InstanceCountMixinBase::LogLeakedInstances(); + } + + // Finally blow away the host window. + if (IsWindow()) + DestroyWindow(); +} + +bool ShellBrowserTestBase::NavigateBrowser(const std::wstring& url) { + CComVariant empty; + HRESULT hr = browser_->Navigate2(&CComVariant(url.c_str()), + &empty, &empty, + &empty, &empty); + EXPECT_HRESULT_SUCCEEDED(hr); + if (FAILED(hr)) + return false; + + return WaitForReadystateComplete(); +} + +bool ShellBrowserTestBase::WaitForReadystateComplete() { + return WaitForReadystate(READYSTATE_COMPLETE); +} + +bool ShellBrowserTestBase::WaitForReadystateLoading() { + return WaitForReadystate(READYSTATE_LOADING); +} + +bool ShellBrowserTestBase::WaitForReadystateWithTimerId( + READYSTATE waiting_for, UINT_PTR timer_id) { + while (true) { + if (!browser_ || !event_sink_) + return false; + + // Is the browser in the required state now? + READYSTATE ready_state = READYSTATE_UNINITIALIZED; + HRESULT hr = browser_->get_ReadyState(&ready_state); + if (FAILED(hr)) + return false; + + if (ready_state == waiting_for) { + event_sink_->remove_state(waiting_for); + return true; + } + + // Has the state been seen? + if (event_sink_->has_state(waiting_for)) { + event_sink_->remove_state(waiting_for); + return true; + } + + MSG msg = {}; + if (!GetMessage(&msg, 0, 0, 0)) { + // WM_QUIT. + return false; + } + + if (msg.message == WM_TIMER && + msg.hwnd == NULL && + msg.wParam == timer_id) { + // Timeout. + return false; + } + + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + +bool ShellBrowserTestBase::WaitForReadystate(READYSTATE waiting_for) { + if (!event_sink_) + return false; + + // Clear the seen states. + event_sink_->clear_states(); + + // Bound the wait by setting a timer. + UINT_PTR timer_id = ::SetTimer(NULL, + 0, + wait_timeout_ms_, + NULL); + EXPECT_NE(0, timer_id); + bool ret = WaitForReadystateWithTimerId(waiting_for, timer_id); + EXPECT_TRUE(::KillTimer(NULL, timer_id)); + + return ret; +} + +} // namespace testing diff --git a/ceee/ie/testing/mediumtest_ie_common.h b/ceee/ie/testing/mediumtest_ie_common.h new file mode 100644 index 0000000..048e620 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_common.h @@ -0,0 +1,204 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A common test fixture for testing against a captive shell browser +// control instance - which is as close to habitating the belly of the IE +// beast as can be done with a modicum of safety and sanitation. +#ifndef CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ +#define CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ + +#include <atlbase.h> +#include <atlcrack.h> +#include <atlwin.h> +#include <exdisp.h> +#include <exdispid.h> +#include <mshtmdid.h> + +#include <set> +#include <string> + +#include "base/logging.h" +#include "base/file_path.h" +#include "base/path_service.h" +#include "base/base_paths_win.h" +#include "gtest/gtest.h" +#include "ceee/common/com_utils.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/test_utils.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace testing { + +// A test resource that's a simple single-resource page. +extern const wchar_t* kSimplePage; +// A test resource that's a frameset referencing the two frames below. +extern const wchar_t* kTwoFramesPage; +extern const wchar_t* kFrameOne; +extern const wchar_t* kFrameTwo; + +// Another copy of above on new URLs. +extern const wchar_t* kAnotherTwoFramesPage; +extern const wchar_t* kAnotherFrameOne; +extern const wchar_t* kAnotherFrameTwo; + +// A test resource that's a top-level frameset with two nested iframes +// the inner one of which refers frame_one.html. +extern const wchar_t* kDeepFramesPage; +extern const wchar_t* kLevelOneFrame; +extern const wchar_t* kLevelTwoFrame; + +// A test resource that have javascript generate frames that look orphan. +extern const wchar_t* kOrphansPage; + +// Constructs a file: url to the test file our source directory. +std::wstring GetTestUrl(const wchar_t* resource_name); + +// Returns the path to our temp folder. +std::wstring GetTempPath(); + +extern _ATL_FUNC_INFO handler_type_idispatch_; +extern _ATL_FUNC_INFO handler_type_idispatch_variantptr_; + +// Base class that implements the rudiments of sinking events from +// an IWebBrowser. +class BrowserEventSinkBase + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<BrowserEventSinkBase>, + public InstanceCountMixin<BrowserEventSinkBase>, + public IDispEventSimpleImpl<0, + BrowserEventSinkBase, + &DIID_DWebBrowserEvents2>, + public IPropertyNotifySink { + public: + BEGIN_COM_MAP(BrowserEventSinkBase) + COM_INTERFACE_ENTRY(IPropertyNotifySink) + END_COM_MAP() + + BEGIN_SINK_MAP(BrowserEventSinkBase) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_NAVIGATECOMPLETE2, + OnNavigateComplete, &handler_type_idispatch_variantptr_) + SINK_ENTRY_INFO(0, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, + OnDocumentComplete, &handler_type_idispatch_variantptr_) + END_SINK_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT(); + + BrowserEventSinkBase() : prop_notify_cookie_(-1) { + } + + virtual void GetDescription(std::string* description) const; + HRESULT Initialize(IWebBrowser2* browser); + + void TearDown(); + + // @name IPropertyNotifySink implementation. + // @{ + STDMETHOD(OnChanged)(DISPID changed_property); + STDMETHOD(OnRequestEdit)(DISPID changed_property); + // @} + + // @name DWebBrowserEvents event handler implementation. + // Override these as needed. + // @{ + STDMETHOD_(void, OnNavigateComplete)(IDispatch* browser_disp, + VARIANT* url_var); + STDMETHOD_(void, OnDocumentComplete)(IDispatch* browser_disp, VARIANT* url); + + static bool has_state(READYSTATE state) { + return seen_states_.find(state) != seen_states_.end(); + } + static void add_state(READYSTATE state) { + seen_states_.insert(state); + } + static void remove_state(READYSTATE state) { + seen_states_.erase(state); + } + static void clear_states() { + seen_states_.clear(); + } + + private: + // This bitset records the readystate values seen in OnChange. + // The point of this is to make the WaitForReadyState function + // below work in the case where the browser takes multiple + // readystate changes while processing a single event. This + // will occur in the refresh case, where the top-level browser + // and individual document objects transition from COMPLETE to + // LOADING and then back to COMPLETE on processing a single event. + static std::set<READYSTATE> seen_states_; + + // Advise cookie for property notify sink. + DWORD prop_notify_cookie_; + + CComPtr<IWebBrowser2> browser_; +}; + +// Test fixture base class. +class ShellBrowserTestBase + : public testing::Test, + public CWindowImpl<ShellBrowserTestBase> { + public: + // Default timeout for pumping events. + static const UINT kWaitTimeoutMs = 2000; + + ShellBrowserTestBase() + : wait_timeout_ms_(kWaitTimeoutMs) { + } + + static void SetUpTestCase(); + static void TearDownTestCase(); + + void SetUp(); + void TearDown(); + + // Navigates the browser to the given URL and waits for + // the top-level browser's readystate to reach complete. + bool NavigateBrowser(const std::wstring& url); + + // Wait for the top-level browser's readystate to reach + // READYSTATE_COMPLETE, READYSTATE_LOADING or wait_for, + // respectively. + bool WaitForReadystateComplete(); + bool WaitForReadystateLoading(); + bool WaitForReadystate(READYSTATE wait_for); + + BEGIN_MSG_MAP_EX(BrowserEventTest) + END_MSG_MAP() + + protected: + bool WaitForReadystateWithTimerId(READYSTATE waiting_for, UINT_PTR timer_id); + virtual HRESULT CreateEventSink(IWebBrowser2* browser) = 0; + + // Timeout for pumping messages in WaitForReadyState. + UINT wait_timeout_ms_; + + CComPtr<IUnknown> host_; + CComPtr<IWebBrowser2> browser_; + + BrowserEventSinkBase* event_sink_; + CComPtr<IUnknown> event_sink_keeper_; +}; + +template <class EventSinkImpl> +class ShellBrowserTestImpl: public ShellBrowserTestBase { + public: + protected: + virtual HRESULT CreateEventSink(IWebBrowser2* browser) { + EventSinkImpl* sink; + HRESULT hr = EventSinkImpl::CreateInitialized(&sink, + browser, + &event_sink_keeper_); + event_sink_ = sink; + return hr; + } + + // Handy accessor to retrieve the event sink by actual type. + EventSinkImpl* event_sink() const { + return static_cast<EventSinkImpl*>(event_sink_); + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MEDIUMTEST_IE_COMMON_H_ diff --git a/ceee/ie/testing/mediumtest_ie_main.cc b/ceee/ie/testing/mediumtest_ie_main.cc new file mode 100644 index 0000000..b79fe46 --- /dev/null +++ b/ceee/ie/testing/mediumtest_ie_main.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Main function for large IE tests. +#include <atlbase.h> +#include <atlcom.h> +#include "base/at_exit.h" +#include "base/command_line.h" +#include "ceee/testing/utils/gflag_utils.h" +#include "ceee/testing/utils/nt_internals.h" +#include "chrome/common/url_constants.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + + +// Stub for unittesting. +namespace ceee_module_util { + +LONG LockModule() { + return 0; +} +LONG UnlockModule() { + return 0; +} +void Lock() { +} +void Unlock() { +} +void AddRefModuleWorkerThread() { +} +void ReleaseModuleWorkerThread() { +} +void FireEventToBroker(const std::string& event_name, + const std::string& event_args) { +} + +} // namespace ceee_module_util + + +// We're testing some ATL code that requires a module object. +class ObligatoryModule: public CAtlDllModuleT<ObligatoryModule> { +}; + +ObligatoryModule g_obligatory_atl_module; + +const DWORD kTestGFlags = FLG_USER_STACK_TRACE_DB | + FLG_ENABLE_HANDLE_EXCEPTIONS | + FLG_ENABLE_CLOSE_EXCEPTIONS | + FLG_HEAP_VALIDATE_PARAMETERS; + +int main(int argc, char **argv) { + // Disabled, bb2560934. + // if (!IsDebuggerPresent()) + // testing::RelaunchWithGFlags(kTestGFlags); + + testing::InitGoogleMock(&argc, argv); + testing::InitGoogleTest(&argc, argv); + + // Obligatory Chrome base initialization. + CommandLine::Init(argc, argv); + base::AtExitManager at_exit_manager; + + // Needs to be called before we can use GURL. + chrome::RegisterChromeSchemes(); + + return RUN_ALL_TESTS(); +} diff --git a/ceee/ie/testing/mock_broker_and_friends.h b/ceee/ie/testing/mock_broker_and_friends.h new file mode 100644 index 0000000..7c02845 --- /dev/null +++ b/ceee/ie/testing/mock_broker_and_friends.h @@ -0,0 +1,359 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of Broker and related objects. +#ifndef CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ +#define CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ + +#include <string> + +#include "ceee/ie/broker/api_dispatcher.h" +#include "ceee/ie/plugin/bho/cookie_events_funnel.h" +#include "ceee/ie/plugin/bho/tab_events_funnel.h" +#include "ceee/ie/plugin/bho/webnavigation_events_funnel.h" +#include "ceee/ie/plugin/bho/webrequest_events_funnel.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +#include "broker_lib.h" // NOLINT +#include "toolband.h" // NOLINT + +namespace testing { + +class MockBrokerImpl : public ICeeeBroker { + public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, Execute, HRESULT(BSTR, BSTR*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, FireEvent, HRESULT(BSTR, BSTR)); +}; + +class MockBrokerRegistrarImpl : public ICeeeBrokerRegistrar { + public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, RegisterWindowExecutor, + HRESULT(long, IUnknown*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, UnregisterExecutor, HRESULT(long)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, RegisterTabExecutor, + HRESULT(long, IUnknown*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetTabIdForHandle, + HRESULT(long, CeeeWindowHandle)); +}; + +class MockBroker + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockBroker>, + public InstanceCountMixin<MockBroker>, + public StrictMock<MockBrokerRegistrarImpl>, + public StrictMock<MockBrokerImpl> { + public: + BEGIN_COM_MAP(MockBroker) + COM_INTERFACE_ENTRY(ICeeeBrokerRegistrar) + COM_INTERFACE_ENTRY(ICeeeBroker) + END_COM_MAP() + + HRESULT Initialize(MockBroker** self) { + *self = this; + return S_OK; + } +}; + +class MockExecutorIUnknown + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockExecutorIUnknown>, + public InstanceCountMixin<MockExecutorIUnknown>, + public IObjectWithSiteImpl<MockExecutorIUnknown> { + public: + BEGIN_COM_MAP(MockExecutorIUnknown) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockExecutorIUnknown** self) { + *self = this; + return S_OK; + } +}; + +class MockCeeeWindowExecutorImpl : public ICeeeWindowExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Initialize, HRESULT(CeeeWindowHandle)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetWindow, + HRESULT(BOOL, CeeeWindowInfo*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabs, HRESULT(BSTR*)); + MOCK_METHOD5_WITH_CALLTYPE(__stdcall, UpdateWindow, HRESULT(long, long, + long, long, CeeeWindowInfo*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, RemoveWindow, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetTabIndex, HRESULT(CeeeWindowHandle, + long*)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, MoveTab, HRESULT(CeeeWindowHandle, + long)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, RemoveTab, HRESULT(CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SelectTab, HRESULT(CeeeWindowHandle)); +}; + +class MockWindowExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockWindowExecutor>, + public InstanceCountMixin<MockWindowExecutor>, + public IObjectWithSiteImpl<MockWindowExecutor>, + public StrictMock<MockCeeeWindowExecutorImpl> { + public: + BEGIN_COM_MAP(MockWindowExecutor) + COM_INTERFACE_ENTRY(ICeeeWindowExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockWindowExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockCeeeTabExecutorImpl : public ICeeeTabExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, Initialize, HRESULT(CeeeWindowHandle)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetTabInfo, + HRESULT(CeeeTabInfo*)); + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, Navigate, HRESULT(BSTR, long, BSTR)); + MOCK_METHOD4_WITH_CALLTYPE(__stdcall, InsertCode, HRESULT( + BSTR, BSTR, BOOL, CeeeTabCodeType)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, PopupizeFrameWindow, HRESULT(long)); +}; + +class MockTabExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockTabExecutor>, + public InstanceCountMixin<MockTabExecutor>, + public IObjectWithSiteImpl<MockTabExecutor>, + public StrictMock<MockCeeeTabExecutorImpl> { + public: + BEGIN_COM_MAP(MockTabExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockTabExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockTabEventsFunnel : public TabEventsFunnel { + public: + MOCK_METHOD4(OnMoved, HRESULT(HWND tab, int window_id, int from_index, + int to_index)); + MOCK_METHOD1(OnRemoved, HRESULT(HWND tab)); + MOCK_METHOD2(OnSelectionChanged, HRESULT(HWND tab, int window_id)); + MOCK_METHOD3(OnCreated, HRESULT(HWND tab, BSTR url, bool completed)); + MOCK_METHOD3(OnUpdated, HRESULT(HWND tab, BSTR url, + READYSTATE ready_state)); + MOCK_METHOD2(OnTabUnmapped, HRESULT(HWND tab, int tab_id)); +}; + +class MockCeeeCookieExecutorImpl : public ICeeeCookieExecutor { + public: + MOCK_METHOD3_WITH_CALLTYPE(__stdcall, GetCookie, + HRESULT(BSTR, BSTR, CeeeCookieInfo*)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, RegisterCookieStore, HRESULT()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, CookieStoreIsRegistered, HRESULT()); +}; + +class MockCookieExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockCookieExecutor>, + public InstanceCountMixin<MockCookieExecutor>, + public IObjectWithSiteImpl<MockCookieExecutor>, + public StrictMock<MockCeeeCookieExecutorImpl> { + public: + BEGIN_COM_MAP(MockCookieExecutor) + COM_INTERFACE_ENTRY(ICeeeCookieExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockCookieExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockCookieEventsFunnel : public CookieEventsFunnel { + public: + MOCK_METHOD2(OnChanged, HRESULT(bool removed, + const cookie_api::CookieInfo& cookie)); +}; + +class MockCeeeInfobarExecutorImpl : public ICeeeInfobarExecutor { + public: + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetExtensionId, HRESULT(BSTR)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, ShowInfobar, + HRESULT(BSTR, CeeeWindowHandle*)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, OnTopFrameBeforeNavigate, + HRESULT(BSTR)); +}; + +class MockInfobarExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockInfobarExecutor>, + public InstanceCountMixin<MockInfobarExecutor>, + public IObjectWithSiteImpl<MockInfobarExecutor>, + public StrictMock<MockCeeeInfobarExecutorImpl> { + public: + BEGIN_COM_MAP(MockInfobarExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockInfobarExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockTabInfobarExecutor + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<MockTabInfobarExecutor>, + public InstanceCountMixin<MockTabInfobarExecutor>, + public IObjectWithSiteImpl<MockTabInfobarExecutor>, + public StrictMock<MockCeeeTabExecutorImpl>, + public StrictMock<MockCeeeInfobarExecutorImpl> { + public: + BEGIN_COM_MAP(MockTabInfobarExecutor) + COM_INTERFACE_ENTRY(ICeeeTabExecutor) + COM_INTERFACE_ENTRY(ICeeeInfobarExecutor) + COM_INTERFACE_ENTRY(IObjectWithSite) + END_COM_MAP() + + HRESULT Initialize(MockTabInfobarExecutor** self) { + *self = this; + return S_OK; + } +}; + +class MockApiDispatcher : public ApiDispatcher { + public: + MOCK_METHOD2(HandleApiRequest, void(BSTR, BSTR*)); + MOCK_METHOD2(RegisterInvocation, void(const char* function_name, + InvocationFactory factory)); + MOCK_METHOD3(RegisterEphemeralEventHandler, + void(const char*, EphemeralEventHandler, InvocationResult*)); + MOCK_METHOD3(GetExecutor, void(HWND, REFIID, void**)); + MOCK_METHOD2(FireEvent, void(BSTR event_name, BSTR event_args)); + + MOCK_CONST_METHOD1(GetTabHandleFromId, HWND(int)); + MOCK_CONST_METHOD1(GetWindowHandleFromId, HWND(int)); + MOCK_CONST_METHOD1(GetTabIdFromHandle, int(HWND)); + MOCK_CONST_METHOD1(GetWindowIdFromHandle, int(HWND)); +}; + +// A mock class for an API invocation class that derives from ApiResultCreator, +// defined in ceee\ie\broker\api_dispatcher.h. This class enables the injection +// of a mock API result instance when the API is invoked. +template<class ResultType, class MockResultType, class BaseClass> +class MockApiInvocation : public BaseClass { + public: + MockApiInvocation() : request_id_(kRequestId) {} + + // Calls the ContinueExecution method of the base class, using the mock + // invocation result and mock API dispatcher. + HRESULT CallContinueExecution(const std::string& input_args) { + HRESULT hr = ContinueExecution(input_args, invocation_result_.get(), + GetDispatcher()); + + // NOTE: the ContinueExecution method has already deleted the invocation + // result if its return is not S_FALSE. In that case, we have to release + // the pointer so that we don't delete the same object twice. + if (hr != S_FALSE) + invocation_result_.release(); + + return hr; + } + + // We need to create the results before we get asked for one + // so that we can set expectations on it. And we need to allocate them + // because the callers of CreateApiResult take ownership of the memory. + void AllocateNewResult(int request_id) { + request_id_ = request_id; + invocation_result_.reset(new StrictMock<MockResultType>(request_id)); + } + virtual ResultType* CreateApiResult(int request_id) { + EXPECT_EQ(request_id, request_id_); + EXPECT_NE(static_cast<ResultType*>(NULL), invocation_result_.get()); + return invocation_result_.release(); // The caller becomes the owner. + } + virtual ApiDispatcher* GetDispatcher() { + return &mock_api_dispatcher_; + } + + // public so that the tests can set expectations on them. + scoped_ptr<StrictMock<MockResultType> > invocation_result_; + StrictMock<MockApiDispatcher> mock_api_dispatcher_; + + private: + int request_id_; +}; + +class MockWebNavigationEventsFunnel : public WebNavigationEventsFunnel { + public: + MOCK_METHOD5(OnBeforeNavigate, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + int request_id, + const base::Time& time_stamp)); + MOCK_METHOD4(OnBeforeRetarget, HRESULT(CeeeWindowHandle source_tab_handle, + BSTR source_url, + BSTR target_url, + const base::Time& time_stamp)); + MOCK_METHOD6(OnCommitted, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const char* transition_type, + const char* transition_qualifiers, + const base::Time& time_stamp)); + MOCK_METHOD4(OnCompleted, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp)); + MOCK_METHOD4(OnDOMContentLoaded, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + const base::Time& time_stamp)); + MOCK_METHOD5(OnErrorOccurred, HRESULT(CeeeWindowHandle tab_handle, + BSTR url, + int frame_id, + BSTR error, + const base::Time& time_stamp)); +}; + +class MockWebRequestEventsFunnel : public WebRequestEventsFunnel { + public: + MOCK_METHOD5(OnBeforeRedirect, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const wchar_t* redirect_url, + const base::Time& time_stamp)); + MOCK_METHOD6(OnBeforeRequest, HRESULT(int request_id, + const wchar_t* url, + const char* method, + CeeeWindowHandle tab_handle, + const char* type, + const base::Time& time_stamp)); + MOCK_METHOD4(OnCompleted, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp)); + MOCK_METHOD4(OnErrorOccurred, HRESULT(int request_id, + const wchar_t* url, + const wchar_t* error, + const base::Time& time_stamp)); + MOCK_METHOD4(OnHeadersReceived, HRESULT(int request_id, + const wchar_t* url, + DWORD status_code, + const base::Time& time_stamp)); + MOCK_METHOD4(OnRequestSent, HRESULT(int request_id, + const wchar_t* url, + const char* ip, + const base::Time& time_stamp)); +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_BROKER_AND_FRIENDS_H_ diff --git a/ceee/ie/testing/mock_browser_and_friends.h b/ceee/ie/testing/mock_browser_and_friends.h new file mode 100644 index 0000000..76db63c --- /dev/null +++ b/ceee/ie/testing/mock_browser_and_friends.h @@ -0,0 +1,113 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Implementation of TestBrowser and related objects. +#ifndef CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ +#define CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ + +#include <string> + +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" + +namespace testing { + +class MockIOleWindow : public IOleWindow { + public: + // Simple IOleWindow implementation here, no need for a MockXxxImpl. + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetWindow, HRESULT(HWND* window)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, ContextSensitiveHelp, HRESULT(BOOL)); +}; + +class TestBrowserSite + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowserSite>, + public InitializingCoClass<TestBrowserSite>, + public IServiceProviderImpl<TestBrowserSite>, + public StrictMock<MockIOleWindow> { + public: + BEGIN_COM_MAP(TestBrowserSite) + COM_INTERFACE_ENTRY(IServiceProvider) + COM_INTERFACE_ENTRY(IOleWindow) + END_COM_MAP() + + BEGIN_SERVICE_MAP(TestDumbSite) + if (browser_ && (guidService == SID_SWebBrowserApp || + guidService == SID_SShellBrowser)) { + return browser_->QueryInterface(riid, ppvObject); + } + END_SERVICE_MAP() + + HRESULT Initialize(TestBrowserSite** self) { + *self = this; + return S_OK; + } + + public: + CComPtr<IDispatch> browser_; +}; + +class TestBrowser + : public CComObjectRootEx<CComSingleThreadModel>, + public InstanceCountMixin<TestBrowser>, + public InitializingCoClass<TestBrowser>, + public StrictMock<IWebBrowser2MockImpl>, + public IConnectionPointImpl<TestBrowser, &DIID_DWebBrowserEvents2>, + public IConnectionPointContainerImpl<TestBrowser> { + public: + typedef IConnectionPointImpl<TestBrowser, &DIID_DWebBrowserEvents2> + WebBrowserEvents; + + BEGIN_COM_MAP(TestBrowser) + COM_INTERFACE_ENTRY(IDispatch) + COM_INTERFACE_ENTRY(IWebBrowser) + COM_INTERFACE_ENTRY(IWebBrowserApp) + COM_INTERFACE_ENTRY(IWebBrowser2) + COM_INTERFACE_ENTRY(IConnectionPointContainer) + END_COM_MAP() + + BEGIN_CONNECTION_POINT_MAP(TestBrowser) + CONNECTION_POINT_ENTRY(DIID_DWebBrowserEvents2) + END_CONNECTION_POINT_MAP() + + // Override from IConnectionPointContainerImpl. + STDMETHOD(FindConnectionPoint)(REFIID iid, IConnectionPoint** cp) { + typedef IConnectionPointContainerImpl<TestBrowser> CPC; + + if (iid == DIID_DWebBrowserEvents2 && no_events_) + return CONNECT_E_NOCONNECTION; + + return CPC::FindConnectionPoint(iid, cp); + } + + void FireOnNavigateComplete(IDispatch* browser, VARIANT *url) { + CComVariant args[] = { 0, browser }; + args[0].vt = VT_BYREF | VT_BSTR; + args[0].pvarVal = url; + + testing::FireEvent(static_cast<WebBrowserEvents*>(this), + DISPID_NAVIGATECOMPLETE2, + arraysize(args), + args); + } + + TestBrowser() : no_events_(false) { + } + + HRESULT Initialize(TestBrowser** self) { + *self = this; + return S_OK; + } + + public: + // If true, won't return the WebBrowserEvents connection point. + bool no_events_; +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_BROWSER_AND_FRIENDS_H_ diff --git a/ceee/ie/testing/mock_chrome_frame_host.h b/ceee/ie/testing/mock_chrome_frame_host.h new file mode 100644 index 0000000..8ef07ff --- /dev/null +++ b/ceee/ie/testing/mock_chrome_frame_host.h @@ -0,0 +1,62 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of ChromeFrameHost. +#ifndef CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ +#define CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ + +#include <string> +#include "ceee/ie/common/chrome_frame_host.h" +#include "gmock/gmock.h" +#include "ceee/common/initializing_coclass.h" +#include "ceee/testing/utils/instance_count_mixin.h" + +namespace testing { + +class IChromeFrameHostMockImpl : public IChromeFrameHost { + public: + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, SetAsChromeFrameMaster, void()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetChromeProfileName, + void(const wchar_t* chrome_profile_name)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetUrl, HRESULT(BSTR url)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, StartChromeFrame, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, PostMessage, + HRESULT(BSTR message, BSTR target)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, TearDown, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, SetEventSink, + void(IChromeFrameHostEvents* event_sink)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, InstallExtension, + HRESULT(BSTR crx_path)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, LoadExtension, + HRESULT(BSTR extension_dir)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, GetEnabledExtensions, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetExtensionApisToAutomate, + HRESULT(BSTR* enabled_functions)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, GetSessionId, HRESULT(int*)); +}; + +// A mock implementation of ChromeFrameHost. +class MockChromeFrameHost + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<StrictMock<MockChromeFrameHost> >, + public InstanceCountMixin<MockChromeFrameHost>, + public StrictMock<IChromeFrameHostMockImpl> { + public: + BEGIN_COM_MAP(MockChromeFrameHost) + COM_INTERFACE_ENTRY_IID(IID_IChromeFrameHost, IChromeFrameHost) + END_COM_MAP() + + HRESULT Initialize() { + return S_OK; + } + + HRESULT Initialize(MockChromeFrameHost** self) { + *self = this; + return S_OK; + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_CHROME_FRAME_HOST_H_ diff --git a/ceee/ie/testing/mock_frame_event_handler_host.h b/ceee/ie/testing/mock_frame_event_handler_host.h new file mode 100644 index 0000000..18e9572 --- /dev/null +++ b/ceee/ie/testing/mock_frame_event_handler_host.h @@ -0,0 +1,81 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Mock implementation of ChromeFrameHost. +#ifndef CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ +#define CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ + +#include <string> + +#include "ceee/common/initializing_coclass.h" +#include "ceee/ie/plugin/bho/frame_event_handler.h" +#include "ceee/ie/plugin/scripting/content_script_native_api.h" +#include "ceee/testing/utils/instance_count_mixin.h" +#include "gmock/gmock.h" + +namespace testing { + +class IFrameEventHandlerHostMockImpl : public IFrameEventHandlerHost { + public: + MOCK_METHOD3(AttachBrowser, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler)); + MOCK_METHOD3(DetachBrowser, HRESULT(IWebBrowser2* browser, + IWebBrowser2* parent_browser, + IFrameEventHandler* handler)); + MOCK_METHOD1(GetTopLevelBrowser, HRESULT(IWebBrowser2** browser)); + MOCK_METHOD1(GetReadyState, HRESULT(READYSTATE* ready_state)); + MOCK_METHOD1(OnReadyStateChanged, HRESULT(READYSTATE ready_state)); + MOCK_METHOD3(GetMatchingUserScriptsCssContent, + HRESULT(const GURL& url, bool require_all_frames, + std::string* css_content)); + MOCK_METHOD4(GetMatchingUserScriptsJsContent, + HRESULT(const GURL& url, + UserScript::RunLocation location, + bool require_all_frames, + UserScriptsLibrarian::JsFileList* js_file_list)); + MOCK_METHOD1(GetExtensionId, HRESULT(std::wstring* extension_id)); + MOCK_METHOD1(GetExtensionPath, HRESULT(std::wstring* extension_path)); + MOCK_METHOD1(GetExtensionPortMessagingProvider, + HRESULT(IExtensionPortMessagingProvider** messaging_provider)); + MOCK_METHOD4(InsertCode, HRESULT(BSTR, BSTR, BOOL, CeeeTabCodeType)); +}; + +class IExtensionPortMessagingProviderMockImpl + : public IExtensionPortMessagingProvider { + public: + MOCK_METHOD1(CloseAll, void(IContentScriptNativeApi* instance)); + MOCK_METHOD4(OpenChannelToExtension, + HRESULT(IContentScriptNativeApi* instance, + const std::string& extension, + const std::string& channel_name, + int cookie)); + MOCK_METHOD2(PostMessage, HRESULT(int port_id, const std::string& message)); +}; + +template<class BaseClass> +class MockFrameEventHandlerHostBase + : public CComObjectRootEx<CComSingleThreadModel>, + public InitializingCoClass<BaseClass>, + public InstanceCountMixin<BaseClass>, + public StrictMock<IFrameEventHandlerHostMockImpl>, + public StrictMock<IExtensionPortMessagingProviderMockImpl> { + public: + BEGIN_COM_MAP(MockFrameEventHandlerHostBase) + COM_INTERFACE_ENTRY_IID(IID_IFrameEventHandlerHost, IFrameEventHandlerHost) + END_COM_MAP() +}; + +class MockFrameEventHandlerHost + : public MockFrameEventHandlerHostBase<MockFrameEventHandlerHost> { + public: + HRESULT Initialize(MockFrameEventHandlerHost** self) { + *self = this; + return S_OK; + } +}; + +} // namespace testing + +#endif // CEEE_IE_TESTING_MOCK_FRAME_EVENT_HANDLER_HOST_H_ diff --git a/ceee/ie/testing/precompile.cc b/ceee/ie/testing/precompile.cc new file mode 100644 index 0000000..975a615 --- /dev/null +++ b/ceee/ie/testing/precompile.cc @@ -0,0 +1,6 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile generator file. +#include "ceee/ie/testing/precompile.h" diff --git a/ceee/ie/testing/precompile.h b/ceee/ie/testing/precompile.h new file mode 100644 index 0000000..ef85751 --- /dev/null +++ b/ceee/ie/testing/precompile.h @@ -0,0 +1,19 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Precompile header for IE CEEE unittests. + +#ifndef CEEE_IE_TESTING_PRECOMPILE_H_ +#define CEEE_IE_TESTING_PRECOMPILE_H_ + +#include <atlbase.h> +#include <atlcom.h> +#include <atlstr.h> + +#include "ceee/testing/utils/mock_com.h" +#include "ceee/testing/utils/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#endif // CEEE_IE_TESTING_PRECOMPILE_H_ diff --git a/ceee/ie/testing/test_data/another_frame_one.html b/ceee/ie/testing/test_data/another_frame_one.html new file mode 100644 index 0000000..a97a903 --- /dev/null +++ b/ceee/ie/testing/test_data/another_frame_one.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another Frame One</title></head> +<body> +<h1>Another Frame One</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/another_frame_two.html b/ceee/ie/testing/test_data/another_frame_two.html new file mode 100644 index 0000000..fbd64e6 --- /dev/null +++ b/ceee/ie/testing/test_data/another_frame_two.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another Frame Two</title></head> +<body> +<h1>Another Frame Two</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/another_two_frames.html b/ceee/ie/testing/test_data/another_two_frames.html new file mode 100644 index 0000000..339f133 --- /dev/null +++ b/ceee/ie/testing/test_data/another_two_frames.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Another two frames in a frameset</title></head> +<frameset cols="50%,50%"> + <frame src="another_frame_one.html" /> + <frame src="another_frame_two.html" /> +</frameset> +</html> diff --git a/ceee/ie/testing/test_data/deep_frames.html b/ceee/ie/testing/test_data/deep_frames.html new file mode 100644 index 0000000..a89090c --- /dev/null +++ b/ceee/ie/testing/test_data/deep_frames.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Deep frames</title></head> +<frameset cols="50%,50%"> + <frame src="level_one_frame.html" name="deep_frames" /> +</frameset> +</html> diff --git a/ceee/ie/testing/test_data/frame_one.html b/ceee/ie/testing/test_data/frame_one.html new file mode 100644 index 0000000..3bba64e --- /dev/null +++ b/ceee/ie/testing/test_data/frame_one.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Frame One</title></head> +<body> +<h1>Frame One</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/frame_two.html b/ceee/ie/testing/test_data/frame_two.html new file mode 100644 index 0000000..e4bdd8c --- /dev/null +++ b/ceee/ie/testing/test_data/frame_two.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Frame Two</title></head> +<body> +<h1>Frame Two</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/level_one_frame.html b/ceee/ie/testing/test_data/level_one_frame.html new file mode 100644 index 0000000..4422375 --- /dev/null +++ b/ceee/ie/testing/test_data/level_one_frame.html @@ -0,0 +1,14 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Level One Frame</title></head> +<body> + <h1>Level One Frame</h1> + <iframe width="100%" height="400px" src="level_two_frame.html" + name="level_one" /> +</body> +</html> diff --git a/ceee/ie/testing/test_data/level_two_frame.html b/ceee/ie/testing/test_data/level_two_frame.html new file mode 100644 index 0000000..0c84867 --- /dev/null +++ b/ceee/ie/testing/test_data/level_two_frame.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Level Two Frame</title></head> +<body> + <h1>Level Two Frame</h1> + <iframe width="100%" height="400px" src="frame_one.html" name="level_two"/> +</body> +</html> diff --git a/ceee/ie/testing/test_data/orphans.html b/ceee/ie/testing/test_data/orphans.html new file mode 100644 index 0000000..65fcf10 --- /dev/null +++ b/ceee/ie/testing/test_data/orphans.html @@ -0,0 +1,72 @@ +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> + <head> + <script> +var isIECache = "unset"; +function isIE() { + if (isIECache == "unset") { + isIECache = 0; + try { + if (navigator.appName == "Microsoft Internet Explorer") { + var a = navigator.userAgent, b = /MSIE ([0-9]{1,}[.0-9]{0,})/; + if (b.exec(a) != null) { + rv = parseFloat(RegExp.$1); + if (rv > 0) + isIECache = rv; + } + } + } catch (c) { + } + } + return isIECache; +} + +function ie7Mode() { + if (isIE()) + if (document.compatMode == "BackCompat") + return true; + else if (isIE() < 8) + return true; + else if (document.documentMode > 0 && document.documentMode < 8) + return true; + return false; +} + +function go() { + var temp_frame = document.createElement("iframe"); + temp_frame.src="#BlaBlaBla"; + var j = document.getElementById("segmentDiv"); + j.appendChild(temp_frame); + var inner_content = null; + if (temp_frame.contentDocument) { + inner_content = temp_frame.contentDocument; + } else if (temp_frame.contentWindow) { + inner_content = temp_frame.contentWindow; + } + if (inner_content) { + var inner_document = null; + if (ie7Mode()) + inner_document = inner_content.window.document; + else { + inner_content.open(); + inner_content.close(); + inner_document = inner_content; + } + inner_document.write('<html><body><div id="iframeDIV" ></div></body></html>'); + inner_document.close(); + + inner_document.getElementById("iframeDIV").innerHTML = '<iframe src="./frame_one.html"></iframe>'; + } else { + alert('no content!'); + } +} + </script> + </head> + <body> + <div id="segmentDiv"></div> + <script> go(); </script> + </body> +</html> diff --git a/ceee/ie/testing/test_data/simple_page.html b/ceee/ie/testing/test_data/simple_page.html new file mode 100644 index 0000000..74fd506 --- /dev/null +++ b/ceee/ie/testing/test_data/simple_page.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Simple Page</title></head> +<body> +<h1>This is the simplest possible page.</h1> +</body> +</html> diff --git a/ceee/ie/testing/test_data/two_frames.html b/ceee/ie/testing/test_data/two_frames.html new file mode 100644 index 0000000..dd8b0c4 --- /dev/null +++ b/ceee/ie/testing/test_data/two_frames.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head><title>Two frames in a frameset</title></head> +<frameset cols="50%,50%"> + <frame src="frame_one.html" /> + <frame src="frame_two.html" /> +</frameset> +</html> |