/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "CEHHelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef SHOW_CONSOLE #define DEBUG_DELAY_SHUTDOWN 1 #endif // Heartbeat timer duration used while waiting for an incoming request. #define HEARTBEAT_MSEC 1000 // Total number of heartbeats we wait before giving up and shutting down. #define REQUEST_WAIT_TIMEOUT 30 // Pulled from desktop browser's shell #define APP_REG_NAME L"Firefox" static const WCHAR* kFirefoxExe = L"firefox.exe"; static const WCHAR* kMetroFirefoxExe = L"firefox.exe"; static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL"; static const WCHAR* kDemoMetroBrowserIDPathKey = L"Mozilla.Firefox.URL"; template void SafeRelease(T **ppT) { if (*ppT) { (*ppT)->Release(); *ppT = NULL; } } template HRESULT SetInterface(T **ppT, IUnknown *punk) { SafeRelease(ppT); return punk ? punk->QueryInterface(ppT) : E_NOINTERFACE; } class __declspec(uuid("5100FEC1-212B-4BF5-9BF8-3E650FD794A3")) CExecuteCommandVerb : public IExecuteCommand, public IObjectWithSelection, public IInitializeCommand, public IObjectWithSite, public IExecuteCommandApplicationHostEnvironment { public: CExecuteCommandVerb() : mRef(1), mShellItemArray(NULL), mUnkSite(NULL), mTargetIsFileSystemLink(false), mIsDesktopRequest(true), mRequestMet(false) { } bool RequestMet() { return mRequestMet; } long RefCount() { return mRef; } // IUnknown IFACEMETHODIMP QueryInterface(REFIID aRefID, void **aInt) { static const QITAB qit[] = { QITABENT(CExecuteCommandVerb, IExecuteCommand), QITABENT(CExecuteCommandVerb, IObjectWithSelection), QITABENT(CExecuteCommandVerb, IInitializeCommand), QITABENT(CExecuteCommandVerb, IObjectWithSite), QITABENT(CExecuteCommandVerb, IExecuteCommandApplicationHostEnvironment), { 0 }, }; return QISearch(this, qit, aRefID, aInt); } IFACEMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&mRef); } IFACEMETHODIMP_(ULONG) Release() { long cRef = InterlockedDecrement(&mRef); if (!cRef) { delete this; } return cRef; } // IExecuteCommand IFACEMETHODIMP SetKeyState(DWORD aKeyState) { mKeyState = aKeyState; return S_OK; } IFACEMETHODIMP SetParameters(PCWSTR aParameters) { Log(L"SetParameters: '%s'", aParameters); mParameters = aParameters; return S_OK; } IFACEMETHODIMP SetPosition(POINT aPoint) { return S_OK; } IFACEMETHODIMP SetShowWindow(int aShowFlag) { return S_OK; } IFACEMETHODIMP SetNoShowUI(BOOL aNoUI) { return S_OK; } IFACEMETHODIMP SetDirectory(PCWSTR aDirPath) { return S_OK; } IFACEMETHODIMP Execute(); // IObjectWithSelection IFACEMETHODIMP SetSelection(IShellItemArray *aArray) { if (!aArray) { return E_FAIL; } SetInterface(&mShellItemArray, aArray); DWORD count = 0; aArray->GetCount(&count); if (!count) { return E_FAIL; } #ifdef SHOW_CONSOLE Log(L"SetSelection param count: %d", count); for (DWORD idx = 0; idx < count; idx++) { IShellItem* item = NULL; if (SUCCEEDED(aArray->GetItemAt(idx, &item))) { LPWSTR str = NULL; if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &str))) { if (FAILED(item->GetDisplayName(SIGDN_URL, &str))) { Log(L"Failed to get a shell item array item."); item->Release(); continue; } } item->Release(); Log(L"SetSelection param: '%s'", str); CoTaskMemFree(str); } } #endif IShellItem* item = NULL; if (FAILED(aArray->GetItemAt(0, &item))) { return E_FAIL; } bool isFileSystem = false; if (!SetTargetPath(item) || !mTarget.GetLength()) { Log(L"SetTargetPath failed."); return E_FAIL; } item->Release(); Log(L"SetSelection target: %s", mTarget); return S_OK; } IFACEMETHODIMP GetSelection(REFIID aRefID, void **aInt) { *aInt = NULL; return mShellItemArray ? mShellItemArray->QueryInterface(aRefID, aInt) : E_FAIL; } // IInitializeCommand IFACEMETHODIMP Initialize(PCWSTR aVerb, IPropertyBag* aPropBag) { if (!aVerb) return E_FAIL; // 'open', 'edit', etc. Based on our registry settings Log(L"Initialize(%s)", aVerb); mVerb = aVerb; return S_OK; } // IObjectWithSite IFACEMETHODIMP SetSite(IUnknown *aUnkSite) { SetInterface(&mUnkSite, aUnkSite); return S_OK; } IFACEMETHODIMP GetSite(REFIID aRefID, void **aInt) { *aInt = NULL; return mUnkSite ? mUnkSite->QueryInterface(aRefID, aInt) : E_FAIL; } // IExecuteCommandApplicationHostEnvironment IFACEMETHODIMP GetValue(AHE_TYPE *aLaunchType) { Log(L"IExecuteCommandApplicationHostEnvironment::GetValue()"); *aLaunchType = AHE_DESKTOP; mIsDesktopRequest = true; if (!mUnkSite) { Log(L"No mUnkSite."); return S_OK; } HRESULT hr; IServiceProvider* pSvcProvider = NULL; hr = mUnkSite->QueryInterface(IID_IServiceProvider, (void**)&pSvcProvider); if (!pSvcProvider) { Log(L"Couldn't get IServiceProvider service from explorer. (%X)", hr); return S_OK; } IExecuteCommandHost* pHost = NULL; // If we can't get this it's a conventional desktop launch hr = pSvcProvider->QueryService(SID_ExecuteCommandHost, IID_IExecuteCommandHost, (void**)&pHost); if (!pHost) { Log(L"Couldn't get IExecuteCommandHost service from explorer. (%X)", hr); SafeRelease(&pSvcProvider); return S_OK; } SafeRelease(&pSvcProvider); EC_HOST_UI_MODE mode; if (FAILED(pHost->GetUIMode(&mode))) { Log(L"GetUIMode failed."); SafeRelease(&pHost); return S_OK; } // 0 - launched from desktop // 1 - ? // 2 - launched from tile interface Log(L"GetUIMode: %d", mode); if (!IsDefaultBrowser()) { mode = ECHUIM_DESKTOP; } if (mode == ECHUIM_DESKTOP) { Log(L"returning AHE_DESKTOP"); SafeRelease(&pHost); return S_OK; } SafeRelease(&pHost); if (!IsDX10Available()) { Log(L"returning AHE_DESKTOP because DX10 is not available"); *aLaunchType = AHE_DESKTOP; mIsDesktopRequest = true; } else { Log(L"returning AHE_IMMERSIVE"); *aLaunchType = AHE_IMMERSIVE; mIsDesktopRequest = false; } return S_OK; } bool IsDefaultBrowser() { bool result = false; IApplicationAssociationRegistration* pAAR; HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration, NULL, CLSCTX_INPROC, IID_IApplicationAssociationRegistration, (void**)&pAAR); if (SUCCEEDED(hr)) { BOOL res; hr = pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REG_NAME, &res); Log(L"QueryAppIsDefaultAll: %d", res); if (!res) return false; // Make sure the Prog ID matches what we have LPWSTR registeredApp; hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE, ®isteredApp); Log(L"QueryCurrentDefault: %X", hr); if (SUCCEEDED(hr)) { Log(L"registeredApp=%s", registeredApp); result = !wcsicmp(registeredApp, kDefaultMetroBrowserIDPathKey); if (!result) { result = !wcsicmp(registeredApp, kDemoMetroBrowserIDPathKey); } CoTaskMemFree(registeredApp); } else { result = false; } pAAR->Release(); return result; } return result; } private: ~CExecuteCommandVerb() { SafeRelease(&mShellItemArray); SafeRelease(&mUnkSite); } void LaunchDesktopBrowser(); bool SetTargetPath(IShellItem* aItem); bool IsTargetBrowser(); long mRef; IShellItemArray *mShellItemArray; IUnknown *mUnkSite; CStringW mVerb; CStringW mTarget; CStringW mParameters; bool mTargetIsFileSystemLink; DWORD mKeyState; bool mIsDesktopRequest; bool mRequestMet; }; /* * Retrieve our module dir path. * * @aPathBuffer Buffer to fill */ static bool GetModulePath(CStringW& aPathBuffer) { WCHAR buffer[MAX_PATH]; memset(buffer, 0, sizeof(buffer)); if (!GetModuleFileName(NULL, buffer, MAX_PATH)) { Log(L"GetModuleFileName failed."); return false; } WCHAR* slash = wcsrchr(buffer, '\\'); if (!slash) return false; *slash = '\0'; aPathBuffer = buffer; return true; } /* * Retrieve 'module dir path\firefox.exe' * * @aPathBuffer Buffer to fill */ static bool GetDesktopBrowserPath(CStringW& aPathBuffer) { if (!GetModulePath(aPathBuffer)) return false; // ceh.exe sits in dist/bin root with the desktop browser. Since this // is a firefox only component, this hardcoded filename is ok. aPathBuffer.Append(L"\\"); aPathBuffer.Append(kFirefoxExe); return true; } /* * Retrieve the app model id of the firefox metro browser. * * @aPathBuffer Buffer to fill * @aCharLength Length of buffer to fill in characters */ static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer, long aCharLength) { if (!aIDBuffer || aCharLength <= 0) return false; memset(aIDBuffer, 0, (sizeof(WCHAR)*aCharLength)); HKEY key; if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDefaultMetroBrowserIDPathKey, 0, KEY_READ, &key) != ERROR_SUCCESS) { return false; } DWORD len = aCharLength * sizeof(WCHAR); memset(aIDBuffer, 0, len); if (RegQueryValueExW(key, L"AppUserModelID", NULL, NULL, (LPBYTE)aIDBuffer, &len) != ERROR_SUCCESS || !len) { RegCloseKey(key); return false; } RegCloseKey(key); return true; } /* * Determines if the current target points directly to a particular * browser or to a file or url. */ bool CExecuteCommandVerb::IsTargetBrowser() { if (!mTarget.GetLength() || !mTargetIsFileSystemLink) return false; CStringW modulePath; if (!GetModulePath(modulePath)) return false; modulePath.MakeLower(); CStringW tmpTarget = mTarget; tmpTarget.Replace(L"\"", L""); tmpTarget.MakeLower(); CStringW checkPath; checkPath = modulePath; checkPath.Append(L"\\"); checkPath.Append(kFirefoxExe); if (tmpTarget == checkPath) { return true; } return false; } namespace { const FORMATETC kPlainTextFormat = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL}; const FORMATETC kPlainTextWFormat = {CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL}; } bool HasPlainText(IDataObject* aDataObj) { return SUCCEEDED(aDataObj->QueryGetData((FORMATETC*)&kPlainTextWFormat)) || SUCCEEDED(aDataObj->QueryGetData((FORMATETC*)&kPlainTextFormat)); } bool GetPlainText(IDataObject* aDataObj, CStringW& cstrText) { if (!HasPlainText(aDataObj)) return false; STGMEDIUM store; // unicode text if (SUCCEEDED(aDataObj->GetData((FORMATETC*)&kPlainTextWFormat, &store))) { // makes a copy cstrText = static_cast(GlobalLock(store.hGlobal)); GlobalUnlock(store.hGlobal); ReleaseStgMedium(&store); return true; } // ascii text if (SUCCEEDED(aDataObj->GetData((FORMATETC*)&kPlainTextFormat, &store))) { // makes a copy cstrText = static_cast(GlobalLock(store.hGlobal)); GlobalUnlock(store.hGlobal); ReleaseStgMedium(&store); return true; } return false; } /* * Updates the current target based on the contents of * a shell item. */ bool CExecuteCommandVerb::SetTargetPath(IShellItem* aItem) { if (!aItem) return false; CString cstrText; CComPtr object; // Check the underlying data object first to insure we get // absolute uri. See chromium bug 157184. if (SUCCEEDED(aItem->BindToHandler(NULL, BHID_DataObject, IID_IDataObject, reinterpret_cast(&object))) && GetPlainText(object, cstrText)) { wchar_t scheme[16]; URL_COMPONENTS components = {0}; components.lpszScheme = scheme; components.dwSchemeLength = sizeof(scheme)/sizeof(scheme[0]); components.dwStructSize = sizeof(components); // note, more advanced use may have issues with paths with spaces. if (!InternetCrackUrlW(cstrText, 0, 0, &components)) { Log(L"Failed to identify object text '%s'", cstrText); return false; } mTargetIsFileSystemLink = (components.nScheme == INTERNET_SCHEME_FILE); mTarget = cstrText; return true; } Log(L"No data object or data object has no text."); // Use the shell item display name LPWSTR str = NULL; mTargetIsFileSystemLink = true; if (FAILED(aItem->GetDisplayName(SIGDN_FILESYSPATH, &str))) { mTargetIsFileSystemLink = false; if (FAILED(aItem->GetDisplayName(SIGDN_URL, &str))) { Log(L"Failed to get parameter string."); return false; } } mTarget = str; CoTaskMemFree(str); return true; } /* * Desktop launch - Launch the destop browser to display the current * target using shellexecute. */ void CExecuteCommandVerb::LaunchDesktopBrowser() { CStringW browserPath; if (!GetDesktopBrowserPath(browserPath)) { return; } // If a taskbar shortcut, link or local file is clicked, the target will // be the browser exe or file. CStringW params; if (!IsTargetBrowser()) { params += "-url "; params += "\""; params += mTarget; params += "\""; } Log(L"Desktop Launch: verb:%s exe:%s params:%s", mVerb, browserPath, params); SHELLEXECUTEINFOW seinfo; memset(&seinfo, 0, sizeof(seinfo)); seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); seinfo.fMask = NULL; seinfo.hwnd = NULL; seinfo.lpVerb = NULL; seinfo.lpFile = browserPath; seinfo.lpParameters = params; seinfo.lpDirectory = NULL; seinfo.nShow = SW_SHOWNORMAL; ShellExecuteExW(&seinfo); } class AutoSetRequestMet { public: explicit AutoSetRequestMet(bool* aFlag) : mFlag(aFlag) {} ~AutoSetRequestMet() { if (mFlag) *mFlag = true; } private: bool* mFlag; }; IFACEMETHODIMP CExecuteCommandVerb::Execute() { Log(L"Execute()"); // We shut down when this flips to true AutoSetRequestMet asrm(&mRequestMet); if (!mTarget.GetLength()) { return E_FAIL; } // Launch on the desktop if (mIsDesktopRequest) { LaunchDesktopBrowser(); return S_OK; } // Launch into Metro IApplicationActivationManager* activateMgr = NULL; DWORD processID; if (FAILED(CoCreateInstance(CLSID_ApplicationActivationManager, NULL, CLSCTX_LOCAL_SERVER, IID_IApplicationActivationManager, (void**)&activateMgr))) { Log(L"CoCreateInstance failed, launching on desktop."); LaunchDesktopBrowser(); return S_OK; } HRESULT hr; WCHAR appModelID[256]; if (!GetDefaultBrowserAppModelID(appModelID, (sizeof(appModelID)/sizeof(WCHAR)))) { Log(L"GetDefaultBrowserAppModelID failed, launching on desktop."); activateMgr->Release(); LaunchDesktopBrowser(); return S_OK; } // Hand off focus rights to the out-of-process activation server. Without // this the metro interface won't launch. hr = CoAllowSetForegroundWindow(activateMgr, NULL); if (FAILED(hr)) { Log(L"CoAllowSetForegroundWindow result %X", hr); activateMgr->Release(); return false; } Log(L"Metro Launch: verb:%s appid:%s params:%s", mVerb, appModelID, mTarget); // shortcuts to the application if (IsTargetBrowser()) { hr = activateMgr->ActivateApplication(appModelID, L"", AO_NONE, &processID); Log(L"ActivateApplication result %X", hr); // files } else if (mTargetIsFileSystemLink) { hr = activateMgr->ActivateForFile(appModelID, mShellItemArray, mVerb, &processID); Log(L"ActivateForFile result %X", hr); // protocols } else { hr = activateMgr->ActivateForProtocol(appModelID, mShellItemArray, &processID); Log(L"ActivateForProtocol result %X", hr); } activateMgr->Release(); return S_OK; } class ClassFactory : public IClassFactory { public: ClassFactory(IUnknown *punkObject); ~ClassFactory(); STDMETHODIMP Register(CLSCTX classContent, REGCLS classUse); STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef() { return 2; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv); STDMETHODIMP LockServer(BOOL); private: IUnknown* mUnkObject; DWORD mRegID; }; ClassFactory::ClassFactory(IUnknown* aUnkObj) : mUnkObject(aUnkObj), mRegID(0) { if (mUnkObject) { mUnkObject->AddRef(); } } ClassFactory::~ClassFactory() { if (mRegID) { CoRevokeClassObject(mRegID); } mUnkObject->Release(); } STDMETHODIMP ClassFactory::Register(CLSCTX aClass, REGCLS aUse) { return CoRegisterClassObject(__uuidof(CExecuteCommandVerb), static_cast(this), aClass, aUse, &mRegID); } STDMETHODIMP ClassFactory::QueryInterface(REFIID riid, void **ppv) { IUnknown *punk = NULL; if (riid == IID_IUnknown || riid == IID_IClassFactory) { punk = static_cast(this); } *ppv = punk; if (punk) { punk->AddRef(); return S_OK; } else { return E_NOINTERFACE; } } STDMETHODIMP ClassFactory::CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv) { *ppv = NULL; if (punkOuter) return CLASS_E_NOAGGREGATION; return mUnkObject->QueryInterface(riid, ppv); } LONG gObjRefCnt; STDMETHODIMP ClassFactory::LockServer(BOOL fLock) { if (fLock) InterlockedIncrement(&gObjRefCnt); else InterlockedDecrement(&gObjRefCnt); Log(L"ClassFactory::LockServer() %d", gObjRefCnt); return S_OK; } int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR pszCmdLine, int) { #if defined(SHOW_CONSOLE) SetupConsole(); #endif //Log(pszCmdLine); if (!wcslen(pszCmdLine) || StrStrI(pszCmdLine, L"-Embedding")) { CoInitialize(NULL); CExecuteCommandVerb *pHandler = new CExecuteCommandVerb(); if (!pHandler) return E_OUTOFMEMORY; IUnknown* ppi; pHandler->QueryInterface(IID_IUnknown, (void**)&ppi); if (!ppi) return E_FAIL; ClassFactory classFactory(ppi); ppi->Release(); ppi = NULL; // REGCLS_SINGLEUSE insures we only get used once and then discarded. if (FAILED(classFactory.Register(CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE))) return -1; if (!SetTimer(NULL, 1, HEARTBEAT_MSEC, NULL)) { Log(L"Failed to set timer, can't process request."); return -1; } MSG msg; long beatCount = 0; while (GetMessage(&msg, 0, 0, 0) > 0) { if (msg.message == WM_TIMER) { if (++beatCount > REQUEST_WAIT_TIMEOUT || (pHandler->RequestMet() && pHandler->RefCount() < 2)) { break; } } TranslateMessage(&msg); DispatchMessage(&msg); } #ifdef DEBUG_DELAY_SHUTDOWN Sleep(10000); #endif CoUninitialize(); return 0; } return 0; }