1/****************************************************************************
2**
3** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
4** Copyright (C) 2019 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com>
5** Contact: https://www.qt.io/licensing/
6**
7** This file is part of the QtWebChannel module of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:LGPL$
10** Commercial License Usage
11** Licensees holding valid commercial Qt licenses may use this file in
12** accordance with the commercial license agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and The Qt Company. For licensing terms
15** and conditions see https://www.qt.io/terms-conditions. For further
16** information use the contact form at https://www.qt.io/contact-us.
17**
18** GNU Lesser General Public License Usage
19** Alternatively, this file may be used under the terms of the GNU Lesser
20** General Public License version 3 as published by the Free Software
21** Foundation and appearing in the file LICENSE.LGPL3 included in the
22** packaging of this file. Please review the following information to
23** ensure the GNU Lesser General Public License version 3 requirements
24** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
25**
26** GNU General Public License Usage
27** Alternatively, this file may be used under the terms of the GNU
28** General Public License version 2.0 or (at your option) the GNU General
29** Public license version 3 or any later version approved by the KDE Free
30** Qt Foundation. The licenses are as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
32** included in the packaging of this file. Please review the following
33** information to ensure the GNU General Public License requirements will
34** be met: https://www.gnu.org/licenses/gpl-2.0.html and
35** https://www.gnu.org/licenses/gpl-3.0.html.
36**
37** $QT_END_LICENSE$
38**
39****************************************************************************/
40
41#include "qmetaobjectpublisher_p.h"
42#include "qwebchannel.h"
43#include "qwebchannel_p.h"
44#include "qwebchannelabstracttransport.h"
45
46#include <QEvent>
47#include <QJsonDocument>
48#include <QDebug>
49#include <QJsonObject>
50#include <QJsonArray>
51#ifndef QT_NO_JSVALUE
52#include <QJSValue>
53#endif
54#include <QUuid>
55
56QT_BEGIN_NAMESPACE
57
58namespace {
59
60// FIXME: QFlags don't have the QMetaType::IsEnumeration flag set, although they have a QMetaEnum entry in the QMetaObject.
61// They only way to detect registered QFlags types is to find the named entry in the QMetaObject's enumerator list.
62// Ideally, this would be fixed in QMetaType.
63bool isQFlagsType(uint id)
64{
65 QMetaType type(id);
66
67 // Short-circuit to avoid more expensive operations
68 QMetaType::TypeFlags flags = type.flags();
69 if (flags.testFlag(flag: QMetaType::PointerToQObject) || flags.testFlag(flag: QMetaType::IsEnumeration)
70 || flags.testFlag(flag: QMetaType::SharedPointerToQObject) || flags.testFlag(flag: QMetaType::WeakPointerToQObject)
71 || flags.testFlag(flag: QMetaType::TrackingPointerToQObject) || flags.testFlag(flag: QMetaType::IsGadget))
72 {
73 return false;
74 }
75
76 const QMetaObject *mo = type.metaObject();
77 if (!mo) {
78 return false;
79 }
80
81 QByteArray name = QMetaType::typeName(type: id);
82 name = name.mid(index: name.lastIndexOf(c: ":") + 1);
83 return mo->indexOfEnumerator(name: name.constData()) > -1;
84}
85
86// Common scores for overload resolution
87enum OverloadScore {
88 PerfectMatchScore = 0,
89 VariantScore = 1,
90 NumberBaseScore = 2,
91 GenericConversionScore = 100,
92 IncompatibleScore = 10000,
93};
94
95// Scores the conversion of a double to a number-like user type. Better matches
96// for a JS 'number' get a lower score.
97int doubleToNumberConversionScore(int userType)
98{
99 switch (userType) {
100 case QMetaType::Bool:
101 return NumberBaseScore + 7;
102 case QMetaType::Char:
103 case QMetaType::SChar:
104 case QMetaType::UChar:
105 return NumberBaseScore + 6;
106 case QMetaType::Short:
107 case QMetaType::UShort:
108 return NumberBaseScore + 5;
109 case QMetaType::Int:
110 case QMetaType::UInt:
111 return NumberBaseScore + 4;
112 case QMetaType::Long:
113 case QMetaType::ULong:
114 return NumberBaseScore + 3;
115 case QMetaType::LongLong:
116 case QMetaType::ULongLong:
117 return NumberBaseScore + 2;
118 case QMetaType::Float:
119 return NumberBaseScore + 1;
120 case QMetaType::Double:
121 return NumberBaseScore;
122 default:
123 break;
124 }
125
126 if (QMetaType::typeFlags(type: userType) & QMetaType::IsEnumeration)
127 return doubleToNumberConversionScore(userType: QMetaType::Int);
128
129 return IncompatibleScore;
130}
131
132// Keeps track of the badness of a QMetaMethod candidate for overload resolution
133struct OverloadResolutionCandidate
134{
135 OverloadResolutionCandidate(const QMetaMethod &method = QMetaMethod(), int badness = PerfectMatchScore)
136 : method(method), badness(badness)
137 {}
138
139 QMetaMethod method;
140 int badness;
141
142 bool operator<(const OverloadResolutionCandidate &other) const { return badness < other.badness; }
143};
144
145MessageType toType(const QJsonValue &value)
146{
147 int i = value.toInt(defaultValue: -1);
148 if (i >= TYPES_FIRST_VALUE && i <= TYPES_LAST_VALUE) {
149 return static_cast<MessageType>(i);
150 } else {
151 return TypeInvalid;
152 }
153}
154
155const QString KEY_SIGNALS = QStringLiteral("signals");
156const QString KEY_METHODS = QStringLiteral("methods");
157const QString KEY_PROPERTIES = QStringLiteral("properties");
158const QString KEY_ENUMS = QStringLiteral("enums");
159const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
160const QString KEY_ID = QStringLiteral("id");
161const QString KEY_DATA = QStringLiteral("data");
162const QString KEY_OBJECT = QStringLiteral("object");
163const QString KEY_DESTROYED = QStringLiteral("destroyed");
164const QString KEY_SIGNAL = QStringLiteral("signal");
165const QString KEY_TYPE = QStringLiteral("type");
166const QString KEY_METHOD = QStringLiteral("method");
167const QString KEY_ARGS = QStringLiteral("args");
168const QString KEY_PROPERTY = QStringLiteral("property");
169const QString KEY_VALUE = QStringLiteral("value");
170
171QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data)
172{
173 QJsonObject response;
174 response[KEY_TYPE] = TypeResponse;
175 response[KEY_ID] = id;
176 response[KEY_DATA] = data;
177 return response;
178}
179
180/// TODO: what is the proper value here?
181const int PROPERTY_UPDATE_INTERVAL = 50;
182}
183
184Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_MOVABLE_TYPE);
185
186QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel)
187 : QObject(webChannel)
188 , webChannel(webChannel)
189 , signalHandler(this)
190 , clientIsIdle(false)
191 , blockUpdates(false)
192 , propertyUpdatesInitialized(false)
193{
194}
195
196QMetaObjectPublisher::~QMetaObjectPublisher()
197{
198
199}
200
201void QMetaObjectPublisher::registerObject(const QString &id, QObject *object)
202{
203 registeredObjects[id] = object;
204 registeredObjectIds[object] = id;
205 if (propertyUpdatesInitialized) {
206 if (!webChannel->d_func()->transports.isEmpty()) {
207 qWarning(msg: "Registered new object after initialization, existing clients won't be notified!");
208 // TODO: send a message to clients that an object was added
209 }
210 initializePropertyUpdates(object, objectInfo: classInfoForObject(object, Q_NULLPTR));
211 }
212}
213
214QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object, QWebChannelAbstractTransport *transport)
215{
216 QJsonObject data;
217 if (!object) {
218 qWarning(msg: "null object given to MetaObjectPublisher - bad API usage?");
219 return data;
220 }
221
222 QJsonArray qtSignals;
223 QJsonArray qtMethods;
224 QJsonArray qtProperties;
225 QJsonObject qtEnums;
226
227 const QMetaObject *metaObject = object->metaObject();
228 QSet<int> notifySignals;
229 QSet<QString> identifiers;
230 for (int i = 0; i < metaObject->propertyCount(); ++i) {
231 const QMetaProperty &prop = metaObject->property(index: i);
232 QJsonArray propertyInfo;
233 const QString &propertyName = QString::fromLatin1(str: prop.name());
234 propertyInfo.append(value: i);
235 propertyInfo.append(value: propertyName);
236 identifiers << propertyName;
237 QJsonArray signalInfo;
238 if (prop.hasNotifySignal()) {
239 notifySignals << prop.notifySignalIndex();
240 // optimize: compress the common propertyChanged notification names, just send a 1
241 const QByteArray &notifySignal = prop.notifySignal().name();
242 static const QByteArray changedSuffix = QByteArrayLiteral("Changed");
243 if (notifySignal.length() == changedSuffix.length() + propertyName.length() &&
244 notifySignal.endsWith(a: changedSuffix) && notifySignal.startsWith(c: prop.name()))
245 {
246 signalInfo.append(value: 1);
247 } else {
248 signalInfo.append(value: QString::fromLatin1(str: notifySignal));
249 }
250 signalInfo.append(value: prop.notifySignalIndex());
251 } else if (!prop.isConstant()) {
252 qWarning(msg: "Property '%s'' of object '%s' has no notify signal and is not constant, "
253 "value updates in HTML will be broken!",
254 prop.name(), object->metaObject()->className());
255 }
256 propertyInfo.append(value: signalInfo);
257 propertyInfo.append(value: wrapResult(result: prop.read(obj: object), transport));
258 qtProperties.append(value: propertyInfo);
259 }
260 auto addMethod = [&qtSignals, &qtMethods, &identifiers](int i, const QMetaMethod &method, const QByteArray &rawName) {
261 //NOTE: the name must be a string, otherwise it will be converted to '{}' in QML
262 const auto name = QString::fromLatin1(str: rawName);
263 // only the first method gets called with its name directly
264 // others must be called by explicitly passing the method signature
265 if (identifiers.contains(value: name))
266 return;
267 identifiers << name;
268 // send data as array to client with format: [name, index]
269 QJsonArray data;
270 data.append(value: name);
271 data.append(value: i);
272 if (method.methodType() == QMetaMethod::Signal) {
273 qtSignals.append(value: data);
274 } else if (method.access() == QMetaMethod::Public) {
275 qtMethods.append(value: data);
276 }
277 };
278 for (int i = 0; i < metaObject->methodCount(); ++i) {
279 if (notifySignals.contains(value: i)) {
280 continue;
281 }
282 const QMetaMethod &method = metaObject->method(index: i);
283 addMethod(i, method, method.name());
284 // for overload resolution also pass full method signature
285 addMethod(i, method, method.methodSignature());
286 }
287 for (int i = 0; i < metaObject->enumeratorCount(); ++i) {
288 QMetaEnum enumerator = metaObject->enumerator(index: i);
289 QJsonObject values;
290 for (int k = 0; k < enumerator.keyCount(); ++k) {
291 values[QString::fromLatin1(str: enumerator.key(index: k))] = enumerator.value(index: k);
292 }
293 qtEnums[QString::fromLatin1(str: enumerator.name())] = values;
294 }
295 data[KEY_SIGNALS] = qtSignals;
296 data[KEY_METHODS] = qtMethods;
297 data[KEY_PROPERTIES] = qtProperties;
298 if (!qtEnums.isEmpty()) {
299 data[KEY_ENUMS] = qtEnums;
300 }
301 return data;
302}
303
304void QMetaObjectPublisher::setClientIsIdle(bool isIdle)
305{
306 if (clientIsIdle == isIdle) {
307 return;
308 }
309 clientIsIdle = isIdle;
310 if (!isIdle && timer.isActive()) {
311 timer.stop();
312 } else if (isIdle && !timer.isActive()) {
313 timer.start(msec: PROPERTY_UPDATE_INTERVAL, obj: this);
314 }
315}
316
317QJsonObject QMetaObjectPublisher::initializeClient(QWebChannelAbstractTransport *transport)
318{
319 QJsonObject objectInfos;
320 {
321 const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd();
322 for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) {
323 const QJsonObject &info = classInfoForObject(object: it.value(), transport);
324 if (!propertyUpdatesInitialized) {
325 initializePropertyUpdates(object: it.value(), objectInfo: info);
326 }
327 objectInfos[it.key()] = info;
328 }
329 }
330 propertyUpdatesInitialized = true;
331 return objectInfos;
332}
333
334void QMetaObjectPublisher::initializePropertyUpdates(const QObject *const object, const QJsonObject &objectInfo)
335{
336 foreach (const QJsonValue &propertyInfoVar, objectInfo[KEY_PROPERTIES].toArray()) {
337 const QJsonArray &propertyInfo = propertyInfoVar.toArray();
338 if (propertyInfo.size() < 2) {
339 qWarning() << "Invalid property info encountered:" << propertyInfoVar;
340 continue;
341 }
342 const int propertyIndex = propertyInfo.at(i: 0).toInt();
343 const QJsonArray &signalData = propertyInfo.at(i: 2).toArray();
344
345 if (signalData.isEmpty()) {
346 // Property without NOTIFY signal
347 continue;
348 }
349
350 const int signalIndex = signalData.at(i: 1).toInt();
351
352 QSet<int> &connectedProperties = signalToPropertyMap[object][signalIndex];
353
354 // Only connect for a property update once
355 if (connectedProperties.isEmpty()) {
356 signalHandler.connectTo(object, signalIndex);
357 }
358
359 connectedProperties.insert(value: propertyIndex);
360 }
361
362 // also always connect to destroyed signal
363 signalHandler.connectTo(object, signalIndex: s_destroyedSignalIndex);
364}
365
366void QMetaObjectPublisher::sendPendingPropertyUpdates()
367{
368 if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) {
369 return;
370 }
371
372 QJsonArray data;
373 QHash<QWebChannelAbstractTransport*, QJsonArray> specificUpdates;
374
375 // convert pending property updates to JSON data
376 const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd();
377 for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) {
378 const QObject *object = it.key();
379 const QMetaObject *const metaObject = object->metaObject();
380 const QString objectId = registeredObjectIds.value(akey: object);
381 const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(akey: object);
382 // maps property name to current property value
383 QJsonObject properties;
384 // maps signal index to list of arguments of the last emit
385 QJsonObject sigs;
386 const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd();
387 for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) {
388 // TODO: can we get rid of the int <-> string conversions here?
389 foreach (const int propertyIndex, objectsSignalToPropertyMap.value(sigIt.key())) {
390 const QMetaProperty &property = metaObject->property(index: propertyIndex);
391 Q_ASSERT(property.isValid());
392 properties[QString::number(propertyIndex)] = wrapResult(result: property.read(obj: object), Q_NULLPTR, parentObjectId: objectId);
393 }
394 sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(list: sigIt.value());
395 }
396 QJsonObject obj;
397 obj[KEY_OBJECT] = objectId;
398 obj[KEY_SIGNALS] = sigs;
399 obj[KEY_PROPERTIES] = properties;
400
401 // if the object is auto registered, just send the update only to clients which know this object
402 if (wrappedObjects.contains(akey: objectId)) {
403 foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectId).transports) {
404 QJsonArray &arr = specificUpdates[transport];
405 arr.push_back(t: obj);
406 }
407 } else {
408 data.push_back(t: obj);
409 }
410 }
411
412 pendingPropertyUpdates.clear();
413 QJsonObject message;
414 message[KEY_TYPE] = TypePropertyUpdate;
415
416 // data does not contain specific updates
417 if (!data.isEmpty()) {
418 setClientIsIdle(false);
419
420 message[KEY_DATA] = data;
421 broadcastMessage(message);
422 }
423
424 // send every property update which is not supposed to be broadcasted
425 const QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator suend = specificUpdates.constEnd();
426 for (QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator it = specificUpdates.constBegin(); it != suend; ++it) {
427 message[KEY_DATA] = it.value();
428 it.key()->sendMessage(message);
429 }
430}
431
432QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QMetaMethod &method,
433 const QJsonArray &args)
434{
435 if (method.name() == QByteArrayLiteral("deleteLater")) {
436 // invoke `deleteLater` on wrapped QObject indirectly
437 deleteWrappedObject(object);
438 return QJsonValue();
439 } else if (!method.isValid()) {
440 qWarning() << "Cannot invoke invalid method on object" << object << '.';
441 return QJsonValue();
442 } else if (method.access() != QMetaMethod::Public) {
443 qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
444 return QJsonValue();
445 } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) {
446 qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
447 return QJsonValue();
448 } else if (args.size() > 10) {
449 qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke.";
450 return QJsonValue();
451 } else if (args.size() > method.parameterCount()) {
452 qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':'
453 << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.';
454 }
455
456 // construct converter objects of QVariant to QGenericArgument
457 VariantArgument arguments[10];
458 for (int i = 0; i < qMin(a: args.size(), b: method.parameterCount()); ++i) {
459 arguments[i].value = toVariant(value: args.at(i), targetType: method.parameterType(index: i));
460 }
461 // construct QGenericReturnArgument
462 QVariant returnValue;
463 if (method.returnType() == QMetaType::Void) {
464 // Skip return for void methods (prevents runtime warnings inside Qt), and allows
465 // QMetaMethod to invoke void-returning methods on QObjects in a different thread.
466 method.invoke(object,
467 val0: arguments[0], val1: arguments[1], val2: arguments[2], val3: arguments[3], val4: arguments[4],
468 val5: arguments[5], val6: arguments[6], val7: arguments[7], val8: arguments[8], val9: arguments[9]);
469 } else {
470 // Only init variant with return type if its not a variant itself, which would
471 // lead to nested variants which is not what we want.
472 if (method.returnType() != QMetaType::QVariant)
473 returnValue = QVariant(method.returnType(), 0);
474
475 QGenericReturnArgument returnArgument(method.typeName(), returnValue.data());
476 method.invoke(object, returnValue: returnArgument,
477 val0: arguments[0], val1: arguments[1], val2: arguments[2], val3: arguments[3], val4: arguments[4],
478 val5: arguments[5], val6: arguments[6], val7: arguments[7], val8: arguments[8], val9: arguments[9]);
479 }
480 // now we can call the method
481 return returnValue;
482}
483
484QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex,
485 const QJsonArray &args)
486{
487 const QMetaMethod &method = object->metaObject()->method(index: methodIndex);
488 if (!method.isValid()) {
489 qWarning() << "Cannot invoke method of unknown index" << methodIndex << "on object"
490 << object << '.';
491 return QJsonValue();
492 }
493 return invokeMethod(object, method, args);
494}
495
496QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QByteArray &methodName,
497 const QJsonArray &args)
498{
499 QVector<OverloadResolutionCandidate> candidates;
500
501 const QMetaObject *mo = object->metaObject();
502 for (int i = 0; i < mo->methodCount(); ++i) {
503 QMetaMethod method = mo->method(index: i);
504 if (method.name() != methodName || method.parameterCount() != args.count()
505 || method.access() != QMetaMethod::Public
506 || (method.methodType() != QMetaMethod::Method
507 && method.methodType() != QMetaMethod::Slot)
508 || method.parameterCount() > 10)
509 {
510 // Not a candidate
511 continue;
512 }
513
514 candidates.append(t: {method, methodOverloadBadness(method, args)});
515 }
516
517 if (candidates.isEmpty()) {
518 qWarning() << "No candidates found for" << methodName << "with" << args.size()
519 << "arguments on object" << object << '.';
520 return QJsonValue();
521 }
522
523 std::sort(first: candidates.begin(), last: candidates.end());
524
525 if (candidates.size() > 1 && candidates[0].badness == candidates[1].badness) {
526 qWarning().nospace() << "Ambiguous overloads for method " << methodName << ". Choosing "
527 << candidates.first().method.methodSignature();
528 }
529
530 return invokeMethod(object, method: candidates.first().method, args);
531}
532
533void QMetaObjectPublisher::setProperty(QObject *object, const int propertyIndex, const QJsonValue &value)
534{
535 QMetaProperty property = object->metaObject()->property(index: propertyIndex);
536 if (!property.isValid()) {
537 qWarning() << "Cannot set unknown property" << propertyIndex << "of object" << object;
538 } else if (!property.write(obj: object, value: toVariant(value, targetType: property.userType()))) {
539 qWarning() << "Could not write value " << value << "to property" << property.name() << "of object" << object;
540 }
541}
542
543void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments)
544{
545 if (!webChannel || webChannel->d_func()->transports.isEmpty()) {
546 if (signalIndex == s_destroyedSignalIndex)
547 objectDestroyed(object);
548 return;
549 }
550 if (!signalToPropertyMap.value(akey: object).contains(akey: signalIndex)) {
551 QJsonObject message;
552 const QString &objectName = registeredObjectIds.value(akey: object);
553 Q_ASSERT(!objectName.isEmpty());
554 message[KEY_OBJECT] = objectName;
555 message[KEY_SIGNAL] = signalIndex;
556 if (!arguments.isEmpty()) {
557 message[KEY_ARGS] = wrapList(list: arguments, Q_NULLPTR, parentObjectId: objectName);
558 }
559 message[KEY_TYPE] = TypeSignal;
560
561 // if the object is wrapped, just send the response to clients which know this object
562 if (wrappedObjects.contains(akey: objectName)) {
563 foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectName).transports) {
564 transport->sendMessage(message);
565 }
566 } else {
567 broadcastMessage(message);
568 }
569
570 if (signalIndex == s_destroyedSignalIndex) {
571 objectDestroyed(object);
572 }
573 } else {
574 pendingPropertyUpdates[object][signalIndex] = arguments;
575 if (clientIsIdle && !blockUpdates && !timer.isActive()) {
576 timer.start(msec: PROPERTY_UPDATE_INTERVAL, obj: this);
577 }
578 }
579}
580
581void QMetaObjectPublisher::objectDestroyed(const QObject *object)
582{
583 const QString &id = registeredObjectIds.take(akey: object);
584 Q_ASSERT(!id.isEmpty());
585 bool removed = registeredObjects.remove(akey: id)
586 || wrappedObjects.remove(akey: id);
587 Q_ASSERT(removed);
588 Q_UNUSED(removed);
589
590 // only remove from handler when we initialized the property updates
591 // cf: https://bugreports.qt.io/browse/QTBUG-60250
592 if (propertyUpdatesInitialized) {
593 signalHandler.remove(object);
594 signalToPropertyMap.remove(akey: object);
595 }
596 pendingPropertyUpdates.remove(akey: object);
597}
598
599QObject *QMetaObjectPublisher::unwrapObject(const QString &objectId) const
600{
601 if (!objectId.isEmpty()) {
602 ObjectInfo objectInfo = wrappedObjects.value(akey: objectId);
603 if (objectInfo.object)
604 return objectInfo.object;
605 QObject *object = registeredObjects.value(akey: objectId);
606 if (object)
607 return object;
608 }
609
610 qWarning() << "No wrapped object" << objectId;
611 return Q_NULLPTR;
612}
613
614QVariant QMetaObjectPublisher::toVariant(const QJsonValue &value, int targetType) const
615{
616 if (targetType == QMetaType::QJsonValue) {
617 return QVariant::fromValue(value);
618 } else if (targetType == QMetaType::QJsonArray) {
619 if (!value.isArray())
620 qWarning() << "Cannot not convert non-array argument" << value << "to QJsonArray.";
621 return QVariant::fromValue(value: value.toArray());
622 } else if (targetType == QMetaType::QJsonObject) {
623 if (!value.isObject())
624 qWarning() << "Cannot not convert non-object argument" << value << "to QJsonObject.";
625 return QVariant::fromValue(value: value.toObject());
626 } else if (QMetaType::typeFlags(type: targetType) & QMetaType::PointerToQObject) {
627 QObject *unwrappedObject = unwrapObject(objectId: value.toObject()[KEY_ID].toString());
628 if (unwrappedObject == Q_NULLPTR)
629 qWarning() << "Cannot not convert non-object argument" << value << "to QObject*.";
630 return QVariant::fromValue(value: unwrappedObject);
631 } else if (isQFlagsType(id: targetType)) {
632 int flagsValue = value.toInt();
633 return QVariant(targetType, reinterpret_cast<const void*>(&flagsValue));
634 }
635
636 // this converts QJsonObjects to QVariantMaps, which is not desired when
637 // we want to get a QJsonObject or QJsonValue (see above)
638 QVariant variant = value.toVariant();
639 if (targetType != QMetaType::QVariant && !variant.convert(targetTypeId: targetType)) {
640 qWarning() << "Could not convert argument" << value << "to target type" << QVariant::typeToName(typeId: targetType) << '.';
641 }
642 return variant;
643}
644
645int QMetaObjectPublisher::conversionScore(const QJsonValue &value, int targetType) const
646{
647 if (targetType == QMetaType::QJsonValue) {
648 return PerfectMatchScore;
649 } else if (targetType == QMetaType::QJsonArray) {
650 return value.isArray() ? PerfectMatchScore : IncompatibleScore;
651 } else if (targetType == QMetaType::QJsonObject) {
652 return value.isObject() ? PerfectMatchScore : IncompatibleScore;
653 } else if (QMetaType::typeFlags(type: targetType) & QMetaType::PointerToQObject) {
654 if (value.isNull())
655 return PerfectMatchScore;
656 if (!value.isObject())
657 return IncompatibleScore;
658
659 QJsonObject object = value.toObject();
660 if (object[KEY_ID].isUndefined())
661 return IncompatibleScore;
662
663 QObject *unwrappedObject = unwrapObject(objectId: object[KEY_ID].toString());
664 return unwrappedObject != Q_NULLPTR ? PerfectMatchScore : IncompatibleScore;
665 } else if (targetType == QMetaType::QVariant) {
666 return VariantScore;
667 }
668
669 // Check if this is a number conversion
670 if (value.isDouble()) {
671 int score = doubleToNumberConversionScore(userType: targetType);
672 if (score != IncompatibleScore) {
673 return score;
674 }
675 }
676
677 QVariant variant = value.toVariant();
678 if (variant.userType() == targetType) {
679 return PerfectMatchScore;
680 } else if (variant.canConvert(targetTypeId: targetType)) {
681 return GenericConversionScore;
682 }
683
684 return IncompatibleScore;
685}
686
687int QMetaObjectPublisher::methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const
688{
689 int badness = PerfectMatchScore;
690 for (int i = 0; i < args.size(); ++i) {
691 badness += conversionScore(value: args[i], targetType: method.parameterType(index: i));
692 }
693 return badness;
694}
695
696void QMetaObjectPublisher::transportRemoved(QWebChannelAbstractTransport *transport)
697{
698 auto it = transportedWrappedObjects.find(akey: transport);
699 // It is not allowed to modify a container while iterating over it. So save
700 // objects which should be removed and call objectDestroyed() on them later.
701 QVector<QObject*> objectsForDeletion;
702 while (it != transportedWrappedObjects.end() && it.key() == transport) {
703 if (wrappedObjects.contains(akey: it.value())) {
704 QVector<QWebChannelAbstractTransport*> &transports = wrappedObjects[it.value()].transports;
705 transports.removeOne(t: transport);
706 if (transports.isEmpty())
707 objectsForDeletion.append(t: wrappedObjects[it.value()].object);
708 }
709
710 it++;
711 }
712
713 transportedWrappedObjects.remove(akey: transport);
714
715 foreach (QObject *obj, objectsForDeletion)
716 objectDestroyed(object: obj);
717}
718
719// NOTE: transport can be a nullptr
720// in such a case, we need to ensure that the property is registered to
721// the target transports of the parentObjectId
722QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport,
723 const QString &parentObjectId)
724{
725 if (QObject *object = result.value<QObject *>()) {
726 QString id = registeredObjectIds.value(akey: object);
727
728 QJsonObject classInfo;
729 if (id.isEmpty()) {
730 // neither registered, nor wrapped, do so now
731 id = QUuid::createUuid().toString();
732 // store ID before the call to classInfoForObject()
733 // in case of self-contained objects it avoids
734 // infinite loops
735 registeredObjectIds[object] = id;
736
737 classInfo = classInfoForObject(object, transport);
738
739 ObjectInfo oi(object);
740 if (transport) {
741 oi.transports.append(t: transport);
742 transportedWrappedObjects.insert(akey: transport, avalue: id);
743 } else {
744 // use the transports from the parent object
745 oi.transports = wrappedObjects.value(akey: parentObjectId).transports;
746 // or fallback to all transports if the parent is not wrapped
747 if (oi.transports.isEmpty())
748 oi.transports = webChannel->d_func()->transports;
749
750 for (auto transport : qAsConst(t&: oi.transports)) {
751 transportedWrappedObjects.insert(akey: transport, avalue: id);
752 }
753 }
754 wrappedObjects.insert(akey: id, avalue: oi);
755
756 initializePropertyUpdates(object, objectInfo: classInfo);
757 } else {
758 auto oi = wrappedObjects.find(akey: id);
759 if (oi != wrappedObjects.end() && !oi->isBeingWrapped) {
760 Q_ASSERT(object == oi->object);
761 // check if this transport is already assigned to the object
762 if (transport && !oi->transports.contains(t: transport)) {
763 oi->transports.append(t: transport);
764 transportedWrappedObjects.insert(akey: transport, avalue: id);
765 }
766 // QTBUG-84007: Block infinite recursion for self-contained objects
767 // which have already been wrapped
768 oi->isBeingWrapped = true;
769 classInfo = classInfoForObject(object, transport);
770 oi->isBeingWrapped = false;
771 }
772 }
773
774 QJsonObject objectInfo;
775 objectInfo[KEY_QOBJECT] = true;
776 objectInfo[KEY_ID] = id;
777 if (!classInfo.isEmpty())
778 objectInfo[KEY_DATA] = classInfo;
779
780 return objectInfo;
781 } else if (QMetaType::typeFlags(type: result.userType()).testFlag(flag: QMetaType::IsEnumeration)) {
782 return result.toInt();
783 } else if (isQFlagsType(id: result.userType())) {
784 return *reinterpret_cast<const int*>(result.constData());
785#ifndef QT_NO_JSVALUE
786 } else if (result.canConvert<QJSValue>()) {
787 // Workaround for keeping QJSValues from QVariant.
788 // Calling QJSValue::toVariant() converts JS-objects/arrays to QVariantMap/List
789 // instead of stashing a QJSValue itself into a variant.
790 // TODO: Improve QJSValue-QJsonValue conversion in Qt.
791 return wrapResult(result: result.value<QJSValue>().toVariant(), transport, parentObjectId);
792#endif
793 } else if (result.canConvert<QVariantList>()) {
794 // recurse and potentially wrap contents of the array
795 // *don't* use result.toList() as that *only* works for QVariantList and QStringList!
796 // Also, don't use QSequentialIterable (yet), since that seems to trigger QTBUG-42016
797 // in certain cases.
798 // additionally, when there's a direct converter to QVariantList, use that one via convert
799 // but recover when conversion fails and fall back to the .value<QVariantList> conversion
800 // see also: https://bugreports.qt.io/browse/QTBUG-80751
801 auto list = result;
802 if (!list.convert(targetTypeId: qMetaTypeId<QVariantList>()))
803 list = result;
804 return wrapList(list: list.value<QVariantList>(), transport);
805 } else if (result.canConvert<QVariantMap>()) {
806 // recurse and potentially wrap contents of the map
807 auto map = result;
808 if (!map.convert(targetTypeId: qMetaTypeId<QVariantMap>()))
809 map = result;
810 return wrapMap(map: map.value<QVariantMap>(), transport);
811 }
812
813 return QJsonValue::fromVariant(variant: result);
814}
815
816QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list, QWebChannelAbstractTransport *transport, const QString &parentObjectId)
817{
818 QJsonArray array;
819 foreach (const QVariant &arg, list) {
820 array.append(value: wrapResult(result: arg, transport, parentObjectId));
821 }
822 return array;
823}
824
825QJsonObject QMetaObjectPublisher::wrapMap(const QVariantMap &map, QWebChannelAbstractTransport *transport, const QString &parentObjectId)
826{
827 QJsonObject obj;
828 for (QVariantMap::const_iterator i = map.begin(); i != map.end(); i++) {
829 obj.insert(key: i.key(), value: wrapResult(result: i.value(), transport, parentObjectId));
830 }
831 return obj;
832}
833
834void QMetaObjectPublisher::deleteWrappedObject(QObject *object) const
835{
836 if (!wrappedObjects.contains(akey: registeredObjectIds.value(akey: object))) {
837 qWarning() << "Not deleting non-wrapped object" << object;
838 return;
839 }
840 object->deleteLater();
841}
842
843void QMetaObjectPublisher::broadcastMessage(const QJsonObject &message) const
844{
845 if (webChannel->d_func()->transports.isEmpty()) {
846 qWarning(msg: "QWebChannel is not connected to any transports, cannot send message: %s", QJsonDocument(message).toJson().constData());
847 return;
848 }
849
850 foreach (QWebChannelAbstractTransport *transport, webChannel->d_func()->transports) {
851 transport->sendMessage(message);
852 }
853}
854
855void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannelAbstractTransport *transport)
856{
857 if (!webChannel->d_func()->transports.contains(t: transport)) {
858 qWarning() << "Refusing to handle message of unknown transport:" << transport;
859 return;
860 }
861
862 if (!message.contains(key: KEY_TYPE)) {
863 qWarning(msg: "JSON message object is missing the type property: %s", QJsonDocument(message).toJson().constData());
864 return;
865 }
866
867 const MessageType type = toType(value: message.value(key: KEY_TYPE));
868 if (type == TypeIdle) {
869 setClientIsIdle(true);
870 } else if (type == TypeInit) {
871 if (!message.contains(key: KEY_ID)) {
872 qWarning(msg: "JSON message object is missing the id property: %s",
873 QJsonDocument(message).toJson().constData());
874 return;
875 }
876 transport->sendMessage(message: createResponse(id: message.value(key: KEY_ID), data: initializeClient(transport)));
877 } else if (type == TypeDebug) {
878 static QTextStream out(stdout);
879 out << "DEBUG: " << message.value(key: KEY_DATA).toString() << Qt::endl;
880 } else if (message.contains(key: KEY_OBJECT)) {
881 const QString &objectName = message.value(key: KEY_OBJECT).toString();
882 QObject *object = registeredObjects.value(akey: objectName);
883 if (!object)
884 object = wrappedObjects.value(akey: objectName).object;
885
886 if (!object) {
887 qWarning() << "Unknown object encountered" << objectName;
888 return;
889 }
890
891 if (type == TypeInvokeMethod) {
892 if (!message.contains(key: KEY_ID)) {
893 qWarning(msg: "JSON message object is missing the id property: %s",
894 QJsonDocument(message).toJson().constData());
895 return;
896 }
897
898 QPointer<QMetaObjectPublisher> publisherExists(this);
899 QPointer<QWebChannelAbstractTransport> transportExists(transport);
900 QJsonValue method = message.value(key: KEY_METHOD);
901 QVariant result;
902
903 if (method.isString()) {
904 result = invokeMethod(object,
905 methodName: method.toString().toUtf8(),
906 args: message.value(key: KEY_ARGS).toArray());
907 } else {
908 result = invokeMethod(object,
909 methodIndex: method.toInt(defaultValue: -1),
910 args: message.value(key: KEY_ARGS).toArray());
911 }
912 if (!publisherExists || !transportExists)
913 return;
914 transport->sendMessage(message: createResponse(id: message.value(key: KEY_ID), data: wrapResult(result, transport)));
915 } else if (type == TypeConnectToSignal) {
916 signalHandler.connectTo(object, signalIndex: message.value(key: KEY_SIGNAL).toInt(defaultValue: -1));
917 } else if (type == TypeDisconnectFromSignal) {
918 signalHandler.disconnectFrom(object, signalIndex: message.value(key: KEY_SIGNAL).toInt(defaultValue: -1));
919 } else if (type == TypeSetProperty) {
920 setProperty(object, propertyIndex: message.value(key: KEY_PROPERTY).toInt(defaultValue: -1),
921 value: message.value(key: KEY_VALUE));
922 }
923 }
924}
925
926void QMetaObjectPublisher::setBlockUpdates(bool block)
927{
928 if (blockUpdates == block) {
929 return;
930 }
931 blockUpdates = block;
932
933 if (!blockUpdates) {
934 sendPendingPropertyUpdates();
935 } else if (timer.isActive()) {
936 timer.stop();
937 }
938
939 emit blockUpdatesChanged(block);
940}
941
942void QMetaObjectPublisher::timerEvent(QTimerEvent *event)
943{
944 if (event->timerId() == timer.timerId()) {
945 sendPendingPropertyUpdates();
946 } else {
947 QObject::timerEvent(event);
948 }
949}
950
951QT_END_NAMESPACE
952

source code of qtwebchannel/src/webchannel/qmetaobjectpublisher.cpp