| /**************************************************************************** |
| ** |
| ** 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> |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtWebChannel module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qmetaobjectpublisher_p.h" |
| #include "qwebchannel.h" |
| #include "qwebchannel_p.h" |
| #include "qwebchannelabstracttransport.h" |
| |
| #include <QEvent> |
| #include <QJsonDocument> |
| #include <QDebug> |
| #include <QJsonObject> |
| #include <QJsonArray> |
| #ifndef QT_NO_JSVALUE |
| #include <QJSValue> |
| #endif |
| #include <QUuid> |
| |
| 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 = QMetaType::typeName(id); |
| 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::typeFlags(userType) & 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; |
| } |
| |
| /// TODO: what is the proper value here? |
| const int PROPERTY_UPDATE_INTERVAL = 50; |
| } |
| |
| Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_MOVABLE_TYPE); |
| |
| QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel) |
| : QObject(webChannel) |
| , webChannel(webChannel) |
| , signalHandler(this) |
| , clientIsIdle(false) |
| , blockUpdates(false) |
| , propertyUpdatesInitialized(false) |
| { |
| } |
| |
| 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, Q_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 ¬ifySignal = prop.notifySignal().name(); |
| static const QByteArray changedSuffix = QByteArrayLiteral("Changed"); |
| if (notifySignal.length() == changedSuffix.length() + propertyName.length() && |
| notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name())) |
| { |
| signalInfo.append(1); |
| } else { |
| signalInfo.append(QString::fromLatin1(notifySignal)); |
| } |
| signalInfo.append(prop.notifySignalIndex()); |
| } else if (!prop.isConstant()) { |
| qWarning("Property '%s'' of object '%s' has no notify signal 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) |
| { |
| if (clientIsIdle == isIdle) { |
| return; |
| } |
| clientIsIdle = isIdle; |
| if (!isIdle && timer.isActive()) { |
| timer.stop(); |
| } else if (isIdle && !timer.isActive()) { |
| timer.start(PROPERTY_UPDATE_INTERVAL, this); |
| } |
| } |
| |
| 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(const QObject *const object, const QJsonObject &objectInfo) |
| { |
| foreach (const QJsonValue &propertyInfoVar, objectInfo[KEY_PROPERTIES].toArray()) { |
| 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(); |
| |
| if (signalData.isEmpty()) { |
| // Property without NOTIFY signal |
| continue; |
| } |
| |
| 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); |
| } |
| |
| // also always connect to destroyed signal |
| signalHandler.connectTo(object, s_destroyedSignalIndex); |
| } |
| |
| void QMetaObjectPublisher::sendPendingPropertyUpdates() |
| { |
| if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) { |
| 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 SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd(); |
| for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) { |
| // TODO: can we get rid of the int <-> string conversions here? |
| foreach (const int propertyIndex, objectsSignalToPropertyMap.value(sigIt.key())) { |
| const QMetaProperty &property = metaObject->property(propertyIndex); |
| Q_ASSERT(property.isValid()); |
| properties[QString::number(propertyIndex)] = wrapResult(property.read(object), Q_NULLPTR, objectId); |
| } |
| 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)) { |
| foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectId).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()) { |
| setClientIsIdle(false); |
| |
| message[KEY_DATA] = data; |
| broadcastMessage(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(); |
| it.key()->sendMessage(message); |
| } |
| } |
| |
| 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() > 10) { |
| qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke."; |
| 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() << '.'; |
| } |
| |
| // construct converter objects of QVariant to QGenericArgument |
| VariantArgument arguments[10]; |
| for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) { |
| arguments[i].value = toVariant(args.at(i), method.parameterType(i)); |
| } |
| // construct QGenericReturnArgument |
| QVariant returnValue; |
| if (method.returnType() == QMetaType::Void) { |
| // Skip return for void methods (prevents runtime warnings inside Qt), and allows |
| // QMetaMethod to invoke void-returning methods on QObjects in a different thread. |
| method.invoke(object, |
| arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], |
| arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); |
| } else { |
| // 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 (method.returnType() != QMetaType::QVariant) |
| returnValue = QVariant(method.returnType(), 0); |
| |
| QGenericReturnArgument returnArgument(method.typeName(), returnValue.data()); |
| method.invoke(object, returnArgument, |
| arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], |
| arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); |
| } |
| // now we can call the method |
| return returnValue; |
| } |
| |
| 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) |
| { |
| QVector<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) |
| || method.parameterCount() > 10) |
| { |
| // 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(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; |
| } else if (!property.write(object, toVariant(value, property.userType()))) { |
| 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, Q_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)) { |
| foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectName).transports) { |
| transport->sendMessage(message); |
| } |
| } else { |
| broadcastMessage(message); |
| } |
| |
| if (signalIndex == s_destroyedSignalIndex) { |
| objectDestroyed(object); |
| } |
| } else { |
| pendingPropertyUpdates[object][signalIndex] = arguments; |
| if (clientIsIdle && !blockUpdates && !timer.isActive()) { |
| timer.start(PROPERTY_UPDATE_INTERVAL, this); |
| } |
| } |
| } |
| |
| 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 |
| // cf: https://bugreports.qt.io/browse/QTBUG-60250 |
| if (propertyUpdatesInitialized) { |
| signalHandler.remove(object); |
| signalToPropertyMap.remove(object); |
| } |
| pendingPropertyUpdates.remove(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 Q_NULLPTR; |
| } |
| |
| QVariant QMetaObjectPublisher::toVariant(const QJsonValue &value, int targetType) const |
| { |
| if (targetType == QMetaType::QJsonValue) { |
| return QVariant::fromValue(value); |
| } else if (targetType == QMetaType::QJsonArray) { |
| if (!value.isArray()) |
| qWarning() << "Cannot not convert non-array argument" << value << "to QJsonArray."; |
| return QVariant::fromValue(value.toArray()); |
| } else if (targetType == QMetaType::QJsonObject) { |
| if (!value.isObject()) |
| qWarning() << "Cannot not convert non-object argument" << value << "to QJsonObject."; |
| return QVariant::fromValue(value.toObject()); |
| } else if (QMetaType::typeFlags(targetType) & QMetaType::PointerToQObject) { |
| QObject *unwrappedObject = unwrapObject(value.toObject()[KEY_ID].toString()); |
| if (unwrappedObject == Q_NULLPTR) |
| qWarning() << "Cannot not convert non-object argument" << value << "to QObject*."; |
| return QVariant::fromValue(unwrappedObject); |
| } else if (isQFlagsType(targetType)) { |
| int flagsValue = value.toInt(); |
| return QVariant(targetType, reinterpret_cast<const void*>(&flagsValue)); |
| } |
| |
| // this converts QJsonObjects to QVariantMaps, which is not desired when |
| // we want to get a QJsonObject or QJsonValue (see above) |
| QVariant variant = value.toVariant(); |
| if (targetType != QMetaType::QVariant && !variant.convert(targetType)) { |
| qWarning() << "Could not convert argument" << value << "to target type" << QVariant::typeToName(targetType) << '.'; |
| } |
| return variant; |
| } |
| |
| int QMetaObjectPublisher::conversionScore(const QJsonValue &value, int targetType) const |
| { |
| 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 (QMetaType::typeFlags(targetType) & 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()); |
| return unwrappedObject != Q_NULLPTR ? PerfectMatchScore : IncompatibleScore; |
| } 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(targetType)) { |
| 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) |
| { |
| 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. |
| QVector<QObject*> objectsForDeletion; |
| while (it != transportedWrappedObjects.end() && it.key() == transport) { |
| if (wrappedObjects.contains(it.value())) { |
| QVector<QWebChannelAbstractTransport*> &transports = wrappedObjects[it.value()].transports; |
| transports.removeOne(transport); |
| if (transports.isEmpty()) |
| objectsForDeletion.append(wrappedObjects[it.value()].object); |
| } |
| |
| it++; |
| } |
| |
| transportedWrappedObjects.remove(transport); |
| |
| foreach (QObject *obj, 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 : qAsConst(oi.transports)) { |
| transportedWrappedObjects.insert(transport, id); |
| } |
| } |
| wrappedObjects.insert(id, oi); |
| |
| initializePropertyUpdates(object, classInfo); |
| } else if (wrappedObjects.contains(id)) { |
| Q_ASSERT(object == wrappedObjects.value(id).object); |
| // check if this transport is already assigned to the object |
| if (transport && !wrappedObjects.value(id).transports.contains(transport)) { |
| wrappedObjects[id].transports.append(transport); |
| transportedWrappedObjects.insert(transport, id); |
| } |
| classInfo = classInfoForObject(object, transport); |
| } |
| |
| QJsonObject objectInfo; |
| objectInfo[KEY_QOBJECT] = true; |
| objectInfo[KEY_ID] = id; |
| if (!classInfo.isEmpty()) |
| objectInfo[KEY_DATA] = classInfo; |
| |
| return objectInfo; |
| } else if (QMetaType::typeFlags(result.userType()).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.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 QSequentialIterable (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 |
| // see also: https://bugreports.qt.io/browse/QTBUG-80751 |
| auto list = result; |
| if (!list.convert(qMetaTypeId<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(qMetaTypeId<QVariantMap>())) |
| map = result; |
| return wrapMap(map.value<QVariantMap>(), transport); |
| } |
| |
| return QJsonValue::fromVariant(result); |
| } |
| |
| QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list, QWebChannelAbstractTransport *transport, const QString &parentObjectId) |
| { |
| QJsonArray array; |
| foreach (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()) { |
| qWarning("QWebChannel is not connected to any transports, cannot send message: %s", QJsonDocument(message).toJson().constData()); |
| return; |
| } |
| |
| foreach (QWebChannelAbstractTransport *transport, webChannel->d_func()->transports) { |
| 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); |
| } 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()); |
| } |
| if (!publisherExists || !transportExists) |
| return; |
| transport->sendMessage(createResponse(message.value(KEY_ID), wrapResult(result, transport))); |
| } else if (type == TypeConnectToSignal) { |
| signalHandler.connectTo(object, message.value(KEY_SIGNAL).toInt(-1)); |
| } else if (type == TypeDisconnectFromSignal) { |
| signalHandler.disconnectFrom(object, message.value(KEY_SIGNAL).toInt(-1)); |
| } else if (type == TypeSetProperty) { |
| setProperty(object, message.value(KEY_PROPERTY).toInt(-1), |
| message.value(KEY_VALUE)); |
| } |
| } |
| } |
| |
| void QMetaObjectPublisher::setBlockUpdates(bool block) |
| { |
| if (blockUpdates == block) { |
| return; |
| } |
| blockUpdates = block; |
| |
| if (!blockUpdates) { |
| sendPendingPropertyUpdates(); |
| } else if (timer.isActive()) { |
| timer.stop(); |
| } |
| |
| emit blockUpdatesChanged(block); |
| } |
| |
| void QMetaObjectPublisher::timerEvent(QTimerEvent *event) |
| { |
| if (event->timerId() == timer.timerId()) { |
| sendPendingPropertyUpdates(); |
| } else { |
| QObject::timerEvent(event); |
| } |
| } |
| |
| QT_END_NAMESPACE |