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 | |
56 | QT_BEGIN_NAMESPACE |
57 | |
58 | namespace { |
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. |
63 | bool 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 |
87 | enum 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. |
97 | int 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 |
133 | struct 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 | |
145 | MessageType 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 | |
155 | const QString KEY_SIGNALS = QStringLiteral("signals" ); |
156 | const QString KEY_METHODS = QStringLiteral("methods" ); |
157 | const QString KEY_PROPERTIES = QStringLiteral("properties" ); |
158 | const QString KEY_ENUMS = QStringLiteral("enums" ); |
159 | const QString KEY_QOBJECT = QStringLiteral("__QObject*__" ); |
160 | const QString KEY_ID = QStringLiteral("id" ); |
161 | const QString KEY_DATA = QStringLiteral("data" ); |
162 | const QString KEY_OBJECT = QStringLiteral("object" ); |
163 | const QString KEY_DESTROYED = QStringLiteral("destroyed" ); |
164 | const QString KEY_SIGNAL = QStringLiteral("signal" ); |
165 | const QString KEY_TYPE = QStringLiteral("type" ); |
166 | const QString KEY_METHOD = QStringLiteral("method" ); |
167 | const QString KEY_ARGS = QStringLiteral("args" ); |
168 | const QString KEY_PROPERTY = QStringLiteral("property" ); |
169 | const QString KEY_VALUE = QStringLiteral("value" ); |
170 | |
171 | QJsonObject 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? |
181 | const int PROPERTY_UPDATE_INTERVAL = 50; |
182 | } |
183 | |
184 | Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_MOVABLE_TYPE); |
185 | |
186 | QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel) |
187 | : QObject(webChannel) |
188 | , webChannel(webChannel) |
189 | , signalHandler(this) |
190 | , clientIsIdle(false) |
191 | , blockUpdates(false) |
192 | , propertyUpdatesInitialized(false) |
193 | { |
194 | } |
195 | |
196 | QMetaObjectPublisher::~QMetaObjectPublisher() |
197 | { |
198 | |
199 | } |
200 | |
201 | void 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 | |
214 | QJsonObject 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 ¬ifySignal = 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 | |
304 | void 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 | |
317 | QJsonObject 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 | |
334 | void 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 | |
366 | void 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 | |
432 | QVariant 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 | |
484 | QVariant 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 | |
496 | QVariant 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 | |
533 | void 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 | |
543 | void 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 | |
581 | void 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 | |
599 | QObject *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 | |
614 | QVariant 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 | |
645 | int 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 | |
687 | int 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 | |
696 | void 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 |
722 | QJsonValue 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 | |
816 | QJsonArray 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 | |
825 | QJsonObject 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 | |
834 | void 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 | |
843 | void 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 | |
855 | void 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 | |
926 | void 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 | |
942 | void QMetaObjectPublisher::timerEvent(QTimerEvent *event) |
943 | { |
944 | if (event->timerId() == timer.timerId()) { |
945 | sendPendingPropertyUpdates(); |
946 | } else { |
947 | QObject::timerEvent(event); |
948 | } |
949 | } |
950 | |
951 | QT_END_NAMESPACE |
952 | |