diff options
Diffstat (limited to 'ceee/ie/plugin')
97 files changed, 24319 insertions, 0 deletions
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 |