summaryrefslogtreecommitdiff
path: root/src/webchannel
diff options
context:
space:
mode:
Diffstat (limited to 'src/webchannel')
-rw-r--r--src/webchannel/qmetaobjectpublisher.cpp730
-rw-r--r--src/webchannel/qmetaobjectpublisher.h114
-rw-r--r--src/webchannel/qobject.js305
-rw-r--r--src/webchannel/qwebchannel.cpp207
-rw-r--r--src/webchannel/qwebchannel.h91
-rw-r--r--src/webchannel/qwebchannelglobal.h62
-rw-r--r--src/webchannel/qwebsocketserver.cpp424
-rw-r--r--src/webchannel/qwebsocketserver_p.h160
-rw-r--r--src/webchannel/resources.qrc6
-rw-r--r--src/webchannel/signalhandler_p.h278
-rw-r--r--src/webchannel/variantargument_p.h103
-rw-r--r--src/webchannel/webchannel.js129
-rw-r--r--src/webchannel/webchannel.pro28
13 files changed, 2637 insertions, 0 deletions
diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp
new file mode 100644
index 0000000..c141fb2
--- /dev/null
+++ b/src/webchannel/qmetaobjectpublisher.cpp
@@ -0,0 +1,730 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qmetaobjectpublisher.h"
+#include "qwebchannel.h"
+
+#include "variantargument_p.h"
+#include "signalhandler_p.h"
+
+#include <QStringList>
+#include <QMetaObject>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QBasicTimer>
+#include <QDebug>
+#include <QPointer>
+#include <QEvent>
+
+namespace {
+const QString KEY_SIGNALS = QStringLiteral("signals");
+const QString KEY_METHODS = QStringLiteral("methods");
+const QString KEY_PROPERTIES = QStringLiteral("properties");
+const QString KEY_ENUMS = QStringLiteral("enums");
+const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
+const QString KEY_ID = QStringLiteral("id");
+const QString KEY_DATA = QStringLiteral("data");
+const QString KEY_OBJECT = QStringLiteral("object");
+const QString KEY_DESTROYED = QStringLiteral("destroyed");
+const QString KEY_SIGNAL = QStringLiteral("signal");
+const QString KEY_TYPE = QStringLiteral("type");
+const QString KEY_MESSAGE = QStringLiteral("message");
+const QString KEY_METHOD = QStringLiteral("method");
+const QString KEY_ARGS = QStringLiteral("args");
+const QString KEY_PROPERTY = QStringLiteral("property");
+const QString KEY_VALUE = QStringLiteral("value");
+
+const QString TYPE_SIGNAL = QStringLiteral("Qt.signal");
+const QString TYPE_PROPERTY_UPDATE = QStringLiteral("Qt.propertyUpdate");
+const QString TYPE_INIT = QStringLiteral("Qt.init");
+const QString TYPE_IDLE = QStringLiteral("Qt.idle");
+const QString TYPE_DEBUG = QStringLiteral("Qt.debug");
+const QString TYPE_INVOKE_METHOD = QStringLiteral("Qt.invokeMethod");
+const QString TYPE_CONNECT_TO_SIGNAL = QStringLiteral("Qt.connectToSignal");
+const QString TYPE_DISCONNECT_FROM_SIGNAL = QStringLiteral("Qt.disconnectFromSignal");
+const QString TYPE_SET_PROPERTY = QStringLiteral("Qt.setProperty");
+
+QString objectId(const QObject *object)
+{
+ return QString::number(quintptr(object), 16);
+}
+
+const int s_destroyedSignalIndex = QObject::staticMetaObject.indexOfMethod("destroyed(QObject*)");
+
+/// TODO: what is the proper value here?
+const int PROPERTY_UPDATE_INTERVAL = 50;
+}
+
+struct QMetaObjectPublisherPrivate
+{
+ QMetaObjectPublisherPrivate(QMetaObjectPublisher *q)
+ : q(q)
+ , signalHandler(this)
+ , clientIsIdle(false)
+ , blockUpdates(false)
+ , pendingInit(false)
+ , propertyUpdatesInitialized(false)
+ {
+ }
+
+ /**
+ * Set the client to idle or busy, based on the value of @p isIdle.
+ *
+ * When the value changed, start/stop the property update timer accordingly.
+ */
+ void setClientIsIdle(bool isIdle);
+
+ /**
+ * Initialize clients by sending them the class information of the registered objects.
+ *
+ * Furthermore, if that was not done already, connect to their property notify signals.
+ */
+ void initializeClients();
+
+ /**
+ * Go through all properties of the given object and connect to their notify signal.
+ *
+ * When receiving a notify signal, it will store the information in pendingPropertyUpdates which
+ * gets send via a Qt.propertyUpdate message to the server when the grouping timer timeouts.
+ */
+ void initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo);
+
+ /**
+ * Send the clients the new property values since the last time this function was invoked.
+ *
+ * This is a grouped batch of all properties for which their notify signal was emitted.
+ * The list of signals as well as the arguments they contained, are also transmitted to
+ * the remote clients.
+ *
+ * @sa timer, initializePropertyUpdates
+ */
+ void sendPendingPropertyUpdates();
+
+ /**
+ * Invoke the method of index @p methodIndex on @p object with the arguments @p args.
+ *
+ * The return value of the method invocation is then transmitted to the calling client
+ * via a webchannel response to the message identified by @p id.
+ */
+ bool invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args, const QJsonValue &id);
+
+ /**
+ * Callback of the signalHandler which forwards the signal invocation to the webchannel clients.
+ */
+ void signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments);
+
+ /**
+ * Callback for registered or wrapped objects which erases all data related to @p object.
+ *
+ * @sa signalEmitted
+ */
+ void objectDestroyed(const QObject *object);
+
+ /**
+ * Given a QVariant containing a QObject*, wrap the object and register for property updates
+ * return the objects class information.
+ *
+ * All other input types are returned as-is.
+ *
+ * TODO: support wrapping of initially-registered objects
+ */
+ QVariant wrapResult(const QVariant &result);
+
+ /**
+ * Invoke delete later on @p object.
+ */
+ void deleteWrappedObject(QObject *object) const;
+
+ QMetaObjectPublisher *q;
+ QPointer<QWebChannel> webChannel;
+ SignalHandler<QMetaObjectPublisherPrivate> signalHandler;
+
+ // true when the client is idle, false otherwise
+ bool clientIsIdle;
+
+ // true when no property updates should be sent, false otherwise
+ bool blockUpdates;
+
+ // true when at least one client needs to be initialized,
+ // i.e. when a Qt.init came in which was not handled yet.
+ bool pendingInit;
+
+ // true when at least one client was initialized and thus
+ // the property updates have been initialized and the
+ // object info map set.
+ bool propertyUpdatesInitialized;
+
+ // Map of registered objects indexed by their id.
+ QHash<QString, QObject *> registeredObjects;
+
+ // Map the registered objects to their id.
+ QHash<const QObject *, QString> registeredObjectIds;
+
+ // Map of object names to maps of signal indices to a set of all their properties.
+ // The last value is a set as a signal can be the notify signal of multiple properties.
+ typedef QHash<int, QSet<QString> > SignalToPropertyNameMap;
+ QHash<const QObject *, SignalToPropertyNameMap> signalToPropertyMap;
+
+ // Objects that changed their properties and are waiting for idle client.
+ // map of object name to map of signal index to arguments
+ typedef QHash<int, QVariantList> SignalToArgumentsMap;
+ typedef QHash<const QObject *, SignalToArgumentsMap> PendingPropertyUpdates;
+ PendingPropertyUpdates pendingPropertyUpdates;
+
+ // Maps wrapped object to class info
+ QHash<const QObject *, QVariantMap> 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.
+ QBasicTimer timer;
+};
+
+void QMetaObjectPublisherPrivate::setClientIsIdle(bool isIdle)
+{
+ if (clientIsIdle == isIdle) {
+ return;
+ }
+ clientIsIdle = isIdle;
+ if (!isIdle && timer.isActive()) {
+ timer.stop();
+ } else if (isIdle && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+}
+
+void QMetaObjectPublisherPrivate::initializeClients()
+{
+ QJsonObject objectInfos;
+ {
+ const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd();
+ for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) {
+ const QVariantMap &info = q->classInfoForObject(it.value());
+ if (!propertyUpdatesInitialized) {
+ initializePropertyUpdates(it.value(), info);
+ }
+ objectInfos[it.key()] = QJsonObject::fromVariantMap(info);
+ }
+ }
+ webChannel->sendMessage(TYPE_INIT, objectInfos);
+ propertyUpdatesInitialized = true;
+ pendingInit = false;
+}
+
+void QMetaObjectPublisherPrivate::initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo)
+{
+ foreach (const QVariant &propertyInfoVar, objectInfo[KEY_PROPERTIES].toList()) {
+ const QVariantList &propertyInfo = propertyInfoVar.toList();
+ if (propertyInfo.size() < 2) {
+ qWarning() << "Invalid property info encountered:" << propertyInfoVar;
+ continue;
+ }
+ const QString &propertyName = propertyInfo.at(0).toString();
+ const QVariantList &signalData = propertyInfo.at(1).toList();
+
+ if (signalData.isEmpty()) {
+ // Property without NOTIFY signal
+ continue;
+ }
+
+ const int signalIndex = signalData.at(1).toInt();
+
+ QSet<QString> &connectedProperties = signalToPropertyMap[object][signalIndex];
+
+ // Only connect for a property update once
+ if (connectedProperties.isEmpty()) {
+ signalHandler.connectTo(object, signalIndex);
+ }
+
+ connectedProperties.insert(propertyName);
+ }
+
+ // also always connect to destroyed signal
+ signalHandler.connectTo(object, s_destroyedSignalIndex);
+}
+
+void QMetaObjectPublisherPrivate::sendPendingPropertyUpdates()
+{
+ if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) {
+ return;
+ }
+
+ QJsonArray data;
+
+ // convert pending property updates to JSON data
+ const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd();
+ for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) {
+ const QObject *object = it.key();
+ const QMetaObject *const metaObject = object->metaObject();
+ const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object);
+ // maps property name to current property value
+ QJsonObject properties;
+ // maps signal index to list of arguments of the last emit
+ QJsonObject sigs;
+ const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd();
+ for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) {
+ // TODO: use property indices
+ foreach (const QString &propertyName, objectsSignalToPropertyMap.value(sigIt.key())) {
+ int propertyIndex = metaObject->indexOfProperty(qPrintable(propertyName));
+ if (propertyIndex == -1) {
+ qWarning("Unknown property %d encountered", propertyIndex);
+ continue;
+ }
+ const QMetaProperty &property = metaObject->property(propertyIndex);
+ properties[QString::fromLatin1(property.name())] = QJsonValue::fromVariant(property.read(object));
+ }
+ // TODO: can we get rid of the int <-> string conversions here?
+ sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value());
+ }
+ QJsonObject obj;
+ obj[KEY_OBJECT] = registeredObjectIds.value(object);
+ obj[KEY_SIGNALS] = sigs;
+ obj[KEY_PROPERTIES] = properties;
+ data.push_back(obj);
+ }
+
+ pendingPropertyUpdates.clear();
+ webChannel->sendMessage(TYPE_PROPERTY_UPDATE, data);
+ setClientIsIdle(false);
+}
+
+bool QMetaObjectPublisherPrivate::invokeMethod(QObject *const object, const int methodIndex,
+ const QJsonArray &args, const QJsonValue &id)
+{
+ const QMetaMethod &method = object->metaObject()->method(methodIndex);
+
+ if (method.name() == QByteArrayLiteral("deleteLater")) {
+ // invoke `deleteLater` on wrapped QObject indirectly
+ deleteWrappedObject(object);
+ return true;
+ } else if (!method.isValid()) {
+ qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.';
+ return false;
+ } else if (method.access() != QMetaMethod::Public) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (args.size() > 10) {
+ qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke.";
+ return false;
+ } else if (args.size() > method.parameterCount()) {
+ qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':'
+ << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.';
+ }
+
+ // construct converter objects of QVariant to QGenericArgument
+ VariantArgument arguments[10];
+ for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) {
+ arguments[i].setValue(args.at(i).toVariant(), method.parameterType(i));
+ }
+
+ // construct QGenericReturnArgument
+ QVariant returnValue;
+ if (method.returnType() != qMetaTypeId<QVariant>() && method.returnType() != qMetaTypeId<void>()) {
+ // Only init variant with return type if its not a variant itself, which would
+ // lead to nested variants which is not what we want.
+ // Also, skip void-return types for obvious reasons (and to prevent a runtime warning inside Qt).
+ returnValue = QVariant(method.returnType(), 0);
+ }
+ QGenericReturnArgument returnArgument(method.typeName(), returnValue.data());
+
+ // now we can call the method
+ method.invoke(object, returnArgument,
+ arguments[0], arguments[1], arguments[2], arguments[3], arguments[4],
+ arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]);
+
+ // and send the return value to the client
+ webChannel->respond(id, QJsonValue::fromVariant(wrapResult(returnValue)));
+
+ return true;
+}
+
+void QMetaObjectPublisherPrivate::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments)
+{
+ if (!webChannel) {
+ return;
+ }
+ if (!signalToPropertyMap.value(object).contains(signalIndex)) {
+ QJsonObject data;
+ const QString &objectName = registeredObjectIds.value(object);
+ Q_ASSERT(!objectName.isEmpty());
+ data[KEY_OBJECT] = objectName;
+ data[KEY_SIGNAL] = signalIndex;
+ if (!arguments.isEmpty()) {
+ // TODO: wrap (new) objects on the fly
+ data[KEY_ARGS] = QJsonArray::fromVariantList(arguments);
+ }
+ webChannel->sendMessage(TYPE_SIGNAL, data);
+
+ if (signalIndex == s_destroyedSignalIndex) {
+ objectDestroyed(object);
+ }
+ } else {
+ pendingPropertyUpdates[object][signalIndex] = arguments;
+ if (clientIsIdle && !blockUpdates && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+ }
+}
+
+void QMetaObjectPublisherPrivate::objectDestroyed(const QObject *object)
+{
+ const QString &id = registeredObjectIds.take(object);
+ Q_ASSERT(!id.isEmpty());
+ bool removed = registeredObjects.remove(id);
+ Q_ASSERT(removed);
+ Q_UNUSED(removed);
+
+ signalToPropertyMap.remove(object);
+ pendingPropertyUpdates.remove(object);
+ wrappedObjects.remove(object);
+}
+
+QVariant QMetaObjectPublisherPrivate::wrapResult(const QVariant &result)
+{
+ if (QObject *object = result.value<QObject *>()) {
+ QVariantMap &objectInfo = wrappedObjects[object];
+ if (!objectInfo.isEmpty()) {
+ // already registered, use cached information
+ Q_ASSERT(registeredObjectIds.contains(object));
+ return objectInfo;
+ } // else the object is not yet wrapped, do it now
+
+ const QString &id = objectId(object);
+ Q_ASSERT(!registeredObjects.contains(id));
+ Q_ASSERT(!registeredObjectIds.contains(object));
+
+ objectInfo[KEY_QOBJECT] = true;
+ objectInfo[KEY_ID] = id;
+ objectInfo[KEY_DATA] = q->classInfoForObject(object);
+
+ registeredObjectIds[object] = id;
+ registeredObjects[id] = object;
+ wrappedObjects.insert(object, objectInfo);
+
+ initializePropertyUpdates(object, objectInfo);
+ return objectInfo;
+ }
+
+ // no need to wrap this
+ return result;
+}
+
+void QMetaObjectPublisherPrivate::deleteWrappedObject(QObject *object) const
+{
+ if (!wrappedObjects.contains(object)) {
+ qWarning() << "Not deleting non-wrapped object" << object;
+ return;
+ }
+ object->deleteLater();
+}
+
+QMetaObjectPublisher::QMetaObjectPublisher(QObject *parent)
+ : QObject(parent)
+ , d(new QMetaObjectPublisherPrivate(this))
+{
+}
+
+QMetaObjectPublisher::~QMetaObjectPublisher()
+{
+
+}
+
+QVariantMap QMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const
+{
+ QVariantMap ret;
+ QMap<QString, QVariant>::const_iterator it = objectMap.constBegin();
+ while (it != objectMap.constEnd()) {
+ QObject *object = it.value().value<QObject *>();
+ if (object) {
+ const QVariantMap &info = classInfoForObject(object);
+ if (!info.isEmpty()) {
+ ret[it.key()] = info;
+ }
+ }
+ ++it;
+ }
+ return ret;
+}
+
+QVariantMap QMetaObjectPublisher::classInfoForObject(QObject *object) const
+{
+ QVariantMap data;
+ if (!object) {
+ qWarning("null object given to MetaObjectPublisher - bad API usage?");
+ return data;
+ }
+ QVariantList qtSignals, qtMethods;
+ QVariantList qtProperties;
+ QVariantMap qtEnums;
+ const QMetaObject *metaObject = object->metaObject();
+ QSet<int> notifySignals;
+ QSet<QString> identifiers;
+ for (int i = 0; i < metaObject->propertyCount(); ++i) {
+ const QMetaProperty &prop = metaObject->property(i);
+ QVariantList propertyInfo;
+ const QString &propertyName = QString::fromLatin1(prop.name());
+ propertyInfo.append(propertyName);
+ identifiers << propertyName;
+ if (prop.hasNotifySignal()) {
+ notifySignals << prop.notifySignalIndex();
+ const int numParams = prop.notifySignal().parameterCount();
+ if (numParams > 1) {
+ qWarning("Notify signal for property '%s' has %d parameters, expected zero or one.",
+ prop.name(), numParams);
+ }
+ // optimize: compress the common propertyChanged notification names, just send a 1
+ const QByteArray &notifySignal = prop.notifySignal().name();
+ static const QByteArray changedSuffix = QByteArrayLiteral("Changed");
+ if (notifySignal.length() == changedSuffix.length() + propertyName.length() &&
+ notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name()))
+ {
+ propertyInfo.append(QVariant::fromValue(QVariantList() << 1 << prop.notifySignalIndex()));
+ } else {
+ propertyInfo.append(QVariant::fromValue(QVariantList() << QString::fromLatin1(notifySignal) << prop.notifySignalIndex()));
+ }
+ } else {
+ if (!prop.isConstant()) {
+ qWarning("Property '%s'' of object '%s' has no notify signal and is not constant, "
+ "value updates in HTML will be broken!",
+ prop.name(), object->metaObject()->className());
+ }
+ propertyInfo.append(QVariant::fromValue(QVariantList()));
+ }
+ propertyInfo.append(prop.read(object));
+ qtProperties.append(QVariant::fromValue(propertyInfo));
+ }
+ for (int i = 0; i < metaObject->methodCount(); ++i) {
+ if (notifySignals.contains(i)) {
+ continue;
+ }
+ const QMetaMethod &method = metaObject->method(i);
+ //NOTE: this must be a string, otherwise it will be converted to '{}' in QML
+ const QString &name = QString::fromLatin1(method.name());
+ // optimize: skip overloaded methods/signals or property getters, on the JS side we can only
+ // call one of them anyways
+ // TODO: basic support for overloaded signals, methods
+ if (identifiers.contains(name)) {
+ continue;
+ }
+ identifiers << name;
+ // send data as array to client with format: [name, index]
+ const QVariant data = QVariant::fromValue(QVariantList() << name << i);
+ if (method.methodType() == QMetaMethod::Signal) {
+ qtSignals.append(data);
+ } else if (method.access() == QMetaMethod::Public) {
+ qtMethods.append(data);
+ }
+ }
+ for (int i = 0; i < metaObject->enumeratorCount(); ++i) {
+ QMetaEnum enumerator = metaObject->enumerator(i);
+ QVariantMap values;
+ for (int k = 0; k < enumerator.keyCount(); ++k) {
+ values[QString::fromLatin1(enumerator.key(k))] = enumerator.value(k);
+ }
+ qtEnums[QString::fromLatin1(enumerator.name())] = values;
+ }
+ data[KEY_SIGNALS] = qtSignals;
+ data[KEY_METHODS] = qtMethods;
+ data[KEY_PROPERTIES] = QVariant::fromValue(qtProperties);
+ data[KEY_ENUMS] = qtEnums;
+ return data;
+}
+
+void QMetaObjectPublisher::registerObjects(const QVariantMap &objects)
+{
+ if (d->propertyUpdatesInitialized) {
+ qWarning("Registered new object after initialization. This does not work!");
+ return;
+ }
+ const QMap<QString, QVariant>::const_iterator end = objects.end();
+ for (QMap<QString, QVariant>::const_iterator it = objects.begin(); it != end; ++it) {
+ QObject *object = it.value().value<QObject *>();
+ if (!object) {
+ qWarning("Invalid QObject given to register under name %s", qPrintable(it.key()));
+ continue;
+ }
+ d->registeredObjects[it.key()] = object;
+ d->registeredObjectIds[object] = it.key();
+ }
+}
+
+bool QMetaObjectPublisher::handleRequest(const QJsonObject &message)
+{
+ if (!message.contains(KEY_DATA)) {
+ return false;
+ }
+
+ const QJsonObject &payload = message.value(KEY_DATA).toObject();
+ if (!payload.contains(KEY_TYPE)) {
+ return false;
+ }
+
+ const QString &type = payload.value(KEY_TYPE).toString();
+ if (type == TYPE_IDLE) {
+ d->setClientIsIdle(true);
+ return true;
+ } else if (type == TYPE_INIT) {
+ if (!d->blockUpdates) {
+ d->initializeClients();
+ } else {
+ d->pendingInit = true;
+ }
+ return true;
+ } else if (type == TYPE_DEBUG) {
+ static QTextStream out(stdout);
+ out << "DEBUG: " << payload.value(KEY_MESSAGE).toString() << endl;
+ return true;
+ } else if (payload.contains(KEY_OBJECT)) {
+ const QString &objectName = payload.value(KEY_OBJECT).toString();
+ QObject *object = d->registeredObjects.value(objectName);
+ if (!object) {
+ qWarning() << "Unknown object encountered" << objectName;
+ return false;
+ }
+
+ if (type == TYPE_INVOKE_METHOD) {
+ return d->invokeMethod(object, payload.value(KEY_METHOD).toInt(-1), payload.value(KEY_ARGS).toArray(), message.value(KEY_ID));
+ } else if (type == TYPE_CONNECT_TO_SIGNAL) {
+ d->signalHandler.connectTo(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_DISCONNECT_FROM_SIGNAL) {
+ d->signalHandler.disconnectFrom(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_SET_PROPERTY) {
+ // TODO: use property indices
+ const QString &propertyName = payload.value(KEY_PROPERTY).toString();
+ const int propertyIdx = object->metaObject()->indexOfProperty(qPrintable(propertyName));
+ if (propertyIdx == -1) {
+ qWarning() << "Cannot set unknown property" << propertyName << "of object" << objectName;
+ return false;
+ }
+ object->metaObject()->property(propertyIdx).write(object, payload.value(KEY_VALUE).toVariant());
+ return true;
+ }
+ }
+ return false;
+}
+
+QWebChannel *QMetaObjectPublisher::webChannel() const
+{
+ return d->webChannel;
+}
+
+void QMetaObjectPublisher::setWebChannel(QWebChannel *webChannel)
+{
+ if (d->webChannel == webChannel) {
+ return;
+ }
+
+ d->webChannel = webChannel;
+
+ emit webChannelChanged(webChannel);
+}
+
+bool QMetaObjectPublisher::blockUpdates() const
+{
+ return d->blockUpdates;
+}
+
+void QMetaObjectPublisher::setBlockUpdates(bool block)
+{
+ if (d->blockUpdates == block) {
+ return;
+ }
+ d->blockUpdates = block;
+
+ if (!d->blockUpdates) {
+ if (d->pendingInit) {
+ d->initializeClients();
+ } else {
+ d->sendPendingPropertyUpdates();
+ }
+ } else if (d->timer.isActive()) {
+ d->timer.stop();
+ }
+
+ emit blockUpdatesChanged(block);
+}
+
+void QMetaObjectPublisher::bench_ensureUpdatesInitialized()
+{
+ if (!d->propertyUpdatesInitialized) {
+ d->initializeClients();
+ }
+}
+
+void QMetaObjectPublisher::bench_sendPendingPropertyUpdates()
+{
+ d->clientIsIdle = true;
+ d->sendPendingPropertyUpdates();
+}
+
+void QMetaObjectPublisher::bench_initializeClients()
+{
+ d->propertyUpdatesInitialized = false;
+ d->signalToPropertyMap.clear();
+ d->signalHandler.clear();
+ d->initializeClients();
+}
+
+void QMetaObjectPublisher::bench_registerObjects(const QVariantMap &objects)
+{
+ d->propertyUpdatesInitialized = false;
+ registerObjects(objects);
+}
+
+bool QMetaObjectPublisher::test_clientIsIdle() const
+{
+ return d->clientIsIdle;
+}
+
+void QMetaObjectPublisher::timerEvent(QTimerEvent *event)
+{
+ if (event->timerId() == d->timer.timerId()) {
+ d->sendPendingPropertyUpdates();
+ } else {
+ QObject::timerEvent(event);
+ }
+}
diff --git a/src/webchannel/qmetaobjectpublisher.h b/src/webchannel/qmetaobjectpublisher.h
new file mode 100644
index 0000000..da733b2
--- /dev/null
+++ b/src/webchannel/qmetaobjectpublisher.h
@@ -0,0 +1,114 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QTMETAOBJECTPUBLISHER_H
+#define QTMETAOBJECTPUBLISHER_H
+
+#include <QObject>
+#include <QVariant>
+
+#include "qwebchannelglobal.h"
+
+class QWebChannel;
+
+struct QMetaObjectPublisherPrivate;
+
+class Q_WEBCHANNEL_EXPORT QMetaObjectPublisher : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QWebChannel *webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged);
+ Q_PROPERTY(bool blockUpdates READ blockUpdates WRITE setBlockUpdates NOTIFY blockUpdatesChanged);
+
+public:
+ explicit QMetaObjectPublisher(QObject *parent = 0);
+ virtual ~QMetaObjectPublisher();
+
+ Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const;
+ Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const;
+
+ /**
+ * Register a map of string ID to QObject* objects.
+ *
+ * The properties, signals and public methods of the QObject are
+ * published to the remote client, where an object with the given identifier
+ * is constructed.
+ *
+ * TODO: This must be called, before clients are initialized.
+ */
+ Q_INVOKABLE void registerObjects(const QVariantMap &objects);
+
+ /**
+ * Handle the given WebChannel client request and potentially give a response.
+ *
+ * @return true if the request was handled, false otherwise.
+ */
+ Q_INVOKABLE bool handleRequest(const QJsonObject &message);
+
+ QWebChannel *webChannel() const;
+ void setWebChannel(QWebChannel *webChannel);
+
+ /**
+ * When updates are blocked, no property updates are transmitted to remote clients.
+ */
+ bool blockUpdates() const;
+ void setBlockUpdates(bool block);
+
+ /// TODO: cleanup: rewrite tests in C++ and access PIMPL data from there
+ Q_INVOKABLE void bench_ensureUpdatesInitialized();
+ Q_INVOKABLE void bench_sendPendingPropertyUpdates();
+ Q_INVOKABLE void bench_registerObjects(const QVariantMap &objects);
+ Q_INVOKABLE void bench_initializeClients();
+ Q_INVOKABLE bool test_clientIsIdle() const;
+
+signals:
+ void webChannelChanged(QWebChannel *channel);
+ void blockUpdatesChanged(bool block);
+
+protected:
+ void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE;
+
+private:
+ QScopedPointer<QMetaObjectPublisherPrivate> d;
+ friend struct QMetaObjectPublisherPrivate;
+};
+
+#endif // QMETAOBJECTPUBLISHER_H
diff --git a/src/webchannel/qobject.js b/src/webchannel/qobject.js
new file mode 100644
index 0000000..da8b3de
--- /dev/null
+++ b/src/webchannel/qobject.js
@@ -0,0 +1,305 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+"use strict";
+
+function QObject(name, data, webChannel)
+{
+ this.__id__ = name;
+ webChannel.objectMap[name] = this;
+
+ // List of callbacks that get invoked upon signal emission
+ this.__objectSignals__ = {};
+
+ // Cache of all properties, updated when a notify signal is emitted
+ this.__propertyCache__ = {};
+
+ var object = this;
+
+ // ----------------------------------------------------------------------
+
+ function unwrapQObject( response )
+ {
+ if (!response["__QObject*__"]
+ || response["id"] === undefined
+ || response["data"] === undefined) {
+ return response;
+ }
+ var objectId = response.id;
+ if (webChannel.objectMap[objectId])
+ return webChannel.objectMap[objectId];
+
+ var qObject = new QObject( objectId, response.data, webChannel );
+ qObject.destroyed.connect(function() {
+ if (webChannel.objectMap[objectId] === qObject) {
+ delete webChannel.objectMap[objectId];
+ // reset the now deleted QObject to an empty {} object
+ // just assigning {} though would not have the desired effect, but the
+ // below also ensures all external references will see the empty map
+ for (var prop in qObject) {
+ delete qObject[prop];
+ }
+ }
+ });
+ return qObject;
+ }
+
+ function addSignal(signalData, isPropertyNotifySignal)
+ {
+ var signalName = signalData[0];
+ var signalIndex = signalData[1];
+ object[signalName] = {
+ connect: function(callback) {
+ if (typeof(callback) !== "function") {
+ console.error("Bad callback given to connect to signal " + signalName);
+ return;
+ }
+
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ object.__objectSignals__[signalIndex].push(callback);
+
+ if (!isPropertyNotifySignal) {
+ // only required for "pure" signals, handled separately for properties in propertyUpdate
+ webChannel.exec({
+ type: "Qt.connectToSignal",
+ object: object.__id__,
+ signal: signalIndex
+ });
+ }
+ },
+ disconnect: function(callback) {
+ if (typeof(callback) !== "function") {
+ console.error("Bad callback given to disconnect from signal " + signalName);
+ return;
+ }
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ var idx = object.__objectSignals__[signalIndex].indexOf(callback);
+ if (idx === -1) {
+ console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
+ return;
+ }
+ object.__objectSignals__[signalIndex].splice(idx, 1);
+ if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
+ // only required for "pure" signals, handled separately for properties in propertyUpdate
+ webChannel.exec({
+ type: "Qt.disconnectFromSignal",
+ object: object.__id__,
+ signal: signalIndex
+ });
+ }
+ }
+ };
+ }
+
+ /**
+ * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
+ */
+ function invokeSignalCallbacks(signalName, signalArgs)
+ {
+ var connections = object.__objectSignals__[signalName];
+ if (connections) {
+ connections.forEach(function(callback) {
+ callback.apply(callback, signalArgs);
+ });
+ }
+ }
+
+ this.propertyUpdate = function(signals, propertyMap)
+ {
+ // update property cache
+ for (var propertyName in propertyMap) {
+ var propertyValue = propertyMap[propertyName];
+ object.__propertyCache__[propertyName] = propertyValue;
+ }
+
+ for (var signalName in signals) {
+ // Invoke all callbacks, as signalEmitted() does not. This ensures the
+ // property cache is updated before the callbacks are invoked.
+ invokeSignalCallbacks(signalName, signals[signalName]);
+ }
+ }
+
+ this.signalEmitted = function(signalName, signalArgs)
+ {
+ invokeSignalCallbacks(signalName, signalArgs);
+ }
+
+ function addMethod(methodData)
+ {
+ var methodName = methodData[0];
+ var methodIdx = methodData[1];
+ object[methodName] = function() {
+ var args = [];
+ var callback;
+ for (var i = 0; i < arguments.length; ++i) {
+ if (typeof arguments[i] === "function")
+ callback = arguments[i];
+ else
+ args.push(arguments[i]);
+ }
+
+ webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": methodIdx, "args": args}, function(response) {
+ if ( (response !== undefined) && callback ) {
+ (callback)(unwrapQObject(response));
+ }
+ });
+ };
+ }
+
+ function bindGetterSetter(propertyInfo)
+ {
+ var propertyName = propertyInfo[0];
+ var notifySignalData = propertyInfo[1];
+ // initialize property cache with current value
+ object.__propertyCache__[propertyName] = propertyInfo[2]
+
+ if (notifySignalData) {
+ if (notifySignalData[0] === 1) {
+ // signal name is optimized away, reconstruct the actual name
+ notifySignalData[0] = propertyName + "Changed";
+ }
+ addSignal(notifySignalData, true);
+ }
+
+ object.__defineSetter__(propertyName, function(value) {
+ if (value === undefined) {
+ console.warn("Property setter for " + propertyName + " called with undefined value!");
+ return;
+ }
+ object.__propertyCache__[propertyName] = value;
+ webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": propertyName, "value": value });
+
+ });
+ object.__defineGetter__(propertyName, function () {
+ return (function (callback) {
+ var propertyValue = object.__propertyCache__[propertyName];
+ if (propertyValue === undefined) {
+ // This shouldn't happen
+ console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
+ }
+
+ // TODO: A callback is not required here anymore, but is kept for backwards compatibility
+ if (callback !== undefined) {
+ if (typeof(callback) !== "function") {
+ console.error("Bad callback given to get property " + property);
+ return;
+ }
+ callback(propertyValue);
+ } else {
+ return propertyValue;
+ }
+ });
+ });
+ }
+
+ // ----------------------------------------------------------------------
+
+ data.methods.forEach(addMethod);
+
+ data.properties.forEach(bindGetterSetter);
+
+ data.signals.forEach(function(signal) { addSignal(signal, false); });
+
+ for (var name in data.enums) {
+ object[name] = data.enums[name];
+ }
+}
+
+window.setupQObjectWebChannel = function(webChannel, doneCallback)
+{
+ // prevent multiple initialization which might happen with multiple webchannel clients.
+ var initialized = false;
+
+ webChannel.subscribe(
+ "Qt.signal",
+ function(payload) {
+ var object = window[payload.object] || webChannel.objectMap[payload.object];
+ if (object) {
+ object.signalEmitted(payload.signal, payload.args);
+ } else {
+ console.warn("Unhandled signal: " + payload.object + "::" + payload.signal);
+ }
+ }
+ );
+
+ webChannel.subscribe(
+ "Qt.propertyUpdate",
+ function(payload) {
+ for (var i in payload) {
+ var data = payload[i];
+ var object = window[data.object] || webChannel.objectMap[data.object];
+ if (object) {
+ object.propertyUpdate(data.signals, data.properties);
+ } else {
+ console.warn("Unhandled property update: " + data.object + "::" + data.signal);
+ }
+ }
+ setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0);
+ }
+ );
+
+ webChannel.subscribe(
+ "Qt.init",
+ function(payload) {
+ if (initialized) {
+ return;
+ }
+ initialized = true;
+ for (var objectName in payload) {
+ var data = payload[objectName];
+ var object = new QObject(objectName, data, webChannel);
+ window[objectName] = object;
+ }
+ if (doneCallback) {
+ doneCallback();
+ }
+ setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0);
+ }
+ );
+
+ webChannel.exec({type:"Qt.init"});
+
+ webChannel.debug = function(message)
+ {
+ webChannel.send({"data" : {"type" : "Qt.Debug", "message" : message}});
+ };
+};
diff --git a/src/webchannel/qwebchannel.cpp b/src/webchannel/qwebchannel.cpp
new file mode 100644
index 0000000..8dc7e5e
--- /dev/null
+++ b/src/webchannel/qwebchannel.cpp
@@ -0,0 +1,207 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qwebchannel.h"
+
+#include <QUuid>
+#include <QStringList>
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "qwebsocketserver_p.h"
+
+class QWebChannelPrivate : public QWebSocketServer
+{
+ Q_OBJECT
+public:
+ QByteArray m_secret;
+ bool m_useSecret;
+
+ QString m_baseUrl;
+ bool m_starting;
+
+ QWebChannelPrivate(QObject* parent)
+ : QWebSocketServer(parent)
+ , m_useSecret(true)
+ , m_starting(false)
+ {
+ connect(this, SIGNAL(error(QAbstractSocket::SocketError)),
+ SLOT(socketError()));
+ }
+
+ void initLater()
+ {
+ if (m_starting)
+ return;
+ metaObject()->invokeMethod(this, "init", Qt::QueuedConnection);
+ m_starting = true;
+ }
+
+ void sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const;
+
+signals:
+ void failed(const QString& reason);
+ void initialized();
+
+protected:
+ bool isValid(const HeaderData& connection) Q_DECL_OVERRIDE;
+
+private slots:
+ void init();
+ void socketError();
+};
+
+bool QWebChannelPrivate::isValid(const HeaderData& connection)
+{
+ if (!QWebSocketServer::isValid(connection)) {
+ return false;
+ }
+ return connection.protocol == QByteArrayLiteral("QWebChannel")
+ && connection.path == m_secret;
+}
+
+void QWebChannelPrivate::init()
+{
+ close();
+
+ m_starting = false;
+ if (m_useSecret) {
+ m_secret = QUuid::createUuid().toByteArray();
+ // replace { by /
+ m_secret[0] = '/';
+ // chop of trailing }
+ m_secret.chop(1);
+ }
+
+ if (!listen(QHostAddress::LocalHost)) {
+ emit failed(errorString());
+ return;
+ }
+
+ m_baseUrl = QStringLiteral("127.0.0.1:%1%2").arg(port()).arg(QString::fromLatin1(m_secret));
+ emit initialized();
+}
+
+void QWebChannelPrivate::socketError()
+{
+ emit failed(errorString());
+}
+
+void QWebChannelPrivate::sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const
+{
+ QJsonObject obj;
+ if (response) {
+ obj[QStringLiteral("response")] = true;
+ }
+ obj[QStringLiteral("id")] = id;
+ if (!data.isNull()) {
+ obj[QStringLiteral("data")] = data;
+ }
+ QJsonDocument doc(obj);
+ sendMessage(doc.toJson(QJsonDocument::Compact));
+}
+
+QWebChannel::QWebChannel(QObject *parent)
+: QObject(parent)
+, d(new QWebChannelPrivate(this))
+{
+ connect(d, SIGNAL(textDataReceived(QString)),
+ SIGNAL(rawMessageReceived(QString)));
+ connect(d, SIGNAL(failed(QString)),
+ SIGNAL(failed(QString)));
+ connect(d, SIGNAL(initialized()),
+ SLOT(onInitialized()));
+ connect(d, SIGNAL(pongReceived()),
+ SIGNAL(pongReceived()));
+ d->initLater();
+}
+
+QWebChannel::~QWebChannel()
+{
+}
+
+QString QWebChannel::baseUrl() const
+{
+ return d->m_baseUrl;
+}
+
+void QWebChannel::setUseSecret(bool s)
+{
+ if (d->m_useSecret == s)
+ return;
+ d->m_useSecret = s;
+ d->initLater();
+}
+
+bool QWebChannel::useSecret() const
+{
+ return d->m_useSecret;
+}
+
+void QWebChannel::onInitialized()
+{
+ emit initialized();
+ emit baseUrlChanged(d->m_baseUrl);
+}
+
+void QWebChannel::respond(const QJsonValue& messageId, const QJsonValue& data) const
+{
+ d->sendJSONMessage(messageId, data, true);
+}
+
+void QWebChannel::sendMessage(const QJsonValue& id, const QJsonValue& data) const
+{
+ d->sendJSONMessage(id, data, false);
+}
+
+void QWebChannel::sendRawMessage(const QString& message) const
+{
+ d->sendMessage(message.toUtf8());
+}
+
+void QWebChannel::ping() const
+{
+ d->ping();
+}
+
+#include "qwebchannel.moc"
diff --git a/src/webchannel/qwebchannel.h b/src/webchannel/qwebchannel.h
new file mode 100644
index 0000000..321706e
--- /dev/null
+++ b/src/webchannel/qwebchannel.h
@@ -0,0 +1,91 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QWEBCHANNEL_H
+#define QWEBCHANNEL_H
+
+#include <QObject>
+#include <QJsonValue>
+
+#include "qwebchannelglobal.h"
+
+class QWebChannelPrivate;
+
+class Q_WEBCHANNEL_EXPORT QWebChannel : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(QWebChannel)
+ Q_PROPERTY(QString baseUrl READ baseUrl NOTIFY baseUrlChanged)
+ Q_PROPERTY(bool useSecret READ useSecret WRITE setUseSecret)
+
+public:
+ QWebChannel(QObject *parent = 0);
+ ~QWebChannel();
+
+ QString baseUrl() const;
+
+ void setUseSecret(bool);
+ bool useSecret() const;
+
+signals:
+ void baseUrlChanged(const QString& baseUrl);
+ void rawMessageReceived(const QString& rawMessage);
+ void pongReceived();
+ void initialized();
+
+ void failed(const QString& reason);
+
+public slots:
+ void sendMessage(const QJsonValue& id, const QJsonValue& data = QJsonValue()) const;
+ void respond(const QJsonValue& messageId, const QJsonValue& data = QJsonValue()) const;
+ void sendRawMessage(const QString& rawMessage) const;
+ void ping() const;
+
+private slots:
+ void onInitialized();
+
+private:
+ QWebChannelPrivate* d;
+};
+
+#endif // QWEBCHANNEL_H
+
diff --git a/src/webchannel/qwebchannelglobal.h b/src/webchannel/qwebchannelglobal.h
new file mode 100644
index 0000000..f33ada6
--- /dev/null
+++ b/src/webchannel/qwebchannelglobal.h
@@ -0,0 +1,62 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QTWEBCHANNEL_H
+#define QTWEBCHANNEL_H
+
+#include <QtCore/qglobal.h>
+
+QT_BEGIN_NAMESPACE
+
+#ifndef QT_STATIC
+# if defined(QT_BUILD_WEBCHANNEL_LIB)
+# define Q_WEBCHANNEL_EXPORT Q_DECL_EXPORT
+# else
+# define Q_WEBCHANNEL_EXPORT Q_DECL_IMPORT
+# endif
+#else
+# define Q_WEBCHANNEL_EXPORT
+#endif
+
+QT_END_NAMESPACE
+
+#endif // QTWEBCHANNEL_H
diff --git a/src/webchannel/qwebsocketserver.cpp b/src/webchannel/qwebsocketserver.cpp
new file mode 100644
index 0000000..4bc56c8
--- /dev/null
+++ b/src/webchannel/qwebsocketserver.cpp
@@ -0,0 +1,424 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qwebsocketserver_p.h"
+
+#include <QTcpServer>
+#include <QTcpSocket>
+#include <QCryptographicHash>
+#include <QtEndian>
+
+#include <limits>
+
+namespace {
+template<typename T>
+inline static void appendBytes(QByteArray& data, T value)
+{
+ data.append(reinterpret_cast<const char*>(&value), sizeof(value));
+}
+
+inline static void unmask(QByteArray& data, char mask[4])
+{
+ for (int i = 0; i < data.size(); ++i) {
+ int j = i % 4;
+ data[i] = data[i] ^ mask[j];
+ }
+}
+
+inline static char bitMask(int bit)
+{
+ return 1 << bit;
+}
+
+// see: http://tools.ietf.org/html/rfc6455#page-28
+static const char FIN_BIT = bitMask(7);
+static const char MASKED_BIT = bitMask(7);
+static const char OPCODE_RANGE = bitMask(4) - 1;
+static const char PAYLOAD_RANGE = bitMask(7) - 1;
+static const char EXTENDED_PAYLOAD = 126;
+static const char EXTENDED_LONG_PAYLOAD = 127;
+}
+
+QWebSocketServer::QWebSocketServer(QObject* parent)
+: QObject(parent)
+, m_server(new QTcpServer(this))
+{
+ connect(m_server, SIGNAL(newConnection()),
+ SLOT(newConnection()));
+ connect(m_server, SIGNAL(acceptError(QAbstractSocket::SocketError)),
+ SIGNAL(error(QAbstractSocket::SocketError)));
+}
+
+QWebSocketServer::~QWebSocketServer()
+{
+ close();
+}
+
+bool QWebSocketServer::listen(const QHostAddress& address, quint16 port)
+{
+ return m_server->listen(address, port);
+}
+
+void QWebSocketServer::close()
+{
+ sendFrame(Frame::ConnectionClose, QByteArray());
+ m_server->close();
+}
+
+quint16 QWebSocketServer::port() const
+{
+ return m_server->serverPort();
+}
+
+QHostAddress QWebSocketServer::address() const
+{
+ return m_server->serverAddress();
+}
+
+QString QWebSocketServer::errorString() const
+{
+ return m_server->errorString();
+}
+
+void QWebSocketServer::newConnection()
+{
+ if (!m_server->hasPendingConnections())
+ return;
+
+ QTcpSocket* connection = m_server->nextPendingConnection();
+ m_connections.insert(connection, Connection());
+ connect(connection, SIGNAL(readyRead()),
+ SLOT(readSocketData()));
+ connect(connection, SIGNAL(error(QAbstractSocket::SocketError)),
+ SIGNAL(error(QAbstractSocket::SocketError)));
+ connect(connection, SIGNAL(disconnected()),
+ SLOT(disconnected()));
+}
+
+void QWebSocketServer::disconnected()
+{
+ QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender());
+ Q_ASSERT(socket);
+
+ m_connections.remove(socket);
+}
+
+static const QByteArray headerSwitchProtocols = QByteArrayLiteral("HTTP/1.1 101 Switching Protocols");
+static const QByteArray headerGet = QByteArrayLiteral("GET ");
+static const QByteArray headerHTTP = QByteArrayLiteral("HTTP/1.1");
+static const QByteArray headerHost = QByteArrayLiteral("Host: ");
+static const QByteArray headerUpgrade = QByteArrayLiteral("Upgrade: websocket");
+static const QByteArray headerConnection = QByteArrayLiteral("Connection: Upgrade");
+static const QByteArray headerSecKey = QByteArrayLiteral("Sec-WebSocket-Key: ");
+static const QByteArray headerSecProtocol = QByteArrayLiteral("Sec-WebSocket-Protocol: ");
+static const QByteArray headerSecVersion = QByteArrayLiteral("Sec-WebSocket-Version: 13");
+static const QByteArray headerSecAccept = QByteArrayLiteral("Sec-WebSocket-Accept: ");
+static const QByteArray headerOrigin = QByteArrayLiteral("Origin: ");
+static const QByteArray headerMagicKey = QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+static const QByteArray headerEOL = QByteArrayLiteral("\r\n");
+static const QByteArray httpBadRequest = QByteArrayLiteral("HTTP/1.1 400 Bad Request\r\n");
+
+void QWebSocketServer::readSocketData()
+{
+ QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender());
+ Q_ASSERT(socket);
+
+ Connection& connection = m_connections[socket];
+
+ if (!connection.header.wasUpgraded) {
+ readHeaderData(socket, connection.header);
+ }
+
+ if (connection.header.wasUpgraded) {
+ while (socket->bytesAvailable()) {
+ if (!readFrameData(socket, connection.currentFrame)) {
+ close(socket, connection.header);
+ }
+ }
+ }
+}
+
+void QWebSocketServer::readHeaderData(QTcpSocket* socket, HeaderData& header)
+{
+ while (socket->canReadLine()) {
+ QByteArray line = socket->readLine().trimmed();
+ if (line.isEmpty()) {
+ // finalize
+ if (isValid(header)) {
+ upgrade(socket, header);
+ } else {
+ close(socket, header);
+ }
+ break;
+ } else if (line.startsWith(headerGet) && line.endsWith(headerHTTP)) {
+ header.path = line.mid(headerGet.size(), line.size() - headerGet.size() - headerHTTP.size()).trimmed();
+ } else if (line.startsWith(headerHost)) {
+ header.host = line.mid(headerHost.size()).trimmed();
+ } else if (line.startsWith(headerSecKey)) {
+ header.key = line.mid(headerSecKey.size()).trimmed();
+ } else if (line.startsWith(headerOrigin)) {
+ header.origin = line.mid(headerOrigin.size()).trimmed();
+ } else if (line.startsWith(headerSecProtocol)) {
+ header.protocol = line.mid(headerSecProtocol.size()).trimmed();
+ } else if (line == headerUpgrade) {
+ header.hasUpgrade = true;
+ } else if (line == headerConnection) {
+ header.hasConnection = true;
+ } else if (line == headerSecVersion) {
+ header.hasVersion = true;
+ } else {
+ header.otherHeaders << line;
+ }
+ }
+}
+
+// see: http://tools.ietf.org/html/rfc6455#page-28
+bool QWebSocketServer::readFrameData(QTcpSocket* socket, Frame& frame)
+{
+ int bytesAvailable = socket->bytesAvailable();
+ if (frame.state == Frame::ReadStart) {
+ if (bytesAvailable < 2) {
+ return true;
+ }
+ uchar buffer[2];
+ socket->read(reinterpret_cast<char*>(buffer), 2);
+ bytesAvailable -= 2;
+ frame.fin = buffer[0] & FIN_BIT;
+ // skip rsv1, rsv2, rsv3
+ // last four bits are the opcode
+ quint8 opcode = buffer[0] & OPCODE_RANGE;
+ if (opcode != Frame::ContinuationFrame && opcode != Frame::BinaryFrame &&
+ opcode != Frame::ConnectionClose && opcode != Frame::TextFrame &&
+ opcode != Frame::Ping && opcode != Frame::Pong)
+ {
+ qWarning() << "invalid opcode: " << opcode;
+ return false;
+ }
+ frame.opcode = static_cast<Frame::Opcode>(opcode);
+ // test first, i.e. highest bit for mask
+ frame.masked = buffer[1] & MASKED_BIT;
+ if (!frame.masked) {
+ qWarning() << "unmasked frame received";
+ return false;
+ }
+ // final seven bits are the payload length
+ frame.length = static_cast<quint8>(buffer[1] & PAYLOAD_RANGE);
+ if (frame.length == EXTENDED_PAYLOAD) {
+ frame.state = Frame::ReadExtendedPayload;
+ } else if (frame.length == EXTENDED_LONG_PAYLOAD) {
+ frame.state = Frame::ReadExtendedLongPayload;
+ } else {
+ frame.state = Frame::ReadMask;
+ }
+ }
+ if (frame.state == Frame::ReadExtendedPayload) {
+ if (bytesAvailable < 2) {
+ return true;
+ }
+ uchar buffer[2];
+ socket->read(reinterpret_cast<char*>(buffer), 2);
+ bytesAvailable -= 2;
+ frame.length = qFromBigEndian<quint16>(buffer);
+ frame.state = Frame::ReadMask;
+ }
+ if (frame.state == Frame::ReadExtendedLongPayload) {
+ if (bytesAvailable < 8) {
+ return true;
+ }
+ uchar buffer[8];
+ socket->read(reinterpret_cast<char*>(buffer), 8);
+ bytesAvailable -= 8;
+ quint64 longSize = qFromBigEndian<quint64>(buffer);
+ // QByteArray uses int for size type so limit ourselves to that size as well
+ if (longSize > static_cast<quint64>(std::numeric_limits<int>::max())) {
+ return false;
+ }
+ frame.length = static_cast<int>(longSize);
+ frame.state = Frame::ReadMask;
+ }
+ if (frame.state == Frame::ReadMask) {
+ if (bytesAvailable < 4) {
+ return true;
+ }
+ socket->read(frame.mask, 4);
+ bytesAvailable -= 4;
+ frame.state = Frame::ReadData;
+ frame.data.reserve(frame.length);
+ }
+ if (frame.state == Frame::ReadData && (bytesAvailable || !frame.length)) {
+ frame.data.append(socket->read(qMin(frame.length - frame.data.size(), bytesAvailable)));
+ if (frame.data.size() == frame.length) {
+ frame.state = Frame::ReadStart;
+ handleFrame(socket, frame);
+ }
+ }
+ return true;
+}
+
+void QWebSocketServer::handleFrame(QTcpSocket* socket, Frame& frame)
+{
+ unmask(frame.data, frame.mask);
+
+ // fragmentation support - see http://tools.ietf.org/html/rfc6455#page-33
+ if (!frame.fin) {
+ if (frame.opcode != Frame::ContinuationFrame) {
+ frame.initialOpcode = frame.opcode;
+ }
+ frame.fragments += frame.data;
+ } else if (frame.fin && frame.opcode == Frame::ContinuationFrame) {
+ frame.opcode = frame.initialOpcode;
+ frame.data = frame.fragments + frame.data;
+ } // otherwise if it's fin and a non-continuation frame its a single-frame message
+
+ switch (frame.opcode) {
+ case Frame::ContinuationFrame:
+ // do nothing
+ break;
+ case Frame::Ping:
+ socket->write(frameHeader(Frame::Pong, 0));
+ break;
+ case Frame::Pong:
+ emit pongReceived();
+ break;
+ case Frame::ConnectionClose:
+ ///TODO: handle?
+ qWarning("Unhandled connection close frame");
+ break;
+ case Frame::BinaryFrame:
+ emit binaryDataReceived(frame.data);
+ break;
+ case Frame::TextFrame:
+ emit textDataReceived(QString::fromUtf8(frame.data));
+ break;
+ }
+
+ if (frame.fin) {
+ frame = Frame();
+ }
+}
+
+bool QWebSocketServer::isValid(const HeaderData& header)
+{
+ return !header.path.isEmpty() && !header.host.isEmpty() && !header.key.isEmpty()
+ && header.hasUpgrade && header.hasConnection && header.hasVersion;
+}
+
+void QWebSocketServer::close(QTcpSocket* socket, const HeaderData& header)
+{
+ if (header.wasUpgraded) {
+ //TODO: implement this properly - see http://tools.ietf.org/html/rfc6455#page-36
+ socket->write(frameHeader(Frame::Frame::ConnectionClose, 0));
+ } else {
+ socket->write(httpBadRequest);
+ }
+ socket->close();
+}
+
+void QWebSocketServer::upgrade(QTcpSocket* socket, HeaderData& header)
+{
+ socket->write(headerSwitchProtocols);
+ socket->write(headerEOL);
+
+ socket->write(headerUpgrade);
+ socket->write(headerEOL);
+
+ socket->write(headerConnection);
+ socket->write(headerEOL);
+
+ socket->write(headerSecAccept);
+ socket->write(QCryptographicHash::hash( header.key + headerMagicKey, QCryptographicHash::Sha1 ).toBase64());
+ socket->write(headerEOL);
+
+ if (!header.protocol.isEmpty()) {
+ socket->write(headerSecProtocol);
+ socket->write(header.protocol);
+ socket->write(headerEOL);
+ }
+
+ socket->write(headerEOL);
+
+ header.wasUpgraded = true;
+}
+
+void QWebSocketServer::sendMessage(const QByteArray& message) const
+{
+ sendFrame(Frame::TextFrame, message);
+}
+
+void QWebSocketServer::sendFrame(Frame::Opcode opcode, const QByteArray& data) const
+{
+ const QByteArray& header = frameHeader(opcode, data.size());
+ QHash< QTcpSocket*, Connection >::const_iterator it = m_connections.constBegin();
+ while (it != m_connections.constEnd()) {
+ if (it.value().header.wasUpgraded) {
+ it.key()->write(header);
+ it.key()->write(data);
+ }
+ ++it;
+ }
+}
+
+// see: http://tools.ietf.org/html/rfc6455#page-28
+QByteArray QWebSocketServer::frameHeader(QWebSocketServer::Frame::Opcode opcode, const int dataSize) const
+{
+ // we only support single frames for now
+ Q_ASSERT(opcode != Frame::ContinuationFrame);
+
+ QByteArray header;
+ header.reserve(4);
+ header.append(FIN_BIT | opcode);
+ if (dataSize < EXTENDED_PAYLOAD) {
+ header.append(static_cast<char>(dataSize));
+ } else if (dataSize < std::numeric_limits<quint16>::max()) {
+ header.append(EXTENDED_PAYLOAD);
+ appendBytes(header, qToBigEndian<quint16>(dataSize));
+ } else {
+ header.append(EXTENDED_LONG_PAYLOAD);
+ appendBytes(header, qToBigEndian<quint64>(dataSize));
+ }
+ return header;
+}
+
+void QWebSocketServer::ping() const
+{
+ sendFrame(Frame::Ping, QByteArray());
+}
diff --git a/src/webchannel/qwebsocketserver_p.h b/src/webchannel/qwebsocketserver_p.h
new file mode 100644
index 0000000..4651d17
--- /dev/null
+++ b/src/webchannel/qwebsocketserver_p.h
@@ -0,0 +1,160 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QWEBSOCKET_H
+#define QWEBSOCKET_H
+
+#include <QObject>
+#include <QHostAddress>
+
+class QTcpServer;
+class QTcpSocket;
+
+class QWebSocketServer : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit QWebSocketServer(QObject* parent = 0);
+ virtual ~QWebSocketServer();
+
+ bool listen(const QHostAddress& address = QHostAddress::LocalHost, quint16 port = 0);
+ void close();
+
+ QHostAddress address() const;
+ quint16 port() const;
+
+ QString errorString() const;
+
+signals:
+ void opened();
+ void error(QAbstractSocket::SocketError);
+ void textDataReceived(const QString& data);
+ void binaryDataReceived(const QByteArray& data);
+ void pongReceived();
+
+public slots:
+ void sendMessage(const QByteArray& message) const;
+ void ping() const;
+
+private slots:
+ void newConnection();
+ void readSocketData();
+ void disconnected();
+
+protected:
+ struct HeaderData
+ {
+ HeaderData()
+ : hasVersion(false)
+ , hasUpgrade(false)
+ , hasConnection(false)
+ , wasUpgraded(false)
+ {
+ }
+ QByteArray path;
+ QByteArray host;
+ QByteArray origin;
+ QByteArray key;
+ QByteArray protocol;
+ QVector<QByteArray> otherHeaders;
+ // no bitmap here - we only have few of these objects
+ bool hasVersion;
+ bool hasUpgrade;
+ bool hasConnection;
+ bool wasUpgraded;
+ };
+ virtual bool isValid(const HeaderData& connection);
+
+private:
+ struct Frame
+ {
+ enum State {
+ ReadStart,
+ ReadExtendedPayload,
+ ReadExtendedLongPayload,
+ ReadMask,
+ ReadData,
+ Finished
+ };
+ enum Opcode {
+ ContinuationFrame = 0x0,
+ TextFrame = 0x1,
+ BinaryFrame = 0x2,
+ ConnectionClose = 0x8,
+ Ping = 0x9,
+ Pong = 0xA
+ };
+ // no bitmap here - we only have a few of these objects
+ State state;
+ Opcode opcode;
+ bool fin;
+ bool masked;
+ ///NOTE: standard says unsigned 64bit integer but QByteArray only supports 'int' size
+ int length;
+ char mask[4];
+ QByteArray data;
+ // fragmentation support
+ Opcode initialOpcode;
+ QByteArray fragments;
+ };
+ struct Connection
+ {
+ HeaderData header;
+ Frame currentFrame;
+ };
+
+ void readHeaderData(QTcpSocket* socket, HeaderData& header);
+ void close(QTcpSocket* socket, const HeaderData& header);
+ void upgrade(QTcpSocket* socket, HeaderData& header);
+ bool readFrameData(QTcpSocket* socket, Frame& frame);
+ void handleFrame(QTcpSocket* socket, Frame& frame);
+
+ void sendFrame(Frame::Opcode opcode, const QByteArray& data) const;
+ void sendFrame(QTcpSocket* socket, Frame::Opcode opcode, const QByteArray& data) const;
+ QByteArray frameHeader(Frame::Opcode opcode, const int dataSize) const;
+
+ QTcpServer* m_server;
+ QHash<QTcpSocket*, Connection> m_connections;
+};
+
+#endif // QWEBSOCKET_H
diff --git a/src/webchannel/resources.qrc b/src/webchannel/resources.qrc
new file mode 100644
index 0000000..821e911
--- /dev/null
+++ b/src/webchannel/resources.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/qwebchannel/">
+ <file>webchannel.js</file>
+ <file>qobject.js</file>
+ </qresource>
+</RCC>
diff --git a/src/webchannel/signalhandler_p.h b/src/webchannel/signalhandler_p.h
new file mode 100644
index 0000000..2613f92
--- /dev/null
+++ b/src/webchannel/signalhandler_p.h
@@ -0,0 +1,278 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SIGNALHANDLER_H
+#define SIGNALHANDLER_H
+
+#include <QObject>
+#include <QHash>
+#include <QVector>
+#include <QMetaMethod>
+#include <QDebug>
+
+/**
+ * The signal handler is similar to QSignalSpy, but geared towards the usecase of the web channel.
+ *
+ * It allows connecting to any number of signals of arbitrary objects and forwards the signal
+ * invocations to the Receiver by calling its signalEmitted function, which takes the object,
+ * signal index and a QVariantList of arguments.
+ */
+template<class Receiver>
+class SignalHandler : public QObject
+{
+public:
+ SignalHandler(Receiver *receiver, QObject *parent = 0)
+ : QObject(parent)
+ , m_receiver(receiver)
+ {
+ }
+
+ /**
+ * Connect to a signal of @p object identified by @p signalIndex.
+ *
+ * If the handler is already connected to the signal, an internal counter is increased,
+ * i.e. the handler never connects multiple times to the same signal.
+ */
+ void connectTo(const QObject *object, const int signalIndex);
+
+ /**
+ * Decrease the connection counter for the connection to the given signal.
+ *
+ * When the counter drops to zero, the connection is disconnected.
+ */
+ void disconnectFrom(const QObject *object, const int signalIndex);
+
+ /**
+ * @internal
+ *
+ * Custom implementation of qt_metacall which calls dispatch() for connected signals.
+ */
+ int qt_metacall(QMetaObject::Call call, int methodId, void **args) Q_DECL_OVERRIDE;
+
+ /**
+ * Reset all connections, useful for benchmarks.
+ */
+ void clear();
+
+private:
+ /**
+ * Exctract the arguments of a signal call and pass them to the receiver.
+ *
+ * The @p argumentData is converted to a QVariantList and then passed to the receiver's
+ * signalEmitted method.
+ */
+ void dispatch(const QObject *object, const int signalIdx, void **argumentData);
+
+ Receiver *m_receiver;
+
+ // maps meta object -> signalIndex -> list of arguments
+ // NOTE: This data is "leaked" on disconnect until deletion of the handler, is this a problem?
+ typedef QVector<int> ArgumentTypeList;
+ typedef QHash<int, ArgumentTypeList> SignalArgumentHash;
+ QHash<const QMetaObject *, SignalArgumentHash > m_signalArgumentTypes;
+
+ /*
+ * Tracks how many connections are active to object signals.
+ *
+ * Maps object -> signalIndex -> pair of connection and number of connections
+ *
+ * Note that the handler is connected to the signal only once, whereas clients
+ * may have connected multiple times.
+ *
+ * TODO: Move more of this logic to the HTML client side, esp. the connection counting.
+ */
+ typedef QPair<QMetaObject::Connection, int> ConnectionPair;
+ typedef QHash<int, ConnectionPair> SignalConnectionHash;
+ typedef QHash<const QObject*, SignalConnectionHash> ConnectionHash;
+ ConnectionHash m_connectionsCounter;
+};
+
+/**
+ * Find and return the signal of index @p signalIndex in the meta object of @p object and return it.
+ *
+ * The return value is also verified to ensure it is a signal.
+ */
+QMetaMethod findSignal(const QMetaObject *metaObject, const int signalIndex)
+{
+ QMetaMethod signal = metaObject->method(signalIndex);
+ if (!signal.isValid()) {
+ qWarning("Cannot find signal with index %d of object %s", signalIndex, metaObject->className());
+ return QMetaMethod();
+ }
+ Q_ASSERT(signal.methodType() == QMetaMethod::Signal);
+ return signal;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::connectTo(const QObject *object, const int signalIndex)
+{
+ const QMetaObject *metaObject = object->metaObject();
+ const QMetaMethod &signal = findSignal(metaObject, signalIndex);
+ if (!signal.isValid()) {
+ return;
+ }
+
+ ConnectionPair &connectionCounter = m_connectionsCounter[object][signalIndex];
+ if (connectionCounter.first) {
+ // increase connection counter if already connected
+ ++connectionCounter.second;
+ return;
+ } // otherwise not yet connected, do so now
+
+ const int memberOffset = QObject::staticMetaObject.methodCount();
+ QMetaObject::Connection connection = QMetaObject::connect(object, signal.methodIndex(), this, memberOffset, Qt::DirectConnection, 0);
+ if (!connection) {
+ qWarning() << "SignalHandler: QMetaObject::connect returned false. Unable to connect to" << object << signal.name() << signal.methodSignature();
+ return;
+ }
+ connectionCounter.first = connection;
+ connectionCounter.second = 1;
+
+ if (!m_signalArgumentTypes.value(metaObject).contains(signal.methodIndex())) {
+ // find the type ids of the signal parameters, see also QSignalSpy::initArgs
+ QVector<int> args;
+ args.reserve(signal.parameterCount());
+ for (int i = 0; i < signal.parameterCount(); ++i) {
+ int tp = signal.parameterType(i);
+ if (tp == QMetaType::UnknownType && object) {
+ void *argv[] = { &tp, &i };
+ QMetaObject::metacall(const_cast<QObject *>(object),
+ QMetaObject::RegisterMethodArgumentMetaType,
+ signal.methodIndex(), argv);
+ if (tp == -1) {
+ tp = QMetaType::UnknownType;
+ }
+ }
+ if (tp == QMetaType::UnknownType) {
+ Q_ASSERT(tp != QMetaType::Void); // void parameter => metaobject is corrupt
+ qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.",
+ signal.parameterNames().at(i).constData());
+ }
+ args << tp;
+ }
+
+ m_signalArgumentTypes[metaObject][signal.methodIndex()] = args;
+ }
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::dispatch(const QObject *object, const int signalIdx, void **argumentData)
+{
+ Q_ASSERT(m_signalArgumentTypes.contains(object->metaObject()));
+ const QHash<int, QVector<int> > &objectSignalArgumentTypes = m_signalArgumentTypes.value(object->metaObject());
+ QHash<int, QVector<int> >::const_iterator signalIt = objectSignalArgumentTypes.constFind(signalIdx);
+ if (signalIt == objectSignalArgumentTypes.constEnd()) {
+ // not connected to this signal, skip
+ return;
+ }
+ const QVector<int> &argumentTypes = *signalIt;
+ QVariantList arguments;
+ arguments.reserve(argumentTypes.count());
+ // TODO: basic overload resolution based on number of arguments?
+ for (int i = 0; i < argumentTypes.count(); ++i) {
+ const QMetaType::Type type = static_cast<QMetaType::Type>(argumentTypes.at(i));
+ QVariant arg;
+ if (type == QMetaType::QVariant) {
+ arg = *reinterpret_cast<QVariant *>(argumentData[i + 1]);
+ } else {
+ arg = QVariant(type, argumentData[i + 1]);
+ }
+ arguments.append(arg);
+ }
+ m_receiver->signalEmitted(object, signalIdx, arguments);
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::disconnectFrom(const QObject *object, const int signalIndex)
+{
+ Q_ASSERT(m_connectionsCounter.value(object).contains(signalIndex));
+ ConnectionPair &connection = m_connectionsCounter[object][signalIndex];
+ --connection.second;
+ if (!connection.second || !connection.first) {
+ QObject::disconnect(connection.first);
+ m_connectionsCounter[object].remove(signalIndex);
+ if (m_connectionsCounter[object].isEmpty()) {
+ m_connectionsCounter.remove(object);
+ }
+ }
+}
+
+template<class Receiver>
+int SignalHandler<Receiver>::qt_metacall(QMetaObject::Call call, int methodId, void **args)
+{
+ methodId = QObject::qt_metacall(call, methodId, args);
+ if (methodId < 0)
+ return methodId;
+
+ if (call == QMetaObject::InvokeMetaMethod) {
+ if (methodId == 0) {
+ Q_ASSERT(sender());
+ Q_ASSERT(senderSignalIndex() != -1);
+ dispatch(sender(), senderSignalIndex(), args);
+ static const int destroyedIndex = metaObject()->indexOfSignal("destroyed(QObject*)");
+ if (senderSignalIndex() == destroyedIndex) {
+ ConnectionHash::iterator it = m_connectionsCounter.find(sender());
+ Q_ASSERT(it != m_connectionsCounter.end());
+ foreach (const ConnectionPair &connection, *it) {
+ QObject::disconnect(connection.first);
+ }
+ m_connectionsCounter.erase(it);
+ }
+ }
+ --methodId;
+ }
+ return methodId;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::clear()
+{
+ foreach (const SignalConnectionHash &connections, m_connectionsCounter) {
+ foreach (const ConnectionPair &connection, connections) {
+ QObject::disconnect(connection.first);
+ }
+ }
+ m_connectionsCounter.clear();
+ m_signalArgumentTypes.clear();
+}
+
+#endif // SIGNALHANDLER_H
diff --git a/src/webchannel/variantargument_p.h b/src/webchannel/variantargument_p.h
new file mode 100644
index 0000000..ee625fc
--- /dev/null
+++ b/src/webchannel/variantargument_p.h
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef VARIANTARGUMENT_H
+#define VARIANTARGUMENT_H
+
+#include <QVariant>
+#include <QMetaType>
+
+/**
+ * RAII QVariant to Q[Generic]Argument conversion
+ */
+class VariantArgument
+{
+public:
+ explicit VariantArgument()
+ : m_data(0)
+ , m_paramType(0)
+ {
+ }
+
+ /// TODO: test with C++ methods that don't take a QVariant as arg
+ /// also test conversions
+ void setValue(const QVariant &value, int paramType)
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_name.clear();
+ m_data = 0;
+ }
+
+ m_paramType = paramType;
+
+ if (value.isValid()) {
+ m_name = value.typeName();
+ m_data = QMetaType::create(m_paramType, value.constData());
+ }
+ }
+
+ ~VariantArgument()
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_data = 0;
+ }
+ }
+
+ operator QGenericArgument() const
+ {
+ if (!m_data) {
+ return QGenericArgument();
+ }
+ return QGenericArgument(m_name.constData(), m_data);
+ }
+
+private:
+ Q_DISABLE_COPY(VariantArgument)
+
+ QByteArray m_name;
+ void* m_data;
+ int m_paramType;
+};
+
+#endif // VARIANTARGUMENT_H
diff --git a/src/webchannel/webchannel.js b/src/webchannel/webchannel.js
new file mode 100644
index 0000000..8827cbf
--- /dev/null
+++ b/src/webchannel/webchannel.js
@@ -0,0 +1,129 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and 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 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: 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.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+"use strict";
+
+var QWebChannel = function(baseUrl, initCallback)
+{
+ var channel = this;
+ // support multiple channels listening to the same socket
+ // the responses to channel.exec must be distinguishable
+ // see: http://stackoverflow.com/a/2117523/35250
+ this.id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ });
+ ///TODO: use ssl?
+ var socketUrl = "ws://" + baseUrl;
+ this.socket = new WebSocket(socketUrl, "QWebChannel");
+ this.send = function(data)
+ {
+ if (typeof(data) !== "string") {
+ data = JSON.stringify(data);
+ }
+ channel.socket.send(data);
+ };
+
+ this.socket.onopen = function()
+ {
+ initCallback(channel);
+ };
+ this.socket.onclose = function()
+ {
+ console.error("web channel closed");
+ };
+ this.socket.onerror = function(error)
+ {
+ console.error("web channel error: " + error);
+ };
+ this.socket.onmessage = function(message)
+ {
+ var jsonData = JSON.parse(message.data);
+ if (jsonData.id === undefined) {
+ console.error("invalid message received:", message.data);
+ return;
+ }
+ if (jsonData.data === undefined) {
+ jsonData.data = {};
+ }
+ if (jsonData.response) {
+ if (jsonData.id[0] === channel.id) {
+ channel.execCallbacks[jsonData.id[1]](jsonData.data);
+ delete channel.execCallbacks[jsonData.id];
+ }
+ } else if (channel.subscriptions[jsonData.id]) {
+ channel.subscriptions[jsonData.id].forEach(function(callback) {
+ (callback)(jsonData.data); }
+ );
+ }
+ };
+
+ this.subscriptions = {};
+ this.subscribe = function(id, callback)
+ {
+ if (channel.subscriptions[id]) {
+ channel.subscriptions[id].push(callback);
+ } else {
+ channel.subscriptions[id] = [callback];
+ }
+ };
+
+ this.execCallbacks = {};
+ this.execId = 0;
+ this.exec = function(data, callback)
+ {
+ if (!callback) {
+ // if no callback is given, send directly
+ channel.send({data: data});
+ return;
+ }
+ if (channel.execId === Number.MAX_VALUE) {
+ // wrap
+ channel.execId = Number.MIN_VALUE;
+ }
+ var id = channel.execId++;
+ channel.execCallbacks[id] = callback;
+ channel.send({"id": [channel.id, id], "data": data});
+ };
+
+ this.objectMap = {};
+};
diff --git a/src/webchannel/webchannel.pro b/src/webchannel/webchannel.pro
new file mode 100644
index 0000000..a0041bf
--- /dev/null
+++ b/src/webchannel/webchannel.pro
@@ -0,0 +1,28 @@
+TARGET = QtWebChannel
+QT = core network
+CONFIG += warn_on strict_flags
+
+load(qt_module)
+
+RESOURCES += \
+ resources.qrc
+
+OTHER_FILES = \
+ webchannel.js \
+ qobject.js
+
+PUBLIC_HEADERS += \
+ qwebchannel.h \
+ qmetaobjectpublisher.h
+
+PRIVATE_HEADERS += \
+ qwebsocketserver_p.h \
+ variantargument_p.h \
+ signalhandler_p.h
+
+SOURCES += \
+ qwebchannel.cpp \
+ qmetaobjectpublisher.cpp \
+ qwebsocketserver.cpp
+
+HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS