diff options
author | Bernd Lamecker <bernd.lamecker@basyskom.com> | 2014-08-12 16:46:59 +0200 |
---|---|---|
committer | Milian Wolff <milian.wolff@kdab.com> | 2014-12-19 11:39:52 +0100 |
commit | 3ed29bca08dec484003cf573dd428b73627ffa81 (patch) | |
tree | 8e688a00c409cbd4a37412342faa2185b7b3869d | |
parent | 9fdce8e443030ab99d31e42fffc977cf284c36c4 (diff) | |
download | qtwebchannel-3ed29bca08dec484003cf573dd428b73627ffa81.tar.gz |
Do not broadcast signals or property changes of wrapped qobjects
Signals and property changes caused by dynamically created
qobjects (qobjects returned from a method call at runtime)
should not be broadcasted to any client but only sent to clients
which know these qobjects.
Also added testcases for the changes.
Change-Id: I9aacfa9e7e9df9314b44c6ba8e7339a2069e3c37
Reviewed-by: Milian Wolff <milian.wolff@kdab.com>
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 69 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher_p.h | 34 | ||||
-rw-r--r-- | tests/auto/qml/Client.qml | 9 | ||||
-rw-r--r-- | tests/auto/qml/qml.pro | 5 | ||||
-rw-r--r-- | tests/auto/qml/tst_webchannel.qml | 2 | ||||
-rw-r--r-- | tests/auto/qml/tst_webchannelseparation.qml | 356 |
6 files changed, 444 insertions, 31 deletions
diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp index 3e78ebc..a141035 100644 --- a/src/webchannel/qmetaobjectpublisher.cpp +++ b/src/webchannel/qmetaobjectpublisher.cpp @@ -41,6 +41,7 @@ #include <QDebug> #include <QJsonObject> #include <QJsonArray> +#include <QJSValue> #include <QUuid> QT_BEGIN_NAMESPACE @@ -272,6 +273,7 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() } QJsonArray data; + QHash<QWebChannelAbstractTransport*, QJsonArray> specificUpdates; // convert pending property updates to JSON data const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd(); @@ -294,21 +296,41 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value()); } QJsonObject obj; - obj[KEY_OBJECT] = registeredObjectIds.value(object); + const QString objectId = registeredObjectIds.value(object); + obj[KEY_OBJECT] = objectId; obj[KEY_SIGNALS] = sigs; obj[KEY_PROPERTIES] = properties; - data.push_back(obj); + + // 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; - message[KEY_DATA] = data; + message[KEY_DATA] = data; // data does not contain specific updates + setClientIsIdle(false); + 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_TYPE] = TypePropertyUpdate; + message[KEY_DATA] = it.value(); + it.key()->sendMessage(message); + } } -QJsonValue QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args) { const QMetaMethod &method = object->metaObject()->method(methodIndex); @@ -359,7 +381,7 @@ QJsonValue QMetaObjectPublisher::invokeMethod(QObject *const object, const int m arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); - return wrapResult(returnValue); + return returnValue; } void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments) @@ -381,7 +403,15 @@ void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signal message[KEY_ARGS] = args; } message[KEY_TYPE] = TypeSignal; - broadcastMessage(message); + + // 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); @@ -407,7 +437,7 @@ void QMetaObjectPublisher::objectDestroyed(const QObject *object) pendingPropertyUpdates.remove(object); } -QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result) +QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport) { if (QObject *object = result.value<QObject *>()) { QString id = registeredObjectIds.value(object); @@ -416,7 +446,10 @@ QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result) if (!id.isEmpty() && wrappedObjects.contains(id)) { Q_ASSERT(object == wrappedObjects.value(id).object); - return wrappedObjects.value(id).info; + // check if this transport is already assigned to the object + if (!wrappedObjects.value(id).transports.contains(transport)) + wrappedObjects[id].transports.append(transport); + return wrappedObjects.value(id).classinfo; } else { id = QUuid::createUuid().toString(); @@ -427,18 +460,25 @@ QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result) if (!registeredObjects.contains(id)) { registeredObjectIds[object] = id; - ObjectInfo oi = { object, objectInfo }; + ObjectInfo oi(object, objectInfo); + oi.transports.append(transport); wrappedObjects.insert(id, oi); initializePropertyUpdates(object, info); } } - return objectInfo; } - // no need to wrap this - return QJsonValue::fromVariant(result); + // 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. + QVariant jsvVariant = result; + if (result.canConvert<QJSValue>()) + jsvVariant = result.value<QJSValue>().toVariant(); + + return QJsonValue::fromVariant(jsvVariant); } void QMetaObjectPublisher::deleteWrappedObject(QObject *object) const @@ -504,9 +544,10 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel QJsonDocument(message).toJson().constData()); return; } + transport->sendMessage(createResponse(message.value(KEY_ID), - invokeMethod(object, message.value(KEY_METHOD).toInt(-1), - message.value(KEY_ARGS).toArray()))); + wrapResult(invokeMethod(object, message.value(KEY_METHOD).toInt(-1), + message.value(KEY_ARGS).toArray()), transport))); } else if (type == TypeConnectToSignal) { signalHandler.connectTo(object, message.value(KEY_SIGNAL).toInt(-1)); } else if (type == TypeDisconnectFromSignal) { diff --git a/src/webchannel/qmetaobjectpublisher_p.h b/src/webchannel/qmetaobjectpublisher_p.h index 6ba5ee7..1d269f1 100644 --- a/src/webchannel/qmetaobjectpublisher_p.h +++ b/src/webchannel/qmetaobjectpublisher_p.h @@ -41,7 +41,7 @@ #include <QMetaObject> #include <QBasicTimer> #include <QPointer> -#include <QJsonValue> +#include <QJsonObject> #include "qwebchannelglobal.h" @@ -72,7 +72,6 @@ class QWebChannelAbstractTransport; class Q_WEBCHANNEL_EXPORT QMetaObjectPublisher : public QObject { Q_OBJECT - public: explicit QMetaObjectPublisher(QWebChannel *webChannel); virtual ~QMetaObjectPublisher(); @@ -137,7 +136,7 @@ public: * The return value of the method invocation is then serialized and a response message * is returned. */ - QJsonValue invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); + QVariant invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); /** * Callback of the signalHandler which forwards the signal invocation to the webchannel clients. @@ -159,7 +158,7 @@ public: * * TODO: support wrapping of initially-registered objects */ - QJsonValue wrapResult(const QVariant &result); + QJsonValue wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport); /** * Invoke delete later on @p object. @@ -208,6 +207,24 @@ private: // Map the registered objects to their id. QHash<const QObject *, QString> registeredObjectIds; + // Groups individually wrapped objects with their class information and the transports that have access to it. + struct ObjectInfo + { + ObjectInfo() + : object(Q_NULLPTR) + {} + ObjectInfo(QObject *o, const QJsonObject &i) + : object(o) + , classinfo(i) + {} + QObject *object; + QJsonObject classinfo; + QVector<QWebChannelAbstractTransport*> transports; + }; + + // Map of objects wrapped from invocation returns + QHash<QString, ObjectInfo> wrappedObjects; + // Map of objects to maps of signal indices to a set of all their property indices. // The last value is a set as a signal can be the notify signal of multiple properties. typedef QHash<int, QSet<int> > SignalToPropertyNameMap; @@ -219,15 +236,6 @@ private: typedef QHash<const QObject *, SignalToArgumentsMap> PendingPropertyUpdates; PendingPropertyUpdates pendingPropertyUpdates; - // Struct containing the object itself and its ObjectInfo - struct ObjectInfo { - QObject* object; - QJsonValue info; - }; - - // Maps wrapped object to class info - QHash<QString, ObjectInfo> wrappedObjects; - // Aggregate property updates since we get multiple Qt.idle message when we have multiple // clients. They all share the same QWebProcess though so we must take special care to // prevent message flooding. diff --git a/tests/auto/qml/Client.qml b/tests/auto/qml/Client.qml index 6e12993..c18644f 100644 --- a/tests/auto/qml/Client.qml +++ b/tests/auto/qml/Client.qml @@ -98,7 +98,12 @@ Item { for (var i = 0; i < 10 && !root[from].length; ++i) wait(10); - return root[from].shift(); + + var msg = root[from].shift(); + if (debug) { + console.log((root.objectName ? "(" + root.objectName + ")" : ""), "Shifting Message " + from + ":" + JSON.stringify(msg)); + } + return msg; } function awaitMessage(from) @@ -114,7 +119,7 @@ Item { function await(type, from, skip) { var msg; do { - msg = awaitMessage(); + msg = awaitMessage(from); verify(msg); } while (skip && (msg.type === JSClient.QWebChannelMessageTypes.idle)); if (type !== null) { diff --git a/tests/auto/qml/qml.pro b/tests/auto/qml/qml.pro index 89c5f56..5dbac40 100644 --- a/tests/auto/qml/qml.pro +++ b/tests/auto/qml/qml.pro @@ -22,6 +22,9 @@ OTHER_FILES += \ tst_webchannel.qml \ tst_metaobjectpublisher.qml \ tst_bench.qml \ - tst_multiclient.qml \ + tst_multiclient.qml TESTDATA = data/* + +DISTFILES += \ + tst_webchannelseparation.qml diff --git a/tests/auto/qml/tst_webchannel.qml b/tests/auto/qml/tst_webchannel.qml index f304197..8f40cf5 100644 --- a/tests/auto/qml/tst_webchannel.qml +++ b/tests/auto/qml/tst_webchannel.qml @@ -139,7 +139,6 @@ TestCase { compare(myObj.myProperty, 3); client.awaitIdle(); // init - client.awaitIdle(); // property update // change property, should be propagated to HTML client and a message be send there myObj.myProperty = 2; @@ -332,6 +331,7 @@ TestCase { compare(channel.objects[testObjId].myProperty, 42); channel.objects[testObjId].deleteLater(); + client.awaitIdle(); msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); } diff --git a/tests/auto/qml/tst_webchannelseparation.qml b/tests/auto/qml/tst_webchannelseparation.qml new file mode 100644 index 0000000..289f8b2 --- /dev/null +++ b/tests/auto/qml/tst_webchannelseparation.qml @@ -0,0 +1,356 @@ +/**************************************************************************** +** +** Copyright (C) 2014 basysKom GmbH, info@basyskom.com, author Lutz Schönemann <lutz.schoenemann@basyskom.com> +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.0 +import QtTest 1.0 + +import QtWebChannel 1.0 +import QtWebChannel.Tests 1.0 +import "qrc:///qtwebchannel/qwebchannel.js" as JSClient + +TestCase { + name: "WebChannelSeparation" + + Client { + id: client1 + objectName: "client1" + } + Client { + id: client2 + objectName: "client2" + } + + property var lastMethodArg + + QtObject { + id: myObj + property int myProperty: 1 + + signal mySignal(var arg) + + function myMethod(arg) + { + lastMethodArg = arg; + } + + WebChannel.id: "myObj" + } + QtObject { + id: myOtherObj + property var foo: 1 + property var bar: 1 + WebChannel.id: "myOtherObj" + } + QtObject { + id: myObj2 + function myMethod() + { + // return a javascript object which is handled as an Object, but not a QObject + return { "obj1": { "name": "n1", "value": 1 }, "obj2" : {"name": "n2", "value": 2} }; + } + + WebChannel.id: "myObj2" + } + + QtObject { + id: myObj3 + + // always returns the same object + function getObject() + { + return myObj; + } + WebChannel.id: "myObj3" + } + QtObject { + id: myFactory + property var lastObj + property var createdObjects: [] + + function cleanup() + { + while (createdObjects.length) { + var obj = createdObjects.shift(); + if (obj) { + obj.destroy(); + } + } + } + + function create(id) + { + lastObj = component.createObject(myFactory, {objectName: id}); + createdObjects.push(lastObj); + return lastObj; + } + WebChannel.id: "myFactory" + } + + Component { + id: component + QtObject { + property var myProperty : 0 + function myMethod(arg) + { + lastMethodArg = arg; + } + signal mySignal(var arg1, var arg2) + } + } + + TestWebChannel { + id: webChannel + transports: [client1.serverTransport, client2.serverTransport] + registeredObjects: [myObj, myOtherObj, myObj2, myObj3, myFactory] + } + + function init() + { + myObj.myProperty = 1 + client1.cleanup(); + client2.cleanup(); + } + + function cleanup() + { + client1.debug = false; + client2.debug = false; + // delete all created objects + myFactory.cleanup(); + myFactory.lastObj = undefined; + // reschedule current task to end of event loop + wait(1); + } + + function test_signalSeparation() + { + var testObj1; + var testObj1Id; + var testObj2; + var testObj2Id; + + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myFactory.create("testObj1", function (obj1) { + testObj1 = myFactory.lastObj; + testObj1Id = obj1.__id__; + + obj1.mySignal.connect(function (arg1_1, arg1_2) { + console.debug("client 1 received signal 'mySignal' " + arg1_1); + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + var channel2; + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + channel2.objects.myFactory.create("testObj2", function (obj2) { + testObj2 = myFactory.lastObj; + testObj2Id = obj2.__id__; + obj2.mySignal.connect(function (arg2_1, arg2_2) { + console.debug("client 2 received signal 'mySignal'"); + }); + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // dismiss server messages + client1.serverMessages = []; + client2.serverMessages = []; + + // now everything is set-up + // and we can kick off a signal + testObj1.mySignal("foo", "bar"); + + var msg1 = client1.awaitSignal(); + compare(msg1.signal, 6); + + // look if there is a signal send to client2, which should not happen + var msg2 = client2.skipToMessage(1, "server", 10); + console.log("client2 received a signal. let's check that it does not come from testObj1"); + if (msg2 !== false) { + verify(msg2.object !== testObj1Id); + } + } + + function test_separationForSameObject() + { + var testObj1; + var testObj2; + var receivedSignal1 = false; + var receivedSignal2 = false; + + var channel2; + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myObj3.getObject(function (obj) { + testObj1 = obj; + + testObj1.mySignal.connect(function() { + receivedSignal1 = true; + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + verify(channel2.objects.myObj2) + channel2.objects.myObj3.getObject(function (obj) { + testObj2 = obj; + testObj2.mySignal.connect(function() { + receivedSignal2 = true; + }); + + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // trigger signal, signal should be received by both channels + myObj.mySignal("foo", "bar"); + + verify(receivedSignal1, "Channel 1 missed signal") + verify(receivedSignal2, "Channel 2 missed signal") + } + + function test_propertyUpdateSeparation() + { + var testObj1; + var testObj1Id; + var testObj2; + var testObj2Id; + + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myFactory.create("testObj1", function (obj1) { + testObj1 = myFactory.lastObj; + testObj1Id = obj1.__id__; + + obj1.myPropertyChanged.connect(function (arg1_1) { + console.debug("client 1 received property update 'myProperty' " + obj1.myProperty); + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + var channel2; + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + channel2.objects.myFactory.create("testObj2", function (obj2) { + testObj2 = myFactory.lastObj; + testObj2Id = obj2.__id__; + obj2.myPropertyChanged.connect(function (arg1_1) { + console.debug("client 2 received property update 'myProperty' " + obj2.myProperty); + }); + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // dismiss server messages + client1.serverMessages = []; + client2.serverMessages = []; + + // now everything is set-up + // and we can kick off a property change + testObj1.myProperty = 5; + + var msg1 = client1.awaitPropertyUpdate(); + compare(msg1.type, JSClient.QWebChannelMessageTypes.propertyUpdate); + + //look if there is a propertyUpdate sent to client2, which should not happen + var msg2 = client2.skipToMessage(2, "server", 10); + console.log("client2 received a propertyUpdate. let's check that it does not come from testObj1"); + if (msg2 !== false) { + verify(msg2.object !== testObj1Id); + } + } + + function test_returnNonQObject() + { + var retObj; + + var channel = client1.createChannel(function (channel) { + channel.objects.myObj2.myMethod(function(result) { + retObj = result; + }); + }); + + client1.awaitInit(); + + var msg1 = client1.awaitMessage(); + compare(msg1.type, JSClient.QWebChannelMessageTypes.invokeMethod); // create + + msg1 = client1.awaitIdle(); + verify(retObj["obj1"]["name"]); + } +} + |