/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct InternalWebViewType { InternalWebViewType() = default; virtual ~InternalWebViewType() = default; virtual void createBrowser() = 0; virtual bool hasBrowserBeenCreated() = 0; virtual void goToURL (const String&, const StringArray*, const MemoryBlock*) = 0; virtual void stop() = 0; virtual void goBack() = 0; virtual void goForward() = 0; virtual void refresh() = 0; virtual void focusGained() {} virtual void setWebViewSize (int, int) = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InternalWebViewType) }; //============================================================================== class Win32WebView : public InternalWebViewType, public ActiveXControlComponent { public: Win32WebView (WebBrowserComponent& owner) { owner.addAndMakeVisible (this); } ~Win32WebView() override { if (connectionPoint != nullptr) connectionPoint->Unadvise (adviseCookie); if (browser != nullptr) browser->Release(); } void createBrowser() override { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") auto webCLSID = __uuidof (WebBrowser); createControl (&webCLSID); auto iidWebBrowser2 = __uuidof (IWebBrowser2); auto iidConnectionPointContainer = __uuidof (IConnectionPointContainer); browser = (IWebBrowser2*) queryInterface (&iidWebBrowser2); if (auto connectionPointContainer = (IConnectionPointContainer*) queryInterface (&iidConnectionPointContainer)) { connectionPointContainer->FindConnectionPoint (__uuidof (DWebBrowserEvents2), &connectionPoint); if (connectionPoint != nullptr) { if (auto* owner = dynamic_cast (Component::getParentComponent())) { auto handler = new EventHandler (*owner); connectionPoint->Advise (handler, &adviseCookie); handler->Release(); } } } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } bool hasBrowserBeenCreated() override { return browser != nullptr; } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override { if (browser != nullptr) { VARIANT headerFlags, frame, postDataVar, headersVar; // (_variant_t isn't available in all compilers) VariantInit (&headerFlags); VariantInit (&frame); VariantInit (&postDataVar); VariantInit (&headersVar); if (headers != nullptr) { V_VT (&headersVar) = VT_BSTR; V_BSTR (&headersVar) = SysAllocString ((const OLECHAR*) headers->joinIntoString ("\r\n").toWideCharPointer()); } if (postData != nullptr && postData->getSize() > 0) { auto sa = SafeArrayCreateVector (VT_UI1, 0, (ULONG) postData->getSize()); if (sa != nullptr) { void* data = nullptr; SafeArrayAccessData (sa, &data); jassert (data != nullptr); if (data != nullptr) { postData->copyTo (data, 0, postData->getSize()); SafeArrayUnaccessData (sa); VARIANT postDataVar2; VariantInit (&postDataVar2); V_VT (&postDataVar2) = VT_ARRAY | VT_UI1; V_ARRAY (&postDataVar2) = sa; sa = nullptr; postDataVar = postDataVar2; } else { SafeArrayDestroy (sa); } } } auto urlBSTR = SysAllocString ((const OLECHAR*) url.toWideCharPointer()); browser->Navigate (urlBSTR, &headerFlags, &frame, &postDataVar, &headersVar); SysFreeString (urlBSTR); VariantClear (&headerFlags); VariantClear (&frame); VariantClear (&postDataVar); VariantClear (&headersVar); } } void stop() override { if (browser != nullptr) browser->Stop(); } void goBack() override { if (browser != nullptr) browser->GoBack(); } void goForward() override { if (browser != nullptr) browser->GoForward(); } void refresh() override { if (browser != nullptr) browser->Refresh(); } void focusGained() override { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") auto iidOleObject = __uuidof (IOleObject); auto iidOleWindow = __uuidof (IOleWindow); if (auto oleObject = (IOleObject*) queryInterface (&iidOleObject)) { if (auto oleWindow = (IOleWindow*) queryInterface (&iidOleWindow)) { IOleClientSite* oleClientSite = nullptr; if (SUCCEEDED (oleObject->GetClientSite (&oleClientSite))) { HWND hwnd; oleWindow->GetWindow (&hwnd); oleObject->DoVerb (OLEIVERB_UIACTIVATE, nullptr, oleClientSite, 0, hwnd, nullptr); oleClientSite->Release(); } oleWindow->Release(); } oleObject->Release(); } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } using ActiveXControlComponent::focusGained; void setWebViewSize (int width, int height) override { setSize (width, height); } private: IWebBrowser2* browser = nullptr; IConnectionPoint* connectionPoint = nullptr; DWORD adviseCookie = 0; //============================================================================== struct EventHandler : public ComBaseClassHelper, public ComponentMovementWatcher { EventHandler (WebBrowserComponent& w) : ComponentMovementWatcher (&w), owner (w) {} JUCE_COMRESULT GetTypeInfoCount (UINT*) override { return E_NOTIMPL; } JUCE_COMRESULT GetTypeInfo (UINT, LCID, ITypeInfo**) override { return E_NOTIMPL; } JUCE_COMRESULT GetIDsOfNames (REFIID, LPOLESTR*, UINT, LCID, DISPID*) override { return E_NOTIMPL; } JUCE_COMRESULT Invoke (DISPID dispIdMember, REFIID /*riid*/, LCID /*lcid*/, WORD /*wFlags*/, DISPPARAMS* pDispParams, VARIANT* /*pVarResult*/, EXCEPINFO* /*pExcepInfo*/, UINT* /*puArgErr*/) override { if (dispIdMember == DISPID_BEFORENAVIGATE2) { *pDispParams->rgvarg->pboolVal = owner.pageAboutToLoad (getStringFromVariant (pDispParams->rgvarg[5].pvarVal)) ? VARIANT_FALSE : VARIANT_TRUE; return S_OK; } if (dispIdMember == 273 /*DISPID_NEWWINDOW3*/) { owner.newWindowAttemptingToLoad (pDispParams->rgvarg[0].bstrVal); *pDispParams->rgvarg[3].pboolVal = VARIANT_TRUE; return S_OK; } if (dispIdMember == DISPID_DOCUMENTCOMPLETE) { owner.pageFinishedLoading (getStringFromVariant (pDispParams->rgvarg[0].pvarVal)); return S_OK; } if (dispIdMember == 271 /*DISPID_NAVIGATEERROR*/) { int statusCode = pDispParams->rgvarg[1].pvarVal->intVal; *pDispParams->rgvarg[0].pboolVal = VARIANT_FALSE; // IWebBrowser2 also reports http status codes here, we need // report only network errors if (statusCode < 0) { LPTSTR messageBuffer = nullptr; auto size = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, (DWORD) statusCode, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &messageBuffer, 0, nullptr); String message (messageBuffer, size); LocalFree (messageBuffer); if (! owner.pageLoadHadNetworkError (message)) *pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE; } return S_OK; } if (dispIdMember == 263 /*DISPID_WINDOWCLOSING*/) { owner.windowCloseRequest(); // setting this bool tells the browser to ignore the event - we'll handle it. if (pDispParams->cArgs > 0 && pDispParams->rgvarg[0].vt == (VT_BYREF | VT_BOOL)) *pDispParams->rgvarg[0].pboolVal = VARIANT_TRUE; return S_OK; } return E_NOTIMPL; } void componentMovedOrResized (bool, bool) override {} void componentPeerChanged() override {} void componentVisibilityChanged() override { owner.visibilityChanged(); } using ComponentMovementWatcher::componentVisibilityChanged; using ComponentMovementWatcher::componentMovedOrResized; private: WebBrowserComponent& owner; static String getStringFromVariant (VARIANT* v) { return (v->vt & VT_BYREF) != 0 ? *v->pbstrVal : v->bstrVal; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EventHandler) }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32WebView) }; #if JUCE_USE_WIN_WEBVIEW2 using namespace Microsoft::WRL; class WebView2 : public InternalWebViewType, public Component, public ComponentMovementWatcher { public: WebView2 (WebBrowserComponent& o, const WebView2Preferences& prefs) : ComponentMovementWatcher (&o), owner (o), preferences (prefs) { if (! createWebViewEnvironment()) throw std::runtime_error ("Failed to create the CoreWebView2Environemnt"); owner.addAndMakeVisible (this); } ~WebView2() override { removeEventHandlers(); closeWebView(); if (webView2LoaderHandle != nullptr) ::FreeLibrary (webView2LoaderHandle); } void createBrowser() override { if (webView == nullptr) { jassert (webViewEnvironment != nullptr); createWebView(); } } bool hasBrowserBeenCreated() override { return webView != nullptr || isCreating; } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override { urlRequest = { url, headers != nullptr ? *headers : StringArray(), postData != nullptr && postData->getSize() > 0 ? *postData : MemoryBlock() }; if (webView != nullptr) webView->Navigate (urlRequest.url.toWideCharPointer()); } void stop() override { if (webView != nullptr) webView->Stop(); } void goBack() override { if (webView != nullptr) { BOOL canGoBack = false; webView->get_CanGoBack (&canGoBack); if (canGoBack) webView->GoBack(); } } void goForward() override { if (webView != nullptr) { BOOL canGoForward = false; webView->get_CanGoForward (&canGoForward); if (canGoForward) webView->GoForward(); } } void refresh() override { if (webView != nullptr) webView->Reload(); } void setWebViewSize (int width, int height) override { setSize (width, height); } void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override { if (auto* peer = owner.getTopLevelComponent()->getPeer()) setControlBounds (peer->getAreaCoveredBy (owner)); } void componentPeerChanged() override { componentMovedOrResized (true, true); } void componentVisibilityChanged() override { setControlVisible (owner.isShowing()); componentPeerChanged(); owner.visibilityChanged(); } private: //============================================================================== template static String getUriStringFromArgs (ArgType* args) { if (args != nullptr) { LPWSTR uri; args->get_Uri (&uri); return uri; } return {}; } //============================================================================== void addEventHandlers() { if (webView != nullptr) { webView->add_NavigationStarting (Callback ( [this] (ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { auto uriString = getUriStringFromArgs (args); if (uriString.isNotEmpty() && ! owner.pageAboutToLoad (uriString)) args->put_Cancel (true); return S_OK; }).Get(), &navigationStartingToken); webView->add_NewWindowRequested (Callback ( [this] (ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { auto uriString = getUriStringFromArgs (args); if (uriString.isNotEmpty()) { owner.newWindowAttemptingToLoad (uriString); args->put_Handled (true); } return S_OK; }).Get(), &newWindowRequestedToken); webView->add_WindowCloseRequested (Callback ( [this] (ICoreWebView2*, IUnknown*) -> HRESULT { owner.windowCloseRequest(); return S_OK; }).Get(), &windowCloseRequestedToken); webView->add_NavigationCompleted (Callback ( [this] (ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { LPWSTR uri; sender->get_Source (&uri); String uriString (uri); if (uriString.isNotEmpty()) { BOOL success = false; args->get_IsSuccess (&success); COREWEBVIEW2_WEB_ERROR_STATUS errorStatus; args->get_WebErrorStatus (&errorStatus); if (success || errorStatus == COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED) // this error seems to happen erroneously so ignore { owner.pageFinishedLoading (uriString); } else { auto errorString = "Error code: " + String (errorStatus); if (owner.pageLoadHadNetworkError (errorString)) owner.goToURL ("data:text/plain;charset=UTF-8," + errorString); } } return S_OK; }).Get(), &navigationCompletedToken); webView->AddWebResourceRequestedFilter (L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); webView->add_WebResourceRequested (Callback ( [this] (ICoreWebView2*, ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT { if (urlRequest.url.isEmpty()) return S_OK; ComSmartPtr request; args->get_Request (request.resetAndGetPointerAddress()); auto uriString = getUriStringFromArgs (request); if (uriString == urlRequest.url || (uriString.endsWith ("/") && uriString.upToLastOccurrenceOf ("/", false, false) == urlRequest.url)) { String method ("GET"); if (! urlRequest.postData.isEmpty()) { method = "POST"; ComSmartPtr content (SHCreateMemStream ((BYTE*) urlRequest.postData.getData(), (UINT) urlRequest.postData.getSize())); request->put_Content (content); } if (! urlRequest.headers.isEmpty()) { ComSmartPtr headers; request->get_Headers (headers.resetAndGetPointerAddress()); for (auto& header : urlRequest.headers) { headers->SetHeader (header.upToFirstOccurrenceOf (":", false, false).trim().toWideCharPointer(), header.fromFirstOccurrenceOf (":", false, false).trim().toWideCharPointer()); } } request->put_Method (method.toWideCharPointer()); urlRequest = {}; } return S_OK; }).Get(), &webResourceRequestedToken); } } void removeEventHandlers() { if (webView != nullptr) { if (navigationStartingToken.value != 0) webView->remove_NavigationStarting (navigationStartingToken); if (newWindowRequestedToken.value != 0) webView->remove_NewWindowRequested (newWindowRequestedToken); if (windowCloseRequestedToken.value != 0) webView->remove_WindowCloseRequested (windowCloseRequestedToken); if (navigationCompletedToken.value != 0) webView->remove_NavigationCompleted (navigationCompletedToken); if (webResourceRequestedToken.value != 0) { webView->RemoveWebResourceRequestedFilter (L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); webView->remove_WebResourceRequested (webResourceRequestedToken); } } } void setWebViewPreferences() { ComSmartPtr controller2; webViewController->QueryInterface (controller2.resetAndGetPointerAddress()); if (controller2 != nullptr) { const auto bgColour = preferences.getBackgroundColour(); controller2->put_DefaultBackgroundColor ({ (BYTE) bgColour.getAlpha(), (BYTE) bgColour.getRed(), (BYTE) bgColour.getGreen(), (BYTE) bgColour.getBlue() }); } ComSmartPtr settings; webView->get_Settings (settings.resetAndGetPointerAddress()); if (settings != nullptr) { settings->put_IsStatusBarEnabled (! preferences.getIsStatusBarDisabled()); settings->put_IsBuiltInErrorPageEnabled (! preferences.getIsBuiltInErrorPageDisabled()); } } bool createWebViewEnvironment() { using CreateWebViewEnvironmentWithOptionsFunc = HRESULT (*) (PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*, ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); auto dllPath = preferences.getDLLLocation().getFullPathName(); if (dllPath.isEmpty()) dllPath = "WebView2Loader.dll"; webView2LoaderHandle = LoadLibraryA (dllPath.toUTF8()); if (webView2LoaderHandle == nullptr) return false; auto* createWebViewEnvironmentWithOptions = (CreateWebViewEnvironmentWithOptionsFunc) GetProcAddress (webView2LoaderHandle, "CreateCoreWebView2EnvironmentWithOptions"); if (createWebViewEnvironmentWithOptions == nullptr) { // failed to load WebView2Loader.dll jassertfalse; return false; } auto options = Microsoft::WRL::Make(); const auto userDataFolder = preferences.getUserDataFolder().getFullPathName(); auto hr = createWebViewEnvironmentWithOptions (nullptr, userDataFolder.isNotEmpty() ? userDataFolder.toWideCharPointer() : nullptr, options.Get(), Callback( [weakThis = WeakReference { this }] (HRESULT, ICoreWebView2Environment* env) -> HRESULT { if (weakThis != nullptr) weakThis->webViewEnvironment = env; return S_OK; }).Get()); return SUCCEEDED (hr); } void createWebView() { if (auto* peer = getPeer()) { isCreating = true; WeakReference weakThis (this); webViewEnvironment->CreateCoreWebView2Controller ((HWND) peer->getNativeHandle(), Callback ( [weakThis = WeakReference { this }] (HRESULT, ICoreWebView2Controller* controller) -> HRESULT { if (weakThis != nullptr) { weakThis->isCreating = false; if (controller != nullptr) { weakThis->webViewController = controller; controller->get_CoreWebView2 (weakThis->webView.resetAndGetPointerAddress()); if (weakThis->webView != nullptr) { weakThis->addEventHandlers(); weakThis->setWebViewPreferences(); weakThis->componentMovedOrResized (true, true); if (weakThis->urlRequest.url.isNotEmpty()) weakThis->webView->Navigate (weakThis->urlRequest.url.toWideCharPointer()); } } } return S_OK; }).Get()); } } void closeWebView() { if (webViewController != nullptr) { webViewController->Close(); webViewController = nullptr; webView = nullptr; } webViewEnvironment = nullptr; } //============================================================================== void setControlBounds (Rectangle newBounds) const { if (webViewController != nullptr) { #if JUCE_WIN_PER_MONITOR_DPI_AWARE if (auto* peer = owner.getTopLevelComponent()->getPeer()) newBounds = (newBounds.toDouble() * peer->getPlatformScaleFactor()).toNearestInt(); #endif webViewController->put_Bounds({ newBounds.getX(), newBounds.getY(), newBounds.getRight(), newBounds.getBottom() }); } } void setControlVisible (bool shouldBeVisible) const { if (webViewController != nullptr) webViewController->put_IsVisible (shouldBeVisible); } //============================================================================== WebBrowserComponent& owner; WebView2Preferences preferences; HMODULE webView2LoaderHandle = nullptr; ComSmartPtr webViewEnvironment; ComSmartPtr webViewController; ComSmartPtr webView; EventRegistrationToken navigationStartingToken { 0 }, newWindowRequestedToken { 0 }, windowCloseRequestedToken { 0 }, navigationCompletedToken { 0 }, webResourceRequestedToken { 0 }; struct URLRequest { String url; StringArray headers; MemoryBlock postData; }; URLRequest urlRequest; bool isCreating = false; //============================================================================== JUCE_DECLARE_WEAK_REFERENCEABLE (WebView2) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebView2) }; #endif //============================================================================== class WebBrowserComponent::Pimpl { public: Pimpl (WebBrowserComponent& owner, const WebView2Preferences& preferences, bool useWebView2) { if (useWebView2) { #if JUCE_USE_WIN_WEBVIEW2 try { internal.reset (new WebView2 (owner, preferences)); } catch (const std::runtime_error&) {} #endif } ignoreUnused (preferences); if (internal == nullptr) internal.reset (new Win32WebView (owner)); } InternalWebViewType& getInternalWebView() { return *internal; } private: std::unique_ptr internal; }; //============================================================================== WebBrowserComponent::WebBrowserComponent (bool unloadWhenHidden) : browser (new Pimpl (*this, {}, false)), unloadPageWhenHidden (unloadWhenHidden) { setOpaque (true); } WebBrowserComponent::WebBrowserComponent (ConstructWithoutPimpl args) : unloadPageWhenHidden (args.unloadWhenHidden) { setOpaque (true); } WebBrowserComponent::~WebBrowserComponent() { } WindowsWebView2WebBrowserComponent::WindowsWebView2WebBrowserComponent (bool unloadWhenHidden, const WebView2Preferences& preferences) : WebBrowserComponent (ConstructWithoutPimpl { unloadWhenHidden }) { browser = std::make_unique (*this, preferences, true); } //============================================================================== void WebBrowserComponent::goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { lastURL = url; if (headers != nullptr) lastHeaders = *headers; else lastHeaders.clear(); if (postData != nullptr) lastPostData = *postData; else lastPostData.reset(); blankPageShown = false; if (! browser->getInternalWebView().hasBrowserBeenCreated()) checkWindowAssociation(); browser->getInternalWebView().goToURL (url, headers, postData); } void WebBrowserComponent::stop() { browser->getInternalWebView().stop(); } void WebBrowserComponent::goBack() { lastURL.clear(); blankPageShown = false; browser->getInternalWebView().goBack(); } void WebBrowserComponent::goForward() { lastURL.clear(); browser->getInternalWebView().goForward(); } void WebBrowserComponent::refresh() { browser->getInternalWebView().refresh(); } //============================================================================== void WebBrowserComponent::paint (Graphics& g) { if (! browser->getInternalWebView().hasBrowserBeenCreated()) { g.fillAll (Colours::white); checkWindowAssociation(); } } void WebBrowserComponent::checkWindowAssociation() { if (isShowing()) { if (! browser->getInternalWebView().hasBrowserBeenCreated() && getPeer() != nullptr) { browser->getInternalWebView().createBrowser(); reloadLastURL(); } else { if (blankPageShown) goBack(); } } else { if (browser != nullptr && unloadPageWhenHidden && ! blankPageShown) { // when the component becomes invisible, some stuff like flash // carries on playing audio, so we need to force it onto a blank // page to avoid this.. blankPageShown = true; browser->getInternalWebView().goToURL ("about:blank", nullptr, nullptr); } } } void WebBrowserComponent::reloadLastURL() { if (lastURL.isNotEmpty()) { goToURL (lastURL, &lastHeaders, &lastPostData); lastURL.clear(); } } void WebBrowserComponent::parentHierarchyChanged() { checkWindowAssociation(); } void WebBrowserComponent::resized() { browser->getInternalWebView().setWebViewSize (getWidth(), getHeight()); } void WebBrowserComponent::visibilityChanged() { checkWindowAssociation(); } void WebBrowserComponent::focusGained (FocusChangeType) { browser->getInternalWebView().focusGained(); } void WebBrowserComponent::clearCookies() { HeapBlock<::INTERNET_CACHE_ENTRY_INFOA> entry; ::DWORD entrySize = sizeof (::INTERNET_CACHE_ENTRY_INFOA); ::HANDLE urlCacheHandle = ::FindFirstUrlCacheEntryA ("cookie:", entry.getData(), &entrySize); if (urlCacheHandle == nullptr && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { entry.realloc (1, entrySize); urlCacheHandle = ::FindFirstUrlCacheEntryA ("cookie:", entry.getData(), &entrySize); } if (urlCacheHandle != nullptr) { for (;;) { ::DeleteUrlCacheEntryA (entry.getData()->lpszSourceUrlName); if (::FindNextUrlCacheEntryA (urlCacheHandle, entry.getData(), &entrySize) == 0) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { entry.realloc (1, entrySize); if (::FindNextUrlCacheEntryA (urlCacheHandle, entry.getData(), &entrySize) != 0) continue; } break; } } FindCloseUrlCache (urlCacheHandle); } } } // namespace juce