diff --git a/src/3rdparty b/src/3rdparty index 0f35665bef3fc26a423ee7770356749c9a6d7dc9..d23df0c25c334dbde96ef5eb65cc098eee47859b 160000 --- a/src/3rdparty +++ b/src/3rdparty @@ -1 +1 @@ -Subproject commit 0f35665bef3fc26a423ee7770356749c9a6d7dc9 +Subproject commit d23df0c25c334dbde96ef5eb65cc098eee47859b diff --git a/src/core/config/linux.pri b/src/core/config/linux.pri index c2d41b4d0cd299b570f454b94ab8154dc8a4c192..c0f2f6289a199eea6611b021ba40d77eebbbd8be 100644 --- a/src/core/config/linux.pri +++ b/src/core/config/linux.pri @@ -41,7 +41,7 @@ cross_compile:!host_build { !isEmpty(TOOLCHAIN_SYSROOT): gn_args += target_sysroot=\"$${TOOLCHAIN_SYSROOT}\" } -contains(QT_ARCH, "arm"):!host_build { +contains(QT_ARCH, "arm") { # Extract ARM specific compiler options that we have to pass to gn, # but let gn figure out a default if an option is not present. MTUNE = $$extractCFlag("-mtune=.*") @@ -78,7 +78,7 @@ contains(QT_ARCH, "arm"):!host_build { else: contains(QMAKE_CFLAGS, "-mthumb"): gn_args += arm_use_thumb=true } -contains(QT_ARCH, "mips"):!host_build { +contains(QT_ARCH, "mips") { MARCH = $$extractCFlag("-march=.*") !isEmpty(MARCH) { equals(MARCH, "mips32r6"): gn_args += mips_arch_variant=\"r6\" diff --git a/src/core/gl_surface_qt.cpp b/src/core/gl_surface_qt.cpp index 69609f4d4c1c021cea807105b4272beb44cb789b..93c1c52930f127274fb60900ff3ee98ccca692c5 100644 --- a/src/core/gl_surface_qt.cpp +++ b/src/core/gl_surface_qt.cpp @@ -48,6 +48,7 @@ #include <QGuiApplication> #include "gl_context_qt.h" #include "qtwebenginecoreglobal_p.h" +#include "web_engine_context.h" #include "base/logging.h" #include "gpu/ipc/service/image_transport_surface.h" @@ -652,6 +653,11 @@ bool InitializeGLOneOffPlatform() return false; } +bool usingSoftwareDynamicGL() +{ + return QtWebEngineCore::usingSoftwareDynamicGL(); +} + scoped_refptr<GLSurface> CreateOffscreenGLSurfaceWithFormat(const gfx::Size& size, GLSurfaceFormat format) { diff --git a/src/core/renderer_host/user_resource_controller_host.cpp b/src/core/renderer_host/user_resource_controller_host.cpp index f90aebda64b5b18ca1416a691007f87b820a1248..2799d5d8556667fa69e1255107f8e189eb7e3bd1 100644 --- a/src/core/renderer_host/user_resource_controller_host.cpp +++ b/src/core/renderer_host/user_resource_controller_host.cpp @@ -88,11 +88,6 @@ void UserResourceControllerHost::WebContentsObserverHelper::RenderFrameHostChang { if (oldHost) oldHost->Send(new RenderFrameObserverHelper_ClearScripts(oldHost->GetRoutingID())); - - content::WebContents *contents = web_contents(); - Q_FOREACH (const UserScript &script, m_controllerHost->m_perContentsScripts.value(contents)) - newHost->Send(new RenderFrameObserverHelper_AddScript(newHost->GetRoutingID(), - script.data())); } void UserResourceControllerHost::WebContentsObserverHelper::WebContentsDestroyed() diff --git a/src/core/renderer_host/web_channel_ipc_transport_host.cpp b/src/core/renderer_host/web_channel_ipc_transport_host.cpp index 1cd4e4063a7848fc9909d44225a7638f624a8636..c47b255b7b20ff06df666dc41e659622c859543d 100644 --- a/src/core/renderer_host/web_channel_ipc_transport_host.cpp +++ b/src/core/renderer_host/web_channel_ipc_transport_host.cpp @@ -40,6 +40,7 @@ #include "web_channel_ipc_transport_host.h" #include "base/strings/string16.h" +#include "content/public/browser/render_view_host.h" #include "common/qt_messages.h" #include "type_conversion.h" @@ -61,11 +62,16 @@ WebChannelIPCTransportHost::~WebChannelIPCTransportHost() { } -void WebChannelIPCTransportHost::RenderViewHostChanged(content::RenderViewHost *, content::RenderViewHost *) +void WebChannelIPCTransportHost::RenderViewHostChanged(content::RenderViewHost *oldHost, content::RenderViewHost *) { - // This means that we were moved into a different RenderView, possibly in a different - // render process and that we lost our WebChannelIPCTransport object and its state. - Send(new WebChannelIPCTransport_Install(routing_id(), m_worldId)); + if (oldHost) + oldHost->Send(new WebChannelIPCTransport_Uninstall(oldHost->GetRoutingID(), m_worldId)); +} + +void WebChannelIPCTransportHost::RenderViewCreated(content::RenderViewHost *view_host) +{ + // Make sure the new view knows a webchannel is installed and in which world. + view_host->Send(new WebChannelIPCTransport_Install(view_host->GetRoutingID(), m_worldId)); } void WebChannelIPCTransportHost::setWorldId(uint worldId) diff --git a/src/core/renderer_host/web_channel_ipc_transport_host.h b/src/core/renderer_host/web_channel_ipc_transport_host.h index aa406471ce59bf854e503bbd4a5d84e2bc63371c..a1e697a910ad84dd5cc8c5beb78ad616f6ccfaa2 100644 --- a/src/core/renderer_host/web_channel_ipc_transport_host.h +++ b/src/core/renderer_host/web_channel_ipc_transport_host.h @@ -60,6 +60,7 @@ public: // WebContentsObserver void RenderViewHostChanged(content::RenderViewHost* old_host, content::RenderViewHost* new_host) override; + void RenderViewCreated(content::RenderViewHost* render_view_host) override; // QWebChannelAbstractTransport void sendMessage(const QJsonObject &message) override; diff --git a/src/core/web_contents_adapter.cpp b/src/core/web_contents_adapter.cpp index 6181c0b72a10db5698282cc9fbcac4750924fbe5..82734911593faa6b39890c36b3ae77aa81cd6aa6 100644 --- a/src/core/web_contents_adapter.cpp +++ b/src/core/web_contents_adapter.cpp @@ -926,7 +926,9 @@ void WebContentsAdapter::updateWebPreferences(const content::WebPreferences & we d->webContents->GetRenderViewHost()->UpdateWebkitPreferences(webPreferences); } -void WebContentsAdapter::download(const QUrl &url, const QString &suggestedFileName) +void WebContentsAdapter::download(const QUrl &url, const QString &suggestedFileName, + const QUrl &referrerUrl, + ReferrerPolicy referrerPolicy) { Q_D(WebContentsAdapter); content::BrowserContext *bctx = webContents()->GetBrowserContext(); @@ -939,9 +941,19 @@ void WebContentsAdapter::download(const QUrl &url, const QString &suggestedFileN dlmd->setDownloadType(BrowserContextAdapterClient::UserRequested); dlm->SetDelegate(dlmd); + GURL gurl = toGurl(url); std::unique_ptr<content::DownloadUrlParameters> params( - content::DownloadUrlParameters::CreateForWebContentsMainFrame(webContents(), toGurl(url))); + content::DownloadUrlParameters::CreateForWebContentsMainFrame(webContents(), gurl)); + params->set_suggested_name(toString16(suggestedFileName)); + + // referrer logic based on chrome/browser/renderer_context_menu/render_view_context_menu.cc: + params->set_referrer( + content::Referrer::SanitizeForRequest( + gurl, + content::Referrer(toGurl(referrerUrl).GetAsReferrer(), + static_cast<blink::WebReferrerPolicy>(referrerPolicy)))); + dlm->DownloadUrl(std::move(params)); } @@ -1473,4 +1485,13 @@ ASSERT_ENUMS_MATCH(WebContentsAdapterClient::SaveToDiskDisposition, WindowOpenDi ASSERT_ENUMS_MATCH(WebContentsAdapterClient::OffTheRecordDisposition, WindowOpenDisposition::OFF_THE_RECORD) ASSERT_ENUMS_MATCH(WebContentsAdapterClient::IgnoreActionDisposition, WindowOpenDisposition::IGNORE_ACTION) +ASSERT_ENUMS_MATCH(ReferrerPolicy::Always, blink::kWebReferrerPolicyAlways) +ASSERT_ENUMS_MATCH(ReferrerPolicy::Default, blink::kWebReferrerPolicyDefault) +ASSERT_ENUMS_MATCH(ReferrerPolicy::NoReferrerWhenDowngrade, blink::kWebReferrerPolicyNoReferrerWhenDowngrade) +ASSERT_ENUMS_MATCH(ReferrerPolicy::Never, blink::kWebReferrerPolicyNever) +ASSERT_ENUMS_MATCH(ReferrerPolicy::Origin, blink::kWebReferrerPolicyOrigin) +ASSERT_ENUMS_MATCH(ReferrerPolicy::OriginWhenCrossOrigin, blink::kWebReferrerPolicyOriginWhenCrossOrigin) +ASSERT_ENUMS_MATCH(ReferrerPolicy::NoReferrerWhenDowngradeOriginWhenCrossOrigin, blink::kWebReferrerPolicyNoReferrerWhenDowngradeOriginWhenCrossOrigin) +ASSERT_ENUMS_MATCH(ReferrerPolicy::Last, blink::kWebReferrerPolicyLast) + } // namespace QtWebEngineCore diff --git a/src/core/web_contents_adapter.h b/src/core/web_contents_adapter.h index 46c8d2604c9399bc77128eec892ec14c42c04202..67fcbe7af50594cc0a772772acb77ea5932c210d 100644 --- a/src/core/web_contents_adapter.h +++ b/src/core/web_contents_adapter.h @@ -124,7 +124,9 @@ public: quint64 findText(const QString &subString, bool caseSensitively, bool findBackward); void stopFinding(); void updateWebPreferences(const content::WebPreferences &webPreferences); - void download(const QUrl &url, const QString &suggestedFileName); + void download(const QUrl &url, const QString &suggestedFileName, + const QUrl &referrerUrl = QUrl(), + ReferrerPolicy referrerPolicy = ReferrerPolicy::Default); bool isAudioMuted() const; void setAudioMuted(bool mute); bool recentlyAudible(); diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h index 8b73653429ca7436c3da948c6be0d6486484e4ee..4280dbfe1f8ce96ad67d9ac15bee70d07253be19 100644 --- a/src/core/web_contents_adapter_client.h +++ b/src/core/web_contents_adapter_client.h @@ -71,6 +71,17 @@ class WebContentsAdapter; class WebContentsDelegateQt; class WebEngineSettings; +// Must match blink::WebReferrerPolicy +enum class ReferrerPolicy { + Always, + Default, + NoReferrerWhenDowngrade, + Never, + Origin, + OriginWhenCrossOrigin, + NoReferrerWhenDowngradeOriginWhenCrossOrigin, + Last = NoReferrerWhenDowngradeOriginWhenCrossOrigin, +}; class WebEngineContextMenuSharedData : public QSharedData { @@ -97,6 +108,9 @@ public: QString suggestedFileName; QString misspelledWord; QStringList spellCheckerSuggestions; + QUrl pageUrl; + QUrl frameUrl; + ReferrerPolicy referrerPolicy = ReferrerPolicy::Default; // Some likely candidates for future additions as we add support for the related actions: // bool isImageBlocked; // <enum tbd> mediaType; @@ -253,6 +267,34 @@ public: return d->spellCheckerSuggestions; } + void setFrameUrl(const QUrl &url) { + d->frameUrl = url; + } + + QUrl frameUrl() const { + return d->frameUrl; + } + + void setPageUrl(const QUrl &url) { + d->pageUrl = url; + } + + QUrl pageUrl() const { + return d->pageUrl; + } + + QUrl referrerUrl() const { + return !d->frameUrl.isEmpty() ? d->frameUrl : d->pageUrl; + } + + void setReferrerPolicy(ReferrerPolicy referrerPolicy) { + d->referrerPolicy = referrerPolicy; + } + + ReferrerPolicy referrerPolicy() const { + return d->referrerPolicy; + } + private: QSharedDataPointer<WebEngineContextMenuSharedData> d; }; diff --git a/src/core/web_contents_view_qt.cpp b/src/core/web_contents_view_qt.cpp index f9435ac34d902232f474117b7612139afa2d1204..28f202e24288b7879b870db0c297a3bf12d500b8 100644 --- a/src/core/web_contents_view_qt.cpp +++ b/src/core/web_contents_view_qt.cpp @@ -176,6 +176,9 @@ static inline WebEngineContextMenuData fromParams(const content::ContextMenuPara ret.setMisspelledWord(toQt(params.misspelled_word)); ret.setSpellCheckerSuggestions(fromVector(params.dictionary_suggestions)); #endif + ret.setFrameUrl(toQt(params.frame_url)); + ret.setPageUrl(toQt(params.page_url)); + ret.setReferrerPolicy((ReferrerPolicy)params.referrer_policy); return ret; } diff --git a/src/core/web_engine_context.cpp b/src/core/web_engine_context.cpp index 4df4dbfae11b6d2854f86f29f591d72d9647e446..83b7c2d36a35fee29081a38ecf525b6ba04632a3 100644 --- a/src/core/web_engine_context.cpp +++ b/src/core/web_engine_context.cpp @@ -135,21 +135,6 @@ bool usingANGLE() #endif } -bool usingSoftwareDynamicGL() -{ - if (QCoreApplication::testAttribute(Qt::AA_UseSoftwareOpenGL)) - return true; -#if defined(Q_OS_WIN) - HMODULE handle = static_cast<HMODULE>(QOpenGLContext::openGLModuleHandle()); - wchar_t path[MAX_PATH]; - DWORD size = GetModuleFileName(handle, path, MAX_PATH); - QFileInfo openGLModule(QString::fromWCharArray(path, size)); - return openGLModule.fileName() == QLatin1String("opengl32sw.dll"); -#else - return false; -#endif -} - bool usingQtQuick2DRenderer() { const QStringList args = QGuiApplication::arguments(); @@ -186,6 +171,21 @@ void dummyGetPluginCallback(const std::vector<content::WebPluginInfo>&) namespace QtWebEngineCore { +bool usingSoftwareDynamicGL() +{ + if (QCoreApplication::testAttribute(Qt::AA_UseSoftwareOpenGL)) + return true; +#if defined(Q_OS_WIN) + HMODULE handle = static_cast<HMODULE>(QOpenGLContext::openGLModuleHandle()); + wchar_t path[MAX_PATH]; + DWORD size = GetModuleFileName(handle, path, MAX_PATH); + QFileInfo openGLModule(QString::fromWCharArray(path, size)); + return openGLModule.fileName() == QLatin1String("opengl32sw.dll"); +#else + return false; +#endif +} + void WebEngineContext::destroyBrowserContext() { m_defaultBrowserContext.reset(); @@ -287,6 +287,9 @@ WebEngineContext::WebEngineContext() appArgs.append(QString::fromLocal8Bit(qgetenv(kChromiumFlagsEnv)).split(' ')); } + bool enableWebGLSoftwareRendering = + appArgs.removeAll(QStringLiteral("--enable-webgl-software-rendering")); + bool useEmbeddedSwitches = false; #if defined(QTWEBENGINE_EMBEDDED_SWITCHES) useEmbeddedSwitches = !appArgs.removeAll(QStringLiteral("--disable-embedded-switches")); @@ -371,7 +374,20 @@ WebEngineContext::WebEngineContext() const char *glType = 0; #ifndef QT_NO_OPENGL - if (!usingANGLE() && !usingSoftwareDynamicGL() && !usingQtQuick2DRenderer()) { + + bool tryGL = + !usingANGLE() + && (!usingSoftwareDynamicGL() +#ifdef Q_OS_WIN + // If user requested WebGL support on Windows, instead of using Skia rendering to + // bitmaps, use software rendering via opengl32sw.dll. This might be less + // performant, but at least provides WebGL support. + || enableWebGLSoftwareRendering +#endif + ) + && !usingQtQuick2DRenderer(); + + if (tryGL) { if (qt_gl_global_share_context() && qt_gl_global_share_context()->isValid()) { // If the native handle is QEGLNativeContext try to use GL ES/2, if there is no native handle // assume we are using wayland and try GL ES/2, and finally Ozone demands GL ES/2 too. diff --git a/src/core/web_engine_context.h b/src/core/web_engine_context.h index 386f43035972f2627a388b036f343beedd2c1904..92c45e260fea851ced89a6b18ecd4a52fcdc67dd 100644 --- a/src/core/web_engine_context.h +++ b/src/core/web_engine_context.h @@ -74,6 +74,8 @@ class ContentMainDelegateQt; class DevToolsServerQt; class SurfaceFactoryQt; +bool usingSoftwareDynamicGL(); + class WebEngineContext : public base::RefCounted<WebEngineContext> { public: static scoped_refptr<WebEngineContext> current(); diff --git a/src/core/web_event_factory.cpp b/src/core/web_event_factory.cpp index e6ac63c0dd4e323b8d83fa736c8dda3d7c6ea2e6..18f8d393f0d76d057f65ce3610fdd8898b28558d 100644 --- a/src/core/web_event_factory.cpp +++ b/src/core/web_event_factory.cpp @@ -144,7 +144,6 @@ static int windowsKeyCodeForKeyEvent(unsigned int keycode, bool isKeypad) return VK_SHIFT; // (10) SHIFT key case Qt::Key_Control: return VK_CONTROL; // (11) CTRL key - case Qt::Key_Menu: case Qt::Key_Alt: return VK_MENU; // (12) ALT key default: @@ -167,7 +166,6 @@ static int windowsKeyCodeForKeyEvent(unsigned int keycode, bool isKeypad) return VK_SHIFT; // (10) SHIFT key case Qt::Key_Control: return VK_CONTROL; // (11) CTRL key - case Qt::Key_Menu: case Qt::Key_Alt: return VK_MENU; // (12) ALT key @@ -357,7 +355,8 @@ static int windowsKeyCodeForKeyEvent(unsigned int keycode, bool isKeypad) return VK_LWIN; // (5B) Left Windows key (Microsoft Natural keyboard) // case Qt::Key_Meta_R: FIXME: What to do here? // return VK_RWIN; // (5C) Right Windows key (Natural keyboard) - // VK_APPS (5D) Applications key (Natural keyboard) + case Qt::Key_Menu: // (5D) Applications key (Natural keyboard) + return VK_APPS; // VK_SLEEP (5F) Computer Sleep key // VK_SEPARATOR (6C) Separator key // VK_SUBTRACT (6D) Subtract key diff --git a/src/webengine/api/qquickwebengineview.cpp b/src/webengine/api/qquickwebengineview.cpp index 123acb20c842cb627391137975b74b503e0b4103..78fdd8fa819183b177eec00b047812ffad4269a0 100644 --- a/src/webengine/api/qquickwebengineview.cpp +++ b/src/webengine/api/qquickwebengineview.cpp @@ -1639,7 +1639,8 @@ void QQuickWebEngineView::triggerWebAction(WebAction action) break; case DownloadLinkToDisk: if (d->m_contextMenuData.linkUrl().isValid()) - d->adapter->download(d->m_contextMenuData.linkUrl(), d->m_contextMenuData.suggestedFileName()); + d->adapter->download(d->m_contextMenuData.linkUrl(), d->m_contextMenuData.suggestedFileName(), + d->m_contextMenuData.referrerUrl(), d->m_contextMenuData.referrerPolicy()); break; case CopyImageToClipboard: if (d->m_contextMenuData.hasImageContent() && @@ -1666,7 +1667,8 @@ void QQuickWebEngineView::triggerWebAction(WebAction action) case DownloadImageToDisk: case DownloadMediaToDisk: if (d->m_contextMenuData.mediaUrl().isValid()) - d->adapter->download(d->m_contextMenuData.mediaUrl(), d->m_contextMenuData.suggestedFileName()); + d->adapter->download(d->m_contextMenuData.mediaUrl(), d->m_contextMenuData.suggestedFileName(), + d->m_contextMenuData.referrerUrl(), d->m_contextMenuData.referrerPolicy()); break; case CopyMediaUrlToClipboard: if (d->m_contextMenuData.mediaUrl().isValid() && diff --git a/src/webenginewidgets/api/qwebenginepage.cpp b/src/webenginewidgets/api/qwebenginepage.cpp index 178f6ec9868aa579c98964ada04c19e5f3a261bc..76c705c6e8c08725ee4a6737e4fc0accae80202e 100644 --- a/src/webenginewidgets/api/qwebenginepage.cpp +++ b/src/webenginewidgets/api/qwebenginepage.cpp @@ -1275,7 +1275,9 @@ void QWebEnginePage::triggerAction(WebAction action, bool) break; case DownloadLinkToDisk: if (menuData.linkUrl().isValid()) - d->adapter->download(menuData.linkUrl(), menuData.suggestedFileName()); + d->adapter->download(menuData.linkUrl(), menuData.suggestedFileName(), + menuData.referrerUrl(), menuData.referrerPolicy()); + break; case CopyImageToClipboard: if (menuData.hasImageContent() && @@ -1302,7 +1304,8 @@ void QWebEnginePage::triggerAction(WebAction action, bool) case DownloadImageToDisk: case DownloadMediaToDisk: if (menuData.mediaUrl().isValid()) - d->adapter->download(menuData.mediaUrl(), menuData.suggestedFileName()); + d->adapter->download(menuData.mediaUrl(), menuData.suggestedFileName(), + menuData.referrerUrl(), menuData.referrerPolicy()); break; case CopyMediaUrlToClipboard: if (menuData.mediaUrl().isValid() && @@ -1466,7 +1469,7 @@ void QWebEnginePagePrivate::wasHidden() bool QWebEnginePagePrivate::contextMenuRequested(const WebEngineContextMenuData &data) { - if (!view || !view->d_func()->m_pendingContextMenuEvent) + if (!view) return false; contextData.reset(); @@ -1492,7 +1495,6 @@ bool QWebEnginePagePrivate::contextMenuRequested(const WebEngineContextMenuData event.ignore(); return false; } - view->d_func()->m_pendingContextMenuEvent = false; return true; } diff --git a/src/webenginewidgets/api/qwebengineview.cpp b/src/webenginewidgets/api/qwebengineview.cpp index 56948bb18a9d5ba7768c798ae36237e0bb8d1413..0037f7e5001c7febfbbf155f202330b7f947116e 100644 --- a/src/webenginewidgets/api/qwebengineview.cpp +++ b/src/webenginewidgets/api/qwebengineview.cpp @@ -106,7 +106,6 @@ static QAccessibleInterface *webAccessibleFactory(const QString &, QObject *obje QWebEngineViewPrivate::QWebEngineViewPrivate() : page(0) - , m_pendingContextMenuEvent(false) , m_dragEntered(false) { #ifndef QT_NO_ACCESSIBILITY @@ -316,12 +315,10 @@ void QWebEngineView::setZoomFactor(qreal factor) */ bool QWebEngineView::event(QEvent *ev) { - Q_D(QWebEngineView); // We swallow spontaneous contextMenu events and synthethize those back later on when we get the // HandleContextMenu callback from chromium if (ev->type() == QEvent::ContextMenu) { ev->accept(); - d->m_pendingContextMenuEvent = true; return true; } return QWidget::event(ev); diff --git a/src/webenginewidgets/api/qwebengineview_p.h b/src/webenginewidgets/api/qwebengineview_p.h index 45b3e266eb0668c93cfc92baa8e36ae13162b536..f3a37225b619b42a1eb2054134d7249bf2f028c5 100644 --- a/src/webenginewidgets/api/qwebengineview_p.h +++ b/src/webenginewidgets/api/qwebengineview_p.h @@ -70,7 +70,6 @@ public: QWebEngineViewPrivate(); QWebEnginePage *page; - bool m_pendingContextMenuEvent; bool m_dragEntered; }; diff --git a/tests/auto/shared/http.pri b/tests/auto/shared/http.pri new file mode 100644 index 0000000000000000000000000000000000000000..5236e9d26935e2ddee1cc1c5e2db9f75fb1de47b --- /dev/null +++ b/tests/auto/shared/http.pri @@ -0,0 +1,3 @@ +HEADERS += $$PWD/httpserver.h $$PWD/httpreqrep.h +SOURCES += $$PWD/httpserver.cpp $$PWD/httpreqrep.cpp +INCLUDEPATH += $$PWD diff --git a/tests/auto/shared/httpreqrep.cpp b/tests/auto/shared/httpreqrep.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eb2db68901ac4bb72cf2c9f45440c893c44b8a35 --- /dev/null +++ b/tests/auto/shared/httpreqrep.cpp @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "httpreqrep.h" + +HttpReqRep::HttpReqRep(QTcpSocket *socket, QObject *parent) + : QObject(parent), m_socket(socket) +{ + m_socket->setParent(this); + connect(m_socket, &QIODevice::readyRead, this, &HttpReqRep::handleReadyRead); +} + +void HttpReqRep::sendResponse() +{ + m_socket->write("HTTP/1.1 "); + m_socket->write(QByteArray::number(m_responseStatusCode)); + m_socket->write(" OK?\r\n"); + for (const auto & kv : m_responseHeaders) { + m_socket->write(kv.first); + m_socket->write(": "); + m_socket->write(kv.second); + m_socket->write("\r\n"); + } + m_socket->write("\r\n"); + m_socket->write(m_responseBody); + m_socket->close(); +} + +QByteArray HttpReqRep::requestHeader(const QByteArray &key) const +{ + auto it = m_requestHeaders.find(key); + if (it != m_requestHeaders.end()) + return it->second; + return {}; +} + +void HttpReqRep::handleReadyRead() +{ + const auto requestLine = m_socket->readLine(); + const auto requestLineParts = requestLine.split(' '); + if (requestLineParts.size() != 3 || !requestLineParts[2].toUpper().startsWith("HTTP/")) { + qWarning("HttpReqRep: invalid request line"); + Q_EMIT readFinished(false); + return; + } + + decltype(m_requestHeaders) headers; + for (;;) { + const auto headerLine = m_socket->readLine(); + if (headerLine == QByteArrayLiteral("\r\n")) + break; + int colonIndex = headerLine.indexOf(':'); + if (colonIndex < 0) { + qWarning("HttpReqRep: invalid header line"); + Q_EMIT readFinished(false); + return; + } + auto headerKey = headerLine.left(colonIndex).trimmed().toLower(); + auto headerValue = headerLine.mid(colonIndex + 1).trimmed().toLower(); + headers.emplace(headerKey, headerValue); + } + + m_requestMethod = requestLineParts[0]; + m_requestPath = requestLineParts[1]; + m_requestHeaders = headers; + Q_EMIT readFinished(true); +} diff --git a/tests/auto/shared/httpreqrep.h b/tests/auto/shared/httpreqrep.h new file mode 100644 index 0000000000000000000000000000000000000000..4e9f10dff3c5b56b3d1e2c9f3ba87c54938d35dd --- /dev/null +++ b/tests/auto/shared/httpreqrep.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#ifndef HTTPREQREP_H +#define HTTPREQREP_H + +#include <QTcpSocket> + +#include <utility> + +// Represents an HTTP request-response exchange. +class HttpReqRep : public QObject +{ + Q_OBJECT +public: + HttpReqRep(QTcpSocket *socket, QObject *parent = nullptr); + void sendResponse(); + QByteArray requestMethod() const { return m_requestMethod; } + QByteArray requestPath() const { return m_requestPath; } + QByteArray requestHeader(const QByteArray &key) const; + void setResponseStatus(int statusCode) + { + m_responseStatusCode = statusCode; + } + void setResponseHeader(const QByteArray &key, QByteArray value) + { + m_responseHeaders[key.toLower()] = std::move(value); + } + void setResponseBody(QByteArray content) + { + m_responseHeaders["content-length"] = QByteArray::number(content.size()); + m_responseBody = std::move(content); + } + +Q_SIGNALS: + void readFinished(bool ok); + +private Q_SLOTS: + void handleReadyRead(); + +private: + QTcpSocket *m_socket = nullptr; + QByteArray m_requestMethod; + QByteArray m_requestPath; + std::map<QByteArray, QByteArray> m_requestHeaders; + int m_responseStatusCode = 200; + std::map<QByteArray, QByteArray> m_responseHeaders; + QByteArray m_responseBody; +}; + +#endif // !HTTPREQREP_H diff --git a/tests/auto/shared/httpserver.cpp b/tests/auto/shared/httpserver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6012379f2a68d63039f4808656d42db551166842 --- /dev/null +++ b/tests/auto/shared/httpserver.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "httpserver.h" + +#include "waitforsignal.h" + +HttpServer::HttpServer(QObject *parent) : QObject(parent) +{ + connect(&m_tcpServer, &QTcpServer::newConnection, this, &HttpServer::handleNewConnection); + if (!m_tcpServer.listen()) + qWarning("HttpServer: listen() failed"); + m_url = QStringLiteral("http://127.0.0.1:") + QString::number(m_tcpServer.serverPort()); +} + +QUrl HttpServer::url(const QString &path) const +{ + auto copy = m_url; + copy.setPath(path); + return copy; +} + +void HttpServer::handleNewConnection() +{ + auto reqRep = new HttpReqRep(m_tcpServer.nextPendingConnection(), this); + connect(reqRep, &HttpReqRep::readFinished, this, &HttpServer::handleReadFinished); +} + +void HttpServer::handleReadFinished(bool ok) +{ + auto reqRep = qobject_cast<HttpReqRep *>(sender()); + if (ok) + Q_EMIT newRequest(reqRep); + else + reqRep->deleteLater(); +} + +std::unique_ptr<HttpReqRep> waitForRequest(HttpServer *server) +{ + std::unique_ptr<HttpReqRep> result; + waitForSignal(server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + rr->setParent(nullptr); + result.reset(rr); + }); + return result; +} diff --git a/tests/auto/shared/httpserver.h b/tests/auto/shared/httpserver.h new file mode 100644 index 0000000000000000000000000000000000000000..e45743b7b0e19aba95e7eca43d2f176182a746d9 --- /dev/null +++ b/tests/auto/shared/httpserver.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#ifndef HTTPSERVER_H +#define HTTPSERVER_H + +#include "httpreqrep.h" + +#include <QTcpServer> +#include <QUrl> + +#include <memory> + +// Listens on a TCP socket and creates HttpReqReps for each connection. +class HttpServer : public QObject +{ + Q_OBJECT + + QTcpServer m_tcpServer; + QUrl m_url; + +public: + HttpServer(QObject *parent = nullptr); + QUrl url(const QString &path = QStringLiteral("/")) const; + +Q_SIGNALS: + void newRequest(HttpReqRep *reqRep); + +private Q_SLOTS: + void handleNewConnection(); + void handleReadFinished(bool ok); +}; + +// Wait for an HTTP request to be received. +std::unique_ptr<HttpReqRep> waitForRequest(HttpServer *server); + +#endif // !HTTPSERVER_H diff --git a/tests/auto/shared/waitforsignal.h b/tests/auto/shared/waitforsignal.h new file mode 100644 index 0000000000000000000000000000000000000000..9b2daf7af03c5033df955227517de5773927c5f7 --- /dev/null +++ b/tests/auto/shared/waitforsignal.h @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#ifndef WAITFORSIGNAL_H +#define WAITFORSIGNAL_H + +#include <QObject> +#include <QTestEventLoop> + +#include <utility> + +// Implementation details of waitForSignal. +namespace { + // Wraps a functor to set a flag and exit from event loop if called. + template <class SignalHandler> + struct waitForSignal_SignalHandlerWrapper { + waitForSignal_SignalHandlerWrapper(SignalHandler &&sh) + : signalHandler(std::forward<SignalHandler>(sh)) {} + + template <class... Args> + void operator()(Args && ... args) { + signalHandler(std::forward<Args>(args)...); + hasBeenCalled = true; + loop.exitLoop(); + } + + SignalHandler &&signalHandler; + QTestEventLoop loop; + bool hasBeenCalled = false; + }; + + // No-op functor. + struct waitForSignal_DefaultSignalHandler { + template <class... Args> + void operator()(Args && ...) {} + }; +} // namespace + +// Wait for a signal to be emitted. +// +// The given functor is called with the signal arguments allowing the arguments +// to be modified before returning from the signal handler (unlike QSignalSpy). +template <class Sender, class Signal, class SignalHandler> +bool waitForSignal(Sender &&sender, Signal &&signal, SignalHandler &&signalHandler, int timeout = 15000) +{ + waitForSignal_SignalHandlerWrapper<SignalHandler> signalHandlerWrapper( + std::forward<SignalHandler>(signalHandler)); + auto connection = QObject::connect( + std::forward<Sender>(sender), + std::forward<Signal>(signal), + std::ref(signalHandlerWrapper)); + signalHandlerWrapper.loop.enterLoopMSecs(timeout); + QObject::disconnect(connection); + return signalHandlerWrapper.hasBeenCalled; +} + +template <class Sender, class Signal> +bool waitForSignal(Sender &&sender, Signal &&signal, int timeout = 15000) +{ + return waitForSignal(std::forward<Sender>(sender), + std::forward<Signal>(signal), + waitForSignal_DefaultSignalHandler(), + timeout); +} + +#endif // !WAITFORSIGNAL_H diff --git a/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro b/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro new file mode 100644 index 0000000000000000000000000000000000000000..18a66c466abe0f69b56995124f258e3d97f0b157 --- /dev/null +++ b/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro @@ -0,0 +1,3 @@ +include(../tests.pri) +include(../../shared/http.pri) +QT *= core-private diff --git a/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6ba6d78a438c7569911d1db81897474d7c79773d --- /dev/null +++ b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp @@ -0,0 +1,595 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QCoreApplication> +#include <QStandardPaths> +#include <QTemporaryDir> +#include <QTest> +#include <QWebEngineDownloadItem> +#include <QWebEnginePage> +#include <QWebEngineProfile> +#include <QWebEngineView> +#include <httpserver.h> +#include <waitforsignal.h> + +class tst_QWebEngineDownloads : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void downloadLink_data(); + void downloadLink(); + void downloadTwoLinks(); + void downloadPage_data(); + void downloadPage(); +}; + +enum DownloadTestUserAction { + SaveLink, + Navigate, +}; + +enum DownloadTestFileAction { + FileIsDownloaded, + FileIsDisplayed, +}; + +Q_DECLARE_METATYPE(DownloadTestUserAction); +Q_DECLARE_METATYPE(DownloadTestFileAction); + +void tst_QWebEngineDownloads::downloadLink_data() +{ + QTest::addColumn<DownloadTestUserAction>("userAction"); + QTest::addColumn<bool>("anchorHasDownloadAttribute"); + QTest::addColumn<QByteArray>("fileName"); + QTest::addColumn<QByteArray>("fileContents"); + QTest::addColumn<QByteArray>("fileMimeTypeDeclared"); + QTest::addColumn<QByteArray>("fileMimeTypeDetected"); + QTest::addColumn<QByteArray>("fileDisposition"); + QTest::addColumn<bool>("fileHasReferer"); + QTest::addColumn<DownloadTestFileAction>("fileAction"); + QTest::addColumn<QWebEngineDownloadItem::DownloadType>("downloadType"); + + // SaveLink should always trigger a download, even for empty files. + QTest::newRow("save link to empty file") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // SaveLink should always trigger a download, also for text files. + QTest::newRow("save link to text file") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... adding the "download" attribute should have no effect. + QTest::newRow("save link to text file (attribute)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... adding the "attachment" content disposition should also have no effect. + QTest::newRow("save link to text file (attachment)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::UserRequested; + + // ... even adding both should have no effect. + QTest::newRow("save link to text file (attribute+attachment)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::UserRequested; + + // Navigating to an empty file should show an empty page. + QTest::newRow("navigate to empty file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // Navigating to a text file should show the text file. + QTest::newRow("navigate to text file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // ... unless the link has the "download" attribute: then the file should be downloaded. + QTest::newRow("navigate to text file (attribute)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << false // crbug.com/455987 + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... same with the content disposition header save for the download type. + QTest::newRow("navigate to text file (attachment)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::Attachment; + + // ... and both. + QTest::newRow("navigate to text file (attribute+attachment)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << false // crbug.com/455987 + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::Attachment; + + // The file's extension has no effect. + QTest::newRow("navigate to supposed zip file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // ... the file's mime type however does. + QTest::newRow("navigate to supposed zip file (application/zip)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/zip") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... but we're not very picky about the exact type. + QTest::newRow("navigate to supposed zip file (application/octet-stream)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/octet-stream") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // empty zip file (consisting only of "end of central directory record") + QByteArray zipFile = QByteArrayLiteral("PK\x05\x06") + QByteArray(18, 0); + + // The mime type is guessed automatically if not provided by the server. + QTest::newRow("navigate to actual zip file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << zipFile + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // The mime type is not guessed automatically if provided by the server. + QTest::newRow("navigate to actual zip file (application/zip)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << zipFile + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/zip") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; +} + +void tst_QWebEngineDownloads::downloadLink() +{ + QFETCH(DownloadTestUserAction, userAction); + QFETCH(bool, anchorHasDownloadAttribute); + QFETCH(QByteArray, fileName); + QFETCH(QByteArray, fileContents); + QFETCH(QByteArray, fileMimeTypeDeclared); + QFETCH(QByteArray, fileMimeTypeDetected); + QFETCH(QByteArray, fileDisposition); + QFETCH(bool, fileHasReferer); + QFETCH(DownloadTestFileAction, fileAction); + QFETCH(QWebEngineDownloadItem::DownloadType, downloadType); + + HttpServer server; + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QWebEngineView view; + view.setPage(&page); + + // 1. Load an HTML page with a link + // + // The only variation being whether the <a> element has a "download" + // attribute or not. + view.load(server.url()); + view.show(); + auto indexRR = waitForRequest(&server); + QVERIFY(indexRR); + QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); + indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + QByteArray html; + html += QByteArrayLiteral("<html><body><a href=\""); + html += fileName; + html += QByteArrayLiteral("\" "); + if (anchorHasDownloadAttribute) + html += QByteArrayLiteral("download"); + html += QByteArrayLiteral(">Link</a></body></html>"); + indexRR->setResponseBody(html); + indexRR->sendResponse(); + bool loadOk = false; + QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok) { loadOk = ok; })); + QVERIFY(loadOk); + + // 1.1. Ignore favicon request + auto favIconRR = waitForRequest(&server); + QVERIFY(favIconRR); + QCOMPARE(favIconRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(favIconRR->requestPath(), QByteArrayLiteral("/favicon.ico")); + favIconRR->setResponseStatus(404); + favIconRR->sendResponse(); + + // 2. Simulate user action + // + // - Navigate: user left-clicks on link + // - SaveLink: user right-clicks on link and chooses "save link as" from menu + QWidget *renderWidget = view.focusWidget(); + QPoint linkPos(10, 10); + if (userAction == SaveLink) { + view.setContextMenuPolicy(Qt::CustomContextMenu); + auto event1 = new QContextMenuEvent(QContextMenuEvent::Mouse, linkPos); + auto event2 = new QMouseEvent(QEvent::MouseButtonPress, linkPos, Qt::RightButton, {}, {}); + auto event3 = new QMouseEvent(QEvent::MouseButtonRelease, linkPos, Qt::RightButton, {}, {}); + QCoreApplication::postEvent(renderWidget, event1); + QCoreApplication::postEvent(renderWidget, event2); + QCoreApplication::postEvent(renderWidget, event3); + QVERIFY(waitForSignal(&view, &QWidget::customContextMenuRequested)); + page.triggerAction(QWebEnginePage::DownloadLinkToDisk); + } else + QTest::mouseClick(renderWidget, Qt::LeftButton, {}, linkPos); + + // 3. Deliver requested file + // + // Request/response headers vary. + auto fileRR = waitForRequest(&server); + QVERIFY(fileRR); + QCOMPARE(fileRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(fileRR->requestPath(), QByteArrayLiteral("/") + fileName); + if (fileHasReferer) + QCOMPARE(fileRR->requestHeader(QByteArrayLiteral("referer")), server.url().toEncoded()); + else + QCOMPARE(fileRR->requestHeader(QByteArrayLiteral("referer")), QByteArray()); + if (!fileDisposition.isEmpty()) + fileRR->setResponseHeader(QByteArrayLiteral("content-disposition"), fileDisposition); + if (!fileMimeTypeDeclared.isEmpty()) + fileRR->setResponseHeader(QByteArrayLiteral("content-type"), fileMimeTypeDeclared); + fileRR->setResponseBody(fileContents); + fileRR->sendResponse(); + + // 4a. File is displayed and not downloaded - end test + if (fileAction == FileIsDisplayed) { + QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok) { loadOk = ok; })); + QVERIFY(loadOk); + return; + } + + // 4b. File is downloaded - check QWebEngineDownloadItem attributes + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + QByteArray slashFileName = QByteArrayLiteral("/") + fileName; + QString suggestedPath = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + slashFileName; + QString downloadPath = tmpDir.path() + slashFileName; + QUrl downloadUrl = server.url(slashFileName); + QWebEngineDownloadItem *downloadItem = nullptr; + QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, + [&](QWebEngineDownloadItem *item) { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); + QCOMPARE(item->isFinished(), false); + QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->receivedBytes(), 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), downloadType); + QCOMPARE(item->mimeType(), QString(fileMimeTypeDetected)); + QCOMPARE(item->path(), suggestedPath); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), downloadUrl); + item->setPath(downloadPath); + item->accept(); + downloadItem = item; + })); + QVERIFY(downloadItem); + bool finishOk = false; + QVERIFY(waitForSignal(downloadItem, &QWebEngineDownloadItem::finished, [&]() { + auto item = downloadItem; + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); + QCOMPARE(item->isFinished(), true); + QCOMPARE(item->totalBytes(), fileContents.size()); + QCOMPARE(item->receivedBytes(), fileContents.size()); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), downloadType); + QCOMPARE(item->mimeType(), QString(fileMimeTypeDetected)); + QCOMPARE(item->path(), downloadPath); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), downloadUrl); + finishOk = true; + })); + QVERIFY(finishOk); + + // 5. Check actual file contents + QFile file(downloadPath); + QVERIFY(file.open(QIODevice::ReadOnly)); + QCOMPARE(file.readAll(), fileContents); +} + +void tst_QWebEngineDownloads::downloadTwoLinks() +{ + HttpServer server; + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QWebEngineView view; + view.setPage(&page); + + view.load(server.url()); + view.show(); + auto indexRR = waitForRequest(&server); + QVERIFY(indexRR); + QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); + indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + indexRR->setResponseBody(QByteArrayLiteral("<html><body><a href=\"file1\" download>Link1</a><br/><a href=\"file2\">Link2</a></body></html>")); + indexRR->sendResponse(); + bool loadOk = false; + QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok){ loadOk = ok; })); + QVERIFY(loadOk); + + auto favIconRR = waitForRequest(&server); + QVERIFY(favIconRR); + QCOMPARE(favIconRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(favIconRR->requestPath(), QByteArrayLiteral("/favicon.ico")); + favIconRR->setResponseStatus(404); + favIconRR->sendResponse(); + + QWidget *renderWidget = view.focusWidget(); + QTest::mouseClick(renderWidget, Qt::LeftButton, {}, QPoint(10, 10)); + QTest::mouseClick(renderWidget, Qt::LeftButton, {}, QPoint(10, 30)); + + auto file1RR = waitForRequest(&server); + QVERIFY(file1RR); + QCOMPARE(file1RR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(file1RR->requestPath(), QByteArrayLiteral("/file1")); + auto file2RR = waitForRequest(&server); + QVERIFY(file2RR); + QCOMPARE(file2RR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(file2RR->requestPath(), QByteArrayLiteral("/file2")); + + file1RR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); + file1RR->setResponseBody(QByteArrayLiteral("file1")); + file1RR->sendResponse(); + file2RR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); + file2RR->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); + file2RR->setResponseBody(QByteArrayLiteral("file2")); + file2RR->sendResponse(); + + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + QString standardDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + QWebEngineDownloadItem *item1 = nullptr; + QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, + [&](QWebEngineDownloadItem *item) { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); + QCOMPARE(item->isFinished(), false); + QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->receivedBytes(), 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), QWebEngineDownloadItem::DownloadAttribute); + QCOMPARE(item->mimeType(), QStringLiteral("text/plain")); + QCOMPARE(item->path(), standardDir + QByteArrayLiteral("/file1")); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), server.url(QByteArrayLiteral("/file1"))); + item->setPath(tmpDir.path() + QByteArrayLiteral("/file1")); + item->accept(); + item1 = item; + })); + QVERIFY(item1); + + QWebEngineDownloadItem *item2 = nullptr; + QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, + [&](QWebEngineDownloadItem *item) { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); + QCOMPARE(item->isFinished(), false); + QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->receivedBytes(), 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), QWebEngineDownloadItem::DownloadAttribute); + QCOMPARE(item->mimeType(), QStringLiteral("text/plain")); + QCOMPARE(item->path(), standardDir + QByteArrayLiteral("/file2")); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), server.url(QByteArrayLiteral("/file2"))); + item->setPath(tmpDir.path() + QByteArrayLiteral("/file2")); + item->accept(); + item2 = item; + })); + QVERIFY(item2); +} + +void tst_QWebEngineDownloads::downloadPage_data() +{ + QTest::addColumn<QWebEngineDownloadItem::SavePageFormat>("savePageFormat"); + QTest::newRow("SingleHtmlSaveFormat") << QWebEngineDownloadItem::SingleHtmlSaveFormat; + QTest::newRow("CompleteHtmlSaveFormat") << QWebEngineDownloadItem::CompleteHtmlSaveFormat; + QTest::newRow("MimeHtmlSaveFormat") << QWebEngineDownloadItem::MimeHtmlSaveFormat; +} + +void tst_QWebEngineDownloads::downloadPage() +{ + QFETCH(QWebEngineDownloadItem::SavePageFormat, savePageFormat); + + HttpServer server; + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QWebEngineView view; + view.setPage(&page); + + view.load(server.url()); + view.show(); + auto indexRR = waitForRequest(&server); + QVERIFY(indexRR); + QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); + indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + indexRR->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); + indexRR->sendResponse(); + bool loadOk = false; + QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok){ loadOk = ok; })); + QVERIFY(loadOk); + + auto favIconRR = waitForRequest(&server); + QVERIFY(favIconRR); + QCOMPARE(favIconRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(favIconRR->requestPath(), QByteArrayLiteral("/favicon.ico")); + favIconRR->setResponseStatus(404); + favIconRR->sendResponse(); + + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + QString downloadPath = tmpDir.path() + QStringLiteral("/test.html"); + page.save(downloadPath, savePageFormat); + + QWebEngineDownloadItem *downloadItem = nullptr; + QUrl downloadUrl = server.url("/"); + QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, + [&](QWebEngineDownloadItem *item) { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadInProgress); + QCOMPARE(item->isFinished(), false); + QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->receivedBytes(), 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), QWebEngineDownloadItem::SavePage); + // FIXME why is mimeType always the same? + QCOMPARE(item->mimeType(), QStringLiteral("application/x-mimearchive")); + QCOMPARE(item->path(), downloadPath); + QCOMPARE(item->savePageFormat(), savePageFormat); + QCOMPARE(item->url(), downloadUrl); + // no need to call item->accept() + downloadItem = item; + })); + QVERIFY(downloadItem); + bool finishOk = false; + QVERIFY(waitForSignal(downloadItem, &QWebEngineDownloadItem::finished, [&]() { + auto item = downloadItem; + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); + QCOMPARE(item->isFinished(), true); + QCOMPARE(item->totalBytes(), item->receivedBytes()); + QVERIFY(item->receivedBytes() > 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), QWebEngineDownloadItem::SavePage); + QCOMPARE(item->mimeType(), QStringLiteral("application/x-mimearchive")); + QCOMPARE(item->path(), downloadPath); + QCOMPARE(item->savePageFormat(), savePageFormat); + QCOMPARE(item->url(), downloadUrl); + finishOk = true; + })); + QVERIFY(finishOk); + + QFile file(downloadPath); + QVERIFY(file.exists()); +} + +QTEST_MAIN(tst_QWebEngineDownloads) +#include "tst_qwebenginedownloads.moc" diff --git a/tests/auto/widgets/widgets.pro b/tests/auto/widgets/widgets.pro index 90352310e0a2fb8443f3853d04f36faf53858144..441eea0fae8aff04bfc857bf71e5369d55908b0f 100644 --- a/tests/auto/widgets/widgets.pro +++ b/tests/auto/widgets/widgets.pro @@ -3,6 +3,7 @@ TEMPLATE = subdirs SUBDIRS += \ qwebengineaccessibility \ qwebenginedefaultsurfaceformat \ + qwebenginedownloads \ qwebenginefaviconmanager \ qwebenginepage \ qwebenginehistory \ @@ -24,4 +25,5 @@ contains(WEBENGINE_CONFIG, use_spellchecker):!cross_compile { # QTBUG-60268 boot2qt: SUBDIRS -= qwebengineaccessibility qwebenginedefaultsurfaceformat \ qwebenginefaviconmanager qwebenginepage qwebenginehistory \ - qwebengineprofile qwebenginescript qwebengineview + qwebengineprofile qwebenginescript qwebengineview \ + qwebenginedownloads