diff options
Diffstat (limited to 'src/webchannel')
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 730 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.h | 114 | ||||
-rw-r--r-- | src/webchannel/qobject.js | 305 | ||||
-rw-r--r-- | src/webchannel/qwebchannel.cpp | 207 | ||||
-rw-r--r-- | src/webchannel/qwebchannel.h | 91 | ||||
-rw-r--r-- | src/webchannel/qwebchannelglobal.h | 62 | ||||
-rw-r--r-- | src/webchannel/qwebsocketserver.cpp | 424 | ||||
-rw-r--r-- | src/webchannel/qwebsocketserver_p.h | 160 | ||||
-rw-r--r-- | src/webchannel/resources.qrc | 6 | ||||
-rw-r--r-- | src/webchannel/signalhandler_p.h | 278 | ||||
-rw-r--r-- | src/webchannel/variantargument_p.h | 103 | ||||
-rw-r--r-- | src/webchannel/webchannel.js | 129 | ||||
-rw-r--r-- | src/webchannel/webchannel.pro | 28 |
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 ¬ifySignal = 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 |