Add permission backend for WASM

The only permissions we support so far are camera, microphone,
and location. The permission API works even for browsers that
don't provide the Web Permission API, as we plumb the individual
permission requests for media and geolocation back to our API.

Change-Id: I7f5fc2266afee9ada78f2015614a8224e28afa59
Reviewed-by: Mikołaj Boc <Mikolaj.Boc@qt.io>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Tor Arne Vestbø 2022-05-18 13:42:33 +02:00 committed by Timur Pocheptsov
parent b4ef0031c6
commit 1ccb0bd3ef
3 changed files with 285 additions and 1 deletions

View File

@ -1230,6 +1230,12 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_permissions AND ANDROID
kernel/qpermissions_android.cpp
)
qt_internal_extend_target(Core CONDITION QT_FEATURE_permissions AND WASM
SOURCES
kernel/qpermissions_wasm.cpp
)
#### Keys ignored in scope 171:.:mimetypes:mimetypes/mimetypes.pri:QT_FEATURE_mimetype:
# MIME_DATABASE = "mimetypes/mime/packages/freedesktop.org.xml"
# OTHER_FILES = "$$MIME_DATABASE"

View File

@ -951,7 +951,7 @@ qt_feature("permissions" PUBLIC
SECTION "Utilities"
LABEL "Application permissions"
PURPOSE "Provides support for requesting user permission to access restricted data or APIs"
CONDITION APPLE OR ANDROID
CONDITION APPLE OR ANDROID OR WASM
)
qt_configure_add_summary_section(NAME "Qt Core")
qt_configure_add_summary_entry(ARGS "backtrace")

View File

