Revision control

++ // 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
++ // Qt-Security score:significant reason:default
++
++ #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 err)
++ {
++ if (err["name"].as<std::string>() == "NotAllowedError"
++ || err["name"].as<std::string>() == "NotFoundError")
++ return updatePermission(permissionName, wapiDenied, {});
++
++ qCInfo(lcPermissions, "'%s' '%s'", err["name"].as<std::string>().c_str(),
++ err["message"].as<std::string>().c_str());
++ };
++
++ 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 stream)
++ {
++ // turn stream/light off
++ if (!stream.isNull() && !stream.isUndefined() && !stream["getTracks"].isUndefined()) {
++ emscripten::val tracks = stream.call<emscripten::val>("getTracks");
++ if (!tracks.isUndefined() && tracks["length"].as<int>() > 0) {
++ for (int i = 0; i < tracks["length"].as<int>(); i++) {
++ tracks[i].call<void>("stop");
++ }
++ }
++ }
++ stream = emscripten::val::null(); // this causes the garbage collection to turn off media light
++
++ updatePermission(device, wapiGranted, cb);
++ };
++ queryCallbacks.catchFunc = [device, cb](val error)
++ {
++ qCInfo(lcPermissions) << "error"
++ << error["name"].as<std::string>();
++
++ 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);
++
++ 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.value<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