diff options
33 files changed, 1389 insertions, 1041 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/core/api/qwebenginepage.cpp b/src/core/api/qwebenginepage.cpp index bfa184e5c..8d73723f6 100644 --- a/src/core/api/qwebenginepage.cpp +++ b/src/core/api/qwebenginepage.cpp @@ -590,49 +590,6 @@ void QWebEnginePagePrivate::showColorDialog(QSharedPointer<ColorChooserControlle view->showColorDialog(controller); } -void QWebEnginePagePrivate::runMediaAccessPermissionRequest(const QUrl &securityOrigin, WebContentsAdapterClient::MediaRequestFlags requestFlags) -{ - Q_Q(QWebEnginePage); - QWebEnginePermission::PermissionType permissionType; - - if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaAudioVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaAudioCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::DesktopAudioVideoCapture; - else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::DesktopVideoCapture; - - Q_EMIT q->permissionRequested(createFeaturePermissionObject(securityOrigin, permissionType)); - -#if QT_DEPRECATED_SINCE(6, 8) - QT_WARNING_PUSH - QT_WARNING_DISABLE_DEPRECATED - QWebEnginePage::Feature deprecatedFeature; - - if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - deprecatedFeature = QWebEnginePage::MediaAudioVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) - deprecatedFeature = QWebEnginePage::MediaAudioCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - deprecatedFeature = QWebEnginePage::MediaVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - deprecatedFeature = QWebEnginePage::DesktopAudioVideoCapture; - else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - deprecatedFeature = QWebEnginePage::DesktopVideoCapture; - - Q_EMIT q->featurePermissionRequested(securityOrigin, deprecatedFeature); - QT_WARNING_POP -#endif // QT_DEPRECATED_SINCE(6, 8) -} - #if QT_DEPRECATED_SINCE(6, 8) QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED @@ -668,33 +625,19 @@ static QWebEnginePage::Feature toDeprecatedFeature(QWebEnginePermission::Permiss QT_WARNING_POP #endif // QT_DEPRECATED_SINCE(6, 8) -void QWebEnginePagePrivate::runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin) +void QWebEnginePagePrivate::runFeaturePermissionRequest( + QWebEnginePermission::PermissionType permissionType, + const QUrl &securityOrigin, + int childId, const std::string &serializedToken) { Q_Q(QWebEnginePage); - if (QWebEnginePermission::isPersistent(permissionType)) { - Q_EMIT q->permissionRequested(createFeaturePermissionObject(securityOrigin, permissionType)); -#if QT_DEPRECATED_SINCE(6, 8) - QT_WARNING_PUSH - QT_WARNING_DISABLE_DEPRECATED - Q_EMIT q->featurePermissionRequested(securityOrigin, toDeprecatedFeature(permissionType)); - QT_WARNING_POP -#endif // QT_DEPRECATED_SINCE(6, 8) - return; - } - - Q_UNREACHABLE(); -} - -void QWebEnginePagePrivate::runMouseLockPermissionRequest(const QUrl &securityOrigin) -{ - Q_Q(QWebEnginePage); - Q_EMIT q->permissionRequested(createFeaturePermissionObject(securityOrigin, QWebEnginePermission::PermissionType::MouseLock)); - + Q_EMIT q->permissionRequested(QWebEnginePermission( + new QWebEnginePermissionPrivate(securityOrigin, permissionType, profileAdapter(), childId, serializedToken))); #if QT_DEPRECATED_SINCE(6, 8) QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED - Q_EMIT q->featurePermissionRequested(securityOrigin, QWebEnginePage::MouseLock); + Q_EMIT q->featurePermissionRequested(securityOrigin, toDeprecatedFeature(permissionType)); QT_WARNING_POP #endif // QT_DEPRECATED_SINCE(6, 8) } @@ -885,12 +828,6 @@ void QWebEnginePagePrivate::showWebAuthDialog(QWebEngineWebAuthUxRequest *reques Q_EMIT q->webAuthUxRequested(request); } -QWebEnginePermission QWebEnginePagePrivate::createFeaturePermissionObject(const QUrl &securityOrigin, QWebEnginePermission::PermissionType feature) -{ - auto *returnPrivate = new QWebEnginePermissionPrivate(securityOrigin, feature, adapter, profileAdapter()); - return QWebEnginePermission(returnPrivate); -} - QWebEnginePage::QWebEnginePage(QObject* parent) : QObject(parent) , d_ptr(new QWebEnginePagePrivate()) @@ -1918,7 +1855,7 @@ void QWebEnginePage::setFeaturePermission(const QUrl &securityOrigin, QWebEngine Q_UNREACHABLE(); } - d->adapter->setPermission(securityOrigin, f, s); + d->adapter->setPermission(securityOrigin, f, s, {}); } QT_WARNING_POP #endif // QT_DEPRECATED_SINCE(6, 8) diff --git a/src/core/api/qwebenginepage_p.h b/src/core/api/qwebenginepage_p.h index 76b4a4d9d..ba1fbc6d5 100644 --- a/src/core/api/qwebenginepage_p.h +++ b/src/core/api/qwebenginepage_p.h @@ -148,9 +148,8 @@ public: void authenticationRequired( QSharedPointer<QtWebEngineCore::AuthenticationDialogController>) override; void releaseProfile() override; - void runMediaAccessPermissionRequest(const QUrl &securityOrigin, MediaRequestFlags requestFlags) override; - void runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin) override; - void runMouseLockPermissionRequest(const QUrl &securityOrigin) override; + void runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin, + int childId, const std::string &serializedToken) override; void runRegisterProtocolHandlerRequest(QWebEngineRegisterProtocolHandlerRequest) override; void runFileSystemAccessRequest(QWebEngineFileSystemAccessRequest) override; QObject *accessibilityParentObject() override; @@ -181,7 +180,6 @@ public: const QRect &bounds, bool autoselectFirstSuggestion) override; void hideAutofillPopup() override; void showWebAuthDialog(QWebEngineWebAuthUxRequest *controller) override; - QWebEnginePermission createFeaturePermissionObject(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) override; QtWebEngineCore::ProfileAdapter *profileAdapter() override; QtWebEngineCore::WebContentsAdapter *webContentsAdapter() override; diff --git a/src/core/api/qwebenginepermission.cpp b/src/core/api/qwebenginepermission.cpp index ec62f0e4c..1d1b12b7e 100644 --- a/src/core/api/qwebenginepermission.cpp +++ b/src/core/api/qwebenginepermission.cpp @@ -20,11 +20,12 @@ QWebEnginePermissionPrivate::QWebEnginePermissionPrivate() /*! \internal */ QWebEnginePermissionPrivate::QWebEnginePermissionPrivate(const QUrl &origin_, QWebEnginePermission::PermissionType permissionType_, - QSharedPointer<QtWebEngineCore::WebContentsAdapter> webContentsAdapter_, QtWebEngineCore::ProfileAdapter *profileAdapter_) + QtWebEngineCore::ProfileAdapter *profileAdapter_, int childId_, const std::string &serializedToken_) : QSharedData() , origin(origin_) , permissionType(permissionType_) - , webContentsAdapter(webContentsAdapter_) + , childId(childId_) + , serializedToken(serializedToken_) , profileAdapter(profileAdapter_) { } @@ -114,15 +115,12 @@ bool QWebEnginePermission::equals(const QWebEnginePermission &other) const return false; if (!isPersistent(d_ptr->permissionType)) { - if (d_ptr->webContentsAdapter != other.d_ptr->webContentsAdapter) + if (d_ptr->childId != other.d_ptr->childId + && d_ptr->serializedToken != other.d_ptr->serializedToken) return false; } else { - QtWebEngineCore::ProfileAdapter *thisProfile = d_ptr->webContentsAdapter - ? d_ptr->webContentsAdapter.toStrongRef()->profileAdapter() - : d_ptr->profileAdapter.get(); - QtWebEngineCore::ProfileAdapter *otherProfile = d_ptr->webContentsAdapter - ? other.d_ptr->webContentsAdapter.toStrongRef()->profileAdapter() - : other.d_ptr->profileAdapter.get(); + QtWebEngineCore::ProfileAdapter *thisProfile = d_ptr->profileAdapter.get(); + QtWebEngineCore::ProfileAdapter *otherProfile = other.d_ptr->profileAdapter.get(); if (thisProfile != otherProfile) return false; @@ -201,11 +199,7 @@ QWebEnginePermission::State QWebEnginePermission::state() const { if (!isValid()) return State::Invalid; - if (d_ptr->webContentsAdapter) - return d_ptr->webContentsAdapter.toStrongRef()->getPermissionState(origin(), permissionType()); - if (d_ptr->profileAdapter) - return d_ptr->profileAdapter->getPermissionState(origin(), permissionType()); - Q_UNREACHABLE_RETURN(State::Ask); + return d_ptr->profileAdapter->getPermissionState(origin(), permissionType(), d_ptr->childId, d_ptr->serializedToken); } /*! @@ -227,7 +221,7 @@ bool QWebEnginePermission::isValid() const return false; if (permissionType() == PermissionType::Unsupported) return false; - if (!d_ptr->profileAdapter && !d_ptr->webContentsAdapter) + if (!d_ptr->profileAdapter) return false; if (!d_ptr->origin.isValid()) return false; @@ -243,10 +237,7 @@ void QWebEnginePermission::grant() const { if (!isValid()) return; - if (d_ptr->webContentsAdapter) - d_ptr->webContentsAdapter.toStrongRef()->setPermission(origin(), permissionType(), State::Granted); - else if (d_ptr->profileAdapter) - d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Granted); + d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Granted, d_ptr->childId, d_ptr->serializedToken); } /*! @@ -258,10 +249,7 @@ void QWebEnginePermission::deny() const { if (!isValid()) return; - if (d_ptr->webContentsAdapter) - d_ptr->webContentsAdapter.toStrongRef()->setPermission(origin(), permissionType(), State::Denied); - else if (d_ptr->profileAdapter) - d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Denied); + d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Denied, d_ptr->childId, d_ptr->serializedToken); } /*! @@ -279,10 +267,7 @@ void QWebEnginePermission::reset() const { if (!isValid()) return; - if (d_ptr->webContentsAdapter) - d_ptr->webContentsAdapter.toStrongRef()->setPermission(origin(), permissionType(), State::Ask); - else if (d_ptr->profileAdapter) - d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Ask); + d_ptr->profileAdapter->setPermission(origin(), permissionType(), State::Ask, d_ptr->childId, d_ptr->serializedToken); } /*! diff --git a/src/core/api/qwebenginepermission_p.h b/src/core/api/qwebenginepermission_p.h index c6b525b31..aabb5c4b9 100644 --- a/src/core/api/qwebenginepermission_p.h +++ b/src/core/api/qwebenginepermission_p.h @@ -33,12 +33,14 @@ struct QWebEnginePermissionPrivate : public QSharedData { Q_WEBENGINECORE_EXPORT QWebEnginePermissionPrivate(); Q_WEBENGINECORE_EXPORT QWebEnginePermissionPrivate(const QUrl &, QWebEnginePermission::PermissionType, - QSharedPointer<QtWebEngineCore::WebContentsAdapter>, QtWebEngineCore::ProfileAdapter *); + QtWebEngineCore::ProfileAdapter *, int = -1, const std::string & = std::string()); QUrl origin; QWebEnginePermission::PermissionType permissionType; - QWeakPointer<QtWebEngineCore::WebContentsAdapter> webContentsAdapter; + int childId = -1; + std::string serializedToken; + QPointer<QtWebEngineCore::ProfileAdapter> profileAdapter; }; diff --git a/src/core/api/qwebengineprofile.cpp b/src/core/api/qwebengineprofile.cpp index b6c308cf6..f7f6ab551 100644 --- a/src/core/api/qwebengineprofile.cpp +++ b/src/core/api/qwebengineprofile.cpp @@ -1018,7 +1018,7 @@ QWebEnginePermission QWebEngineProfile::queryPermission(const QUrl &securityOrig return QWebEnginePermission(new QWebEnginePermissionPrivate()); } - auto *pvt = new QWebEnginePermissionPrivate(securityOrigin, permissionType, nullptr, d->profileAdapter()); + auto *pvt = new QWebEnginePermissionPrivate(securityOrigin, permissionType, d->profileAdapter()); return QWebEnginePermission(pvt); } diff --git a/src/core/media_capture_devices_dispatcher.cpp b/src/core/media_capture_devices_dispatcher.cpp index 848a92986..d691c65ea 100644 --- a/src/core/media_capture_devices_dispatcher.cpp +++ b/src/core/media_capture_devices_dispatcher.cpp @@ -11,6 +11,8 @@ #include "web_contents_delegate_qt.h" #include "web_contents_view_qt.h" #include "web_engine_settings.h" +#include "permission_manager_qt.h" +#include "type_conversion.h" #include "base/strings/strcat.h" #include "blink/public/common/page/page_zoom.h" @@ -21,6 +23,8 @@ #include "content/public/browser/desktop_streams_registry.h" #include "content/public/browser/host_zoom_map.h" #include "content/public/browser/media_capture_devices.h" +#include "content/public/browser/permission_controller_delegate.h" +#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "media/audio/audio_device_description.h" #include "media/audio/audio_manager_base.h" @@ -493,8 +497,17 @@ void MediaCaptureDevicesDispatcher::processMediaAccessRequest( } enqueueMediaAccessRequest(webContents, request, std::move(callback), id); - // We might not require this approval for pepper requests. - adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), flags); + + PermissionManagerQt *permissionManager = static_cast<PermissionManagerQt *>( + webContents->GetBrowserContext()->GetPermissionControllerDelegate()); + permissionManager->requestMediaPermissions( + content::RenderFrameHost::FromID(request.render_process_id, request.render_frame_id), + flags, + base::BindOnce( + &MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse, + base::Unretained(this), + webContents, + toQt(request.url_origin))); } void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request, content::MediaResponseCallback callback) @@ -558,9 +571,18 @@ void MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest(content::WebConte RequestsQueue &queue(it->second); content::MediaStreamRequest &request = queue.front()->request; + WebContentsAdapterClient::MediaRequestFlags flags = mediaRequestFlagsForRequest(request); - WebContentsAdapterClient *adapterClient = WebContentsViewQt::from(static_cast<content::WebContentsImpl *>(webContents)->GetView())->client(); - adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), mediaRequestFlagsForRequest(request)); + PermissionManagerQt *permissionManager = static_cast<PermissionManagerQt *>( + webContents->GetBrowserContext()->GetPermissionControllerDelegate()); + permissionManager->requestMediaPermissions( + content::RenderFrameHost::FromID(request.render_process_id, request.render_frame_id), + flags, + base::BindOnce( + &MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse, + base::Unretained(this), + webContents, + toQt(request.url_origin))); } void MediaCaptureDevicesDispatcher::getDefaultDevices(const std::string &audioDeviceId, const std::string &videoDeviceId, diff --git a/src/core/permission_manager_qt.cpp b/src/core/permission_manager_qt.cpp index 5258f15cf..eae98b350 100644 --- a/src/core/permission_manager_qt.cpp +++ b/src/core/permission_manager_qt.cpp @@ -40,7 +40,7 @@ static QWebEnginePermission::PermissionType toQt(blink::PermissionType type) case blink::PermissionType::VIDEO_CAPTURE: return QWebEnginePermission::PermissionType::MediaVideoCapture; case blink::PermissionType::DISPLAY_CAPTURE: - return QWebEnginePermission::PermissionType::DesktopAudioVideoCapture; + return QWebEnginePermission::PermissionType::DesktopVideoCapture; // We treat these both as read/write since we do not currently have a // ClipboardSanitizedWrite permission type. case blink::PermissionType::CLIPBOARD_READ_WRITE: @@ -56,6 +56,8 @@ static QWebEnginePermission::PermissionType toQt(blink::PermissionType type) case blink::PermissionType::WINDOW_MANAGEMENT: case blink::PermissionType::BACKGROUND_SYNC: case blink::PermissionType::NUM: + case blink::PermissionType::TOP_LEVEL_STORAGE_ACCESS: + case blink::PermissionType::SPEAKER_SELECTION: return QWebEnginePermission::PermissionType::Unsupported; case blink::PermissionType::MIDI_SYSEX: case blink::PermissionType::PROTECTED_MEDIA_IDENTIFIER: @@ -72,16 +74,13 @@ static QWebEnginePermission::PermissionType toQt(blink::PermissionType type) case blink::PermissionType::AR: case blink::PermissionType::VR: case blink::PermissionType::STORAGE_ACCESS_GRANT: - case blink::PermissionType::TOP_LEVEL_STORAGE_ACCESS: case blink::PermissionType::CAPTURED_SURFACE_CONTROL: case blink::PermissionType::SMART_CARD: case blink::PermissionType::WEB_PRINTING: - case blink::PermissionType::SPEAKER_SELECTION: case blink::PermissionType::KEYBOARD_LOCK: case blink::PermissionType::AUTOMATIC_FULLSCREEN: case blink::PermissionType::HAND_TRACKING: case blink::PermissionType::WEB_APP_INSTALLATION: - LOG(INFO) << "Unexpected unsupported Blink permission type: " << static_cast<int>(type); break; } return QWebEnginePermission::PermissionType::Unsupported; @@ -107,16 +106,50 @@ static blink::PermissionType toBlink(QWebEnginePermission::PermissionType permis return blink::PermissionType::LOCAL_FONTS; case QWebEnginePermission::PermissionType::MouseLock: return blink::PermissionType::POINTER_LOCK; - case QWebEnginePermission::PermissionType::MediaAudioVideoCapture: - LOG(INFO) << "Unexpected unsupported WebEngine permission type: " << static_cast<int>(permissionType); - Q_FALLTHROUGH(); case QWebEnginePermission::PermissionType::Unsupported: return blink::PermissionType::NUM; + case QWebEnginePermission::PermissionType::MediaAudioVideoCapture: + break; } Q_UNREACHABLE_RETURN(blink::PermissionType::NUM); } +static std::vector<QWebEnginePermission::PermissionType> toQt( + const std::vector<blink::PermissionType> &blinkPermissions) +{ + // This function handles the edge case differences between our permission types and Blink's; + // namely, MediaAudioVideoCapture and DesktopAudioVideoCapture + std::vector<QWebEnginePermission::PermissionType> permissions; + for (auto &p : blinkPermissions) { + permissions.push_back(toQt(p)); + } + + for (auto i1 = permissions.begin(); i1 != permissions.end(); ++i1) { + if (*i1 == QWebEnginePermission::PermissionType::MediaAudioCapture) { + for (auto i2 = permissions.begin(); i2 != permissions.end(); ++i2) { + if (*i2 == QWebEnginePermission::PermissionType::MediaVideoCapture) { + // Merge MediaAudioCapture and MediaVideoCapture into MediaAudioVideoCapture + *i1 = QWebEnginePermission::PermissionType::MediaAudioVideoCapture; + permissions.erase(i2); + break; + } + } + } else if (*i1 == QWebEnginePermission::PermissionType::DesktopVideoCapture) { + for (auto i2 = i1 + 1; i2 != permissions.end(); ++i2) { + if (*i2 == QWebEnginePermission::PermissionType::DesktopVideoCapture) { + // Double DesktopVideoCapture means we actually need DesktopAudioVideoCapture + *i2 = QWebEnginePermission::PermissionType::DesktopAudioVideoCapture; + i1 = permissions.erase(i1); + break; + } + } + } + } + + return permissions; +} + static QWebEnginePermission::State toQt(blink::mojom::PermissionStatus state) { switch (state) { @@ -154,6 +187,8 @@ std::string permissionTypeString(QWebEnginePermission::PermissionType permission return "MediaVideoCapture"; case QWebEnginePermission::PermissionType::DesktopAudioVideoCapture: return "DesktopAudioVideoCapture"; + case QWebEnginePermission::PermissionType::DesktopVideoCapture: + return "DesktopVideoCapture"; case QWebEnginePermission::PermissionType::MouseLock: return "MouseLock"; case QWebEnginePermission::PermissionType::Notifications: @@ -213,6 +248,8 @@ PermissionManagerQt::PermissionManagerQt(ProfileAdapter *profileAdapter) m_permissionTypes.push_back(QWebEnginePermission::PermissionType::MediaAudioCapture); m_permissionTypes.push_back(QWebEnginePermission::PermissionType::MediaVideoCapture); + m_permissionTypes.push_back(QWebEnginePermission::PermissionType::DesktopAudioVideoCapture); + m_permissionTypes.push_back(QWebEnginePermission::PermissionType::DesktopVideoCapture); m_permissionTypes.push_back(QWebEnginePermission::PermissionType::MouseLock); m_permissionTypes.push_back(QWebEnginePermission::PermissionType::Notifications); m_permissionTypes.push_back(QWebEnginePermission::PermissionType::Geolocation); @@ -239,38 +276,83 @@ PermissionManagerQt::~PermissionManagerQt() commit(); } +// static +content::GlobalRenderFrameHostToken PermissionManagerQt::deserializeToken(int childId, const std::string &serializedToken) +{ + auto maybeToken = base::UnguessableToken::DeserializeFromString(serializedToken); + if (maybeToken) + return content::GlobalRenderFrameHostToken(childId, blink::LocalFrameToken(maybeToken.value())); + + return content::GlobalRenderFrameHostToken(); +} + void PermissionManagerQt::setPermission( const QUrl &url, - QWebEnginePermission::PermissionType permissionType, - QWebEnginePermission::State state, - content::RenderFrameHost *rfh) + const QWebEnginePermission::PermissionType permissionType, + const QWebEnginePermission::State state, + const content::GlobalRenderFrameHostToken &token) +{ + if (permissionType == QWebEnginePermission::PermissionType::MediaAudioVideoCapture) { + setPermissionImpl(url, QWebEnginePermission::PermissionType::MediaAudioCapture, state, token); + setPermissionImpl(url, QWebEnginePermission::PermissionType::MediaVideoCapture, state, token); + return; + } + + setPermissionImpl(url, permissionType, state, token); +} + +void PermissionManagerQt::setPermission( + const QUrl &url, + const QWebEnginePermission::PermissionType permissionType, + const QWebEnginePermission::State state, + int childId, const std::string &serializedToken) +{ + content::GlobalRenderFrameHostToken token; + auto maybeToken = base::UnguessableToken::DeserializeFromString(serializedToken); + if (maybeToken) + token = content::GlobalRenderFrameHostToken(childId, blink::LocalFrameToken(maybeToken.value())); + + setPermission(url, permissionType, state, token); +} + +void PermissionManagerQt::setPermissionImpl( + const QUrl &url, + const QWebEnginePermission::PermissionType permissionTypeQt, + const QWebEnginePermission::State permissionStateQt, + const content::GlobalRenderFrameHostToken &frameToken) { + const blink::PermissionType permissionTypeBlink = toBlink(permissionTypeQt); + const blink::mojom::PermissionStatus permissionStateBlink = toBlink(permissionStateQt); + // Normalize the QUrl to Chromium origin form. const GURL gorigin = toGurl(url).DeprecatedGetOriginAsURL(); const QUrl origin = gorigin.is_empty() ? url : toQt(gorigin); if (origin.isEmpty()) return; - // Send eligible permissions with an associated rfh to the transient store. When pre-granting + // Send eligible permissions with an associated frameToken to the transient store. When pre-granting // a non-persistent permission (or pre-granting any permission in AskEveryTime mode), it is allowed // to pass through the persistent store. It will be moved to the transient store and associated - // with a rfh the next time its status is requested. - bool inTransientStore = rfh && (!QWebEnginePermission::isPersistent(permissionType) || !m_persistence); + // with a frameToken the next time its status is requested. + bool inTransientStore = frameToken.child_id != content::kInvalidChildProcessUniqueId + && (!QWebEnginePermission::isPersistent(permissionTypeQt) || !m_persistence); - blink::mojom::PermissionStatus blinkStatus = toBlink(state); - if (state == QWebEnginePermission::State::Ask) { + blink::mojom::PermissionStatus blinkStatus = permissionStateBlink; + if (permissionStateQt == QWebEnginePermission::State::Ask) { if (inTransientStore) - resetTransientPermission(toBlink(permissionType), gorigin, rfh->GetGlobalFrameToken()); + resetTransientPermission(permissionTypeBlink, gorigin, frameToken); else - ResetPermission(toBlink(permissionType), gorigin, gorigin); + ResetPermission(permissionTypeBlink, gorigin, gorigin); } else { if (inTransientStore) - setTransientPermission(toBlink(permissionType), gorigin, state == QWebEnginePermission::State::Granted, rfh->GetGlobalFrameToken()); + setTransientPermission(permissionTypeBlink, gorigin, + permissionStateQt == QWebEnginePermission::State::Granted, frameToken); else - setPersistentPermission(toBlink(permissionType), gorigin, state == QWebEnginePermission::State::Granted); + setPersistentPermission(permissionTypeBlink, + gorigin, permissionStateQt == QWebEnginePermission::State::Granted); auto it = m_requests.begin(); while (it != m_requests.end()) { - if (it->origin == origin && it->type == permissionType) { + if (it->origin == origin && it->type == permissionTypeQt) { std::move(it->callback).Run(blinkStatus); it = m_requests.erase(it); } else @@ -292,10 +374,10 @@ void PermissionManagerQt::setPermission( if (subscription->embedding_origin != gorigin) continue; - if (subscription->permission != toBlink(permissionType)) + if (subscription->permission != permissionTypeBlink) continue; - if ((!QWebEnginePermission::isPersistent(permissionType) || !m_persistence) - && targetRfh && targetRfh != rfh) + if ((!QWebEnginePermission::isPersistent(permissionTypeQt) || !m_persistence) + && targetRfh && targetRfh != content::RenderFrameHost::FromFrameToken(frameToken)) continue; // Behavior in callbacks may differ depending on the denial reason. Until we have @@ -312,7 +394,7 @@ void PermissionManagerQt::setPermission( std::move(callback).Run(); } - if (state == QWebEnginePermission::State::Ask) + if (permissionStateQt == QWebEnginePermission::State::Ask) return; auto it = m_multiRequests.begin(); @@ -321,32 +403,37 @@ void PermissionManagerQt::setPermission( bool answerable = true; std::vector<blink::mojom::PermissionStatus> result; result.reserve(it->types.size()); - for (blink::PermissionType permission : it->types) { - if (toQt(permission) == QWebEnginePermission::PermissionType::Unsupported) { + for (blink::PermissionType currentPermissionType : it->types) { + if (toQt(currentPermissionType) == QWebEnginePermission::PermissionType::Unsupported) { result.push_back(blink::mojom::PermissionStatus::DENIED); continue; } blink::mojom::PermissionStatus permissionStatus; if (inTransientStore) - permissionStatus = toBlink(getPermissionState(url, permissionType, rfh)); + permissionStatus = toBlink(getPermissionState(url, toQt(currentPermissionType), frameToken)); else - permissionStatus = GetPermissionStatus(permission, gorigin, GURL()); + permissionStatus = GetPermissionStatus(currentPermissionType, gorigin, GURL()); - if (permissionStatus == toBlink(state)) { + if (permissionStatus == permissionStateBlink) { if (permissionStatus == blink::mojom::PermissionStatus::ASK) { answerable = false; break; } result.push_back(permissionStatus); - } else { + } else if (!m_persistence) { // Reached when the PersistentPermissionsPolicy is set to AskEveryTime - result.push_back(toBlink(state)); + result.push_back(permissionStateBlink); + } else { + // Not all of the permissions in this request have been set yet, bail and wait for the next setPermission() call + answerable = false; + break; } } if (answerable) { - std::move(it->callback).Run(result); + if (!it->callback.is_null()) + std::move(it->callback).Run(result); it = m_multiRequests.erase(it); continue; } @@ -355,23 +442,44 @@ void PermissionManagerQt::setPermission( } } -QWebEnginePermission::State PermissionManagerQt::getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - content::RenderFrameHost *rfh) +QWebEnginePermission::State PermissionManagerQt::getPermissionState( + const QUrl &origin, + const QWebEnginePermission::PermissionType permissionType, + const content::GlobalRenderFrameHostToken &frameToken) { - if (rfh) { - // Ignore the origin parameter - return toQt(GetPermissionStatusForCurrentDocument(toBlink(permissionType), rfh, false)); + std::vector<QWebEnginePermission::PermissionType> types; + if (permissionType == QWebEnginePermission::PermissionType::MediaAudioVideoCapture) { + types.push_back(QWebEnginePermission::PermissionType::MediaAudioCapture); + types.push_back(QWebEnginePermission::PermissionType::MediaVideoCapture); + } else { + types.push_back(permissionType); + } + + auto *rfh = content::RenderFrameHost::FromFrameToken(frameToken); + QWebEnginePermission::State returnState = QWebEnginePermission::State::Invalid; + for (auto type : types) { + QWebEnginePermission::State state = rfh + ? toQt(GetPermissionStatusForCurrentDocument(toBlink(type), rfh, false)) + : toQt(GetPermissionStatus(toBlink(type), toGurl(origin), GURL())); + + if (returnState == QWebEnginePermission::State::Invalid) + returnState = state; + else if (returnState != state) + returnState = QWebEnginePermission::State::Ask; } - return toQt(GetPermissionStatus(toBlink(permissionType), toGurl(origin), GURL())); + return returnState; } -QList<QWebEnginePermission> PermissionManagerQt::listPermissions(const QUrl &origin, QWebEnginePermission::PermissionType permissionType) +QList<QWebEnginePermission> PermissionManagerQt::listPermissions( + const QUrl &origin, + const QWebEnginePermission::PermissionType permissionType) { Q_ASSERT(origin.isEmpty() || permissionType == QWebEnginePermission::PermissionType::Unsupported); + QList<QWebEnginePermission> returnList; - GURL gorigin = toGurl(origin).DeprecatedGetOriginAsURL(); - std::string originSpec = gorigin.spec(); + const GURL gorigin = toGurl(origin).DeprecatedGetOriginAsURL(); + const std::string originSpec = gorigin.spec(); if (!origin.isEmpty() && !gorigin.is_valid()) return returnList; @@ -382,7 +490,7 @@ QList<QWebEnginePermission> PermissionManagerQt::listPermissions(const QUrl &ori else types.push_back(permissionType); - for (auto &type : types) { + for (const auto &type : types) { // Transient types may end up in the permission store as an implementation detail, // but we do not want to expose them to callers. if (!QWebEnginePermission::isPersistent(type)) @@ -399,7 +507,8 @@ QList<QWebEnginePermission> PermissionManagerQt::listPermissions(const QUrl &ori if (!originSpec.empty() && entry.first != originSpec) continue; - auto *pvt = new QWebEnginePermissionPrivate(toQt(GURL(std::string_view(entry.first))), type, nullptr, m_profileAdapter.get()); + auto *pvt = new QWebEnginePermissionPrivate( + toQt(GURL(std::string_view(entry.first))), type, m_profileAdapter.get()); returnList.push_back(QWebEnginePermission(pvt)); } } @@ -407,6 +516,78 @@ QList<QWebEnginePermission> PermissionManagerQt::listPermissions(const QUrl &ori return returnList; } +void PermissionManagerQt::requestMediaPermissions( + content::RenderFrameHost *render_frame_host, + const WebContentsAdapterClient::MediaRequestFlags flags, + base::OnceCallback<void(WebContentsAdapterClient::MediaRequestFlags authorizationFlags)> callback) +{ + std::vector<blink::PermissionType> permissionTypesBlink; + if (flags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) + permissionTypesBlink.push_back(blink::PermissionType::AUDIO_CAPTURE); + if (flags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) + permissionTypesBlink.push_back(blink::PermissionType::VIDEO_CAPTURE); + if (flags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) + || flags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) { + permissionTypesBlink.push_back(blink::PermissionType::DISPLAY_CAPTURE); + if (flags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture)) { + // Inject a second copy of the permission type into the request, + // so we can distinguish between DesktopVideoCapture and DesktopAudioVideoCapture. + permissionTypesBlink.push_back(blink::PermissionType::DISPLAY_CAPTURE); + } + } + + content::PermissionRequestDescription description(permissionTypesBlink, false, render_frame_host->GetLastCommittedOrigin().GetURL()); + + RequestPermissions(render_frame_host, description, base::BindOnce([]( + std::vector<blink::PermissionType> permissionTypesBlink, + base::OnceCallback<void(WebContentsAdapterClient::MediaRequestFlags authorizationFlags)> callback, + const std::vector<blink::mojom::PermissionStatus> &statuses) + { + // This callback converts the Blink permission types to MediaRequestFlags, + // and then runs the callback initially passed to requestMediaPermissions(). + DCHECK(permissionTypesBlink.size() == statuses.size()); + WebContentsAdapterClient::MediaRequestFlags flags = WebContentsAdapterClient::MediaRequestFlag::MediaNone; + for (uint i = 0; i < statuses.size(); ++i) { + if (statuses[i] == blink::mojom::PermissionStatus::GRANTED) { + switch (permissionTypesBlink[i]) { + case blink::PermissionType::AUDIO_CAPTURE: + flags.setFlag(WebContentsAdapterClient::MediaRequestFlag::MediaAudioCapture); + break; + case blink::PermissionType::VIDEO_CAPTURE: + flags.setFlag(WebContentsAdapterClient::MediaRequestFlag::MediaVideoCapture); + break; + case blink::PermissionType::DISPLAY_CAPTURE: + flags.setFlag(WebContentsAdapterClient::MediaRequestFlag::MediaDesktopAudioCapture); + flags.setFlag(WebContentsAdapterClient::MediaRequestFlag::MediaDesktopVideoCapture); + break; + default: + Q_UNREACHABLE(); + break; + } + } + } + std::move(callback).Run(flags); + }, permissionTypesBlink, std::move(callback))); +} + +// Needed for the rare cases where a RenderFrameHost remains the same even after +// a cross-origin navigation (e.g. inside an iframe). Needs to be called every +// time transient permissions are accessed. +void PermissionManagerQt::onCrossOriginNavigation(content::RenderFrameHost *render_frame_host) +{ + if (!render_frame_host) + return; + + auto frameToken = render_frame_host->GetGlobalFrameToken(); + auto &permissionsForToken = m_transientPermissions[frameToken]; + if (!permissionsForToken.size()) + return; + + GURL savedOrigin = get<0>(permissionsForToken[0]); + if (render_frame_host->GetLastCommittedOrigin().GetURL() != savedOrigin) + m_transientPermissions.erase(frameToken); +} + void PermissionManagerQt::commit() { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); @@ -414,15 +595,18 @@ void PermissionManagerQt::commit() m_prefService->CommitPendingWrite(); } -void PermissionManagerQt::RequestPermissions(content::RenderFrameHost *frameHost, - const content::PermissionRequestDescription &requestDescription, - base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)> callback) +void PermissionManagerQt::RequestPermissions( + content::RenderFrameHost *frameHost, + const content::PermissionRequestDescription &requestDescription, + base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)> callback) { if (requestDescription.requesting_origin.is_empty()) { - std::move(callback).Run(std::vector<content::PermissionStatus>(requestDescription.permissions.size(), blink::mojom::PermissionStatus::DENIED)); + std::move(callback).Run(std::vector<content::PermissionStatus>(requestDescription.permissions.size(), + blink::mojom::PermissionStatus::DENIED)); return; } + const auto frameToken = frameHost->GetGlobalFrameToken(); WebContentsDelegateQt *contentsDelegate = static_cast<WebContentsDelegateQt *>( content::WebContents::FromRenderFrameHost(frameHost)->GetDelegate()); Q_ASSERT(contentsDelegate); @@ -430,53 +614,54 @@ void PermissionManagerQt::RequestPermissions(content::RenderFrameHost *frameHost bool answerable = true; std::vector<content::PermissionStatus> result; result.reserve(requestDescription.permissions.size()); - for (blink::PermissionType permission : requestDescription.permissions) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) { + for (const blink::PermissionType permissionTypeBlink : requestDescription.permissions) { + const QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) { result.push_back(blink::mojom::PermissionStatus::DENIED); continue; } - blink::mojom::PermissionStatus permissionStatus = getStatusFromSettings(permission, contentsDelegate->webEngineSettings()); - if (permissionStatus == blink::mojom::PermissionStatus::ASK) { + blink::mojom::PermissionStatus permissionStatusBlink = getStatusFromSettings( + permissionTypeBlink, contentsDelegate->webEngineSettings()); + if (permissionStatusBlink == blink::mojom::PermissionStatus::ASK) { const GURL &rorigin = requestDescription.requesting_origin; + bool maybePreGranted = false; if (!m_persistence) { - answerable = false; - break; + maybePreGranted = true; } - bool inTransientStore = !QWebEnginePermission::isPersistent(toQt(permission)); + bool inTransientStore = !QWebEnginePermission::isPersistent(permissionTypeQt) || maybePreGranted; if (inTransientStore) { - permissionStatus = getTransientPermissionStatus(permission, rorigin, frameHost->GetGlobalFrameToken()); + permissionStatusBlink = getTransientPermissionStatus(permissionTypeBlink, rorigin, frameToken); - if (permissionStatus != blink::mojom::PermissionStatus::ASK) { - result.push_back(permissionStatus); + if (permissionStatusBlink != blink::mojom::PermissionStatus::ASK) { + result.push_back(permissionStatusBlink); continue; } // Fall through to check if permission was pre-granted (and thus landed in the permanent store) } - permissionStatus = GetPermissionStatus(permission, rorigin, rorigin); + permissionStatusBlink = GetPermissionStatus(permissionTypeBlink, rorigin, rorigin); - if (inTransientStore && permissionStatus != blink::mojom::PermissionStatus::ASK) { - // Move the pre-granted permission to the transient store and associate it with the rfh - ResetPermission(permission, rorigin, rorigin); - setTransientPermission(permission, rorigin, permissionStatus == blink::mojom::PermissionStatus::GRANTED, - frameHost->GetGlobalFrameToken()); + if (inTransientStore && permissionStatusBlink != blink::mojom::PermissionStatus::ASK) { + // Move the pre-granted permission to the transient store and associate it with a frame token + ResetPermission(permissionTypeBlink, rorigin, rorigin); + setTransientPermission(permissionTypeBlink, rorigin, + permissionStatusBlink == blink::mojom::PermissionStatus::GRANTED, frameToken); } - if (permissionStatus != blink::mojom::PermissionStatus::ASK) { + if (permissionStatusBlink != blink::mojom::PermissionStatus::ASK) { // Automatically grant/deny without prompt if already asked once - result.push_back(permissionStatus); + result.push_back(permissionStatusBlink); } else { answerable = false; break; } } else { // Reached when clipboard settings have been set - result.push_back(permissionStatus); + result.push_back(permissionStatusBlink); } } @@ -486,80 +671,75 @@ void PermissionManagerQt::RequestPermissions(content::RenderFrameHost *frameHost } int request_id = ++m_requestIdCount; - auto requestOrigin = toQt(requestDescription.requesting_origin); + const auto requestOrigin = toQt(requestDescription.requesting_origin); m_multiRequests.push_back({ request_id, requestDescription.permissions, requestOrigin, std::move(callback) }); - for (blink::PermissionType permission : requestDescription.permissions) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (QWebEnginePermission::isPersistent(permissionType)) - contentsDelegate->requestFeaturePermission(permissionType, requestOrigin); + auto qtPermissions = toQt(requestDescription.permissions); + for (const QWebEnginePermission::PermissionType permissionTypeQt : qtPermissions) { + contentsDelegate->requestFeaturePermission(permissionTypeQt, requestOrigin, frameToken); } } -void PermissionManagerQt::RequestPermissionsFromCurrentDocument(content::RenderFrameHost *frameHost, - const content::PermissionRequestDescription &requestDescription, - base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)> callback) +void PermissionManagerQt::RequestPermissionsFromCurrentDocument( + content::RenderFrameHost *frameHost, + const content::PermissionRequestDescription &requestDescription, + base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)> callback) { RequestPermissions(frameHost, requestDescription, std::move(callback)); } blink::mojom::PermissionStatus PermissionManagerQt::GetPermissionStatus( - blink::PermissionType permission, + blink::PermissionType permissionTypeBlink, const GURL& requesting_origin, const GURL& /*embedding_origin*/) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) + const QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) return blink::mojom::PermissionStatus::DENIED; - permission = toBlink(toQt(permission)); // Filter out merged/unsupported permissions (e.g. clipboard) - auto *pref = m_prefService->FindPreference(permissionTypeString(toQt(permission))); + permissionTypeBlink = toBlink(toQt(permissionTypeBlink)); // Filter out merged/unsupported permissions (e.g. clipboard) + auto *pref = m_prefService->FindPreference(permissionTypeString(permissionTypeQt)); if (!pref) return blink::mojom::PermissionStatus::ASK; // Permission type not in database - const auto *permissions = pref->GetValue()->GetIfDict(); - Q_ASSERT(permissions); + const auto *permissionsDict = pref->GetValue()->GetIfDict(); + Q_ASSERT(permissionsDict); - auto requestedPermission = permissions->FindBool(requesting_origin.DeprecatedGetOriginAsURL().spec()); + const auto requestedPermission = permissionsDict->FindBool(requesting_origin.DeprecatedGetOriginAsURL().spec()); if (!requestedPermission) return blink::mojom::PermissionStatus::ASK; // Origin is not in the current permission type's database - // Workaround: local fonts are entirely managed by Chromium, which only calls RequestPermission() _after_ - // it's checked whether the permission has been granted. By always returning ASK, we force the request to - // come through every time. - if (permission == blink::PermissionType::LOCAL_FONTS && !m_persistence) - return blink::mojom::PermissionStatus::ASK; - if (requestedPermission.value()) return blink::mojom::PermissionStatus::GRANTED; return blink::mojom::PermissionStatus::DENIED; } blink::mojom::PermissionStatus PermissionManagerQt::GetPermissionStatusForCurrentDocument( - blink::PermissionType permission, + blink::PermissionType permissionTypeBlink, content::RenderFrameHost *render_frame_host, bool) { Q_ASSERT(render_frame_host); - if (permission == blink::PermissionType::CLIPBOARD_READ_WRITE || - permission == blink::PermissionType::CLIPBOARD_SANITIZED_WRITE) { + if (permissionTypeBlink == blink::PermissionType::CLIPBOARD_READ_WRITE || + permissionTypeBlink == blink::PermissionType::CLIPBOARD_SANITIZED_WRITE) { WebContentsDelegateQt *delegate = static_cast<WebContentsDelegateQt *>( content::WebContents::FromRenderFrameHost(render_frame_host)->GetDelegate()); Q_ASSERT(delegate); - auto status = getStatusFromSettings(permission, delegate->webEngineSettings()); + auto status = getStatusFromSettings(permissionTypeBlink, delegate->webEngineSettings()); if (status != blink::mojom::PermissionStatus::ASK) return status; } - permission = toBlink(toQt(permission)); // Filter out merged/unsupported permissions (e.g. clipboard) - if (toQt(permission) == QWebEnginePermission::PermissionType::Unsupported) + permissionTypeBlink = toBlink(toQt(permissionTypeBlink)); // Filter out merged/unsupported permissions (e.g. clipboard) + QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) return blink::mojom::PermissionStatus::DENIED; GURL origin = render_frame_host->GetLastCommittedOrigin().GetURL(); auto status = blink::mojom::PermissionStatus::ASK; - bool inTransientStore = !QWebEnginePermission::isPersistent(toQt(permission)) || !m_persistence; + const bool inTransientStore = !QWebEnginePermission::isPersistent(permissionTypeQt) || !m_persistence; if (inTransientStore) { - status = getTransientPermissionStatus(permission, origin, render_frame_host->GetGlobalFrameToken()); + status = getTransientPermissionStatus(permissionTypeBlink, origin, render_frame_host->GetGlobalFrameToken()); if (status != blink::mojom::PermissionStatus::ASK) { return status; @@ -568,12 +748,12 @@ blink::mojom::PermissionStatus PermissionManagerQt::GetPermissionStatusForCurren // Fall through to check if permission was pre-granted (and thus landed in the permanent store) } - status = GetPermissionStatus(permission, origin, origin); + status = GetPermissionStatus(permissionTypeBlink, origin, origin); if (inTransientStore && status != blink::mojom::PermissionStatus::ASK) { // Move the pre-granted permission to the transient store and associate it with the rfh - ResetPermission(permission, origin, origin); - setTransientPermission(permission, origin, status == blink::mojom::PermissionStatus::GRANTED, + ResetPermission(permissionTypeBlink, origin, origin); + setTransientPermission(permissionTypeBlink, origin, status == blink::mojom::PermissionStatus::GRANTED, render_frame_host->GetGlobalFrameToken()); } @@ -610,9 +790,9 @@ content::PermissionResult PermissionManagerQt::GetPermissionResultForOriginWitho } void PermissionManagerQt::ResetPermission( - blink::PermissionType permission, - const GURL& requesting_origin, - const GURL& /*embedding_origin*/) + blink::PermissionType permission, + const GURL& requesting_origin, + const GURL& /*embedding_origin*/) { const QWebEnginePermission::PermissionType permissionType = toQt(permission); if (permissionType == QWebEnginePermission::PermissionType::Unsupported) @@ -622,12 +802,12 @@ void PermissionManagerQt::ResetPermission( updater.Get().Remove(requesting_origin.spec()); } -blink::mojom::PermissionStatus PermissionManagerQt::getTransientPermissionStatus(blink::PermissionType permission, +blink::mojom::PermissionStatus PermissionManagerQt::getTransientPermissionStatus( + blink::PermissionType permissionTypeBlink, const GURL& requesting_origin, content::GlobalRenderFrameHostToken token) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) + if (toQt(permissionTypeBlink) == QWebEnginePermission::PermissionType::Unsupported) return blink::mojom::PermissionStatus::DENIED; if (!m_transientPermissions.contains(token)) @@ -635,8 +815,10 @@ blink::mojom::PermissionStatus PermissionManagerQt::getTransientPermissionStatus auto &permissionsForToken = m_transientPermissions[token]; for (auto p = permissionsForToken.begin(); p != permissionsForToken.end(); ++p) { - if (get<0>(*p) == requesting_origin && get<1>(*p) == permission) { - return get<2>(*p) ? blink::mojom::PermissionStatus::GRANTED : blink::mojom::PermissionStatus::DENIED; + if (get<0>(*p) == requesting_origin && get<1>(*p) == permissionTypeBlink) { + return get<2>(*p) + ? blink::mojom::PermissionStatus::GRANTED + : blink::mojom::PermissionStatus::DENIED; } } @@ -644,47 +826,49 @@ blink::mojom::PermissionStatus PermissionManagerQt::getTransientPermissionStatus } void PermissionManagerQt::setPersistentPermission( - blink::PermissionType permission, - const GURL& requesting_origin, - bool granted) + blink::PermissionType permissionTypeBlink, + const GURL& requesting_origin, + bool granted) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) + const QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) return; - if (!m_prefService->FindPreference(permissionTypeString(permissionType))) + if (!m_prefService->FindPreference(permissionTypeString(permissionTypeQt))) return; - ScopedDictPrefUpdate updater(m_prefService.get(), permissionTypeString(permissionType)); + ScopedDictPrefUpdate updater(m_prefService.get(), permissionTypeString(permissionTypeQt)); updater.Get().Set(requesting_origin.spec(), granted); m_prefService->SchedulePendingLossyWrites(); } -void PermissionManagerQt::setTransientPermission(blink::PermissionType permission, +void PermissionManagerQt::setTransientPermission( + blink::PermissionType permissionTypeBlink, const GURL& requesting_origin, bool granted, content::GlobalRenderFrameHostToken token) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) + const QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) return; auto &permissionsForToken = m_transientPermissions[token]; for (auto &p : permissionsForToken) { - if (get<0>(p) == requesting_origin && get<1>(p) == permission) { + if (get<0>(p) == requesting_origin && get<1>(p) == permissionTypeBlink) { get<2>(p) = granted; return; } } - permissionsForToken.push_back({requesting_origin, permission, granted}); + permissionsForToken.push_back({requesting_origin, permissionTypeBlink, granted}); // Render frame hosts get discarded often, so the map will eventualy fill up with junk unless // periodically cleaned. The number 25 was chosen arbitrarily. if (++m_transientWriteCount > 25) { content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, - base::BindOnce([](PermissionManagerQt *p){ + base::BindOnce([](PermissionManagerQt *p) + { for (auto i = p->m_transientPermissions.begin(); i != p->m_transientPermissions.end(); ++i) { if (content::RenderFrameHost::FromFrameToken(i->first) == nullptr) { i = p->m_transientPermissions.erase(i); @@ -695,17 +879,18 @@ void PermissionManagerQt::setTransientPermission(blink::PermissionType permissio } } -void PermissionManagerQt::resetTransientPermission(blink::PermissionType permission, +void PermissionManagerQt::resetTransientPermission( + blink::PermissionType permissionTypeBlink, const GURL& requesting_origin, content::GlobalRenderFrameHostToken token) { - const QWebEnginePermission::PermissionType permissionType = toQt(permission); - if (permissionType == QWebEnginePermission::PermissionType::Unsupported) + const QWebEnginePermission::PermissionType permissionTypeQt = toQt(permissionTypeBlink); + if (permissionTypeQt == QWebEnginePermission::PermissionType::Unsupported) return; auto &permissionsForToken = m_transientPermissions[token]; for (auto i = permissionsForToken.begin(); i != permissionsForToken.end(); ++i) { - if (get<0>(*i) == requesting_origin && get<1>(*i) == permission) { + if (get<0>(*i) == requesting_origin && get<1>(*i) == permissionTypeBlink) { permissionsForToken.erase(i); return; } diff --git a/src/core/permission_manager_qt.h b/src/core/permission_manager_qt.h index 7468e9861..d8474d1e1 100644 --- a/src/core/permission_manager_qt.h +++ b/src/core/permission_manager_qt.h @@ -6,11 +6,13 @@ #include "base/functional/callback.h" #include "content/public/browser/global_routing_id.h" +#include "content/public/browser/media_stream_request.h" #include "content/public/browser/permission_controller_delegate.h" #include "content/public/browser/render_frame_host.h" #include <QtWebEngineCore/qwebenginepermission.h> #include "profile_adapter.h" +#include "web_contents_adapter_client.h" #include <map> #include <tuple> @@ -25,14 +27,30 @@ public: PermissionManagerQt(ProfileAdapter *adapter); ~PermissionManagerQt(); + static content::GlobalRenderFrameHostToken deserializeToken(int childId, const std::string &serializedToken); + + void setPermission( + const QUrl &origin, + const QWebEnginePermission::PermissionType permissionType, + const QWebEnginePermission::State state, + const content::GlobalRenderFrameHostToken &frameToken); + void setPermission( const QUrl &origin, - QWebEnginePermission::PermissionType permissionType, - QWebEnginePermission::State state, - content::RenderFrameHost *rfh = nullptr); - QWebEnginePermission::State getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - content::RenderFrameHost *rfh = nullptr); - QList<QWebEnginePermission> listPermissions(const QUrl &origin, QWebEnginePermission::PermissionType permissionType); + const QWebEnginePermission::PermissionType permissionType, + const QWebEnginePermission::State state, + int childId, const std::string &serializedToken); + + QWebEnginePermission::State getPermissionState(const QUrl &origin, const QWebEnginePermission::PermissionType permissionType, + const content::GlobalRenderFrameHostToken &frameToken); + QList<QWebEnginePermission> listPermissions(const QUrl &origin, const QWebEnginePermission::PermissionType permissionType); + + void requestMediaPermissions( + content::RenderFrameHost *render_frame_host, + const WebContentsAdapterClient::MediaRequestFlags flags, + base::OnceCallback<void(WebContentsAdapterClient::MediaRequestFlags authorizationFlags)> callback); + + void onCrossOriginNavigation(content::RenderFrameHost *render_frame_host); void commit(); @@ -42,7 +60,6 @@ public: const GURL& requesting_origin, const GURL& embedding_origin) override; - content::PermissionStatus GetPermissionStatusForCurrentDocument(blink::PermissionType, content::RenderFrameHost*, bool) override; blink::mojom::PermissionStatus GetPermissionStatusForWorker(blink::PermissionType, content::RenderProcessHost *, const GURL &) override; @@ -85,6 +102,12 @@ private: base::RepeatingCallback<void(blink::mojom::PermissionStatus)> callback; }; + void setPermissionImpl( + const QUrl &origin, + const QWebEnginePermission::PermissionType permissionType, + const QWebEnginePermission::State state, + const content::GlobalRenderFrameHostToken &frameToken); + blink::mojom::PermissionStatus getTransientPermissionStatus(blink::PermissionType permission, const GURL& requesting_origin, content::GlobalRenderFrameHostToken token); diff --git a/src/core/profile_adapter.cpp b/src/core/profile_adapter.cpp index f0cabc088..1e07f95f3 100644 --- a/src/core/profile_adapter.cpp +++ b/src/core/profile_adapter.cpp @@ -33,7 +33,9 @@ #include "renderer_host/user_resource_controller_host.h" #include "type_conversion.h" #include "visited_links_manager_qt.h" +#include "web_contents_adapter.h" #include "web_contents_adapter_client.h" +#include "web_contents_delegate_qt.h" #include "web_engine_context.h" #include <QCoreApplication> @@ -627,15 +629,29 @@ UserResourceControllerHost *ProfileAdapter::userResourceController() } void ProfileAdapter::setPermission(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - QWebEnginePermission::State state, content::RenderFrameHost *rfh) + QWebEnginePermission::State state, int childId, const std::string &serializedToken) { - static_cast<PermissionManagerQt*>(profile()->GetPermissionControllerDelegate())->setPermission(origin, permissionType, state, rfh); + auto token = PermissionManagerQt::deserializeToken(childId, serializedToken); + + // Check if the frame token is valid, and defer to WebContentsAdapter if so + auto *rfh = content::RenderFrameHost::FromFrameToken(token); + if (rfh) { + static_cast<WebContentsDelegateQt *>(content::WebContents::FromRenderFrameHost(rfh)->GetDelegate()) + ->webContentsAdapter() + ->setPermission(origin, permissionType, state, childId, serializedToken); + return; + } + + // Otherwise, set the permission directly + static_cast<PermissionManagerQt *>(profile()->GetPermissionControllerDelegate()) + ->setPermission(origin, permissionType, state, token); } QWebEnginePermission::State ProfileAdapter::getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - content::RenderFrameHost *rfh) + int childId, const std::string &serializedToken) { - return static_cast<PermissionManagerQt*>(profile()->GetPermissionControllerDelegate())->getPermissionState(origin, permissionType, rfh); + return static_cast<PermissionManagerQt*>(profile()->GetPermissionControllerDelegate()) + ->getPermissionState(origin, permissionType, PermissionManagerQt::deserializeToken(childId, serializedToken)); } QList<QWebEnginePermission> ProfileAdapter::listPermissions(const QUrl &origin, QWebEnginePermission::PermissionType permissionType) diff --git a/src/core/profile_adapter.h b/src/core/profile_adapter.h index 36d286851..22dd65973 100644 --- a/src/core/profile_adapter.h +++ b/src/core/profile_adapter.h @@ -16,6 +16,7 @@ #define PROFILE_ADAPTER_H #include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> +#include <QtWebEngineCore/private/qwebenginepermission_p.h> #include <QHash> #include <QList> @@ -187,9 +188,9 @@ public: UserResourceControllerHost *userResourceController(); void setPermission(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - QWebEnginePermission::State state, content::RenderFrameHost *rfh = nullptr); + QWebEnginePermission::State state, int childId = -1, const std::string &serializedToken = std::string()); QWebEnginePermission::State getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, - content::RenderFrameHost *rfh = nullptr); + int childId = -1, const std::string &serializedToken = std::string()); QList<QWebEnginePermission> listPermissions(const QUrl &origin = QUrl(), QWebEnginePermission::PermissionType permissionType = QWebEnginePermission::PermissionType::Unsupported); diff --git a/src/core/web_contents_adapter.cpp b/src/core/web_contents_adapter.cpp index 8c4d01e58..685b2acfe 100644 --- a/src/core/web_contents_adapter.cpp +++ b/src/core/web_contents_adapter.cpp @@ -16,6 +16,7 @@ #include "find_text_helper.h" #include "media_capture_devices_dispatcher.h" #include "pdf_util_qt.h" +#include "permission_manager_qt.h" #include "profile_adapter.h" #include "profile_qt.h" #include "qwebengineloadinginfo.h" @@ -1417,17 +1418,18 @@ QSizeF WebContentsAdapter::lastContentsSize() const return QSizeF(); } -void WebContentsAdapter::setPermission(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, QWebEnginePermission::State state) +void WebContentsAdapter::setPermission( + const QUrl &origin, + QWebEnginePermission::PermissionType permissionType, + QWebEnginePermission::State state, + int childId, const std::string &serializedToken) { + auto *manager = static_cast<PermissionManagerQt*>(m_profileAdapter->profile()->GetPermissionControllerDelegate()); + if (QWebEnginePermission::isPersistent(permissionType)) { // Do not check for initialization in this path so permissions can be set before first navigation Q_ASSERT(m_profileAdapter); - if (!isInitialized()) { - m_profileAdapter->setPermission(origin, permissionType, state); - } else { - m_profileAdapter->setPermission(origin, permissionType, state, m_webContents.get()->GetPrimaryMainFrame()); - } - + manager->setPermission(origin, permissionType, state, childId, serializedToken); return; } @@ -1440,115 +1442,87 @@ void WebContentsAdapter::setPermission(const QUrl &origin, QWebEnginePermission: // Do nothing break; case QWebEnginePermission::State::Denied: - grantMouseLockPermission(origin, false); + grantMouseLockPermission(origin, childId, serializedToken, false); break; case QWebEnginePermission::State::Granted: - grantMouseLockPermission(origin, true); + grantMouseLockPermission(origin, childId, serializedToken, true); break; } return; } - const WebContentsAdapterClient::MediaRequestFlags audioVideoCaptureFlags( - WebContentsAdapterClient::MediaVideoCapture | - WebContentsAdapterClient::MediaAudioCapture); - const WebContentsAdapterClient::MediaRequestFlags desktopAudioVideoCaptureFlags( - WebContentsAdapterClient::MediaDesktopVideoCapture | - WebContentsAdapterClient::MediaDesktopAudioCapture); - - switch (state) { - case QWebEnginePermission::State::Invalid: - case QWebEnginePermission::State::Ask: - // Do nothing - return; - case QWebEnginePermission::State::Denied: - // Deny all media access - grantMediaAccessPermission(origin, WebContentsAdapterClient::MediaNone); - return; - case QWebEnginePermission::State::Granted: - // Enable only the requested capture type - break; - } + // If we reach this, we must be handling media access permissions + manager->setPermission(origin, permissionType, state, childId, serializedToken); - switch (permissionType) { - case QWebEnginePermission::PermissionType::MediaAudioVideoCapture: - grantMediaAccessPermission(origin, audioVideoCaptureFlags); - break; - case QWebEnginePermission::PermissionType::MediaAudioCapture: - grantMediaAccessPermission(origin, WebContentsAdapterClient::MediaAudioCapture); - break; - case QWebEnginePermission::PermissionType::MediaVideoCapture: - grantMediaAccessPermission(origin, WebContentsAdapterClient::MediaVideoCapture); - break; - case QWebEnginePermission::PermissionType::DesktopAudioVideoCapture: - grantMediaAccessPermission(origin, desktopAudioVideoCaptureFlags); - break; - case QWebEnginePermission::PermissionType::DesktopVideoCapture: - grantMediaAccessPermission(origin, WebContentsAdapterClient::MediaDesktopVideoCapture); - break; - default: - Q_UNREACHABLE(); - break; + WebContentsAdapterClient::MediaRequestFlags flags = WebContentsAdapterClient::MediaNone; + if (state == QWebEnginePermission::State::Granted) { + switch (permissionType) { + case QWebEnginePermission::PermissionType::MediaAudioCapture: + flags.setFlag(WebContentsAdapterClient::MediaAudioCapture); + break; + case QWebEnginePermission::PermissionType::MediaVideoCapture: + flags.setFlag(WebContentsAdapterClient::MediaVideoCapture); + break; + case QWebEnginePermission::PermissionType::MediaAudioVideoCapture: + flags.setFlag(WebContentsAdapterClient::MediaAudioCapture); + flags.setFlag(WebContentsAdapterClient::MediaVideoCapture); + break; + case QWebEnginePermission::PermissionType::DesktopVideoCapture: + flags.setFlag(WebContentsAdapterClient::MediaDesktopVideoCapture); + break; + case QWebEnginePermission::PermissionType::DesktopAudioVideoCapture: + flags.setFlag(WebContentsAdapterClient::MediaDesktopAudioCapture); + flags.setFlag(WebContentsAdapterClient::MediaDesktopVideoCapture); + break; + default: + break; + } } -} -QWebEnginePermission::State WebContentsAdapter::getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType) -{ - return m_profileAdapter->getPermissionState(origin, permissionType, m_webContents.get()->GetPrimaryMainFrame()); -} - -void WebContentsAdapter::grantMediaAccessPermission(const QUrl &origin, WebContentsAdapterClient::MediaRequestFlags flags) -{ - CHECK_INITIALIZED(); - // Let the permission manager remember the reply. - if (flags & WebContentsAdapterClient::MediaAudioCapture) - m_profileAdapter->setPermission(origin, - QWebEnginePermission::PermissionType::MediaAudioCapture, - QWebEnginePermission::State::Granted, - m_webContents.get()->GetPrimaryMainFrame()); - if (flags & WebContentsAdapterClient::MediaVideoCapture) - m_profileAdapter->setPermission(origin, - QWebEnginePermission::PermissionType::MediaVideoCapture, - QWebEnginePermission::State::Granted, - m_webContents.get()->GetPrimaryMainFrame()); MediaCaptureDevicesDispatcher::GetInstance()->handleMediaAccessPermissionResponse(m_webContents.get(), origin, flags); } -void WebContentsAdapter::grantMouseLockPermission(const QUrl &securityOrigin, bool granted) +void WebContentsAdapter::grantMouseLockPermission(const QUrl &securityOrigin, int childId, + const std::string &serializedToken, bool granted) { CHECK_INITIALIZED(); - if (securityOrigin != toQt(m_webContents->GetLastCommittedURL().DeprecatedGetOriginAsURL())) - return; - if (granted) { - if (RenderWidgetHostViewQt *rwhv = static_cast<RenderWidgetHostViewQt *>(m_webContents->GetRenderWidgetHostView())) { - rwhv->Focus(); - if (!rwhv->HasFocus()) { - // We tried to activate our RWHVQtDelegate, but we failed. This probably means that - // the permission was granted from a modal dialog and the windowing system is not ready - // to set focus on the originating view. Since pointer lock strongly requires it, we just - // wait until the next FocusIn event. - m_pendingMouseLockPermissions.insert(securityOrigin, granted); - return; - } - } else - granted = false; + bool focused = false; + if (RenderWidgetHostViewQt *rwhv = static_cast<RenderWidgetHostViewQt *>(m_webContents->GetRenderWidgetHostView())) { + rwhv->Focus(); + if (rwhv->HasFocus()) { + focused = true; + } + } else { + granted = false; } - m_webContents->GotResponseToPointerLockRequest(granted ? blink::mojom::PointerLockResult::kSuccess - : blink::mojom::PointerLockResult::kPermissionDenied); + m_pendingMouseLockPermissions.enqueue({ securityOrigin, granted, childId, serializedToken }); + + if (focused) { + handlePendingMouseLockPermission(); + } } void WebContentsAdapter::handlePendingMouseLockPermission() { CHECK_INITIALIZED(); - auto it = m_pendingMouseLockPermissions.find(toQt(m_webContents->GetLastCommittedURL().DeprecatedGetOriginAsURL())); - if (it != m_pendingMouseLockPermissions.end()) { - m_webContents->GotResponseToPointerLockRequest(it.value() ? blink::mojom::PointerLockResult::kSuccess - : blink::mojom::PointerLockResult::kPermissionDenied); - m_pendingMouseLockPermissions.erase(it); - } + if (!m_pendingMouseLockPermissions.size()) + return; + + auto pending = m_pendingMouseLockPermissions.dequeue(); + + // Simply set the permission in the manager. The callback from WebContentsDelegateQt::RequestPointerLock() + // will ensure WebContents receives the response + auto *manager = static_cast<PermissionManagerQt*>(m_profileAdapter->profile()->GetPermissionControllerDelegate()); + manager->setPermission( + get<0>(pending), // origin + QWebEnginePermission::PermissionType::MouseLock, + get<1>(pending) // granted + ? QWebEnginePermission::State::Granted : QWebEnginePermission::State::Denied, + get<2>(pending), // childId + get<3>(pending)); // serializedToken } void WebContentsAdapter::setBackgroundColor(const QColor &color) diff --git a/src/core/web_contents_adapter.h b/src/core/web_contents_adapter.h index 3bb639b1b..212411109 100644 --- a/src/core/web_contents_adapter.h +++ b/src/core/web_contents_adapter.h @@ -21,6 +21,7 @@ #include <QtCore/QUrl> #include <QtCore/QVariant> #include <QtCore/QPointer> +#include <QtCore/QQueue> #include <QtGui/qtgui-config.h> #include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> #include <QtWebEngineCore/qwebenginecontextmenurequest.h> @@ -48,6 +49,7 @@ namespace content { class WebContents; class SiteInstance; class RenderFrameHost; +struct GlobalRenderFrameHostToken; } QT_BEGIN_NAMESPACE @@ -179,11 +181,9 @@ public: void devToolsFrontendDestroyed(DevToolsFrontendQt *frontend); QString devToolsId(); - void setPermission(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, QWebEnginePermission::State state); - QWebEnginePermission::State getPermissionState(const QUrl &origin, QWebEnginePermission::PermissionType permissionType); - - void grantMediaAccessPermission(const QUrl &origin, WebContentsAdapterClient::MediaRequestFlags flags); - void grantMouseLockPermission(const QUrl &origin, bool granted); + void setPermission(const QUrl &origin, QWebEnginePermission::PermissionType permissionType, + QWebEnginePermission::State state, int childId = -1, const std::string &serializedToken = std::string()); + void grantMouseLockPermission(const QUrl &origin, int childId, const std::string &serializedToken, bool granted); void handlePendingMouseLockPermission(); void setBackgroundColor(const QColor &color); @@ -272,7 +272,7 @@ private: #endif WebContentsAdapterClient *m_adapterClient; quint64 m_nextRequestId; - QMap<QUrl, bool> m_pendingMouseLockPermissions; + QQueue<std::tuple<QUrl, bool, int, std::string>> m_pendingMouseLockPermissions; QMap<quint64, std::function<void(const QVariant &)>> m_javaScriptCallbacks; std::map<quint64, std::function<void(QSharedPointer<QByteArray>)>> m_printCallbacks; std::unique_ptr<content::DropData> m_currentDropData; diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h index 9ce5bebfc..2f93d4783 100644 --- a/src/core/web_contents_adapter_client.h +++ b/src/core/web_contents_adapter_client.h @@ -16,6 +16,7 @@ #define WEB_CONTENTS_ADAPTER_CLIENT_H #include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> +#include <QtWebEngineCore/private/qwebenginepermission_p.h> #include <QtWebEngineCore/qwebenginepermission.h> #include "profile_adapter.h" @@ -195,9 +196,8 @@ public: virtual QObject *accessibilityParentObject() = 0; virtual void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID) = 0; virtual void authenticationRequired(QSharedPointer<AuthenticationDialogController>) = 0; - virtual void runFeaturePermissionRequest(QWebEnginePermission::PermissionType, const QUrl &securityOrigin) = 0; - virtual void runMediaAccessPermissionRequest(const QUrl &securityOrigin, MediaRequestFlags requestFlags) = 0; - virtual void runMouseLockPermissionRequest(const QUrl &securityOrigin) = 0; + virtual void runFeaturePermissionRequest(QWebEnginePermission::PermissionType, const QUrl &securityOrigin, + int childId, const std::string &serializedToken) = 0; virtual void runRegisterProtocolHandlerRequest(QWebEngineRegisterProtocolHandlerRequest) = 0; virtual void runFileSystemAccessRequest(QWebEngineFileSystemAccessRequest) = 0; virtual QWebEngineSettings *webEngineSettings() const = 0; @@ -229,7 +229,6 @@ public: virtual WebContentsAdapter* webContentsAdapter() = 0; virtual void releaseProfile() = 0; virtual void showWebAuthDialog(QWebEngineWebAuthUxRequest *request) = 0; - virtual QWebEnginePermission createFeaturePermissionObject(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) = 0; }; } // namespace QtWebEngineCore diff --git a/src/core/web_contents_delegate_qt.cpp b/src/core/web_contents_delegate_qt.cpp index 89a0a6582..77ba5ec91 100644 --- a/src/core/web_contents_delegate_qt.cpp +++ b/src/core/web_contents_delegate_qt.cpp @@ -19,6 +19,7 @@ #include "javascript_dialog_manager_qt.h" #include "media_capture_devices_dispatcher.h" #include "native_web_keyboard_event_qt.h" +#include "permission_manager_qt.h" #include "profile_adapter.h" #include "profile_qt.h" #include "qwebengineloadinginfo.h" @@ -418,6 +419,12 @@ void WebContentsDelegateQt::emitLoadCommitted() void WebContentsDelegateQt::DidFinishNavigation(content::NavigationHandle *navigation_handle) { + if (navigation_handle->HasCommitted() && !navigation_handle->IsSameOrigin()) { + PermissionManagerQt *permissionManager = static_cast<PermissionManagerQt *>( + navigation_handle->GetWebContents()->GetBrowserContext()->GetPermissionControllerDelegate()); + permissionManager->onCrossOriginNavigation(navigation_handle->GetRenderFrameHost()); + } + if (!navigation_handle->IsInMainFrame()) return; @@ -731,14 +738,32 @@ void WebContentsDelegateQt::ActivateContents(content::WebContents* contents) void WebContentsDelegateQt::RequestPointerLock(content::WebContents *web_contents, bool user_gesture, bool last_unlocked_by_target) { - Q_UNUSED(user_gesture); - if (last_unlocked_by_target) web_contents->GotResponseToPointerLockRequest(blink::mojom::PointerLockResult::kSuccess); - else - m_viewClient->runMouseLockPermissionRequest(toQt(web_contents->GetLastCommittedURL().DeprecatedGetOriginAsURL())); + else { + PermissionManagerQt *permissionManager = static_cast<PermissionManagerQt *>( + web_contents->GetBrowserContext()->GetPermissionControllerDelegate()); + + auto *rfh = web_contents->GetFocusedFrame(); + if (!rfh) + rfh = web_contents->GetPrimaryMainFrame(); + + permissionManager->RequestPermissions( + rfh, + content::PermissionRequestDescription(blink::PermissionType::POINTER_LOCK, user_gesture, rfh->GetLastCommittedOrigin().GetURL()), + base::BindOnce([](content::WebContents *web_contents, PermissionManagerQt *manager, const std::vector<blink::mojom::PermissionStatus> &status) + { + Q_ASSERT(status.size() == 1); + + web_contents->GotResponseToPointerLockRequest(status[0] == blink::mojom::PermissionStatus::GRANTED + ? blink::mojom::PointerLockResult::kSuccess + : blink::mojom::PointerLockResult::kPermissionDenied); + }, web_contents, permissionManager) + ); + } } + void WebContentsDelegateQt::overrideWebPreferences(content::WebContents *webContents, blink::web_pref::WebPreferences *webPreferences) { WebEngineSettings::get(m_viewClient->webEngineSettings())->overrideWebPreferences(webContents, webPreferences); @@ -773,9 +798,12 @@ void WebContentsDelegateQt::selectClientCert(const QSharedPointer<ClientCertSele m_viewClient->selectClientCert(selectController); } -void WebContentsDelegateQt::requestFeaturePermission(QWebEnginePermission::PermissionType permissionType, const QUrl &requestingOrigin) +void WebContentsDelegateQt::requestFeaturePermission( + QWebEnginePermission::PermissionType permissionType, + const QUrl &requestingOrigin, + const content::GlobalRenderFrameHostToken &frameToken) { - m_viewClient->runFeaturePermissionRequest(permissionType, requestingOrigin); + m_viewClient->runFeaturePermissionRequest(permissionType, requestingOrigin, frameToken.child_id, frameToken.frame_token.ToString()); } extern WebContentsAdapterClient::NavigationType pageTransitionToNavigationType(ui::PageTransition transition); @@ -834,18 +862,22 @@ bool WebContentsDelegateQt::CheckMediaAccessPermission(content::RenderFrameHost blink::mojom::MediaStreamType type) { Q_ASSERT(rfh); + + auto token = rfh->GetGlobalFrameToken(); + std::string serializedToken = token.frame_token.ToString(); + switch (type) { case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE: return m_viewClient->profileAdapter()->getPermissionState( toQt(security_origin), QWebEnginePermission::PermissionType::MediaAudioCapture, - rfh) + token.child_id, serializedToken) == QWebEnginePermission::State::Granted; case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE: return m_viewClient->profileAdapter()->getPermissionState( toQt(security_origin), QWebEnginePermission::PermissionType::MediaVideoCapture, - rfh) + token.child_id, serializedToken) == QWebEnginePermission::State::Granted; default: LOG(INFO) << "WebContentsDelegateQt::CheckMediaAccessPermission: " diff --git a/src/core/web_contents_delegate_qt.h b/src/core/web_contents_delegate_qt.h index 099d8280b..383803c4d 100644 --- a/src/core/web_contents_delegate_qt.h +++ b/src/core/web_contents_delegate_qt.h @@ -25,6 +25,7 @@ class ColorChooser; class JavaScriptDialogManager; class WebContents; struct MediaStreamRequest; +struct GlobalRenderFrameHostToken; } namespace QtWebEngineCore { @@ -144,7 +145,7 @@ public: void overrideWebPreferences(content::WebContents *, blink::web_pref::WebPreferences*); void allowCertificateError(const QSharedPointer<CertificateErrorController> &); void selectClientCert(const QSharedPointer<ClientCertSelectController> &); - void requestFeaturePermission(QWebEnginePermission::PermissionType permissionType, const QUrl &requestingOrigin); + void requestFeaturePermission(QWebEnginePermission::PermissionType permissionType, const QUrl &requestingOrigin, const content::GlobalRenderFrameHostToken &frameToken); void launchExternalURL(const QUrl &url, ui::PageTransition page_transition, bool is_main_frame, bool has_user_gesture); FindTextHelper *findTextHelper(); diff --git a/src/webenginequick/api/qquickwebengineprofile.cpp b/src/webenginequick/api/qquickwebengineprofile.cpp index dce3c8822..80e4ec5ac 100644 --- a/src/webenginequick/api/qquickwebengineprofile.cpp +++ b/src/webenginequick/api/qquickwebengineprofile.cpp @@ -1229,7 +1229,7 @@ QWebEnginePermission QQuickWebEngineProfile::queryPermission(const QUrl &securit return QWebEnginePermission(new QWebEnginePermissionPrivate()); } - auto *pvt = new QWebEnginePermissionPrivate(securityOrigin, permissionType, nullptr, d->profileAdapter()); + auto *pvt = new QWebEnginePermissionPrivate(securityOrigin, permissionType, d->profileAdapter()); return QWebEnginePermission(pvt); } 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/src/webenginequick/api/qquickwebengineview.cpp b/src/webenginequick/api/qquickwebengineview.cpp index ade8b451c..619cbaef6 100644 --- a/src/webenginequick/api/qquickwebengineview.cpp +++ b/src/webenginequick/api/qquickwebengineview.cpp @@ -521,22 +521,30 @@ static QQuickWebEngineView::Feature toDeprecatedFeature(QWebEnginePermission::Pe QT_WARNING_POP #endif // QT_DEPRECATED_SINCE(6, 8) -void QQuickWebEngineViewPrivate::runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin) +void QQuickWebEngineViewPrivate::runFeaturePermissionRequest( + QWebEnginePermission::PermissionType permissionType, + const QUrl &securityOrigin, + int childId, const std::string &serializedToken) { Q_Q(QQuickWebEngineView); - if (QWebEnginePermission::isPersistent(permissionType)) { - Q_EMIT q->permissionRequested(createFeaturePermissionObject(securityOrigin, permissionType)); -#if QT_DEPRECATED_SINCE(6, 8) - QT_WARNING_PUSH - QT_WARNING_DISABLE_DEPRECATED - Q_EMIT q->featurePermissionRequested(securityOrigin, toDeprecatedFeature(permissionType)); - QT_WARNING_POP -#endif // QT_DEPRECATED_SINCE(6, 8) + if (permissionType == QWebEnginePermission::PermissionType::MouseLock) { + // Not supported in Qt Quick + auto permission = QWebEnginePermission( + new QWebEnginePermissionPrivate(securityOrigin, permissionType, profileAdapter(), childId, serializedToken)); + permission.deny(); return; } - Q_UNREACHABLE(); + Q_EMIT q->permissionRequested(QWebEnginePermission( + new QWebEnginePermissionPrivate(securityOrigin, permissionType, profileAdapter(), childId, serializedToken))); +#if QT_DEPRECATED_SINCE(6, 8) + QT_WARNING_PUSH + QT_WARNING_DISABLE_DEPRECATED + Q_EMIT q->featurePermissionRequested(securityOrigin, toDeprecatedFeature(permissionType)); + QT_WARNING_POP +#endif // QT_DEPRECATED_SINCE(6, 8) + return; } void QQuickWebEngineViewPrivate::showColorDialog(QSharedPointer<ColorChooserController> controller) @@ -809,54 +817,6 @@ void QQuickWebEngineViewPrivate::authenticationRequired(QSharedPointer<Authentic ui()->showDialog(controller); } -void QQuickWebEngineViewPrivate::runMediaAccessPermissionRequest(const QUrl &securityOrigin, WebContentsAdapterClient::MediaRequestFlags requestFlags) -{ - Q_Q(QQuickWebEngineView); - if (!requestFlags) - return; - QWebEnginePermission::PermissionType permissionType; - if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) && requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaAudioVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaAudioCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::MediaVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) && - requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::DesktopAudioVideoCapture; - else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - permissionType = QWebEnginePermission::PermissionType::DesktopVideoCapture; - Q_EMIT q->permissionRequested(createFeaturePermissionObject(securityOrigin, permissionType)); - -#if QT_DEPRECATED_SINCE(6, 8) - QT_WARNING_PUSH - QT_WARNING_DISABLE_DEPRECATED - QQuickWebEngineView::Feature deprecatedFeature; - - if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - deprecatedFeature = QQuickWebEngineView::MediaAudioVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) - deprecatedFeature = QQuickWebEngineView::MediaAudioCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - deprecatedFeature = QQuickWebEngineView::MediaVideoCapture; - else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) - && requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - deprecatedFeature = QQuickWebEngineView::DesktopAudioVideoCapture; - else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) - deprecatedFeature = QQuickWebEngineView::DesktopVideoCapture; - - Q_EMIT q->featurePermissionRequested(securityOrigin, deprecatedFeature); - QT_WARNING_POP -#endif // QT_DEPRECATED_SINCE(6, 8) -} - -void QQuickWebEngineViewPrivate::runMouseLockPermissionRequest(const QUrl &securityOrigin) -{ - // TODO: Add mouse lock support - adapter->grantMouseLockPermission(securityOrigin, false); -} - void QQuickWebEngineViewPrivate::runRegisterProtocolHandlerRequest(QWebEngineRegisterProtocolHandlerRequest request) { Q_Q(QQuickWebEngineView); @@ -1523,12 +1483,6 @@ void QQuickWebEngineViewPrivate::showWebAuthDialog(QWebEngineWebAuthUxRequest *r Q_EMIT q->webAuthUxRequested(request); } -QWebEnginePermission QQuickWebEngineViewPrivate::createFeaturePermissionObject(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) -{ - auto *returnPrivate = new QWebEnginePermissionPrivate(securityOrigin, permissionType, adapter, profileAdapter()); - return QWebEnginePermission(returnPrivate); -} - bool QQuickWebEngineView::isLoading() const { Q_D(const QQuickWebEngineView); diff --git a/src/webenginequick/api/qquickwebengineview_p_p.h b/src/webenginequick/api/qquickwebengineview_p_p.h index d78157597..8fe98145b 100644 --- a/src/webenginequick/api/qquickwebengineview_p_p.h +++ b/src/webenginequick/api/qquickwebengineview_p_p.h @@ -105,8 +105,6 @@ public: bool passOnFocus(bool reverse) override; void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID) override; void authenticationRequired(QSharedPointer<QtWebEngineCore::AuthenticationDialogController>) override; - void runMediaAccessPermissionRequest(const QUrl &securityOrigin, MediaRequestFlags requestFlags) override; - void runMouseLockPermissionRequest(const QUrl &securityOrigin) override; void runRegisterProtocolHandlerRequest(QWebEngineRegisterProtocolHandlerRequest) override; void runFileSystemAccessRequest(QWebEngineFileSystemAccessRequest) override; QObject *accessibilityParentObject() override; @@ -114,7 +112,8 @@ public: void allowCertificateError(const QWebEngineCertificateError &error) override; void selectClientCert(const QSharedPointer<QtWebEngineCore::ClientCertSelectController> &selectController) override; - void runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin) override; + void runFeaturePermissionRequest(QWebEnginePermission::PermissionType permissionType, const QUrl &securityOrigin, + int childId, const std::string &serializedToken) override; void renderProcessTerminated(RenderProcessTerminationStatus terminationStatus, int exitCode) override; void requestGeometryChange(const QRect &geometry, const QRect &frameGeometry) override; void updateScrollPosition(const QPointF &position) override; @@ -139,7 +138,6 @@ public: const QRect &bounds, bool autoselectFirstSuggestion) override; void hideAutofillPopup() override; void showWebAuthDialog(QWebEngineWebAuthUxRequest *request) override; - QWebEnginePermission createFeaturePermissionObject(const QUrl &securityOrigin, QWebEnginePermission::PermissionType permissionType) override; void updateAction(QQuickWebEngineView::WebAction) const; bool adoptWebContents(QtWebEngineCore::WebContentsAdapter *webContents); diff --git a/tests/auto/core/qwebengineframe/tst_qwebengineframe.cpp b/tests/auto/core/qwebengineframe/tst_qwebengineframe.cpp index c176be70e..d4d6cab99 100644 --- a/tests/auto/core/qwebengineframe/tst_qwebengineframe.cpp +++ b/tests/auto/core/qwebengineframe/tst_qwebengineframe.cpp @@ -109,7 +109,7 @@ void tst_QWebEngineFrame::htmlName() QWebEnginePage page; QSignalSpy loadSpy{ &page, SIGNAL(loadFinished(bool)) }; page.load(QUrl("qrc:/resources/iframes.html")); - QTRY_COMPARE(loadSpy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 10000); auto children = page.mainFrame().children(); QCOMPARE(children.at(0).name(), "test-subframe0"); QCOMPARE(children.at(0).htmlName(), "iframe0-300x200"); 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; diff --git a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp index 1dfa94565..1c2d761fb 100644 --- a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp +++ b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp @@ -300,7 +300,7 @@ void tst_QWebEngineScript::viewSource() page.scripts().insert(script); page.load(QUrl("view-source:about:blank")); QSignalSpy spy(&page, &QWebEnginePage::loadFinished); - QTRY_COMPARE(spy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 10000); QCOMPARE(spy.takeFirst().value(0).toBool(), true); QCOMPARE(evaluateJavaScriptSync(&page, "foo"), QVariant(42)); } @@ -314,12 +314,12 @@ void tst_QWebEngineScript::scriptModifications() script.setWorldId(QWebEngineScript::MainWorld); script.setSourceCode("var foo = \"SUCCESS\";"); page.scripts().insert(script); + QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); page.setHtml(QStringLiteral("<html><head><script>document.addEventListener(\"DOMContentLoaded\", function() {\ document.body.innerText = foo;});\ </script></head><body></body></html>")); QVERIFY(page.scripts().count() == 1); - QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); - QVERIFY(spyFinished.wait()); + QTRY_VERIFY_WITH_TIMEOUT(spyFinished.size(), 10000); QCOMPARE(evaluateJavaScriptSync(&page, "document.body.innerText"), QVariant::fromValue(QStringLiteral("SUCCESS"))); script.setSourceCode("var foo = \"FAILURE\""); page.triggerAction(QWebEnginePage::ReloadAndBypassCache); @@ -403,9 +403,9 @@ void tst_QWebEngineScript::webChannel() QWebEngineScript script = webChannelScript(); script.setWorldId(worldId); page.scripts().insert(script); - page.setHtml(QStringLiteral("<html><body></body></html>")); QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); - QVERIFY(spyFinished.wait()); + page.setHtml(QStringLiteral("<html><body></body></html>")); + QTRY_VERIFY_WITH_TIMEOUT(spyFinished.size(), 10000); if (reloadFirst) { // Check that the transport is also reinstalled on navigation page.triggerAction(QWebEnginePage::Reload); @@ -572,7 +572,7 @@ void tst_QWebEngineScript::navigation() QString url3 = QStringLiteral("qrc:/resources/test_iframe_main.html"); page.setUrl(url3); - QTRY_COMPARE(spyTextChanged.size(), 3); + QTRY_COMPARE_WITH_TIMEOUT(spyTextChanged.size(), 3, 10000); QCOMPARE(testObject.text(), url3); page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp index 3c677c3e9..92d8fc1eb 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp @@ -400,7 +400,7 @@ void tst_QWebEngineView::changePage() QSignalSpy pageFromLoadSpy(pageFrom.get(), &QWebEnginePage::loadFinished); QSignalSpy pageFromIconLoadSpy(pageFrom.get(), &QWebEnginePage::iconChanged); pageFrom->load(urlFrom); - QTRY_COMPARE(pageFromLoadSpy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(pageFromLoadSpy.size(), 1, 10000); QCOMPARE(pageFromLoadSpy.last().value(0).toBool(), true); if (!fromIsNullPage) { QTRY_COMPARE(pageFromIconLoadSpy.size(), 1); @@ -1271,7 +1271,7 @@ void tst_QWebEngineView::focusInternalRenderWidgetHostViewQuickItem() webView->setHtml("<html><body>" " <input id='input1' type='text'/>" "</body></html>"); - QTRY_COMPARE(loadSpy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 10000); QTRY_COMPARE(webView->hasFocus(), false); // Manually trigger focus. @@ -1404,7 +1404,7 @@ void tst_QWebEngineView::mixLangLocale() auto sc = connect(view.page(), &QWebEnginePage::renderProcessTerminated, [&] () { terminated = true; }); view.load(QUrl("qrc:///resources/dummy.html")); - QTRY_VERIFY(terminated || loadSpy.size() == 1); + QTRY_VERIFY_WITH_TIMEOUT(terminated || loadSpy.size() == 1, 10000); QVERIFY2(!terminated, qPrintable(QString("Locale [%1] terminated: %2, loaded: %3").arg(locale).arg(terminated).arg(loadSpy.size()))); @@ -1622,7 +1622,7 @@ void tst_QWebEngineView::keyboardFocusAfterPopup() // Trigger QCompleter's popup and select the first suggestion. QTest::keyClick(QApplication::focusWindow(), Qt::Key_T); - QTRY_VERIFY(QApplication::activePopupWidget()); + QTRY_VERIFY_WITH_TIMEOUT(QApplication::activePopupWidget(), 10000); QTest::keyClick(QApplication::focusWindow(), Qt::Key_Down); QTest::keyClick(QApplication::focusWindow(), Qt::Key_Enter); @@ -2071,7 +2071,7 @@ void tst_QWebEngineView::inputContextQueryInput() view.setHtml("<html><body>" " <input type='text' id='input1' value='' size='50'/>" "</body></html>"); - QTRY_COMPARE(loadFinishedSpy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.size(), 1, 10000); QVERIFY(QTest::qWaitForWindowActive(&view)); QCOMPARE(testContext.infos.size(), 0); @@ -2223,7 +2223,7 @@ void tst_QWebEngineView::inputMethods() view.setHtml("<html><body>" " <input type='text' id='input1' style='font-family: serif' value='' maxlength='20' size='50'/>" "</body></html>"); - QTRY_COMPARE(loadFinishedSpy.size(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.size(), 1, 10000); QVERIFY(QTest::qWaitForWindowExposed(&view)); QPoint textInputCenter = elementCenter(view.page(), "input1"); @@ -2405,7 +2405,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() view.setHtml("<html><body>" " This is a text" "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(selectionChangedSpy.size(), 0); @@ -2499,7 +2499,7 @@ void tst_QWebEngineView::hiddenText() " <input type='text' id='input1' value='QtWebEngine' size='50'/><br>" " <input type='password' id='password1'/>" "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); QPoint passwordInputCenter = elementCenter(view.page(), "password1"); QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, passwordInputCenter); @@ -2526,7 +2526,7 @@ void tst_QWebEngineView::emptyInputMethodEvent() view.setHtml("<html><body>" " <input type='text' id='input1' value='QtWebEngine'/>" "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); @@ -2924,7 +2924,7 @@ void tst_QWebEngineView::imeJSInputEvents() " <div id='input' contenteditable='true' style='border-style: solid;'></div>" " <pre id='log'></pre>" "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "document.getElementById('input').focus()"); @@ -3331,7 +3331,7 @@ void tst_QWebEngineView::mouseLeave() " <div id='testDiv' style='width: 100%; height: 100%; background-color: green' />" "</body>" "</html>"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); // Make sure the testDiv text is empty. evaluateJavaScriptSync(view->page(), "document.getElementById('testDiv').innerText = ''"); QTRY_VERIFY(innerText().isEmpty()); @@ -4015,7 +4015,7 @@ void tst_QWebEngineView::longKeyEventText() view.resize(200, 400); view.show(); view.setHtml(html); - QTRY_VERIFY(loadFinishedSpy.size()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); QSignalSpy consoleMessageSpy(&page, &ConsolePage::done); Qt::Key key(Qt::Key_Shift); QKeyEvent event(QKeyEvent::KeyPress, key, Qt::NoModifier, QKeySequence(key).toString()); @@ -4035,7 +4035,7 @@ void tst_QWebEngineView::deferredDelete() QSignalSpy loadFinishedSpy(view.page(), &QWebEnginePage::loadFinished); view.load(QUrl("chrome://qt")); view.show(); - QTRY_VERIFY(loadFinishedSpy.size()); + QTRY_VERIFY_WITH_TIMEOUT(loadFinishedSpy.size(), 10000); // QWebEngineView and WebEngineQuickWidget QCOMPARE(QApplication::allWidgets().size(), desktopWidget + 2); } @@ -4076,7 +4076,7 @@ void tst_QWebEngineView::setCursorOnEmbeddedView() QVERIFY(QTest::qWaitForWindowActive(&parentWidget)); - QTRY_VERIFY(firstPaintSpy.size()); + QTRY_VERIFY_WITH_TIMEOUT(firstPaintSpy.size(), 10000); const QPoint step = QPoint(25, 25); QPoint cursorPos = view.pos() - step; |