@ -0,0 +1,278 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <private/qpermissions_p.h>
#include <private/qstdweb_p.h>
#include <qglobalstatic.h>
#include <qpermissions.h>
#include <qmetaobject.h>
#include <qnamespace.h>
#include <qmetatype.h>
#include <qstring.h>
#include <qtimer.h>
#include <qhash.h>
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <utility>
#include <string>
#include <queue>
QT_BEGIN_NAMESPACE
using namespace QPermissions::Private;
using namespace emscripten;
namespace
{
constexpr const char *wapiGranted = "granted";
constexpr const char *wapiDenied = "denied";
constexpr const char *wapiPrompt = "prompt";
constexpr const char *wapiCamera = "camera";
constexpr const char *wapiVideoCapture = "video_capture"; // Alternative to "camera".
constexpr const char *wapiMicrophone = "microphone";
constexpr const char *wapiAudioCapture = "audio_capture"; // Alternative to "microphone".
constexpr const char *wapiGeolocation = "geolocation";
void updatePermission(const std::string &name, const std::string &state,
PermissionCallback callback);
void checkPermission(const std::string &permissionName)
{
val permissions = val::global("navigator")["permissions"];
if (permissions.isUndefined() || permissions.isNull())
return;
qstdweb::PromiseCallbacks callbacks;
callbacks.thenFunc = [permissionName](val permissionState)
{
updatePermission(permissionName, permissionState["state"].as<std::string>(), {});
};
callbacks.catchFunc = [permissionName](val)
{
updatePermission(permissionName, wapiDenied, {});
};
val query = val::object();
query.set("name", val(permissionName));
qstdweb::Promise::make(permissions, QStringLiteral("query"), callbacks, query);
}
void checkPermissions()
{
checkPermission(wapiCamera);
checkPermission(wapiMicrophone);
checkPermission(wapiGeolocation);
}
void bootstrapCheckPermissions()
{
QTimer::singleShot(0, []{checkPermissions();});
}
Q_CONSTRUCTOR_FUNCTION(bootstrapCheckPermissions);
int permissionTypeIdFromString(const std::string &permission)
{
if (permission == wapiCamera || permission == wapiVideoCapture)
return qMetaTypeId<QCameraPermission>();
if (permission == wapiMicrophone || permission == wapiAudioCapture)
return qMetaTypeId<QMicrophonePermission>();
if (permission == wapiGeolocation)
return qMetaTypeId<QLocationPermission>();
qCWarning(lcPermissions, "Unknown permission type '%s'", permission.c_str());
return -1;
}
Qt::PermissionStatus permissionStatusFromString(const std::string &state)
{
if (state == wapiGranted)
return Qt::PermissionStatus::Granted;
if (state == wapiDenied)
return Qt::PermissionStatus::Denied;
if (state == wapiPrompt)
return Qt::PermissionStatus::Undetermined;
qCWarning(lcPermissions, "Unknown permission state '%s'", state.c_str());
return Qt::PermissionStatus::Denied;
}
using PermissionHash = QHash<int, Qt::PermissionStatus>;
Q_GLOBAL_STATIC(PermissionHash, permissionStatuses);
void updatePermission(const std::string &name, const std::string &state, PermissionCallback callback)
{
qCDebug(lcPermissions) << "Updating" << name << "permission to" << state;
const int type = permissionTypeIdFromString(name);
if (type == -1)
return; // Unknown permission type
const auto status = permissionStatusFromString(state);
(*permissionStatuses)[type] = status;
if (callback)
callback(status);
}
void requestMediaDevicePermission(const std::string &device, const PermissionCallback &cb)
{
Q_ASSERT(cb);
val mediaDevices = val::global("navigator")["mediaDevices"];
if (mediaDevices.isUndefined())
return cb(Qt::PermissionStatus::Denied);
qstdweb::PromiseCallbacks queryCallbacks;
queryCallbacks.thenFunc = [device, cb](val mediaStream)
{
val tracks = mediaStream.call<val>("getTracks");
if (!tracks.isUndefined() && !tracks.isNull())
tracks[0].call<void>("stop");
updatePermission(device, wapiGranted, cb);
};
queryCallbacks.catchFunc = [device, cb](val error)
{
if (error["name"].as<std::string>() == "NotAllowedError")
return updatePermission(device, wapiDenied, cb);
updatePermission(device, wapiPrompt, cb);
};
val constraint = val::object();
if (device == wapiCamera)
constraint.set("video", true);
else
constraint.set("audio", true);
qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"), queryCallbacks, constraint);
}
using GeoRequest = std::pair<QPermission, PermissionCallback>;
Q_GLOBAL_STATIC(std::deque<GeoRequest>, geolocationRequestQueue);
bool processingLocationRequest = false;
void processNextGeolocationRequest();
void geolocationSuccess(val position)
{
Q_UNUSED(position);
Q_ASSERT(geolocationRequestQueue->size());
processingLocationRequest = false;
auto cb = geolocationRequestQueue->front().second;
geolocationRequestQueue->pop_front();
updatePermission(wapiGeolocation, wapiGranted, cb);
processNextGeolocationRequest();
}
void geolocationError(val error)
{
Q_ASSERT(geolocationRequestQueue->size());
static int deniedError = []
{
val posErr = val::global("GeolocationPositionError");
if (posErr.isUndefined() || posErr.isNull())
return 1;
return posErr["PERMISSION_DENIED"].as<int>();
}();
processingLocationRequest = false;
auto cb = geolocationRequestQueue->front().second;
geolocationRequestQueue->pop_front();
const int errorCode = error["code"].as<int>();
updatePermission(wapiGeolocation, errorCode == deniedError ? wapiDenied : wapiPrompt, cb);
processNextGeolocationRequest();
}
EMSCRIPTEN_BINDINGS(qt_permissions) {
function("qtLocationSuccess", &geolocationSuccess);
function("qtLocationError", &geolocationError);
}
void processNextGeolocationRequest()
{
if (processingLocationRequest)
return;
if (geolocationRequestQueue->empty())
return;
processingLocationRequest = true;
val geolocation = val::global("navigator")["geolocation"];
Q_ASSERT(!geolocation.isUndefined());
Q_ASSERT(!geolocation.isNull());
const auto &permission = geolocationRequestQueue->front().first;
const auto &locationPermission = permission.data<QLocationPermission>();
const bool highAccuracy = locationPermission.accuracy() == QLocationPermission::Precise;
val options = val::object();
options.set("enableHighAccuracy", highAccuracy ? true : false);
geolocation.call<void>("getCurrentPosition", val::module_property("qtLocationSuccess"),
val::module_property("qtLocationError"), options);
}
void requestGeolocationPermission(const QPermission &permission, const PermissionCallback &cb)
{
Q_ASSERT(cb);
Q_UNUSED(permission);
Q_UNUSED(cb);
val geolocation = val::global("navigator")["geolocation"];
if (geolocation.isUndefined() || geolocation.isNull())
return cb(Qt::PermissionStatus::Denied);
if (processingLocationRequest)
qCWarning(lcPermissions, "Permission to access location requested, while another request is in progress");
geolocationRequestQueue->push_back(std::make_pair(permission, cb));
processNextGeolocationRequest();
}
} // Unnamed namespace
namespace QPermissions::Private
{
Qt::PermissionStatus checkPermission(const QPermission &permission)
{
const auto it = permissionStatuses->find(permission.type().id());
return it != permissionStatuses->end() ? it.value() : Qt::PermissionStatus::Undetermined;
}
void requestPermission(const QPermission &permission, const PermissionCallback &callback)
{
Q_ASSERT(permission.type().isValid());
Q_ASSERT(callback);
const auto status = checkPermission(permission);
if (status != Qt::PermissionStatus::Undetermined)
return callback(status);
const int requestedTypeId = permission.type().id();
if (requestedTypeId == qMetaTypeId<QCameraPermission>())
return requestMediaDevicePermission(wapiCamera, callback);
if (requestedTypeId == qMetaTypeId<QMicrophonePermission>())
return requestMediaDevicePermission(wapiMicrophone, callback);
if (requestedTypeId == qMetaTypeId<QLocationPermission>())
return requestGeolocationPermission(permission, callback);
(*permissionStatuses)[requestedTypeId] = Qt::PermissionStatus::Denied;
callback(Qt::PermissionStatus::Denied);
}
}
QT_END_NAMESPACE