Revision control

Copy as Markdown

// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
// Copyright (C) 2019 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qmetaobjectpublisher_p.h"
#include "qwebchannel.h"
#include "qwebchannel_p.h"
#include "qwebchannelabstracttransport.h"
#include <QEvent>
#if QT_CONFIG(future)
#include <QFuture>
#endif
#include <QJsonDocument>
#include <QDebug>
#include <QJsonObject>
#include <QJsonArray>
#ifndef QT_NO_JSVALUE
#include <QJSValue>
#endif
#include <QUuid>
#include <QtCore/private/qmetaobject_p.h>
QT_BEGIN_NAMESPACE
namespace {
// FIXME: QFlags don't have the QMetaType::IsEnumeration flag set, although they have a QMetaEnum entry in the QMetaObject.
// They only way to detect registered QFlags types is to find the named entry in the QMetaObject's enumerator list.
// Ideally, this would be fixed in QMetaType.
bool isQFlagsType(uint id)
{
QMetaType type(id);
// Short-circuit to avoid more expensive operations
QMetaType::TypeFlags flags = type.flags();
if (flags.testFlag(QMetaType::PointerToQObject) || flags.testFlag(QMetaType::IsEnumeration)
|| flags.testFlag(QMetaType::SharedPointerToQObject) || flags.testFlag(QMetaType::WeakPointerToQObject)
|| flags.testFlag(QMetaType::TrackingPointerToQObject) || flags.testFlag(QMetaType::IsGadget))
{
return false;
}
const QMetaObject *mo = type.metaObject();
if (!mo) {
return false;
}
QByteArray name = type.name();
name = name.mid(name.lastIndexOf(":") + 1);
return mo->indexOfEnumerator(name.constData()) > -1;
}
// Common scores for overload resolution
enum OverloadScore {
PerfectMatchScore = 0,
VariantScore = 1,
NumberBaseScore = 2,
GenericConversionScore = 100,
IncompatibleScore = 10000,
};
// Scores the conversion of a double to a number-like user type. Better matches
// for a JS 'number' get a lower score.
int doubleToNumberConversionScore(int userType)
{
switch (userType) {
case QMetaType::Bool:
return NumberBaseScore + 7;
case QMetaType::Char:
case QMetaType::SChar:
case QMetaType::UChar:
return NumberBaseScore + 6;
case QMetaType::Short:
case QMetaType::UShort:
return NumberBaseScore + 5;
case QMetaType::Int:
case QMetaType::UInt:
return NumberBaseScore + 4;
case QMetaType::Long:
case QMetaType::ULong:
return NumberBaseScore + 3;
case QMetaType::LongLong:
case QMetaType::ULongLong:
return NumberBaseScore + 2;
case QMetaType::Float:
return NumberBaseScore + 1;
case QMetaType::Double:
return NumberBaseScore;
default:
break;
}
if (QMetaType(userType).flags() & QMetaType::IsEnumeration)
return doubleToNumberConversionScore(QMetaType::Int);
return IncompatibleScore;
}
// Keeps track of the badness of a QMetaMethod candidate for overload resolution
struct OverloadResolutionCandidate
{
OverloadResolutionCandidate(const QMetaMethod &method = QMetaMethod(), int badness = PerfectMatchScore)
: method(method), badness(badness)
{}
QMetaMethod method;
int badness;
bool operator<(const OverloadResolutionCandidate &other) const { return badness < other.badness; }
};
MessageType toType(const QJsonValue &value)
{
int i = value.toInt(-1);
if (i >= TYPES_FIRST_VALUE && i <= TYPES_LAST_VALUE) {
return static_cast<MessageType>(i);
} else {
return TypeInvalid;
}
}
const QString KEY_SIGNALS = QStringLiteral("signals");
const QString KEY_METHODS = QStringLiteral("methods");
const QString KEY_PROPERTIES = QStringLiteral("properties");
const QString KEY_ENUMS = QStringLiteral("enums");
const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
const QString KEY_ID = QStringLiteral("id");
const QString KEY_DATA = QStringLiteral("data");
const QString KEY_OBJECT = QStringLiteral("object");
const QString KEY_DESTROYED = QStringLiteral("destroyed");
const QString KEY_SIGNAL = QStringLiteral("signal");
const QString KEY_TYPE = QStringLiteral("type");
const QString KEY_METHOD = QStringLiteral("method");
const QString KEY_ARGS = QStringLiteral("args");
const QString KEY_PROPERTY = QStringLiteral("property");
const QString KEY_VALUE = QStringLiteral("value");
QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data)
{
QJsonObject response;
response[KEY_TYPE] = TypeResponse;
response[KEY_ID] = id;
response[KEY_DATA] = data;
return response;
}
#if QT_CONFIG(future)
QMetaType resultTypeOfQFuture(QByteArrayView typeName)
{
if (!typeName.startsWith("QFuture<") || !typeName.endsWith('>'))
return {};
return QMetaType::fromName(typeName.sliced(8, typeName.size() - 9));
}
template<typename Func>
void attachContinuationToFutureInVariant(const QVariant &result, QPointer<QObject> contextObject,
Func continuation)
{
Q_ASSERT(result.canConvert<QFuture<void>>());
// QMetaObject::invokeMethod() indirection to work around an issue with passing
// a context object to QFuture::then(). See below.
const auto safeContinuation = [contextObject, continuation=std::move(continuation)]
(const QVariant &result)
{
if (!contextObject)
return;
QMetaObject::invokeMethod(contextObject.get(), [continuation, result] {
continuation(result);
});
};
auto f = result.value<QFuture<void>>();
// Be explicit about what we capture so that we don't accidentally run into
// threading issues.
f.then([resultType=resultTypeOfQFuture(result.typeName()), f, continuation=safeContinuation]
{
if (!resultType.isValid() || resultType == QMetaType::fromType<void>()) {
continuation(QVariant{});
return;
}
auto iface = QFutureInterfaceBase::get(f);
// If we pass a context object to f.then() and the future originates in a
// different thread, this assertions fails. Why?
// For the time being, work around that with QMetaObject::invokeMethod()
// in safeSendResponse().
Q_ASSERT(iface.resultCount() > 0);
QMutexLocker<QMutex> locker(&iface.mutex());
if (iface.resultStoreBase().resultAt(0).isVector()) {
locker.unlock();
// This won't work because we cannot generically get a QList<T> into
// a QVariant with T only known at runtime.
// TBH, I don't know how to trigger this.
qWarning() << "Result lists in a QFuture return value are not supported!";
continuation(QVariant{});
return;
}
// pointer<void>() wouldn't compile because of the isVector-codepath
// using QList<T> in that method. We're not taking that path anyway (see the
// above check), so we can use char instead to not break strict aliasing
// requirements.
const auto data = iface.resultStoreBase().resultAt(0).pointer<char>();
locker.unlock();
const QVariant result(resultType, data);
continuation(result);
}).onCanceled([continuation=safeContinuation] {
// Will catch both failure and cancellation.
// Maybe send something more meaningful?
continuation(QVariant{});
});
}
#endif
}
Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_RELOCATABLE_TYPE);
void QWebChannelPropertyChangeNotifier::notify(
QPropertyObserver *self, QUntypedPropertyData *)
{
auto This = static_cast<QWebChannelPropertyChangeNotifier*>(self);
// Use the indirection with Qt::AutoConnection to ensure invocation
// in the correct thread.
// Explicitly copy the parameters into the lambda so that this instance can be destroyed after posting a queued
// invocation. The current design doesn't allow this anyway, but I don't want bad surprises in a possible future
// commit.
QMetaObject::invokeMethod(
This->publisher,
[publisher=This->publisher, object=This->object, propertyIndex=This->propertyIndex]
{
publisher->propertyValueChanged(object, propertyIndex);
}, Qt::AutoConnection);
}
QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel)
: QObject(webChannel),
webChannel(webChannel),
blockUpdatesStatus(false),
blockUpdatesHandler(blockUpdatesStatus.onValueChanged(
std::function<void()>([&]() { this->onBlockUpdatesChanged(); }))),
propertyUpdatesInitialized(false),
propertyUpdateIntervalTime(50),
propertyUpdateIntervalHandler(propertyUpdateIntervalTime.onValueChanged(
std::function<void()>([&]() { this->startPropertyUpdateTimer(true); })))
{
}
QMetaObjectPublisher::~QMetaObjectPublisher()
{
}
void QMetaObjectPublisher::registerObject(const QString &id, QObject *object)
{
registeredObjects[id] = object;
registeredObjectIds[object] = id;
if (propertyUpdatesInitialized) {
if (!webChannel->d_func()->transports.isEmpty()) {
qWarning("Registered new object after initialization, existing clients won't be notified!");
// TODO: send a message to clients that an object was added
}
initializePropertyUpdates(object, classInfoForObject(object, nullptr));
}
}
QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object, QWebChannelAbstractTransport *transport)
{
QJsonObject data;
if (!object) {
qWarning("null object given to MetaObjectPublisher - bad API usage?");
return data;
}
QJsonArray qtSignals;
QJsonArray qtMethods;
QJsonArray qtProperties;
QJsonObject qtEnums;
const QMetaObject *metaObject = object->metaObject();
QSet<int> notifySignals;
QSet<QString> identifiers;
for (int i = 0; i < metaObject->propertyCount(); ++i) {
const QMetaProperty &prop = metaObject->property(i);
QJsonArray propertyInfo;
const QString &propertyName = QString::fromLatin1(prop.name());
propertyInfo.append(i);
propertyInfo.append(propertyName);
identifiers << propertyName;
QJsonArray signalInfo;
if (prop.hasNotifySignal()) {
notifySignals << prop.notifySignalIndex();
// optimize: compress the common propertyChanged notification names, just send a 1
const QByteArray &notifySignal = prop.notifySignal().name();
static const QByteArray changedSuffix = QByteArrayLiteral("Changed");
if (notifySignal.size() == changedSuffix.size() + propertyName.size() &&
notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name()))
{
signalInfo.append(1);
} else {
signalInfo.append(QString::fromLatin1(notifySignal));
}
signalInfo.append(prop.notifySignalIndex());
} else if (!prop.isConstant() && !prop.isBindable()) {
qWarning("Property '%s'' of object '%s' has no notify signal, is not bindable and is not constant, "
"value updates in HTML will be broken!",
prop.name(), object->metaObject()->className());
}
propertyInfo.append(signalInfo);
propertyInfo.append(wrapResult(prop.read(object), transport));
qtProperties.append(propertyInfo);
}
auto addMethod = [&qtSignals, &qtMethods, &identifiers](int i, const QMetaMethod &method, const QByteArray &rawName) {
//NOTE: the name must be a string, otherwise it will be converted to '{}' in QML
const auto name = QString::fromLatin1(rawName);
// only the first method gets called with its name directly
// others must be called by explicitly passing the method signature
if (identifiers.contains(name))
return;
identifiers << name;
// send data as array to client with format: [name, index]
QJsonArray data;
data.append(name);
data.append(i);
if (method.methodType() == QMetaMethod::Signal) {
qtSignals.append(data);
} else if (method.access() == QMetaMethod::Public) {
qtMethods.append(data);
}
};
for (int i = 0; i < metaObject->methodCount(); ++i) {
if (notifySignals.contains(i)) {
continue;
}
const QMetaMethod &method = metaObject->method(i);
addMethod(i, method, method.name());
// for overload resolution also pass full method signature
addMethod(i, method, method.methodSignature());
}
for (int i = 0; i < metaObject->enumeratorCount(); ++i) {
QMetaEnum enumerator = metaObject->enumerator(i);
QJsonObject values;
for (int k = 0; k < enumerator.keyCount(); ++k) {
values[QString::fromLatin1(enumerator.key(k))] = enumerator.value(k);
}
qtEnums[QString::fromLatin1(enumerator.name())] = values;
}
data[KEY_SIGNALS] = qtSignals;
data[KEY_METHODS] = qtMethods;
data[KEY_PROPERTIES] = qtProperties;
if (!qtEnums.isEmpty()) {
data[KEY_ENUMS] = qtEnums;
}
return data;
}
void QMetaObjectPublisher::setClientIsIdle(bool isIdle, QWebChannelAbstractTransport *transport)
{
transportState[transport].clientIsIdle = isIdle;
if (isIdle)
sendEnqueuedPropertyUpdates(transport);
}
bool QMetaObjectPublisher::isClientIdle(QWebChannelAbstractTransport *transport)
{
auto found = transportState.find(transport);
return found != transportState.end() && found.value().clientIsIdle;
}
QJsonObject QMetaObjectPublisher::initializeClient(QWebChannelAbstractTransport *transport)
{
QJsonObject objectInfos;
{
const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd();
for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) {
const QJsonObject &info = classInfoForObject(it.value(), transport);
if (!propertyUpdatesInitialized) {
initializePropertyUpdates(it.value(), info);
}
objectInfos[it.key()] = info;
}
}
propertyUpdatesInitialized = true;
return objectInfos;
}
void QMetaObjectPublisher::initializePropertyUpdates(QObject *const object, const QJsonObject &objectInfo)
{
auto *metaObject = object->metaObject();
auto *signalHandler = signalHandlerFor(object);
const QJsonArray array = objectInfo[KEY_PROPERTIES].toArray();
for (const auto propertyInfoVar : array) {
const QJsonArray &propertyInfo = propertyInfoVar.toArray();
if (propertyInfo.size() < 2) {
qWarning() << "Invalid property info encountered:" << propertyInfoVar;
continue;
}
const int propertyIndex = propertyInfo.at(0).toInt();
const QJsonArray &signalData = propertyInfo.at(2).toArray();
const QMetaProperty metaProp = metaObject->property(propertyIndex);
if (!signalData.isEmpty()) {
// Property with a NOTIFY signal
const int signalIndex = signalData.at(1).toInt();
QSet<int> &connectedProperties = signalToPropertyMap[object][signalIndex];
// Only connect for a property update once
if (connectedProperties.isEmpty()) {
signalHandler->connectTo(object, signalIndex);
}
connectedProperties.insert(propertyIndex);
} else if (metaProp.isBindable()) {
const auto [begin, end] = propertyObservers.equal_range(object);
const auto it = std::find_if(begin, end, [&](auto &n) {
return n.second.propertyIndex == propertyIndex;
});
// Only connect for a property update once
if (it == end) {
auto it = propertyObservers.emplace(
object, QWebChannelPropertyChangeNotifier{this, object, propertyIndex});
metaProp.bindable(object).observe(&it->second);
}
}
}
// also always connect to destroyed signal
signalHandler->connectTo(object, s_destroyedSignalIndex);
}
void QMetaObjectPublisher::sendPendingPropertyUpdates()
{
if (blockUpdatesStatus) {
return;
}
QJsonArray data;
QHash<QWebChannelAbstractTransport*, QJsonArray> specificUpdates;
// convert pending property updates to JSON data
const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd();
for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) {
const QObject *object = it.key();
const QMetaObject *const metaObject = object->metaObject();
const QString objectId = registeredObjectIds.value(object);
const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object);
// maps property name to current property value
QJsonObject properties;
// maps signal index to list of arguments of the last emit
QJsonObject sigs;
const auto indexes = it.value().propertyIndices(objectsSignalToPropertyMap);
// TODO: can we get rid of the int <-> string conversions here?
for (const int propertyIndex : indexes) {
const QMetaProperty &property = metaObject->property(propertyIndex);
Q_ASSERT(property.isValid());
properties[QString::number(propertyIndex)] = wrapResult(property.read(object), nullptr, objectId);
}
const auto sigMap = it.value().signalMap;
for (auto sigIt = sigMap.constBegin(); sigIt != sigMap.constEnd(); ++sigIt) {
sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value());
}
QJsonObject obj;
obj[KEY_OBJECT] = objectId;
obj[KEY_SIGNALS] = sigs;
obj[KEY_PROPERTIES] = properties;
// if the object is auto registered, just send the update only to clients which know this object
if (wrappedObjects.contains(objectId)) {
const auto transports = wrappedObjects.value(objectId).transports;
for (QWebChannelAbstractTransport *transport : transports) {
QJsonArray &arr = specificUpdates[transport];
arr.push_back(obj);
}
} else {
data.push_back(obj);
}
}
pendingPropertyUpdates.clear();
QJsonObject message;
message[KEY_TYPE] = TypePropertyUpdate;
// data does not contain specific updates
if (!data.isEmpty()) {
message[KEY_DATA] = data;
enqueueBroadcastMessage(message);
}
// send every property update which is not supposed to be broadcasted
const QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator suend = specificUpdates.constEnd();
for (QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator it = specificUpdates.constBegin(); it != suend; ++it) {
message[KEY_DATA] = it.value();
enqueueMessage(message, it.key());
}
for (auto state = transportState.begin(); state != transportState.end(); ++state)
sendEnqueuedPropertyUpdates(state.key());
}
QVariant QMetaObjectPublisher::invokeMethod_helper(QObject *const object, const QMetaMethod &method,
const QJsonArray &args)
{
// a good value for the number of arguments we'll preallocate in QVLA
constexpr qsizetype ArgumentCount = 16;
QVarLengthArray<QVariant, ArgumentCount> variants;
QVarLengthArray<const char *, ArgumentCount> names(method.parameterCount() + 1);
QVarLengthArray<void *, ArgumentCount> parameters(names.size());
QVarLengthArray<const QtPrivate::QMetaTypeInterface *, ArgumentCount> metaTypes(names.size());
variants.reserve(names.size());
variants << QVariant();
// start with the formal parameters
for (qsizetype i = 0; i < names.size() - 1; ++i) {
QMetaType mt = method.parameterMetaType(i);
std::optional<QVariant> converted = toVariant(args.at(i), mt.id());
if (!converted) {
qWarning() << "Cannot invoke" << method.methodSignature() << "on object" << object
<< ". Parameter" << i << "could not be converted.";
return QJsonValue();
}
QVariant &v = variants.emplace_back(std::move(*converted));
parameters[i + 1] = v.data();
names[i + 1] = mt.name();
metaTypes[i + 1] = mt.iface();
}
// now, the return type
QMetaType mt = method.returnMetaType();
names[0] = mt.name();
metaTypes[0] = mt.iface();
if (int id = mt.id(); id != QMetaType::Void) {
// Only init variant with return type if its not a variant itself,
// which would lead to nested variants which is not what we want.
if (id == QMetaType::QVariant) {
parameters[0] = &variants[0];
} else {
variants[0] = QVariant(mt);
parameters[0] = variants[0].data();
}
} else {
parameters[0] = nullptr;
}
// step 3: make the call
QMetaMethodInvoker::InvokeFailReason r =
QMetaMethodInvoker::invokeImpl(method, object, Qt::AutoConnection,
parameters.size(), parameters.constData(),
names.constData(), metaTypes.constData());
if (r == QMetaMethodInvoker::InvokeFailReason::None)
return variants.first();
// print warnings for failures to match
if (int(r) >= int(QMetaMethodInvoker::InvokeFailReason::FormalParameterMismatch)) {
int n = int(r) - int(QMetaMethodInvoker::InvokeFailReason::FormalParameterMismatch);
QByteArray callee = object->metaObject()->className() + QByteArrayView("::")
+ method.methodSignature();
qWarning() << "Cannot convert formal parameter" << n << "from" << names[n + 1]
<< "in call to" << callee.constData();
}
return QJsonValue();
}
QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QMetaMethod &method,
const QJsonArray &args)
{
if (method.name() == QByteArrayLiteral("deleteLater")) {
// invoke `deleteLater` on wrapped QObject indirectly
deleteWrappedObject(object);
return QJsonValue();
} else if (!method.isValid()) {
qWarning() << "Cannot invoke invalid method on object" << object << '.';
return QJsonValue();
} else if (method.access() != QMetaMethod::Public) {
qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
return QJsonValue();
} else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) {
qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
return QJsonValue();
} else if (args.size() < method.parameterCount()) {
qWarning() << "Cannot invoke" << method.methodSignature() << "on" << object << "object:"
<< args.size() << "arguments given, but method requires"
<< method.parameterCount() << '.';
return QJsonValue();
} else if (args.size() > method.parameterCount()) {
qWarning() << "Ignoring additional arguments while invoking method" << method.name()
<< "on object" << object << ':' << args.size()
<< "arguments given, but method only takes" << method.parameterCount() << '.';
}
return invokeMethod_helper(object, method, args);
}
QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex,
const QJsonArray &args)
{
const QMetaMethod &method = object->metaObject()->method(methodIndex);
if (!method.isValid()) {
qWarning() << "Cannot invoke method of unknown index" << methodIndex << "on object"
<< object << '.';
return QJsonValue();
}
return invokeMethod(object, method, args);
}
QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QByteArray &methodName,
const QJsonArray &args)
{
QList<OverloadResolutionCandidate> candidates;
const QMetaObject *mo = object->metaObject();
for (int i = 0; i < mo->methodCount(); ++i) {
QMetaMethod method = mo->method(i);
if (method.name() != methodName || method.parameterCount() != args.count()
|| method.access() != QMetaMethod::Public
|| (method.methodType() != QMetaMethod::Method
&& method.methodType() != QMetaMethod::Slot))
{
// Not a candidate
continue;
}
candidates.append({method, methodOverloadBadness(method, args)});
}
if (candidates.isEmpty()) {
qWarning() << "No candidates found for" << methodName << "with" << args.size()
<< "arguments on object" << object << '.';
return QJsonValue();
}
std::sort(candidates.begin(), candidates.end());
if (candidates.size() > 1 && candidates[0].badness == candidates[1].badness) {
qWarning().nospace() << "Ambiguous overloads for method " << methodName << ". Choosing "
<< candidates.first().method.methodSignature();
}
return invokeMethod_helper(object, candidates.first().method, args);
}
void QMetaObjectPublisher::setProperty(QObject *object, const int propertyIndex, const QJsonValue &value)
{
QMetaProperty property = object->metaObject()->property(propertyIndex);
if (!property.isValid()) {
qWarning() << "Cannot set unknown property" << propertyIndex << "of object" << object;
return;
}
const std::optional<QVariant> variant = toVariant(value, property.userType());
if (!variant) {
qWarning() << "Conversion failed: Could not write value " << value << "to property"
<< property.name() << "of object" << object;
} else if (!property.write(object, *variant)) {
qWarning() << "Could not write value " << value << "to property" << property.name()
<< "of object" << object;
}
}
void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments)
{
if (!webChannel || webChannel->d_func()->transports.isEmpty()) {
if (signalIndex == s_destroyedSignalIndex)
objectDestroyed(object);
return;
}
if (!signalToPropertyMap.value(object).contains(signalIndex)) {
QJsonObject message;
const QString &objectName = registeredObjectIds.value(object);
Q_ASSERT(!objectName.isEmpty());
message[KEY_OBJECT] = objectName;
message[KEY_SIGNAL] = signalIndex;
if (!arguments.isEmpty()) {
message[KEY_ARGS] = wrapList(arguments, nullptr, objectName);
}
message[KEY_TYPE] = TypeSignal;
// if the object is wrapped, just send the response to clients which know this object
if (wrappedObjects.contains(objectName)) {
const auto transports = wrappedObjects.value(objectName).transports;
for (QWebChannelAbstractTransport *transport : transports) {
transport->sendMessage(message);
}
} else {
broadcastMessage(message);
}
if (signalIndex == s_destroyedSignalIndex) {
objectDestroyed(object);
}
} else {
auto &propertyUpdate = pendingPropertyUpdates[object];
propertyUpdate.signalMap[signalIndex] = arguments;
startPropertyUpdateTimer();
}
}
void QMetaObjectPublisher::propertyValueChanged(const QObject *object, const int propertyIndex)
{
pendingPropertyUpdates[object].plainProperties.insert(propertyIndex);
startPropertyUpdateTimer();
}
void QMetaObjectPublisher::startPropertyUpdateTimer(bool forceRestart)
{
if (blockUpdatesStatus)
return;
if (propertyUpdateIntervalTime >= 0) {
if (forceRestart || !timer.isActive())
timer.start(propertyUpdateIntervalTime, this);
} else {
sendPendingPropertyUpdates();
}
}
void QMetaObjectPublisher::objectDestroyed(const QObject *object)
{
const QString &id = registeredObjectIds.take(object);
Q_ASSERT(!id.isEmpty());
bool removed = registeredObjects.remove(id)
|| wrappedObjects.remove(id);
Q_ASSERT(removed);
Q_UNUSED(removed);
// only remove from handler when we initialized the property updates
if (propertyUpdatesInitialized) {
signalHandlerFor(object)->remove(object);
signalToPropertyMap.remove(object);
}
pendingPropertyUpdates.remove(object);
propertyObservers.erase(object);
}
QObject *QMetaObjectPublisher::unwrapObject(const QString &objectId) const
{
if (!objectId.isEmpty()) {
ObjectInfo objectInfo = wrappedObjects.value(objectId);
if (objectInfo.object)
return objectInfo.object;
QObject *object = registeredObjects.value(objectId);
if (object)
return object;
}
qWarning() << "No wrapped object" << objectId;
return nullptr;
}
QVariant QMetaObjectPublisher::unwrapMap(QVariantMap map) const
{
const auto qobj = map.value(KEY_QOBJECT).toBool();
const auto id = qobj ? map.value(KEY_ID).toString() : QString();
if (!id.isEmpty()) // it's probably a QObject
return QVariant::fromValue(unwrapObject(id));
// it's probably just a normal JS object, continue searching for objects
// that look like QObject*
for (auto &value : map)
value = unwrapVariant(value);
return map;
}
QVariant QMetaObjectPublisher::unwrapList(QVariantList list) const
{
for (auto &value : list)
value = unwrapVariant(value);
return list;
}
QVariant QMetaObjectPublisher::unwrapVariant(const QVariant &value) const
{
switch (value.metaType().id())
{
case QMetaType::QVariantList:
return unwrapList(value.toList());
case QMetaType::QVariantMap:
return unwrapMap(value.toMap());
default:
break;
}
return value;
}
std::optional<QVariant>
QMetaObjectPublisher::toVariant(const QJsonValue &value, int targetType) const
{
QMetaType target(targetType);
if (target.flags() & QMetaType::PointerToQObject) {
if (value.isNull())
return QVariant::fromValue<QObject *>(nullptr);
const QJsonObject obj = value.toObject();
QObject *unwrappedObject = unwrapObject(obj.value(KEY_ID).toString());
if (!unwrappedObject) {
qWarning() << "Cannot convert argument" << value << "to" << target.name();
return std::nullopt;
}
const QMetaObject *targetMeta = target.metaObject();
if (!targetMeta) {
qWarning() << "Cannot convert argument" << value << "to" << target.name()
<< "- no metaobject is available to verify the object's class";
return std::nullopt;
}
if (!unwrappedObject->metaObject()->inherits(targetMeta)) {
qWarning() << "Cannot convert argument" << value << "to" << target.name()
<< "- object is a" << unwrappedObject->metaObject()->className()
<< "which does not inherit" << targetMeta->className();
return std::nullopt;
}
return QVariant::fromValue(unwrappedObject);
} else if (isQFlagsType(targetType)) {
int flagsValue = value.toInt();
return QVariant(target, reinterpret_cast<const void*>(&flagsValue));
}
QVariant variant = value.toVariant();
// Try variant conversion to the target type first. If that fails,
// try conversion over QJsonvalue.
if (auto converted = variant; converted.convert(target)) {
variant = std::move(converted);
} else if (targetType != QMetaType::QVariant) {
if (QVariant converted = value; converted.convert(target)) {
variant = std::move(converted);
} else {
qWarning() << "Could not convert argument" << value
<< "to target type" << target.name() << '.';
return std::nullopt;
}
}
return unwrapVariant(variant);
}
int QMetaObjectPublisher::conversionScore(const QJsonValue &value, int targetType) const
{
QMetaType target(targetType);
if (targetType == QMetaType::QJsonValue) {
return PerfectMatchScore;
} else if (targetType == QMetaType::QJsonArray) {
return value.isArray() ? PerfectMatchScore : IncompatibleScore;
} else if (targetType == QMetaType::QJsonObject) {
return value.isObject() ? PerfectMatchScore : IncompatibleScore;
} else if (target.flags() & QMetaType::PointerToQObject) {
if (value.isNull())
return PerfectMatchScore;
if (!value.isObject())
return IncompatibleScore;
QJsonObject object = value.toObject();
if (object[KEY_ID].isUndefined())
return IncompatibleScore;
QObject *unwrappedObject = unwrapObject(object[KEY_ID].toString());
if (!unwrappedObject)
return IncompatibleScore;
const QMetaObject *targetMeta = target.metaObject();
if (!targetMeta || !unwrappedObject->metaObject()->inherits(targetMeta))
return IncompatibleScore;
return PerfectMatchScore;
} else if (targetType == QMetaType::QVariant) {
return VariantScore;
}
// Check if this is a number conversion
if (value.isDouble()) {
int score = doubleToNumberConversionScore(targetType);
if (score != IncompatibleScore) {
return score;
}
}
QVariant variant = value.toVariant();
if (variant.userType() == targetType) {
return PerfectMatchScore;
} else if (variant.canConvert(target)) {
return GenericConversionScore;
}
return IncompatibleScore;
}
int QMetaObjectPublisher::methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const
{
int badness = PerfectMatchScore;
for (int i = 0; i < args.size(); ++i) {
badness += conversionScore(args[i], method.parameterType(i));
}
return badness;
}
void QMetaObjectPublisher::transportRemoved(QWebChannelAbstractTransport *transport)
{
transportState.remove(transport);
auto it = transportedWrappedObjects.find(transport);
// It is not allowed to modify a container while iterating over it. So save
// objects which should be removed and call objectDestroyed() on them later.
QList<QObject *> objectsForDeletion;
while (it != transportedWrappedObjects.end() && it.key() == transport) {
if (wrappedObjects.contains(it.value())) {
QList<QWebChannelAbstractTransport *> &transports = wrappedObjects[it.value()].transports;
transports.removeOne(transport);
if (transports.isEmpty())
objectsForDeletion.append(wrappedObjects[it.value()].object);
}
it++;
}
transportedWrappedObjects.remove(transport);
for (QObject *obj : std::as_const(objectsForDeletion))
objectDestroyed(obj);
}
// NOTE: transport can be a nullptr
// in such a case, we need to ensure that the property is registered to
// the target transports of the parentObjectId
QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport,
const QString &parentObjectId)
{
if (QObject *object = result.value<QObject *>()) {
QString id = registeredObjectIds.value(object);
QJsonObject classInfo;
if (id.isEmpty()) {
// neither registered, nor wrapped, do so now
id = QUuid::createUuid().toString();
// store ID before the call to classInfoForObject()
// in case of self-contained objects it avoids
// infinite loops
registeredObjectIds[object] = id;
classInfo = classInfoForObject(object, transport);
ObjectInfo oi(object);
if (transport) {
oi.transports.append(transport);
transportedWrappedObjects.insert(transport, id);
} else {
// use the transports from the parent object
oi.transports = wrappedObjects.value(parentObjectId).transports;
// or fallback to all transports if the parent is not wrapped
if (oi.transports.isEmpty())
oi.transports = webChannel->d_func()->transports;
for (auto transport : std::as_const(oi.transports)) {
transportedWrappedObjects.insert(transport, id);
}
}
wrappedObjects.insert(id, oi);
initializePropertyUpdates(object, classInfo);
} else {
auto oi = wrappedObjects.find(id);
if (oi != wrappedObjects.end() && !oi->isBeingWrapped) {
Q_ASSERT(object == oi->object);
// check if this transport is already assigned to the object
if (transport && !oi->transports.contains(transport)) {
oi->transports.append(transport);
transportedWrappedObjects.insert(transport, id);
}
// QTBUG-84007: Block infinite recursion for self-contained objects
// which have already been wrapped
oi->isBeingWrapped = true;
classInfo = classInfoForObject(object, transport);
oi->isBeingWrapped = false;
}
}
QJsonObject objectInfo;
objectInfo[KEY_QOBJECT] = true;
objectInfo[KEY_ID] = id;
if (!classInfo.isEmpty())
objectInfo[KEY_DATA] = classInfo;
return objectInfo;
} else if (result.metaType().flags().testFlag(QMetaType::IsEnumeration)) {
return result.toInt();
} else if (isQFlagsType(result.userType())) {
return *reinterpret_cast<const int*>(result.constData());
#ifndef QT_NO_JSVALUE
} else if (result.canConvert<QJSValue>()) {
// Workaround for keeping QJSValues from QVariant.
// Calling QJSValue::toVariant() converts JS-objects/arrays to QVariantMap/List
// instead of stashing a QJSValue itself into a variant.
// TODO: Improve QJSValue-QJsonValue conversion in Qt.
return wrapResult(result.value<QJSValue>().toVariant(), transport, parentObjectId);
#endif
} else if (result.metaType().id() == QMetaType::QString ||
result.metaType().id() == QMetaType::QByteArray) {
// avoid conversion to QVariantList
} else if (result.canConvert<QVariantList>()) {
// recurse and potentially wrap contents of the array
// *don't* use result.toList() as that *only* works for QVariantList and QStringList!
// Also, don't use QMetaSequence::Iterable (yet), since that seems to trigger QTBUG-42016
// in certain cases.
// additionally, when there's a direct converter to QVariantList, use that one via convert
// but recover when conversion fails and fall back to the .value<QVariantList> conversion
auto list = result;
if (!list.convert(QMetaType::fromType<QVariantList>()))
list = result;
return wrapList(list.value<QVariantList>(), transport);
} else if (result.canConvert<QVariantMap>()) {
// recurse and potentially wrap contents of the map
auto map = result;
if (!map.convert(QMetaType::fromType<QVariantMap>()))
map = result;
return wrapMap(map.value<QVariantMap>(), transport);
} else if (auto v = result; v.convert(QMetaType::fromType<QJsonValue>())) {
// Support custom converters to QJsonValue
return v.value<QJsonValue>();
}
return QJsonValue::fromVariant(result);
}
QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list, QWebChannelAbstractTransport *transport, const QString &parentObjectId)
{
QJsonArray array;
for (const QVariant &arg : list) {
array.append(wrapResult(arg, transport, parentObjectId));
}
return array;
}
QJsonObject QMetaObjectPublisher::wrapMap(const QVariantMap &map, QWebChannelAbstractTransport *transport, const QString &parentObjectId)
{
QJsonObject obj;
for (QVariantMap::const_iterator i = map.begin(); i != map.end(); i++) {
obj.insert(i.key(), wrapResult(i.value(), transport, parentObjectId));
}
return obj;
}
void QMetaObjectPublisher::deleteWrappedObject(QObject *object) const
{
if (!wrappedObjects.contains(registeredObjectIds.value(object))) {
qWarning() << "Not deleting non-wrapped object" << object;
return;
}
object->deleteLater();
}
void QMetaObjectPublisher::broadcastMessage(const QJsonObject &message) const
{
if (webChannel->d_func()->transports.isEmpty()) {
return;
}
for (QWebChannelAbstractTransport *transport : std::as_const(webChannel->d_func()->transports)) {
transport->sendMessage(message);
}
}
void QMetaObjectPublisher::enqueueBroadcastMessage(const QJsonObject &message)
{
if (webChannel->d_func()->transports.isEmpty()) {
return;
}
for (auto *transport : std::as_const(webChannel->d_func()->transports)) {
auto &state = transportState[transport];
state.queuedMessages.append(message);
}
}
void QMetaObjectPublisher::enqueueMessage(const QJsonObject &message,
QWebChannelAbstractTransport *transport)
{
auto &state = transportState[transport];
state.queuedMessages.append(message);
}
void QMetaObjectPublisher::sendEnqueuedPropertyUpdates(QWebChannelAbstractTransport *transport)
{
auto found = transportState.find(transport);
if (found != transportState.end() && found.value().clientIsIdle
&& !found.value().queuedMessages.isEmpty()) {
// If the client is connected with an in-process transport, it can
// happen that a message triggers a subsequent property change. In
// that case, we need to ensure that the queued messages have already
// been cleared; otherwise the recursive call will send everythig again.
// Case in point: The qmlwebchannel tests fail if we don't clear the
// queued messages before sending them out.
// For that same reason set the client to "busy" (aka non-idle) just
// right before sending out the messages; otherwise a potential
// "Idle" type message will not correctly restore the Idle state.
const auto messages = std::move(found.value().queuedMessages);
Q_ASSERT(found.value().queuedMessages.isEmpty());
found.value().clientIsIdle = false;
for (const auto &message : messages) {
transport->sendMessage(message);
}
}
}
void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannelAbstractTransport *transport)
{
if (!webChannel->d_func()->transports.contains(transport)) {
qWarning() << "Refusing to handle message of unknown transport:" << transport;
return;
}
if (!message.contains(KEY_TYPE)) {
qWarning("JSON message object is missing the type property: %s", QJsonDocument(message).toJson().constData());
return;
}
const MessageType type = toType(message.value(KEY_TYPE));
if (type == TypeIdle) {
setClientIsIdle(true, transport);
} else if (type == TypeInit) {
if (!message.contains(KEY_ID)) {
qWarning("JSON message object is missing the id property: %s",
QJsonDocument(message).toJson().constData());
return;
}
transport->sendMessage(createResponse(message.value(KEY_ID), initializeClient(transport)));
} else if (type == TypeDebug) {
static QTextStream out(stdout);
out << "DEBUG: " << message.value(KEY_DATA).toString() << Qt::endl;
} else if (message.contains(KEY_OBJECT)) {
const QString &objectName = message.value(KEY_OBJECT).toString();
QObject *object = registeredObjects.value(objectName);
if (!object)
object = wrappedObjects.value(objectName).object;
if (!object) {
qWarning() << "Unknown object encountered" << objectName;
return;
}
if (type == TypeInvokeMethod) {
if (!message.contains(KEY_ID)) {
qWarning("JSON message object is missing the id property: %s",
QJsonDocument(message).toJson().constData());
return;
}
QPointer<QMetaObjectPublisher> publisherExists(this);
QPointer<QWebChannelAbstractTransport> transportExists(transport);
QJsonValue method = message.value(KEY_METHOD);
QVariant result;
if (method.isString()) {
result = invokeMethod(object,
method.toString().toUtf8(),
message.value(KEY_ARGS).toArray());
} else {
result = invokeMethod(object,
method.toInt(-1),
message.value(KEY_ARGS).toArray());
}
auto sendResponse = [publisherExists, transportExists, id=message.value(KEY_ID)]
(const QVariant &result)
{
if (!publisherExists || !transportExists)
return;
Q_ASSERT(QThread::currentThread() == publisherExists->thread());
const auto wrappedResult =
publisherExists->wrapResult(result, transportExists.get());
transportExists->sendMessage(createResponse(id, wrappedResult));
};
#if QT_CONFIG(future)
if (result.canConvert<QFuture<void>>()) {
attachContinuationToFutureInVariant(result, publisherExists.get(), sendResponse);
} else {
sendResponse(result);
}
#else
sendResponse(result);
#endif
} else if (type == TypeConnectToSignal) {
signalHandlerFor(object)->connectTo(object, message.value(KEY_SIGNAL).toInt(-1));
} else if (type == TypeDisconnectFromSignal) {
signalHandlerFor(object)->disconnectFrom(object, message.value(KEY_SIGNAL).toInt(-1));
} else if (type == TypeSetProperty) {
setProperty(object, message.value(KEY_PROPERTY).toInt(-1),
message.value(KEY_VALUE));
}
}
}
int QMetaObjectPublisher::propertyUpdateInterval()
{
return propertyUpdateIntervalTime;
}
void QMetaObjectPublisher::setPropertyUpdateInterval(int ms)
{
propertyUpdateIntervalTime = ms;
}
void QMetaObjectPublisher::setBlockUpdates(bool block)
{
blockUpdatesStatus = block;
}
void QMetaObjectPublisher::onBlockUpdatesChanged()
{
if (!blockUpdatesStatus) {
startPropertyUpdateTimer();
sendPendingPropertyUpdates();
} else if (timer.isActive()) {
timer.stop();
}
emit blockUpdatesChanged(blockUpdatesStatus);
}
bool QMetaObjectPublisher::blockUpdates() const
{
return blockUpdatesStatus;
}
void QMetaObjectPublisher::timerEvent(QTimerEvent *event)
{
if (event->timerId() == timer.timerId()) {
if (propertyUpdateIntervalTime <= 0)
timer.stop();
sendPendingPropertyUpdates();
} else {
QObject::timerEvent(event);
}
}
SignalHandler<QMetaObjectPublisher> *QMetaObjectPublisher::signalHandlerFor(const QObject *object)
{
auto thread = object->thread();
auto it = signalHandlers.find(thread);
if (it == signalHandlers.end()) {
it = signalHandlers.emplace(thread, this).first;
it->second.moveToThread(thread);
}
return &it->second;
}
QT_END_NAMESPACE