// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome_frame/chrome_frame_activex.h"
#include <wininet.h>
#include <algorithm>
#include <map>
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/debug/trace_event.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_variant.h"
#include "chrome/common/automation_messages.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/automation/tab_proxy.h"
#include "chrome_frame/utils.h"
#include "url/gurl.h"
namespace {
// Class used to maintain a mapping from top-level windows to ChromeFrameActivex
// instances.
class TopLevelWindowMapping {
public:
typedef std::vector<HWND> WindowList;
static TopLevelWindowMapping* GetInstance() {
return Singleton<TopLevelWindowMapping>::get();
}
// Add |cf_window| to the set of windows registered under |top_window|.
void AddMapping(HWND top_window, HWND cf_window) {
top_window_map_lock_.Lock();
top_window_map_[top_window].push_back(cf_window);
top_window_map_lock_.Unlock();
}
// Return the set of Chrome-Frame instances under |window|.
WindowList GetInstances(HWND window) {
top_window_map_lock_.Lock();
WindowList list = top_window_map_[window];
top_window_map_lock_.Unlock();
return list;
}
private:
// Constructor is private as this class it to be used as a singleton.
// See static method instance().
TopLevelWindowMapping() {}
friend struct DefaultSingletonTraits<TopLevelWindowMapping>;
typedef std::map<HWND, WindowList> TopWindowMap;
TopWindowMap top_window_map_;
CComAutoCriticalSection top_window_map_lock_;
DISALLOW_COPY_AND_ASSIGN(TopLevelWindowMapping);
};
// Message pump hook function that monitors for WM_MOVE and WM_MOVING
// messages on a top-level window, and passes notification to the appropriate
// Chrome-Frame instances.
LRESULT CALLBACK TopWindowProc(int code, WPARAM wparam, LPARAM lparam) {
CWPSTRUCT* info = reinterpret_cast<CWPSTRUCT*>(lparam);
const UINT &message = info->message;
const HWND &message_hwnd = info->hwnd;
switch (message) {
case WM_MOVE:
case WM_MOVING: {
TopLevelWindowMapping::WindowList cf_instances =
TopLevelWindowMapping::GetInstance()->GetInstances(message_hwnd);
TopLevelWindowMapping::WindowList::iterator
iter(cf_instances.begin()), end(cf_instances.end());
for (; iter != end; ++iter) {
PostMessage(*iter, WM_HOST_MOVED_NOTIFICATION, NULL, NULL);
}
break;
}
default:
break;
}
return CallNextHookEx(0, code, wparam, lparam);
}
HHOOK InstallLocalWindowHook(HWND window) {
if (!window)
return NULL;
DWORD proc_thread = ::GetWindowThreadProcessId(window, NULL);
if (!proc_thread)
return NULL;
// Note that this hook is installed as a LOCAL hook.
return ::SetWindowsHookEx(WH_CALLWNDPROC,
TopWindowProc,
NULL,
proc_thread);
}
} // unnamed namespace
namespace chrome_frame {
std::string ActiveXCreateUrl(const GURL& parsed_url,
const AttachExternalTabParams& params) {
return base::StringPrintf(
"%hs?attach_external_tab&%I64u&%d&%d&%d&%d&%d&%hs",
parsed_url.GetOrigin().spec().c_str(),
params.cookie,
params.disposition,
params.dimensions.x(),
params.dimensions.y(),
params.dimensions.width(),
params.dimensions.height(),
params.profile_name.c_str());
}
int GetDisposition(const AttachExternalTabParams& params) {
return params.disposition;
}
void GetMiniContextMenuData(UINT cmd,
const MiniContextMenuParams& params,
GURL* referrer,
GURL* url) {
*referrer = params.frame_url.is_empty() ? params.page_url : params.frame_url;
*url = (cmd == IDS_CONTENT_CONTEXT_SAVELINKAS ?
params.link_url : params.src_url);
}
} // namespace chrome_frame
ChromeFrameActivex::ChromeFrameActivex()
: chrome_wndproc_hook_(NULL),
attaching_to_existing_cf_tab_(false) {
TRACE_EVENT_BEGIN_ETW("chromeframe.createactivex", this, "");
}
HRESULT ChromeFrameActivex::FinalConstruct() {
HRESULT hr = Base::FinalConstruct();
if (FAILED(hr))
return hr;
// No need to call FireOnChanged at this point since nobody will be listening.
ready_state_ = READYSTATE_LOADING;
return S_OK;
}
ChromeFrameActivex::~ChromeFrameActivex() {
// We expect these to be released during a call to SetClientSite(NULL).
DCHECK_EQ(0u, onmessage_.size());
DCHECK_EQ(0u, onloaderror_.size());
DCHECK_EQ(0u, onload_.size());
DCHECK_EQ(0u, onreadystatechanged_.size());
DCHECK_EQ(0u, onextensionready_.size());
if (chrome_wndproc_hook_) {
BOOL unhook_success = ::UnhookWindowsHookEx(chrome_wndproc_hook_);
DCHECK(unhook_success);
}
// ChromeFramePlugin::Uninitialize()
Base::Uninitialize();
TRACE_EVENT_END_ETW("chromeframe.createactivex", this, "");
}
LRESULT ChromeFrameActivex::OnCreate(UINT message, WPARAM wparam, LPARAM lparam,
BOOL& handled) {
Base::OnCreate(message, wparam, lparam, handled);
// Install the notification hook on the top-level window, so that we can
// be notified on move events. Note that the return value is not checked.
// This hook is installed here, as opposed to during IOleObject_SetClientSite
// because m_hWnd has not yet been assigned during the SetSite call.
InstallTopLevelHook(m_spClientSite);
return 0;
}
LRESULT ChromeFrameActivex::OnHostMoved(UINT message, WPARAM wparam,
LPARAM lparam, BOOL& handled) {
Base::OnHostMoved();
return 0;
}
HRESULT ChromeFrameActivex::GetContainingDocument(IHTMLDocument2** doc) {
base::win::ScopedComPtr<IOleContainer> container;
HRESULT hr = m_spClientSite->GetContainer(container.Receive());
if (container)
hr = container.QueryInterface(doc);
return hr;
}
HRESULT ChromeFrameActivex::GetDocumentWindow(IHTMLWindow2** window) {
base::win::ScopedComPtr<IHTMLDocument2> document;
HRESULT hr = GetContainingDocument(document.Receive());
if (document)
hr = document->get_parentWindow(window);
return hr;
}
void ChromeFrameActivex::OnLoad(const GURL& gurl) {
base::win::ScopedComPtr<IDispatch> event;
std::string url = gurl.spec();
if (SUCCEEDED(CreateDomEvent("event", url, "", event.Receive())))
Fire_onload(event);
FireEvent(onload_, url);
Base::OnLoad(gurl);
}
void ChromeFrameActivex::OnLoadFailed(int error_code, const std::string& url) {
base::win::ScopedComPtr<IDispatch> event;
if (SUCCEEDED(CreateDomEvent("event", url, "", event.Receive())))
Fire_onloaderror(event);
FireEvent(onloaderror_, url);
Base::OnLoadFailed(error_code, url);
}
void ChromeFrameActivex::OnMessageFromChromeFrame(const std::string& message,
const std::string& origin,
const std::string& target) {
DVLOG(1) << __FUNCTION__;
if (target.compare("*") != 0) {
bool drop = true;
if (is_privileged()) {
// Forward messages if the control is in privileged mode.
base::win::ScopedComPtr<IDispatch> message_event;
if (SUCCEEDED(CreateDomEvent("message", message, origin,
message_event.Receive()))) {
base::win::ScopedBstr target_bstr(UTF8ToWide(target).c_str());
Fire_onprivatemessage(message_event, target_bstr);
FireEvent(onprivatemessage_, message_event, target_bstr);
}
} else {
if (HaveSameOrigin(target, document_url_)) {
drop = false;
} else {
DLOG(WARNING) << "Dropping posted message since target doesn't match "
"the current document's origin. target=" << target;
}
}
if (drop)
return;
}
base::win::ScopedComPtr<IDispatch> message_event;
if (SUCCEEDED(CreateDomEvent("message", message, origin,
message_event.Receive()))) {
Fire_onmessage(message_event);
FireEvent(onmessage_, message_event);
base::win::ScopedVariant event_var;
event_var.Set(static_cast<IDispatch*>(message_event));
InvokeScriptFunction(onmessage_handler_, event_var.AsInput());
}
}
bool ChromeFrameActivex::ShouldShowVersionMismatchDialog(
bool is_privileged,
IOleClientSite* client_site) {
if (!is_privileged) {
return true;
}
if (client_site) {
base::win::ScopedComPtr<IChromeFramePrivileged> service;
HRESULT hr = DoQueryService(SID_ChromeFramePrivileged,
client_site,
service.Receive());
if (SUCCEEDED(hr) && service) {
return (S_FALSE != service->ShouldShowVersionMismatchDialog());
}
}
NOTREACHED();
return true;
}
void ChromeFrameActivex::OnAutomationServerLaunchFailed(
AutomationLaunchResult reason, const std::string& server_version) {
Base::OnAutomationServerLaunchFailed(reason, server_version);
if (reason == AUTOMATION_VERSION_MISMATCH &&
ShouldShowVersionMismatchDialog(is_privileged(), m_spClientSite)) {
UMA_HISTOGRAM_COUNTS("ChromeFrame.VersionMismatchDisplayed", 1);
DisplayVersionMismatchWarning(m_hWnd, server_version);
}
}
void ChromeFrameActivex::OnChannelError() {
Fire_onchannelerror();
}
HRESULT ChromeFrameActivex::OnDraw(ATL_DRAWINFO& draw_info) { // NOLINT
HRESULT hr = S_OK;
int dc_type = ::GetObjectType(draw_info.hicTargetDev);
if (dc_type == OBJ_ENHMETADC) {
RECT print_bounds = {0};
print_bounds.left = draw_info.prcBounds->left;
print_bounds.right = draw_info.prcBounds->right;
print_bounds.top = draw_info.prcBounds->top;
print_bounds.bottom = draw_info.prcBounds->bottom;
automation_client_->Print(draw_info.hdcDraw, print_bounds);
} else {
hr = Base::OnDraw(draw_info);
}
return hr;
}
STDMETHODIMP ChromeFrameActivex::Load(IPropertyBag* bag, IErrorLog* error_log) {
DCHECK(bag);
const wchar_t* event_props[] = {
(L"onload"),
(L"onloaderror"),
(L"onmessage"),
(L"onreadystatechanged"),
};
base::win::ScopedComPtr<IHTMLObjectElement> obj_element;
GetObjectElement(obj_element.Receive());
base::win::ScopedBstr object_id;
GetObjectScriptId(obj_element, object_id.Receive());
base::win::ScopedComPtr<IHTMLElement2> element;
element.QueryFrom(obj_element);
HRESULT hr = S_OK;
for (int i = 0; SUCCEEDED(hr) && i < arraysize(event_props); ++i) {
base::win::ScopedBstr prop(event_props[i]);
base::win::ScopedVariant value;
if (SUCCEEDED(bag->Read(prop, value.Receive(), error_log))) {
if (value.type() != VT_BSTR ||
FAILED(hr = CreateScriptBlockForEvent(element, object_id,
V_BSTR(&value), prop))) {
DLOG(ERROR) << "Failed to create script block for " << prop
<< base::StringPrintf(L"hr=0x%08X, vt=%i", hr,
value.type());
} else {
DVLOG(1) << "script block created for event " << prop
<< base::StringPrintf(" (0x%08X)", hr) << " connections: " <<
ProxyDIChromeFrameEvents<ChromeFrameActivex>::m_vec.GetSize();
}
} else {
DVLOG(1) << "event property " << prop << " not in property bag";
}
}
base::win::ScopedVariant src;
if (SUCCEEDED(bag->Read(base::win::ScopedBstr(L"src"), src.Receive(),
error_log))) {
if (src.type() == VT_BSTR) {
hr = put_src(V_BSTR(&src));
DCHECK(hr != E_UNEXPECTED);
}
}
base::win::ScopedVariant use_chrome_network;
if (SUCCEEDED(bag->Read(base::win::ScopedBstr(L"useChromeNetwork"),
use_chrome_network.Receive(), error_log))) {
VariantChangeType(use_chrome_network.AsInput(),
use_chrome_network.AsInput(),
0, VT_BOOL);
if (use_chrome_network.type() == VT_BOOL) {
hr = put_useChromeNetwork(V_BOOL(&use_chrome_network));
DCHECK(hr != E_UNEXPECTED);
}
}
DLOG_IF(ERROR, FAILED(hr))
<< base::StringPrintf("Failed to load property bag: 0x%08X", hr);
return hr;
}
const wchar_t g_activex_insecure_content_error[] = {
L"data:text/html,<html><body><b>ChromeFrame Security Error<br><br>"
L"Cannot navigate to HTTP url when document URL is HTTPS</body></html>"};
STDMETHODIMP ChromeFrameActivex::put_src(BSTR src) {
GURL document_url(GetDocumentUrl());
if (document_url.SchemeIsSecure()) {
GURL source_url(src);
if (!source_url.SchemeIsSecure()) {
Base::put_src(base::win::ScopedBstr(g_activex_insecure_content_error));
return E_ACCESSDENIED;
}
}
HRESULT hr = S_OK;
// If we are connecting to an existing ExternalTabContainer instance in
// Chrome then we should wait for Chrome to initiate the navigation.
if (!attaching_to_existing_cf_tab_) {
hr = Base::put_src(src);
} else {
url_.Reset(::SysAllocString(src));
attaching_to_existing_cf_tab_ = false;
}
return S_OK;
}
HRESULT ChromeFrameActivex::IOleObject_SetClientSite(
IOleClientSite* client_site) {
HRESULT hr = Base::IOleObject_SetClientSite(client_site);
if (FAILED(hr) || !client_site) {
EventHandlers* handlers[] = {
&onmessage_,
&onloaderror_,
&onload_,
&onreadystatechanged_,
&onextensionready_,
};
for (int i = 0; i < arraysize(handlers); ++i)
handlers[i]->clear();
// Drop privileged mode on uninitialization.
set_is_privileged(false);
} else {
base::win::ScopedComPtr<IHTMLDocument2> document;
GetContainingDocument(document.Receive());
if (document) {
base::win::ScopedBstr url;
if (SUCCEEDED(document->get_URL(url.Receive())))
WideToUTF8(url, url.Length(), &document_url_);
}
// Probe to see whether the host implements the privileged service.
base::win::ScopedComPtr<IChromeFramePrivileged> service;
HRESULT service_hr = DoQueryService(SID_ChromeFramePrivileged,
m_spClientSite,
service.Receive());
if (SUCCEEDED(service_hr) && service) {
// Does the host want privileged mode?
boolean wants_privileged = false;
service_hr = service->GetWantsPrivileged(&wants_privileged);
if (SUCCEEDED(service_hr) && wants_privileged)
set_is_privileged(true);
url_fetcher_->set_privileged_mode(is_privileged());
}
std::wstring profile_name(GetHostProcessName(false));
if (is_privileged()) {
base::win::ScopedBstr profile_name_arg;
service_hr = service->GetChromeProfileName(profile_name_arg.Receive());
if (S_OK == service_hr && profile_name_arg)
profile_name.assign(profile_name_arg, profile_name_arg.Length());
}
std::string utf8_url;
if (url_.Length()) {
WideToUTF8(url_, url_.Length(), &utf8_url);
}
InitializeAutomationSettings();
if (service) {
base::win::ScopedBstr navigation_url;
service->GetNavigationUrl(navigation_url.Receive());
if (navigation_url.Length()) {
ChromeFrameUrl cf_url;
cf_url.Parse(navigation_url.operator BSTR());
if (cf_url.attach_to_external_tab()) {
automation_client_->AttachExternalTab(cf_url.cookie());
attaching_to_existing_cf_tab_ = true;
}
}
}
url_fetcher_->set_frame_busting(!is_privileged());
automation_client_->SetUrlFetcher(url_fetcher_.get());
if (!InitializeAutomation(profile_name, IsIEInPrivate(), true,
GURL(utf8_url), GURL(), false)) {
DLOG(ERROR) << "Failed to navigate to url:" << utf8_url;
return E_FAIL;
}
// Log a metric that Chrome Frame is being used in Widget mode
UMA_LAUNCH_TYPE_COUNT(RENDERER_TYPE_CHROME_WIDGET);
}
return hr;
}
HRESULT ChromeFrameActivex::GetObjectScriptId(IHTMLObjectElement* object_elem,
BSTR* id) {
DCHECK(object_elem != NULL);
DCHECK(id != NULL);
HRESULT hr = E_FAIL;
if (object_elem) {
base::win::ScopedComPtr<IHTMLElement> elem;
hr = elem.QueryFrom(object_elem);
if (elem) {
hr = elem->get_id(id);
}
}
return hr;
}
HRESULT ChromeFrameActivex::GetObjectElement(IHTMLObjectElement** element) {
DCHECK(m_spClientSite);
if (!m_spClientSite)
return E_UNEXPECTED;
base::win::ScopedComPtr<IOleControlSite> site;
HRESULT hr = site.QueryFrom(m_spClientSite);
if (site) {
base::win::ScopedComPtr<IDispatch> disp;
hr = site->GetExtendedControl(disp.Receive());
if (disp) {
hr = disp.QueryInterface(element);
} else {
DCHECK(FAILED(hr));
}
}
return hr;
}
HRESULT ChromeFrameActivex::CreateScriptBlockForEvent(
IHTMLElement2* insert_after, BSTR instance_id, BSTR script,
BSTR event_name) {
DCHECK(insert_after);
DCHECK_GT(::SysStringLen(event_name), 0UL); // should always have this
// This might be 0 if not specified in the HTML document.
if (!::SysStringLen(instance_id)) {
// TODO(tommi): Should we give ourselves an ID if this happens?
NOTREACHED() << "Need to handle this";
return E_INVALIDARG;
}
base::win::ScopedComPtr<IHTMLDocument2> document;
HRESULT hr = GetContainingDocument(document.Receive());
if (SUCCEEDED(hr)) {
base::win::ScopedComPtr<IHTMLElement> element, new_element;
document->createElement(base::win::ScopedBstr(L"script"),
element.Receive());
if (element) {
base::win::ScopedComPtr<IHTMLScriptElement> script_element;
if (SUCCEEDED(hr = script_element.QueryFrom(element))) {
script_element->put_htmlFor(instance_id);
script_element->put_event(event_name);
script_element->put_text(script);
hr = insert_after->insertAdjacentElement(
base::win::ScopedBstr(L"afterEnd"),
element,
new_element.Receive());
}
}
}
return hr;
}
void ChromeFrameActivex::FireEvent(const EventHandlers& handlers,
const std::string& arg) {
if (handlers.size()) {
base::win::ScopedComPtr<IDispatch> event;
if (SUCCEEDED(CreateDomEvent("event", arg, "", event.Receive()))) {
FireEvent(handlers, event);
}
}
}
void ChromeFrameActivex::FireEvent(const EventHandlers& handlers,
IDispatch* event) {
DCHECK(event != NULL);
VARIANT arg = { VT_DISPATCH };
arg.pdispVal = event;
DISPPARAMS params = { &arg, NULL, 1, 0 };
for (EventHandlers::const_iterator it = handlers.begin();
it != handlers.end();
++it) {
HRESULT hr = (*it)->Invoke(DISPID_VALUE, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
// 0x80020101 == SCRIPT_E_REPORTED.
// When the script we're invoking has an error, we get this error back.
DLOG_IF(ERROR, FAILED(hr) && hr != 0x80020101)
<< base::StringPrintf(L"Failed to invoke script: 0x%08X", hr);
}
}
void ChromeFrameActivex::FireEvent(const EventHandlers& handlers,
IDispatch* event, BSTR target) {
DCHECK(event != NULL);
// Arguments in reverse order to event handler function declaration,
// because that's what DISPPARAMS requires.
VARIANT args[2] = { { VT_BSTR }, { VT_DISPATCH }, };
args[0].bstrVal = target;
args[1].pdispVal = event;
DISPPARAMS params = { args, NULL, arraysize(args), 0 };
for (EventHandlers::const_iterator it = handlers.begin();
it != handlers.end();
++it) {
HRESULT hr = (*it)->Invoke(DISPID_VALUE, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
// 0x80020101 == SCRIPT_E_REPORTED.
// When the script we're invoking has an error, we get this error back.
DLOG_IF(ERROR, FAILED(hr) && hr != 0x80020101)
<< base::StringPrintf(L"Failed to invoke script: 0x%08X", hr);
}
}
HRESULT ChromeFrameActivex::InstallTopLevelHook(IOleClientSite* client_site) {
// Get the parent window of the site, and install our hook on the topmost
// window of the parent.
base::win::ScopedComPtr<IOleWindow> ole_window;
HRESULT hr = ole_window.QueryFrom(client_site);
if (FAILED(hr))
return hr;
HWND parent_wnd;
hr = ole_window->GetWindow(&parent_wnd);
if (FAILED(hr))
return hr;
HWND top_window = ::GetAncestor(parent_wnd, GA_ROOT);
chrome_wndproc_hook_ = InstallLocalWindowHook(top_window);
if (chrome_wndproc_hook_)
TopLevelWindowMapping::GetInstance()->AddMapping(top_window, m_hWnd);
return chrome_wndproc_hook_ ? S_OK : E_FAIL;
}
HRESULT ChromeFrameActivex::registerBhoIfNeeded() {
if (!m_spUnkSite) {
NOTREACHED() << "Invalid client site";
return E_FAIL;
}
if (NavigationManager::GetThreadInstance() != NULL) {
DVLOG(1) << "BHO already loaded";
return S_OK;
}
base::win::ScopedComPtr<IWebBrowser2> web_browser2;
HRESULT hr = DoQueryService(SID_SWebBrowserApp, m_spUnkSite,
web_browser2.Receive());
if (FAILED(hr) || web_browser2.get() == NULL) {
DLOG(WARNING) << "Failed to get IWebBrowser2 from client site. Error:"
<< base::StringPrintf(" 0x%08X", hr);
return hr;
}
wchar_t bho_class_id_as_string[MAX_PATH] = {0};
StringFromGUID2(CLSID_ChromeFrameBHO, bho_class_id_as_string,
arraysize(bho_class_id_as_string));
base::win::ScopedComPtr<IObjectWithSite> bho;
hr = bho.CreateInstance(CLSID_ChromeFrameBHO, NULL, CLSCTX_INPROC_SERVER);
if (FAILED(hr)) {
NOTREACHED() << "Failed to register ChromeFrame BHO. Error:"
<< base::StringPrintf(" 0x%08X", hr);
return hr;
}
hr = UrlMkSetSessionOption(URLMON_OPTION_USERAGENT_REFRESH, NULL, 0, 0);
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to refresh user agent string from registry. "
<< "UrlMkSetSessionOption returned "
<< base::StringPrintf("0x%08x", hr);
return hr;
}
hr = bho->SetSite(web_browser2);
if (FAILED(hr)) {
NOTREACHED() << "ChromeFrame BHO SetSite failed. Error:"
<< base::StringPrintf(" 0x%08X", hr);
return hr;
}
web_browser2->PutProperty(base::win::ScopedBstr(bho_class_id_as_string),
base::win::ScopedVariant(bho));
return S_OK;
}