summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernd Lamecker <bernd.lamecker@basyskom.com>2014-08-12 16:46:59 +0200
committerMilian Wolff <milian.wolff@kdab.com>2014-12-19 11:39:52 +0100
commit3ed29bca08dec484003cf573dd428b73627ffa81 (patch)
tree8e688a00c409cbd4a37412342faa2185b7b3869d
parent9fdce8e443030ab99d31e42fffc977cf284c36c4 (diff)
downloadqtwebchannel-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.cpp69
-rw-r--r--src/webchannel/qmetaobjectpublisher_p.h34
-rw-r--r--tests/auto/qml/Client.qml9
-rw-r--r--tests/auto/qml/qml.pro5
-rw-r--r--tests/auto/qml/tst_webchannel.qml2
-rw-r--r--tests/auto/qml/tst_webchannelseparation.qml356
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"]);
+ }
+}
+