diff options
-rw-r--r-- | REUSE.toml | 2 | ||||
-rw-r--r-- | src/webenginequick/api/qquickwebengineprofile.h | 2 | ||||
-rw-r--r-- | tests/auto/quick/qmltests/data/tst_getUserMedia.qml | 13 | ||||
-rw-r--r-- | tests/auto/util/util.h | 7 | ||||
-rw-r--r-- | tests/auto/widgets/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp | 417 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepermission/CMakeLists.txt | 28 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepermission/resources/iframe.html | 5 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepermission/resources/index.html | 14 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepermission/resources/qt144.png | bin | 0 -> 8315 bytes | |||
-rw-r--r-- | tests/auto/widgets/qwebenginepermission/tst_qwebenginepermission.cpp | 727 | ||||
-rw-r--r-- | tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp | 191 |
12 files changed, 814 insertions, 593 deletions
diff --git a/REUSE.toml b/REUSE.toml index 19f9b97b3..075c0b340 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -24,6 +24,7 @@ path = ["tests/auto/widgets/qwebenginepage/resources/*", "tests/auto/widgets/qwebengineview/resources/*", "tests/auto/widgets/qwebengineprofile/resources/*", "tests/auto/widgets/qwebengineprofilebuilder/resources/*", + "tests/auto/widgets/qwebenginepermission/resources/*", "tests/auto/widgets/qwebenginehistory/resources/*", "tests/auto/core/certificateerror/resources/*", "tests/auto/core/origins/resources/subdir/*", @@ -121,4 +122,3 @@ precedence = "override" comment = "License file." SPDX-FileCopyrightText = "None" SPDX-License-Identifier = "CC0-1.0" - diff --git a/src/webenginequick/api/qquickwebengineprofile.h b/src/webenginequick/api/qquickwebengineprofile.h index 899d431a1..0995538be 100644 --- a/src/webenginequick/api/qquickwebengineprofile.h +++ b/src/webenginequick/api/qquickwebengineprofile.h @@ -44,7 +44,7 @@ class Q_WEBENGINEQUICK_EXPORT QQuickWebEngineProfile : public QObject { Q_PROPERTY(bool isPushServiceEnabled READ isPushServiceEnabled WRITE setPushServiceEnabled NOTIFY pushServiceEnabledChanged FINAL REVISION(6,5)) Q_PROPERTY(QWebEngineClientHints *clientHints READ clientHints FINAL REVISION(6,8)) #if QT_CONFIG(webengine_extensions) - Q_PROPERTY(QWebEngineExtensionManager *extensionManager READ extensionManager REVISION(6, 10)) + Q_PROPERTY(QWebEngineExtensionManager *extensionManager READ extensionManager CONSTANT REVISION(6, 10)) #endif QML_NAMED_ELEMENT(WebEngineProfile) QML_ADDED_IN_VERSION(1, 1) diff --git a/tests/auto/quick/qmltests/data/tst_getUserMedia.qml b/tests/auto/quick/qmltests/data/tst_getUserMedia.qml index ebb49f9df..a56584230 100644 --- a/tests/auto/quick/qmltests/data/tst_getUserMedia.qml +++ b/tests/auto/quick/qmltests/data/tst_getUserMedia.qml @@ -75,6 +75,9 @@ TestWebEngineView { rejectPendingRequest() tryVerify(jsPromiseRejected) + resetRequestState() + wait(1000) + // 2. Accepting request on QML side should either fulfill or reject the // Promise on JS side. Due to the potential lack of physical media devices // deeper in the content layer we cannot guarantee that the promise will @@ -85,11 +88,16 @@ TestWebEngineView { acceptPendingRequest() tryVerify(jsPromiseSettled) + resetRequestState() + wait(1000) + // 3. Media feature permissions are not remembered. jsGetUserMedia(row.constraints); verifyPermissionType(row.feature) acceptPendingRequest() tryVerify(jsPromiseSettled) + + resetRequestState() } } @@ -158,10 +166,12 @@ TestWebEngineView { function acceptPendingRequest() { if (permissionObject) permissionObject.grant() - resetRequestState() } function resetRequestState() { + if (permissionObject) + permissionObject.reset() + permissionObject = undefined isDesktopMediaRequestHandled = false gotEmptyDesktopMediaRequest = false @@ -170,7 +180,6 @@ TestWebEngineView { function rejectPendingRequest() { if (permissionObject) permissionObject.deny() - resetRequestState() } //// diff --git a/tests/auto/util/util.h b/tests/auto/util/util.h index 65e7fb8b5..6dc420194 100644 --- a/tests/auto/util/util.h +++ b/tests/auto/util/util.h @@ -134,6 +134,13 @@ static inline QVariant evaluateJavaScriptSync(QWebEnginePage *page, const QStrin return spy.waitForResult(); } +static inline QVariant evaluateJavaScriptSync(QWebEngineFrame *frame, const QString &script) +{ + CallbackSpy<QVariant> spy; + frame->runJavaScript(script, spy.ref()); + return spy.waitForResult(); +} + static inline QVariant evaluateJavaScriptSyncInWorld(QWebEnginePage *page, const QString &script, int worldId) { CallbackSpy<QVariant> spy; diff --git a/tests/auto/widgets/CMakeLists.txt b/tests/auto/widgets/CMakeLists.txt index e31ff2170..2195dd5e6 100644 --- a/tests/auto/widgets/CMakeLists.txt +++ b/tests/auto/widgets/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(defaultsurfaceformat) add_subdirectory(qwebenginepage) +add_subdirectory(qwebenginepermission) add_subdirectory(qwebengineprofile) add_subdirectory(qwebengineprofilebuilder) add_subdirectory(qwebengineview) diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp index 6df52dc61..10851580e 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp @@ -128,10 +128,6 @@ private Q_SLOTS: void acceptNavigationRequestWithFormData(); void acceptNavigationRequestNavigationType(); void acceptNavigationRequestRelativeToNothing(); -#ifndef Q_OS_MACOS - void geolocationRequestJS_data(); - void geolocationRequestJS(); -#endif void loadFinished(); void actionStates(); void pasteImage(); @@ -240,16 +236,8 @@ private Q_SLOTS: void triggerActionWithoutMenu(); void dynamicFrame(); - void notificationPermission_data(); - void notificationPermission(); void sendNotification(); - void clipboardReadWritePermissionInitialState_data(); - void clipboardReadWritePermissionInitialState(); - void clipboardReadWritePermission_data(); - void clipboardReadWritePermission(); void contentsSize(); - void localFontAccessPermission_data(); - void localFontAccessPermission(); void setLifecycleState(); void setVisible(); @@ -448,79 +436,6 @@ void tst_QWebEnginePage::acceptNavigationRequest() QCOMPARE(toPlainTextSync(&page), QString("/foo?")); } -class JSTestPage : public QWebEnginePage -{ -Q_OBJECT -public: - JSTestPage(QObject* parent = 0) - : QWebEnginePage(parent) {} - - virtual bool shouldInterruptJavaScript() - { - return true; - } -public Q_SLOTS: - void requestPermission(QWebEnginePermission permission) - { - if (m_allowGeolocation) - permission.grant(); - else - permission.deny(); - } - -public: - void setGeolocationPermission(bool allow) - { - m_allowGeolocation = allow; - } - -private: - bool m_allowGeolocation; -}; - -#ifndef Q_OS_MACOS -void tst_QWebEnginePage::geolocationRequestJS_data() -{ - QTest::addColumn<bool>("allowed"); - QTest::addColumn<int>("errorCode"); - QTest::newRow("allowed") << true << 0; - QTest::newRow("not allowed") << false << 1; -} - -void tst_QWebEnginePage::geolocationRequestJS() -{ - QFETCH(bool, allowed); - QFETCH(int, errorCode); - QWebEngineView view; - JSTestPage *newPage = new JSTestPage(&view); - view.setPage(newPage); - newPage->profile()->setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); - newPage->setGeolocationPermission(allowed); - - connect(newPage, SIGNAL(permissionRequested(QWebEnginePermission)), - newPage, SLOT(requestPermission(QWebEnginePermission))); - - QSignalSpy spyLoadFinished(newPage, SIGNAL(loadFinished(bool))); - newPage->setHtml(QString("<html><body>test</body></html>"), QUrl("qrc://secure/origin")); - QTRY_COMPARE_WITH_TIMEOUT(spyLoadFinished.size(), 1, 20000); - - // Geolocation is only enabled for visible WebContents. - view.show(); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - if (evaluateJavaScriptSync(newPage, QLatin1String("!navigator.geolocation")).toBool()) - QSKIP("Geolocation is not supported."); - - evaluateJavaScriptSync(newPage, "var errorCode = 0; var done = false; function error(err) { errorCode = err.code; done = true; } function success(pos) { done = true; } navigator.geolocation.getCurrentPosition(success, error)"); - - QTRY_VERIFY(evaluateJavaScriptSync(newPage, "done").toBool()); - int result = evaluateJavaScriptSync(newPage, "errorCode").toInt(); - if (result == 2) - QEXPECT_FAIL("", "No location service available.", Continue); - QCOMPARE(result, errorCode); -} -#endif - void tst_QWebEnginePage::loadFinished() { QWebEnginePage page; @@ -1784,14 +1699,21 @@ public: { if (m_permission) m_permission->deny(); - resetRequestState(); } void acceptPendingRequest() { if (m_permission) m_permission->grant(); - resetRequestState(); + } + + void resetRequestState() + { + m_gotDesktopMediaRequest = false; + m_gotEmptyDesktopMediaRequest = false; + if (m_permission) + m_permission->reset(); + m_permission.reset(); } bool gotExpectedRequests(bool isDesktopPermission, @@ -1828,13 +1750,6 @@ private Q_SLOTS: } private: - void resetRequestState() - { - m_gotDesktopMediaRequest = false; - m_gotEmptyDesktopMediaRequest = false; - m_permission.reset(); - } - void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel, const QString &message, int, const QString &) override { @@ -1888,7 +1803,7 @@ void tst_QWebEnginePage::getUserMediaRequest() QVERIFY(QTest::qWaitForWindowExposed(&view)); } - QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 60000); + QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 10000); page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); // 1. Rejecting request on C++ side should reject promise on JS side. @@ -1897,6 +1812,9 @@ void tst_QWebEnginePage::getUserMediaRequest() page.rejectPendingRequest(); QTRY_VERIFY(page.jsPromiseRejected()); + page.resetRequestState(); + QTest::qWait(1000); + // 2. Accepting request on C++ side should either fulfill or reject the // Promise on JS side. Due to the potential lack of physical media devices // deeper in the content layer we cannot guarantee that the promise will @@ -1905,19 +1823,22 @@ void tst_QWebEnginePage::getUserMediaRequest() page.jsGetMedia(call); QTRY_VERIFY(page.gotExpectedRequests(isDesktopPermission, permissionType)); page.acceptPendingRequest(); - QTRY_VERIFY(page.jsPromiseSettled()); + QTRY_VERIFY_WITH_TIMEOUT(page.jsPromiseSettled(), 10000); + + page.resetRequestState(); + QTest::qWait(1000); // 3. Media permissions are not remembered. page.jsGetMedia(call); QTRY_VERIFY(page.gotExpectedRequests(isDesktopPermission, permissionType)); page.acceptPendingRequest(); - QTRY_VERIFY(page.jsPromiseSettled()); + QTRY_VERIFY_WITH_TIMEOUT(page.jsPromiseSettled(), 10000); } void tst_QWebEnginePage::getUserMediaRequestDesktopAudio() { GetUserMediaTestPage page; - QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 20000); + QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 10000); page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); // Audio-only desktop capture is not supported. JS Promise should be @@ -3848,76 +3769,6 @@ public: } }; -void tst_QWebEnginePage::notificationPermission_data() -{ - QTest::addColumn<bool>("setOnInit"); - QTest::addColumn<QWebEnginePermission::State>("policy"); - QTest::addColumn<QString>("permission"); - QTest::newRow("denyOnInit") << true << QWebEnginePermission::State::Denied << "denied"; - QTest::newRow("deny") << false << QWebEnginePermission::State::Denied << "denied"; - QTest::newRow("grant") << false << QWebEnginePermission::State::Granted << "granted"; - QTest::newRow("grantOnInit") << true << QWebEnginePermission::State::Granted << "granted"; -} - -void tst_QWebEnginePage::notificationPermission() -{ - QFETCH(bool, setOnInit); - QFETCH(QWebEnginePermission::State, policy); - QFETCH(QString, permission); - - QWebEngineProfile otr; - otr.setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); - QWebEnginePage page(&otr, nullptr); - - QUrl baseUrl("https://www.example.com/somepage.html"); - - bool permissionRequested = false, errorState = false; - connect(&page, &QWebEnginePage::permissionRequested, &page, [&] (QWebEnginePermission permission) { - if (permission.permissionType() != QWebEnginePermission::PermissionType::Notifications) - return; - if (permissionRequested || permission.origin() != baseUrl.url(QUrl::RemoveFilename)) { - qWarning() << "Unexpected case. Can't proceed." << setOnInit << permissionRequested << permission.origin(); - errorState = true; - return; - } - permissionRequested = true; - - if (policy == QWebEnginePermission::State::Granted) - permission.grant(); - else - permission.deny(); - }); - - QWebEnginePermission permissionObject = otr.queryPermission(baseUrl, QWebEnginePermission::PermissionType::Notifications); - if (setOnInit) { - if (policy == QWebEnginePermission::State::Granted) - permissionObject.grant(); - else - permissionObject.deny(); - } - - QSignalSpy spy(&page, &QWebEnginePage::loadFinished); - page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); - QTRY_COMPARE(spy.size(), 1); - - QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), setOnInit ? permission : QLatin1String("default")); - - if (!setOnInit) { - if (policy == QWebEnginePermission::State::Granted) - permissionObject.grant(); - else - permissionObject.deny(); - QTRY_COMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), permission); - } - - auto js = QStringLiteral("var permission; Notification.requestPermission().then(p => { permission = p })"); - evaluateJavaScriptSync(&page, js); - QTRY_COMPARE(evaluateJavaScriptSync(&page, "permission").toString(), permission); - // permission is not 'remembered' from api standpoint, hence is not suppressed on explicit call from JS - QVERIFY(permissionRequested); - QVERIFY(!errorState); -} - void tst_QWebEnginePage::sendNotification() { NotificationPage page(QWebEnginePermission::State::Granted); @@ -3958,180 +3809,6 @@ void tst_QWebEnginePage::sendNotification() QTRY_VERIFY2(page.messages.contains("onclose"), page.messages.join("\n").toLatin1().constData()); } -static QString clipboardPermissionQuery(QString variableName, QString permissionName) -{ - return QString("var %1; navigator.permissions.query({ name:'%2' }).then((p) => { %1 = p.state; " - "});") - .arg(variableName) - .arg(permissionName); -} - - -void tst_QWebEnginePage::clipboardReadWritePermissionInitialState_data() -{ - QTest::addColumn<bool>("canAccessClipboard"); - QTest::addColumn<bool>("canPaste"); - QTest::addColumn<QString>("readPermission"); - QTest::addColumn<QString>("writePermission"); - QTest::newRow("access and paste should grant both") << true << true << "granted" << "granted"; - QTest::newRow("paste only should prompt for both") << false << true << "prompt" << "prompt"; - QTest::newRow("access only should grant for write only") - << true << false << "prompt" << "granted"; - QTest::newRow("no access or paste should prompt for both") - << false << false << "prompt" << "prompt"; -} - -void tst_QWebEnginePage::clipboardReadWritePermissionInitialState() -{ - QFETCH(bool, canAccessClipboard); - QFETCH(bool, canPaste); - QFETCH(QString, readPermission); - QFETCH(QString, writePermission); - - QWebEngineProfile otr; - otr.setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); - QWebEngineView view(&otr); - QWebEnginePage &page = *view.page(); - view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); - page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, - canAccessClipboard); - page.settings()->setAttribute(QWebEngineSettings::JavascriptCanPaste, canPaste); - - QSignalSpy spy(&page, &QWebEnginePage::loadFinished); - QUrl baseUrl("https://www.example.com/somepage.html"); - page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); - QTRY_COMPARE(spy.size(), 1); - - evaluateJavaScriptSync(&page, clipboardPermissionQuery("readPermission", "clipboard-read")); - QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("readPermission")), readPermission); - evaluateJavaScriptSync(&page, clipboardPermissionQuery("writePermission", "clipboard-write")); - QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("writePermission")), writePermission); -} - -void tst_QWebEnginePage::clipboardReadWritePermission_data() -{ - QTest::addColumn<bool>("canAccessClipboard"); - QTest::addColumn<QWebEnginePermission::State>("initialPolicy"); - QTest::addColumn<QString>("initialPermission"); - QTest::addColumn<QWebEnginePermission::State>("requestPolicy"); - QTest::addColumn<QString>("finalPermission"); - - QTest::newRow("noAccessGrantGrant") - << false << QWebEnginePermission::State::Granted << "granted" - << QWebEnginePermission::State::Granted << "granted"; - QTest::newRow("noAccessGrantDeny") - << false << QWebEnginePermission::State::Granted << "granted" - << QWebEnginePermission::State::Denied << "denied"; - QTest::newRow("noAccessDenyGrant") - << false << QWebEnginePermission::State::Denied << "denied" - << QWebEnginePermission::State::Granted << "granted"; - QTest::newRow("noAccessDenyDeny") << false << QWebEnginePermission::State::Denied << "denied" - << QWebEnginePermission::State::Denied << "denied"; - QTest::newRow("noAccessAskGrant") << false << QWebEnginePermission::State::Ask << "prompt" - << QWebEnginePermission::State::Granted << "granted"; - - // All policies are ignored and overridden by setting JsCanAccessClipboard and JsCanPaste to - // true - QTest::newRow("accessGrantGrant") - << true << QWebEnginePermission::State::Granted << "granted" - << QWebEnginePermission::State::Granted << "granted"; - QTest::newRow("accessDenyDeny") << true << QWebEnginePermission::State::Denied << "granted" - << QWebEnginePermission::State::Denied << "granted"; - QTest::newRow("accessAskAsk") << true << QWebEnginePermission::State::Ask << "granted" - << QWebEnginePermission::State::Ask << "granted"; -} - -void tst_QWebEnginePage::clipboardReadWritePermission() -{ - QFETCH(bool, canAccessClipboard); - QFETCH(QWebEnginePermission::State, initialPolicy); - QFETCH(QString, initialPermission); - QFETCH(QWebEnginePermission::State, requestPolicy); - QFETCH(QString, finalPermission); - - QWebEngineProfile otr; - otr.setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); - QWebEngineView view(&otr); - QWebEnginePage &page = *view.page(); - view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); - page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, - canAccessClipboard); - page.settings()->setAttribute(QWebEngineSettings::JavascriptCanPaste, true); - - QUrl baseUrl("https://www.example.com/somepage.html"); - - int permissionRequestCount = 0; - bool errorState = false; - - // if JavascriptCanAccessClipboard is true, this never fires - connect(&page, &QWebEnginePage::permissionRequested, &page, - [&](QWebEnginePermission permission) { - if (permission.permissionType() != QWebEnginePermission::PermissionType::ClipboardReadWrite) - return; - if (permission.origin() != baseUrl.url(QUrl::RemoveFilename)) { - qWarning() << "Unexpected case. Can't proceed." << permission.origin(); - errorState = true; - return; - } - permissionRequestCount++; - switch (requestPolicy) { - case QWebEnginePermission::State::Granted: - permission.grant(); - break; - case QWebEnginePermission::State::Denied: - permission.deny(); - break; - case QWebEnginePermission::State::Ask: - permission.reset(); - break; - default: - break; - } - }); - - QWebEnginePermission permissionObject = otr.queryPermission(baseUrl, QWebEnginePermission::PermissionType::ClipboardReadWrite); - switch (initialPolicy) { - case QWebEnginePermission::State::Granted: - permissionObject.grant(); - break; - case QWebEnginePermission::State::Denied: - permissionObject.deny(); - break; - case QWebEnginePermission::State::Ask: - permissionObject.reset(); - break; - case QWebEnginePermission::State::Invalid: - break; - } - - QSignalSpy spy(&page, &QWebEnginePage::loadFinished); - page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); - QTRY_COMPARE(spy.size(), 1); - - evaluateJavaScriptSync(&page, clipboardPermissionQuery("readPermission", "clipboard-read")); - QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("readPermission")), initialPermission); - evaluateJavaScriptSync(&page, clipboardPermissionQuery("writePermission", "clipboard-write")); - QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("writePermission")), initialPermission); - - auto triggerRequest = [&page](QString variableName, QString apiCall) - { - auto js = QString("var %1; navigator.clipboard.%2.then((v) => { %1 = 'granted' }, (v) => { %1 = " - "'denied' });") - .arg(variableName) - .arg(apiCall); - evaluateJavaScriptSync(&page, js); - }; - - // permission is not 'remembered' from api standpoint, hence is not suppressed on explicit call - // from JS - triggerRequest("readState", "readText()"); - QTRY_COMPARE(evaluateJavaScriptSync(&page, "readState"), finalPermission); - triggerRequest("writeState", "writeText('foo')"); - QTRY_COMPARE(evaluateJavaScriptSync(&page, "writeState"), finalPermission); - QCOMPARE(permissionRequestCount, canAccessClipboard ? 0 : 2); - QVERIFY(!errorState); -} - void tst_QWebEnginePage::contentsSize() { m_view->resize(800, 600); @@ -4160,62 +3837,6 @@ void tst_QWebEnginePage::contentsSize() QCOMPARE(m_page->contentsSize().height(), 1216); } -void tst_QWebEnginePage::localFontAccessPermission_data() -{ - QTest::addColumn<QWebEnginePermission::State>("policy"); - QTest::addColumn<bool>("ignore"); - QTest::addColumn<bool>("shouldBeEmpty"); - - QTest::newRow("ignore") << QWebEnginePermission::State::Denied << true << true; - QTest::newRow("setDeny") << QWebEnginePermission::State::Denied << false << true; - QTest::newRow("setGrant") << QWebEnginePermission::State::Granted << false << false; -} - -void tst_QWebEnginePage::localFontAccessPermission() { - QFETCH(QWebEnginePermission::State, policy); - QFETCH(bool, ignore); - QFETCH(bool, shouldBeEmpty); - - QWebEngineView view; - QWebEnginePage page(&view); - page.profile()->setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); - view.setPage(&page); - - connect(&page, &QWebEnginePage::permissionRequested, &page, [&] (QWebEnginePermission permission) { - if (permission.permissionType() != QWebEnginePermission::PermissionType::LocalFontsAccess) - return; - - if (!ignore) { - if (policy == QWebEnginePermission::State::Granted) - permission.grant(); - else - permission.deny(); - } - }); - - QSignalSpy spy(&page, &QWebEnginePage::loadFinished); - page.load(QUrl("qrc:///resources/fontaccess.html")); - QTRY_COMPARE(spy.size(), 1); - - // Font access is only enabled for visible WebContents. - view.show(); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - if (evaluateJavaScriptSync(&page, QStringLiteral("!window.queryLocalFonts")).toBool()) - QSKIP("Local fonts access is not supported."); - - // Access to the API requires recent user interaction - QTest::keyPress(view.focusProxy(), Qt::Key_Space); - QTRY_COMPARE(evaluateJavaScriptSync(&page, QStringLiteral("activated")).toBool(), true); - - if (ignore) { - QTRY_COMPARE_NE_WITH_TIMEOUT(evaluateJavaScriptSync(&page, QStringLiteral("done")).toBool(), true, 1000); - } else { - QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(&page, QStringLiteral("done")).toBool() == true, 1000); - QVERIFY((evaluateJavaScriptSync(&page, QStringLiteral("fonts.length")).toInt() == 0) == shouldBeEmpty); - } -} - void tst_QWebEnginePage::setLifecycleState() { qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState"); diff --git a/tests/auto/widgets/qwebenginepermission/CMakeLists.txt b/tests/auto/widgets/qwebenginepermission/CMakeLists.txt new file mode 100644 index 000000000..d6092c8c5 --- /dev/null +++ b/tests/auto/widgets/qwebenginepermission/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../util/util.cmake) + +qt_internal_add_test(tst_qwebenginepermission + SOURCES + tst_qwebenginepermission.cpp + LIBRARIES + Qt::WebEngineCore + Qt::WebEngineWidgets + Qt::WebEngineCorePrivate + Qt::CorePrivate + Test::Util +) + +set(tst_qwebenginepermission_resource_files + "resources/index.html" + "resources/iframe.html" + "resources/qt144.png" +) + +qt_internal_add_resource(tst_qwebenginepermission "tst_qwebenginepermission" + PREFIX + "/" + FILES + ${tst_qwebenginepermission_resource_files} +) diff --git a/tests/auto/widgets/qwebenginepermission/resources/iframe.html b/tests/auto/widgets/qwebenginepermission/resources/iframe.html new file mode 100644 index 000000000..483800cd6 --- /dev/null +++ b/tests/auto/widgets/qwebenginepermission/resources/iframe.html @@ -0,0 +1,5 @@ +<html> +<body> + <iframe name="frame" allow="camera; microphone; display-capture; geolocation; local-fonts; clipboard-read; clipboard-write;" src="qrc:///resources/index.html" width="400" height="400"></iframe> +</body> +</html> diff --git a/tests/auto/widgets/qwebenginepermission/resources/index.html b/tests/auto/widgets/qwebenginepermission/resources/index.html new file mode 100644 index 000000000..6150a2bd9 --- /dev/null +++ b/tests/auto/widgets/qwebenginepermission/resources/index.html @@ -0,0 +1,14 @@ +<html> +<body onclick='onClick()'> +<script> +var triggerFunc = undefined; +var testFunc = undefined; +var done = false; +var skipReason = undefined; +var data = undefined; +function onClick() { + triggerFunc(); +} +</script> +</body> +</html> diff --git a/tests/auto/widgets/qwebenginepermission/resources/qt144.png b/tests/auto/widgets/qwebenginepermission/resources/qt144.png Binary files differnew file mode 100644 index 000000000..050b1e066 --- /dev/null +++ b/tests/auto/widgets/qwebenginepermission/resources/qt144.png diff --git a/tests/auto/widgets/qwebenginepermission/tst_qwebenginepermission.cpp b/tests/auto/widgets/qwebenginepermission/tst_qwebenginepermission.cpp new file mode 100644 index 000000000..bfc70557e --- /dev/null +++ b/tests/auto/widgets/qwebenginepermission/tst_qwebenginepermission.cpp @@ -0,0 +1,727 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <util.h> + +#include <QtTest/QtTest> +#include <QDir> +#include <QStringLiteral> +#include <QWebEngineDesktopMediaRequest> +#include <QWebEngineFrame> +#include <QWebEnginePage> +#include <QWebEnginePermission> +#include <QWebEngineProfile> +#include <QWebEngineSettings> +#include <QWebEngineView> + +using namespace Qt::StringLiterals; + +class tst_QWebEnginePermission : public QObject +{ + Q_OBJECT + +public: + tst_QWebEnginePermission(); + ~tst_QWebEnginePermission(); + +public Q_SLOTS: + void init(); + void cleanup(); + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void triggerFromJavascript_data(); + void triggerFromJavascript(); + void preGrant_data(); + void preGrant(); + void iframe_data(); + void iframe(); + + void permissionPersistence_data(); + void permissionPersistence(); + + void queryPermission_data(); + void queryPermission(); + void listPermissions(); + + void clipboardReadWritePermissionInitialState_data(); + void clipboardReadWritePermissionInitialState(); + void clipboardReadWritePermission_data(); + void clipboardReadWritePermission(); + +private: + std::unique_ptr<QWebEngineProfile> m_profile; + QString m_profileName; +}; + +tst_QWebEnginePermission::tst_QWebEnginePermission() + : m_profileName("tst_QWebEnginePermission") +{ +} + +tst_QWebEnginePermission::~tst_QWebEnginePermission() +{ +} + +void tst_QWebEnginePermission::initTestCase() +{ +} + +void tst_QWebEnginePermission::cleanupTestCase() +{ +} + +void tst_QWebEnginePermission::init() +{ + m_profile.reset(new QWebEngineProfile("tst_QWebEnginePermission")); +} + +void tst_QWebEnginePermission::cleanup() +{ + if (m_profile && m_profile->persistentPermissionsPolicy() + == QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk) { + QDir dir(m_profile->persistentStoragePath()); + dir.remove("permissions.json"); + + // Set a persistent permission to force the creation of a permission.json + // in test cases where it wouldn't be created otherwise + m_profile->queryPermission(QUrl("https://google.com"), + QWebEnginePermission::PermissionType::Notifications).grant(); + + // This will trigger the writing of permissions to disk + m_profile.reset(); + + // Wait for the new permissions.json to be written to disk before deleting + QTRY_VERIFY_WITH_TIMEOUT(dir.exists("permissions.json"), 5000); + dir.remove("permissions.json"); + } else { + m_profile.reset(); + } +} + +static QString MediaAudioCapture_trigger = + "navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then(s => { data = s; done = true; })" + ".catch(err => { skipReason = err.message; done = true; });"_L1; +static QString MediaAudioCapture_check = + "return data != undefined;"_L1; + +static QString MediaVideoCapture_trigger = + "navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then(s => { data = s; done = true; })" + ".catch(err => { skipReason = err.message; done = true; });"_L1; +static QString MediaVideoCapture_check = + "return data != undefined;"_L1; + +static QString MediaAudioVideoCapture_trigger = + "navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(s => { data = s; done = true; })" + ".catch(err => { skipReason = err.message; done = true; });"_L1; +static QString MediaAudioVideoCapture_check = + "return data != undefined;"_L1; + +static QString DesktopVideoCapture_trigger = + "navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }).then(s => { data = s; done = true; })" + ".catch(err => { skipReason = err.message; done = true; });"_L1; +static QString DesktopVideoCapture_check = + "return data != undefined;"_L1; + +static QString DesktopAudioVideoCapture_trigger = + "navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }).then(s => { data = s; done = true; })" + ".catch(err => { skipReason = err.message; done = true; });"_L1; +static QString DesktopAudioVideoCapture_check = + "return data != undefined;"_L1; + +static QString MouseLock_trigger = + "document.documentElement.requestPointerLock().then(() => { data = document.pointerLockElement(); done = true; }).catch(() => { done = true; });"_L1; +static QString MouseLock_check = + "var ret = (data != undefined); document.exitPointerLock(); return ret;"_L1; + +static QString Notifications_trigger = + "Notification.requestPermission().then(p => { data = p; done = true; }).catch(() => { done = true; });"_L1; +static QString Notifications_check = + "return data != undefined && Notification.permission === 'granted';"_L1; + +static QString Geolocation_trigger = + "success = function(p) { data = p; done = true; };" + "failure = function(err) { if (err.code === 2) skipReason = 'Positioning is unavailable'; done = true; };" + "navigator.geolocation.getCurrentPosition(success, failure);"_L1; +static QString Geolocation_check = + "return data != undefined;"_L1; + +static QString ClipboardReadWrite_trigger = + "navigator.clipboard.readText().then(c => { data = c; done = true; }).catch(() => { done = true; });"_L1; +static QString ClipboardReadWrite_check = + "return data != undefined;"_L1; + +static QString LocalFontsAccess_trigger = + "if (!window.queryLocalFonts) { skipReason = 'Local fonts access is not supported on this system'; done = true; }" + "else { window.queryLocalFonts().then(f => { data = f; done = true; }); };"_L1; +static QString LocalFontsAccess_check = + "return data.length != 0;"_L1; + +static void commonTestData() +{ + QTest::addColumn<QWebEnginePermission::PermissionType>("permissionType"); + QTest::addColumn<QString>("triggerFunction"); + QTest::addColumn<QString>("testFunction"); + QTest::addColumn<QWebEngineProfile::PersistentPermissionsPolicy>("policy"); + +#define QWebEnginePermissionTestCase(pt) \ + QTest::newRow(#pt "_AskEveryTime") \ + << QWebEnginePermission::PermissionType::pt \ + << pt ## _trigger << pt ## _check \ + << QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime; \ + QTest::newRow(#pt "_StoreInMemory") \ + << QWebEnginePermission::PermissionType::pt \ + << pt ## _trigger << pt ## _check \ + << QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory; \ + QTest::newRow(#pt "_StoreOnDisk") \ + << QWebEnginePermission::PermissionType::pt \ + << pt ## _trigger << pt ## _check \ + << QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk; + + QWebEnginePermissionTestCase(MediaAudioCapture); + + // Video capture tests don't work with offscreen + if (QGuiApplication::platformName() != QLatin1String("offscreen")) { + QWebEnginePermissionTestCase(MediaVideoCapture); + QWebEnginePermissionTestCase(MediaAudioVideoCapture); + QWebEnginePermissionTestCase(DesktopVideoCapture); + QWebEnginePermissionTestCase(DesktopAudioVideoCapture); + } + // QWebEnginePermissionTestCase(MouseLock); // currently untestable + QWebEnginePermissionTestCase(Notifications); +#ifndef Q_OS_MACOS + QWebEnginePermissionTestCase(Geolocation); +#endif + QWebEnginePermissionTestCase(ClipboardReadWrite); + QWebEnginePermissionTestCase(LocalFontsAccess); + +#undef QWebEnginePermissionTestCase +} + +void tst_QWebEnginePermission::triggerFromJavascript_data() +{ + commonTestData(); +} + +void tst_QWebEnginePermission::triggerFromJavascript() +{ + QFETCH(QWebEnginePermission::PermissionType, permissionType); + QFETCH(QString, triggerFunction); + QFETCH(QString, testFunction); + QFETCH(QWebEngineProfile::PersistentPermissionsPolicy, policy); + + QWebEngineView view; + QWebEnginePage page(m_profile.get(), &view); + m_profile->setPersistentPermissionsPolicy(policy); + view.setPage(&page); + + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, true); + connect(&page, &QWebEnginePage::desktopMediaRequested, &page, [&](const QWebEngineDesktopMediaRequest &request) { + request.selectScreen(request.screensModel()->index(0)); + }); + + bool grant = true; + QWebEnginePermission permission; + connect(&page, &QWebEnginePage::permissionRequested, &page, [&](QWebEnginePermission p) { + QCOMPARE(p.permissionType(), permissionType); + grant ? p.grant() : p.deny(); + permission = p; + }); + + QSignalSpy spy(&page, &QWebEnginePage::loadFinished); + page.load(QUrl("qrc:///resources/index.html")); + QTRY_COMPARE(spy.size(), 1); + + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + evaluateJavaScriptSync(&page, "triggerFunc = function() {"_L1 + triggerFunction + "}"_L1); + evaluateJavaScriptSync(&page, "testFunc = function() {"_L1 + testFunction + "done = true;" + "}"_L1); + + // Access to some pf the APIs requires recent user interaction + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint{100, 100}); + + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(&page, QStringLiteral("done")).toBool(), 5000); + if (evaluateJavaScriptSync(&page, QStringLiteral("skipReason")).toBool()) { + // Catch expected failures and skip test + QSKIP(("Skipping test. Reason: " + evaluateJavaScriptSync(&page, QStringLiteral("skipReason")).toString()).toStdString().c_str()); + } + qWarning() << evaluateJavaScriptSync(&page, QStringLiteral("data")); + + QVERIFY(evaluateJavaScriptSync(&page, QStringLiteral("testFunc()")).toBool()); + QCOMPARE(permission.state(), QWebEnginePermission::State::Granted); + + // Now reset the permission, and try denying it + permission.reset(); + QCOMPARE(permission.state(), QWebEnginePermission::State::Ask); + evaluateJavaScriptSync(&page, "done = false; data = undefined"_L1); + grant = false; + + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint{100, 100}); + + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(&page, QStringLiteral("done")).toBool(), 5000); + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("testFunc()")).toBool(), false); + QCOMPARE(permission.state(), QWebEnginePermission::State::Denied); +} + +void tst_QWebEnginePermission::preGrant_data() +{ + commonTestData(); +} + +void tst_QWebEnginePermission::preGrant() +{ + QFETCH(QWebEnginePermission::PermissionType, permissionType); + QFETCH(QString, triggerFunction); + QFETCH(QString, testFunction); + QFETCH(QWebEngineProfile::PersistentPermissionsPolicy, policy); + + QWebEngineView view; + QWebEnginePage page(m_profile.get(), &view); + m_profile->setPersistentPermissionsPolicy(policy); + view.setPage(&page); + + QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); + page.load(QUrl("qrc:///resources/index.html")); + QTRY_COMPARE(loadSpy.size(), 1); + + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, true); + connect(&page, &QWebEnginePage::desktopMediaRequested, &page, [&](const QWebEngineDesktopMediaRequest &request) { + request.selectScreen(request.screensModel()->index(0)); + }); + + QWebEnginePermission permission = m_profile->queryPermission(page.url(), permissionType); + QVERIFY(permission.state() == QWebEnginePermission::State::Ask); + permission.grant(); + + evaluateJavaScriptSync(&page, "triggerFunc = function() {"_L1 + triggerFunction + "}"_L1); + evaluateJavaScriptSync(&page, "testFunc = function() {"_L1 + testFunction + "done = true;" + "}"_L1); + + QSignalSpy spy(&page, &QWebEnginePage::permissionRequested); + + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint{100, 100}); + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(&page, QStringLiteral("done")).toBool(), 5000); + if (evaluateJavaScriptSync(&page, QStringLiteral("skipReason")).toBool()) { + // No media devices, or no geolocation plugin + QSKIP(("Skipping test. Reason: " + evaluateJavaScriptSync(&page, QStringLiteral("skipReason")).toString()).toStdString().c_str()); + } + QVERIFY(evaluateJavaScriptSync(&page, QStringLiteral("testFunc()")).toBool()); + + // The permissionRequested signal must NOT fire + QCOMPARE(spy.size(), 0); +} + +void tst_QWebEnginePermission::iframe_data() +{ + commonTestData(); +} + +void tst_QWebEnginePermission::iframe() +{ + QFETCH(QWebEnginePermission::PermissionType, permissionType); + QFETCH(QString, triggerFunction); + QFETCH(QString, testFunction); + QFETCH(QWebEngineProfile::PersistentPermissionsPolicy, policy); + + QWebEngineView view; + QWebEnginePage page(m_profile.get(), &view); + m_profile->setPersistentPermissionsPolicy(policy); + view.setPage(&page); + + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, true); + connect(&page, &QWebEnginePage::desktopMediaRequested, &page, [&](const QWebEngineDesktopMediaRequest &request) { + request.selectScreen(request.screensModel()->index(0)); + }); + + bool grant = true; + QWebEnginePermission permission; + connect(&page, &QWebEnginePage::permissionRequested, &page, [&](QWebEnginePermission p) { + grant ? p.grant() : p.deny(); + permission = p; + }); + + QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); + page.load(QUrl("qrc:///resources/iframe.html")); + QTRY_COMPARE(loadSpy.size(), 1); + + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + auto maybeFrame = page.findFrameByName("frame"); + QVERIFY(maybeFrame); + QWebEngineFrame &frame = maybeFrame.value(); + + evaluateJavaScriptSync(&frame, "triggerFunc = function() {"_L1 + triggerFunction + "}"_L1); + evaluateJavaScriptSync(&frame, "testFunc = function() {"_L1 + testFunction + "done = true;" + "}"_L1); + + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint{100, 100}); + + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(&frame, QStringLiteral("done")).toBool(), 10000); + if (evaluateJavaScriptSync(&frame, QStringLiteral("skipReason")).toBool()) { + // Catch expected failures and skip test + QSKIP(("Skipping test. Reason: " + evaluateJavaScriptSync(&frame, QStringLiteral("skipReason")).toString()).toStdString().c_str()); + } + + QVERIFY(evaluateJavaScriptSync(&frame, QStringLiteral("testFunc()")).toBool()); + QCOMPARE(permission.state(), QWebEnginePermission::State::Granted); + + // Now reset the permission, and try denying it + permission.reset(); + QCOMPARE(permission.state(), QWebEnginePermission::State::Ask); + evaluateJavaScriptSync(&frame, "done = false; data = undefined"_L1); + grant = false; + + // Only test non-persistent permissions past this point + if (QWebEnginePermission::isPersistent(permissionType) + && policy != QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime) + return; + + // Perform a cross-origin navigation and then go back to check if the permission has been cleared + // We don't need a valid URL to trigger the cross-origin logic. + evaluateJavaScriptSync(&page, "document.getElementsByName('frame')[0].src = 'http://bad-url.bad-url'"_L1); + QTRY_VERIFY_WITH_TIMEOUT(frame.url() != QUrl("qrc:///resources/index.html"_L1), 10000); + evaluateJavaScriptSync(&page, "document.getElementsByName('frame')[0].src = 'qrc:///resources/index.html'"_L1); + QTRY_VERIFY_WITH_TIMEOUT(frame.url() == QUrl("qrc:///resources/index.html"_L1), 10000); + + QCOMPARE(permission.state(), QWebEnginePermission::State::Ask); +} + +void tst_QWebEnginePermission::permissionPersistence_data() +{ + QTest::addColumn<QWebEngineProfile::PersistentPermissionsPolicy>("policy"); + QTest::addColumn<bool>("granted"); + + QTest::newRow("noPersistenceDeny") << QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime << false; + QTest::newRow("noPersistenceGrant") << QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime << true; + QTest::newRow("memoryPersistenceDeny") << QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory << false; + QTest::newRow("memoryPersistenceGrant") << QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory << true; + QTest::newRow("diskPersistenceDeny") << QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk << false; + QTest::newRow("diskPersistenceGrant") << QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk << true; +} + +void tst_QWebEnginePermission::permissionPersistence() +{ + QFETCH(QWebEngineProfile::PersistentPermissionsPolicy, policy); + QFETCH(bool, granted); + + m_profile->setPersistentPermissionsPolicy(policy); + + std::unique_ptr<QWebEnginePage> page(new QWebEnginePage(m_profile.get())); + std::unique_ptr<QSignalSpy> loadSpy(new QSignalSpy(page.get(), &QWebEnginePage::loadFinished)); + QDir storageDir = QDir(m_profile->persistentStoragePath()); + + page->load(QUrl("qrc:///resources/index.html"_L1)); + QTRY_COMPARE(loadSpy->size(), 1); + + QVariant variant = granted ? "granted" : "denied"; + QVariant defaultVariant = "default"; + + QWebEnginePermission permissionObject = m_profile->queryPermission( + QUrl("qrc:///resources/index.html"_L1), QWebEnginePermission::PermissionType::Notifications); + if (granted) + permissionObject.grant(); + else + permissionObject.deny(); + QCOMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), variant); + + page.reset(); + m_profile.reset(); + loadSpy.reset(); + + bool expectSame = false; + if (policy == QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk) { + expectSame = true; + + // File is written asynchronously, wait for it to be created + QTRY_COMPARE(storageDir.exists("permissions.json"), true); + } + + m_profile.reset(new QWebEngineProfile(m_profileName)); + m_profile->setPersistentPermissionsPolicy(policy); + + page.reset(new QWebEnginePage(m_profile.get())); + loadSpy.reset(new QSignalSpy(page.get(), &QWebEnginePage::loadFinished)); + page->load(QUrl("qrc:///resources/index.html"_L1)); + QTRY_COMPARE(loadSpy->size(), 1); + QTRY_COMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), + expectSame ? variant : defaultVariant); + + // Re-acquire the permission, since deleting the Profile makes it invalid + permissionObject = m_profile->queryPermission(QUrl("qrc:///resources/index.html"_L1), QWebEnginePermission::PermissionType::Notifications); + permissionObject.reset(); + QCOMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), defaultVariant); +} + +void tst_QWebEnginePermission::queryPermission_data() +{ + QTest::addColumn<QWebEnginePermission::PermissionType>("permissionType"); + QTest::addColumn<QUrl>("url"); + QTest::addColumn<bool>("expectedValid"); + + QTest::newRow("badUrl") + << QWebEnginePermission::PermissionType::Notifications << QUrl("//:bad-url"_L1) << false; + QTest::newRow("badFeature") + << QWebEnginePermission::PermissionType::Unsupported << QUrl("qrc:/resources/index.html"_L1) << false; + QTest::newRow("transientFeature") + << QWebEnginePermission::PermissionType::MouseLock << QUrl("qrc:/resources/index.html"_L1) << true; + QTest::newRow("good") + << QWebEnginePermission::PermissionType::Notifications << QUrl("qrc:/resources/index.html"_L1) << true; +} + +void tst_QWebEnginePermission::queryPermission() +{ + QFETCH(QWebEnginePermission::PermissionType, permissionType); + QFETCH(QUrl, url); + QFETCH(bool, expectedValid); + + // In-memory is the default for otr profiles + m_profile.reset(new QWebEngineProfile()); + QVERIFY(m_profile->persistentPermissionsPolicy() == QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory); + + QWebEnginePermission permission = m_profile->queryPermission(url, permissionType); + bool valid = permission.isValid(); + QCOMPARE(valid, expectedValid); + if (!valid) + QCOMPARE(permission.state(), QWebEnginePermission::State::Invalid); + + // Verify that we can grant a valid permission, and we can't grant an invalid one... + permission.grant(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Granted : QWebEnginePermission::State::Invalid); + + // ...and that doing so twice doesn't mess up the state... + permission.grant(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Granted : QWebEnginePermission::State::Invalid); + + // ...and that the same thing applies to denying them... + permission.deny(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Denied : QWebEnginePermission::State::Invalid); + permission.deny(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Denied : QWebEnginePermission::State::Invalid); + + // ...and that resetting works + permission.reset(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Ask : QWebEnginePermission::State::Invalid); + permission.reset(); + QCOMPARE(permission.state(), valid ? QWebEnginePermission::State::Ask : QWebEnginePermission::State::Invalid); +} + +void tst_QWebEnginePermission::listPermissions() +{ + // In-memory is the default for otr profiles + m_profile.reset(new QWebEngineProfile()); + QVERIFY(m_profile->persistentPermissionsPolicy() == QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory); + + QUrl commonUrl = QUrl(QStringLiteral("https://www.bing.com/maps")); + QWebEnginePermission::PermissionType commonType = QWebEnginePermission::PermissionType::Notifications; + + // First, set several permissions at once + m_profile->queryPermission(commonUrl, QWebEnginePermission::PermissionType::Geolocation).deny(); + m_profile->queryPermission(commonUrl, QWebEnginePermission::PermissionType::Unsupported).grant(); // Invalid + m_profile->queryPermission(commonUrl, commonType).grant(); + m_profile->queryPermission(QUrl(QStringLiteral("https://www.google.com/translate")), commonType).grant(); + + QList<QWebEnginePermission> permissionsListAll = m_profile->listAllPermissions(); + QList<QWebEnginePermission> permissionsListUrl = m_profile->listPermissionsForOrigin(commonUrl); + QList<QWebEnginePermission> permissionsListFeature = m_profile->listPermissionsForPermissionType(commonType); + + // Order of returned permissions is not guaranteed, so we must iterate until we find the one we need + auto findInList = [](QList<QWebEnginePermission> list, const QUrl &url, + QWebEnginePermission::PermissionType permissionType, QWebEnginePermission::State state) + { + bool found = false; + for (auto &permission : list) { + if (permission.origin().adjusted(QUrl::RemovePath) == url.adjusted(QUrl::RemovePath) + && permission.permissionType() == permissionType && permission.state() == state) { + found = true; + break; + } + } + return found; + }; + + // Check full list + QVERIFY(permissionsListAll.size() == 3); + QVERIFY(findInList(permissionsListAll, commonUrl, QWebEnginePermission::PermissionType::Geolocation, QWebEnginePermission::State::Denied)); + QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); + QVERIFY(findInList(permissionsListAll, QUrl(QStringLiteral("https://www.google.com")), commonType, QWebEnginePermission::State::Granted)); + + // Check list filtered by URL + QVERIFY(permissionsListUrl.size() == 2); + QVERIFY(findInList(permissionsListUrl, commonUrl, QWebEnginePermission::PermissionType::Geolocation, QWebEnginePermission::State::Denied)); + QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); + + // Check list filtered by feature + QVERIFY(permissionsListFeature.size() == 2); + QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); + QVERIFY(findInList(permissionsListAll, QUrl(QStringLiteral("https://www.google.com")), commonType, QWebEnginePermission::State::Granted)); +} + +static QString clipboardPermissionQuery(QString variableName, QString permissionName) +{ + return QString("var %1; navigator.permissions.query({ name:'%2' }).then((p) => { %1 = p.state; " + "});") + .arg(variableName) + .arg(permissionName); +} + +void tst_QWebEnginePermission::clipboardReadWritePermissionInitialState_data() +{ + QTest::addColumn<bool>("canAccessClipboard"); + QTest::addColumn<bool>("canPaste"); + QTest::addColumn<QString>("readPermission"); + QTest::addColumn<QString>("writePermission"); + QTest::newRow("access and paste should grant both") << true << true << "granted" << "granted"; + QTest::newRow("paste only should prompt for both") << false << true << "prompt" << "prompt"; + QTest::newRow("access only should grant for write only") + << true << false << "prompt" << "granted"; + QTest::newRow("no access or paste should prompt for both") + << false << false << "prompt" << "prompt"; +} + +void tst_QWebEnginePermission::clipboardReadWritePermissionInitialState() +{ + QFETCH(bool, canAccessClipboard); + QFETCH(bool, canPaste); + QFETCH(QString, readPermission); + QFETCH(QString, writePermission); + + m_profile->setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); + QWebEngineView view(m_profile.get()); + QWebEnginePage &page = *view.page(); + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, + canAccessClipboard); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanPaste, canPaste); + + QSignalSpy spy(&page, &QWebEnginePage::loadFinished); + QUrl baseUrl("https://www.example.com/somepage.html"); + page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); + QTRY_COMPARE(spy.size(), 1); + + evaluateJavaScriptSync(&page, clipboardPermissionQuery("readPermission", "clipboard-read")); + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("readPermission")), readPermission); + evaluateJavaScriptSync(&page, clipboardPermissionQuery("writePermission", "clipboard-write")); + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("writePermission")), writePermission); +} + +void tst_QWebEnginePermission::clipboardReadWritePermission_data() +{ + QTest::addColumn<bool>("canAccessClipboard"); + QTest::addColumn<QWebEnginePermission::State>("initialPolicy"); + QTest::addColumn<QString>("initialPermission"); + QTest::addColumn<QString>("finalPermission"); + + QTest::newRow("noAccessGrant") + << false << QWebEnginePermission::State::Granted << "granted" << "granted"; + QTest::newRow("noAccessDeny") + << false << QWebEnginePermission::State::Denied << "denied" << "denied"; + QTest::newRow("noAccessAsk") + << false << QWebEnginePermission::State::Ask << "prompt" << "granted"; + + // All policies are ignored and overridden by setting JsCanAccessClipboard and JsCanPaste to + // true + QTest::newRow("accessGrant") + << true << QWebEnginePermission::State::Granted << "granted" << "granted"; + QTest::newRow("accessDeny") + << true << QWebEnginePermission::State::Denied << "granted" << "granted"; + QTest::newRow("accessAsk") + << true << QWebEnginePermission::State::Ask << "granted" << "granted"; +} + +void tst_QWebEnginePermission::clipboardReadWritePermission() +{ + QFETCH(bool, canAccessClipboard); + QFETCH(QWebEnginePermission::State, initialPolicy); + QFETCH(QString, initialPermission); + QFETCH(QString, finalPermission); + + m_profile->setPersistentPermissionsPolicy(QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime); + QWebEngineView view(m_profile.get()); + QWebEnginePage &page = *view.page(); + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, + canAccessClipboard); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanPaste, true); + + QUrl baseUrl("https://www.example.com/somepage.html"); + + int permissionRequestCount = 0; + bool errorState = false; + + // This should only fire in the noAccessAsk case. The other NoAccess cases will remember the initial permission, + // and the Access cases will auto-grant because JavascriptCanPaste and JavascriptCanAccessClipboard are set. + connect(&page, &QWebEnginePage::permissionRequested, &page, + [&](QWebEnginePermission permission) { + if (permission.permissionType() != QWebEnginePermission::PermissionType::ClipboardReadWrite) + return; + if (permission.origin() != baseUrl.url(QUrl::RemoveFilename)) { + qWarning() << "Unexpected case. Can't proceed." << permission.origin(); + errorState = true; + return; + } + permissionRequestCount++; + // Deliberately set to the opposite state; we want to force a fail when this triggers + if (initialPolicy == QWebEnginePermission::State::Granted) + permission.deny(); + else + permission.grant(); + }); + + QWebEnginePermission permissionObject = m_profile->queryPermission(baseUrl, QWebEnginePermission::PermissionType::ClipboardReadWrite); + switch (initialPolicy) { + case QWebEnginePermission::State::Granted: + permissionObject.grant(); + break; + case QWebEnginePermission::State::Denied: + permissionObject.deny(); + break; + case QWebEnginePermission::State::Ask: + permissionObject.reset(); + break; + case QWebEnginePermission::State::Invalid: + break; + } + + QSignalSpy spy(&page, &QWebEnginePage::loadFinished); + page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); + QTRY_COMPARE(spy.size(), 1); + + evaluateJavaScriptSync(&page, clipboardPermissionQuery("readPermission", "clipboard-read")); + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("readPermission")), initialPermission); + evaluateJavaScriptSync(&page, clipboardPermissionQuery("writePermission", "clipboard-write")); + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("writePermission")), initialPermission); + + auto triggerRequest = [&page](QString variableName, QString apiCall) + { + auto js = QString("var %1; navigator.clipboard.%2.then((v) => { %1 = 'granted' }, (v) => { %1 = " + "'denied' });") + .arg(variableName) + .arg(apiCall); + evaluateJavaScriptSync(&page, js); + }; + + // Permission is remembered, and shouldn't trigger a new request when called from JS + triggerRequest("readState", "readText()"); + QTRY_COMPARE(evaluateJavaScriptSync(&page, "readState"), finalPermission); + triggerRequest("writeState", "writeText('foo')"); + QTRY_COMPARE(evaluateJavaScriptSync(&page, "writeState"), finalPermission); + + if (initialPermission != finalPermission) { + QCOMPARE(permissionRequestCount, 1); + } else { + QCOMPARE(permissionRequestCount, 0); + } + + QVERIFY(!errorState); +} + +QTEST_MAIN(tst_QWebEnginePermission) +#include "tst_qwebenginepermission.moc" diff --git a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp index eefb41863..ff3eeae65 100644 --- a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp +++ b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp @@ -58,11 +58,6 @@ private Q_SLOTS: void changePersistentCookiesPolicy(); void initiator(); void badDeleteOrder(); - void permissionPersistence_data(); - void permissionPersistence(); - void queryPermission_data(); - void queryPermission(); - void listPermissions(); void qtbug_71895(); // this should be the last test }; @@ -1034,192 +1029,6 @@ void tst_QWebEngineProfile::badDeleteOrder() delete view; } -void tst_QWebEngineProfile::permissionPersistence_data() -{ - QTest::addColumn<QWebEngineProfile::PersistentPermissionsPolicy>("policy"); - QTest::addColumn<bool>("granted"); - - QTest::newRow("noPersistenceNotificationsNoGrant") << QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime << false; - QTest::newRow("noPersistenceNotificationsGrant") << QWebEngineProfile::PersistentPermissionsPolicy::AskEveryTime << true; - QTest::newRow("memoryPersistenceNotificationsNoGrant") << QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory << false; - QTest::newRow("diskPersistenceNotificationsGrant") << QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk << true; -} - -void tst_QWebEngineProfile::permissionPersistence() -{ - QFETCH(QWebEngineProfile::PersistentPermissionsPolicy, policy); - QFETCH(bool, granted); - - TestServer server; - QVERIFY(server.start()); - - std::unique_ptr<QWebEngineProfile> profile(new QWebEngineProfile("tst_persistence")); - profile->setPersistentPermissionsPolicy(policy); - - std::unique_ptr<QWebEnginePage> page(new QWebEnginePage(profile.get())); - std::unique_ptr<QSignalSpy> loadSpy(new QSignalSpy(page.get(), &QWebEnginePage::loadFinished)); - QDir storageDir = QDir(profile->persistentStoragePath()); - - // Delete permissions file if it somehow survived on disk - storageDir.remove("permissions.json"); - - page->load(server.url("/hedgehog.html")); - QTRY_COMPARE(loadSpy->size(), 1); - - QVariant variant = granted ? "granted" : "denied"; - QVariant defaultVariant = "default"; - - QWebEnginePermission permissionObject = profile->queryPermission(server.url("/hedgehog.html"), QWebEnginePermission::PermissionType::Notifications); - if (granted) - permissionObject.grant(); - else - permissionObject.deny(); - QCOMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), variant); - - page.reset(); - profile.reset(); - loadSpy.reset(); - - bool expectSame = false; - if (policy == QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk) { - expectSame = true; - - // File is written asynchronously, wait for it to be created - QTRY_COMPARE(storageDir.exists("permissions.json"), true); - } - - profile.reset(new QWebEngineProfile("tst_persistence")); - profile->setPersistentPermissionsPolicy(policy); - - page.reset(new QWebEnginePage(profile.get())); - loadSpy.reset(new QSignalSpy(page.get(), &QWebEnginePage::loadFinished)); - page->load(server.url("/hedgehog.html")); - QTRY_COMPARE(loadSpy->size(), 1); - QTRY_COMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), - expectSame ? variant : defaultVariant); - - // Re-acquire the permission, since deleting the Profile makes it invalid - permissionObject = profile->queryPermission(server.url("/hedgehog.html"), QWebEnginePermission::PermissionType::Notifications); - permissionObject.reset(); - QCOMPARE(evaluateJavaScriptSync(page.get(), "Notification.permission"), defaultVariant); - - page.reset(); - profile.reset(); - loadSpy.reset(); - - if (policy == QWebEngineProfile::PersistentPermissionsPolicy::StoreOnDisk) { - // Wait for file to be written to before deleting - QTest::qWait(1000); - storageDir.remove("permissions.json"); - } - - QTRY_VERIFY(server.stop()); -} - -void tst_QWebEngineProfile::queryPermission_data() -{ - QTest::addColumn<QWebEnginePermission::PermissionType>("permissionType"); - QTest::addColumn<QUrl>("url"); - QTest::addColumn<bool>("expectedValid"); - - QTest::newRow("badUrl") - << QWebEnginePermission::PermissionType::Notifications << QUrl(QStringLiteral("//:bad-url")) << false; - QTest::newRow("badFeature") - << QWebEnginePermission::PermissionType::Unsupported << QUrl(QStringLiteral("qrc:/resources/permission.html")) << false; - QTest::newRow("transientFeature") - << QWebEnginePermission::PermissionType::MouseLock << QUrl(QStringLiteral("qrc:/resources/permission.html")) << true; - QTest::newRow("good") - << QWebEnginePermission::PermissionType::Notifications << QUrl(QStringLiteral("qrc:/resources/permission.html")) << true; -} - -void tst_QWebEngineProfile::queryPermission() -{ - QFETCH(QWebEnginePermission::PermissionType, permissionType); - QFETCH(QUrl, url); - QFETCH(bool, expectedValid); - - QWebEngineProfile profile; - // In-memory is the default for otr profiles - QVERIFY(profile.persistentPermissionsPolicy() == QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory); - - QWebEnginePermission permission = profile.queryPermission(url, permissionType); - bool valid = permission.isValid(); - QVERIFY(valid == expectedValid); - if (!valid) - QVERIFY(permission.state() == QWebEnginePermission::State::Invalid); - - // Verify that we can grant a valid permission, and we can't grant an invalid one... - permission.grant(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Granted : QWebEnginePermission::State::Invalid)); - - // ...and that doing so twice doesn't mess up the state... - permission.grant(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Granted : QWebEnginePermission::State::Invalid)); - - // ...and that the same thing applies to denying them... - permission.deny(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Denied : QWebEnginePermission::State::Invalid)); - permission.deny(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Denied : QWebEnginePermission::State::Invalid)); - - // ...and that resetting works - permission.reset(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Ask : QWebEnginePermission::State::Invalid)); - permission.reset(); - QVERIFY(permission.state() == (valid ? QWebEnginePermission::State::Ask : QWebEnginePermission::State::Invalid)); -} - -void tst_QWebEngineProfile::listPermissions() -{ - QWebEngineProfile profile; - // In-memory is the default for otr profiles - QVERIFY(profile.persistentPermissionsPolicy() == QWebEngineProfile::PersistentPermissionsPolicy::StoreInMemory); - - QUrl commonUrl = QUrl(QStringLiteral("http://www.bing.com/maps")); - QWebEnginePermission::PermissionType commonType = QWebEnginePermission::PermissionType::Notifications; - - // First, set several permissions at once - profile.queryPermission(commonUrl, QWebEnginePermission::PermissionType::Geolocation).deny(); - profile.queryPermission(commonUrl, QWebEnginePermission::PermissionType::Unsupported).grant(); // Invalid - profile.queryPermission(commonUrl, commonType).grant(); - profile.queryPermission(QUrl(QStringLiteral("http://www.google.com/translate")), commonType).grant(); - - QList<QWebEnginePermission> permissionsListAll = profile.listAllPermissions(); - QList<QWebEnginePermission> permissionsListUrl = profile.listPermissionsForOrigin(commonUrl); - QList<QWebEnginePermission> permissionsListFeature = profile.listPermissionsForPermissionType(commonType); - - // Order of returned permissions is not guaranteed, so we must iterate until we find the one we need - auto findInList = [](QList<QWebEnginePermission> list, const QUrl &url, - QWebEnginePermission::PermissionType permissionType, QWebEnginePermission::State state) - { - bool found = false; - for (auto &permission : list) { - if (permission.origin().adjusted(QUrl::RemovePath) == url.adjusted(QUrl::RemovePath) - && permission.permissionType() == permissionType && permission.state() == state) { - found = true; - break; - } - } - return found; - }; - - // Check full list - QVERIFY(permissionsListAll.size() == 3); - QVERIFY(findInList(permissionsListAll, commonUrl, QWebEnginePermission::PermissionType::Geolocation, QWebEnginePermission::State::Denied)); - QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); - QVERIFY(findInList(permissionsListAll, QUrl(QStringLiteral("http://www.google.com")), commonType, QWebEnginePermission::State::Granted)); - - // Check list filtered by URL - QVERIFY(permissionsListUrl.size() == 2); - QVERIFY(findInList(permissionsListUrl, commonUrl, QWebEnginePermission::PermissionType::Geolocation, QWebEnginePermission::State::Denied)); - QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); - - // Check list filtered by feature - QVERIFY(permissionsListFeature.size() == 2); - QVERIFY(findInList(permissionsListAll, commonUrl, commonType, QWebEnginePermission::State::Granted)); - QVERIFY(findInList(permissionsListAll, QUrl(QStringLiteral("http://www.google.com")), commonType, QWebEnginePermission::State::Granted)); -} - void tst_QWebEngineProfile::qtbug_71895() { QWebEngineView view; |