// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include 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 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("permissionType"); QTest::addColumn("triggerFunction"); QTest::addColumn("testFunction"); QTest::addColumn("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()); } 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("policy"); QTest::addColumn("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 page(new QWebEnginePage(m_profile.get())); std::unique_ptr 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("permissionType"); QTest::addColumn("url"); QTest::addColumn("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 permissionsListAll = m_profile->listAllPermissions(); QList permissionsListUrl = m_profile->listPermissionsForOrigin(commonUrl); QList 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 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("canAccessClipboard"); QTest::addColumn("canPaste"); QTest::addColumn("readPermission"); QTest::addColumn("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("Test"), 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("canAccessClipboard"); QTest::addColumn("initialPolicy"); QTest::addColumn("initialPermission"); QTest::addColumn("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("Test"), 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"