From: Manuel Nickschas Date: Wed, 21 Mar 2018 23:58:46 +0000 (+0100) Subject: common: Represent core/client features as string list in the protocol X-Git-Tag: travis-deploy-test~161 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=9f91e0dd3c4eb5c2e2dedfc8d36a068d433d51b1 common: Represent core/client features as string list in the protocol The previous way of representing all supported features as a bit-wise combination of flags is reaching its limits, due to the fact that the old Features enum didn't have a defined width, and thus the compiler can (and does) choose to represent it as a 16 bit value even though the serialization in the protocol is defined as a 32 bit value. In order to solve this problem, and to make feature negotiation more robust, a new field "FeatureList" is added to both "ClientInit" and "ClientInitAck" handshake messages that holds a string list with the supported features. Clients still send both "Features" and "FeatureList" fields when registering, in order to support older cores. The core now omits the legacy "CoreFeatures" field if the client indicates support for extended features. Support for extended features is indicated as the legacy feature 0x8000. Note that legacy features are a subset of the new extended features. Clients supporting extended features are encouraged to send the full list, including legacy ones, in their FeatureList field. Internally, the string-based feature list is mapped to a std::vector to provide for fast lookup of enabled features. The new methods Client::isCoreFeatureEnabled() and Peer::hasFeature() allow for convenient checking. Both client and core now output a log message if there is a mismatch between the peers. --- diff --git a/src/client/client.cpp b/src/client/client.cpp index dd20ca0a..d6dc3d1b 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -56,7 +56,6 @@ #include QPointer Client::instanceptr = 0; -Quassel::Features Client::_coreFeatures = 0; /*** Initialization/destruction ***/ @@ -188,9 +187,9 @@ AbstractUi *Client::mainUi() } -void Client::setCoreFeatures(Quassel::Features features) +bool Client::isCoreFeatureEnabled(Quassel::Feature feature) { - _coreFeatures = features; + return coreConnection()->peer() ? coreConnection()->peer()->hasFeature(feature) : false; } @@ -430,7 +429,7 @@ void Client::setSyncedToCore() // create TransferManager and DccConfig if core supports them Q_ASSERT(!_dccConfig); Q_ASSERT(!_transferManager); - if (coreFeatures() & Quassel::DccFileTransfer) { + if (isCoreFeatureEnabled(Quassel::Feature::DccFileTransfer)) { _dccConfig = new DccConfig(this); p->synchronize(dccConfig()); _transferManager = new ClientTransferManager(this); @@ -461,7 +460,7 @@ void Client::finishConnectionInitialization() disconnect(bufferSyncer(), SIGNAL(initDone()), this, SLOT(finishConnectionInitialization())); requestInitialBacklog(); - if (coreFeatures().testFlag(Quassel::BufferActivitySync)) + if (isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) bufferSyncer()->markActivitiesChanged(); } @@ -484,7 +483,6 @@ void Client::disconnectFromCore() void Client::setDisconnectedFromCore() { _connected = false; - _coreFeatures = 0; emit disconnected(); emit coreConnectionStateChanged(false); diff --git a/src/client/client.h b/src/client/client.h index 225caab8..6732d595 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -131,9 +131,7 @@ public: static inline CoreAccountModel *coreAccountModel() { return instance()->_coreAccountModel; } static inline CoreConnection *coreConnection() { return instance()->_coreConnection; } static inline CoreAccount currentCoreAccount() { return coreConnection()->currentAccount(); } - static inline Quassel::Features coreFeatures() { return _coreFeatures; } - - static void setCoreFeatures(Quassel::Features features); + static bool isCoreFeatureEnabled(Quassel::Feature feature); static bool isConnected(); static bool internalCore(); @@ -282,7 +280,6 @@ private: QHash _identities; bool _connected; - static Quassel::Features _coreFeatures; QString _debugLogBuffer; QTextStream _debugLog; diff --git a/src/client/clientauthhandler.cpp b/src/client/clientauthhandler.cpp index 2c96a1c9..54afcd3f 100644 --- a/src/client/clientauthhandler.cpp +++ b/src/client/clientauthhandler.cpp @@ -32,6 +32,7 @@ #include "client.h" #include "clientsettings.h" +#include "logger.h" #include "peerfactory.h" #if QT_VERSION < 0x050000 @@ -42,7 +43,7 @@ using namespace Protocol; ClientAuthHandler::ClientAuthHandler(CoreAccount account, QObject *parent) : AuthHandler(parent), - _peer(0), + _peer(nullptr), _account(account), _probing(false), _legacy(false), @@ -52,6 +53,12 @@ ClientAuthHandler::ClientAuthHandler(CoreAccount account, QObject *parent) } +Peer *ClientAuthHandler::peer() const +{ + return _peer; +} + + void ClientAuthHandler::connectToCore() { CoreAccountSettings s; @@ -299,7 +306,7 @@ void ClientAuthHandler::startRegistration() useSsl = _account.useSsl(); #endif - _peer->dispatch(RegisterClient(Quassel::buildInfo().fancyVersionString, Quassel::buildInfo().commitDate, useSsl, Quassel::features())); + _peer->dispatch(RegisterClient(Quassel::Features{}, Quassel::buildInfo().fancyVersionString, Quassel::buildInfo().commitDate, useSsl)); } @@ -316,8 +323,7 @@ void ClientAuthHandler::handle(const ClientRegistered &msg) _backendInfo = msg.backendInfo; _authenticatorInfo = msg.authenticatorInfo; - Client::setCoreFeatures(Quassel::Features(msg.coreFeatures)); - SignalProxy::current()->sourcePeer()->setFeatures(Quassel::Features(msg.coreFeatures)); + _peer->setFeatures(std::move(msg.features)); // The legacy protocol enables SSL at this point if(_legacy && _account.useSsl()) @@ -329,6 +335,15 @@ void ClientAuthHandler::handle(const ClientRegistered &msg) void ClientAuthHandler::onConnectionReady() { + const auto &coreFeatures = _peer->features(); + auto unsupported = coreFeatures.toStringList(false); + if (!unsupported.isEmpty()) { + quInfo() << qPrintable(tr("Core does not support the following features: %1").arg(unsupported.join(", "))); + } + if (!coreFeatures.unknownFeatures().isEmpty()) { + quInfo() << qPrintable(tr("Core supports unknown features: %1").arg(coreFeatures.unknownFeatures().join(", "))); + } + emit connectionReady(); emit statusMessage(tr("Connected to %1").arg(_account.accountName())); diff --git a/src/client/clientauthhandler.h b/src/client/clientauthhandler.h index 5ebd5305..b80397f2 100644 --- a/src/client/clientauthhandler.h +++ b/src/client/clientauthhandler.h @@ -18,8 +18,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ -#ifndef CLIENTAUTHHANDLER_H -#define CLIENTAUTHHANDLER_H +#pragma once #include "compressor.h" #include "authhandler.h" @@ -34,14 +33,16 @@ class ClientAuthHandler : public AuthHandler Q_OBJECT public: - ClientAuthHandler(CoreAccount account, QObject *parent = 0); - enum DigestVersion { Md5, Sha2_512, Latest=Sha2_512 }; + ClientAuthHandler(CoreAccount account, QObject *parent = 0); + + Peer *peer() const; + public slots: void connectToCore(); @@ -119,5 +120,3 @@ private: bool _legacy; quint8 _connectionFeatures; }; - -#endif diff --git a/src/client/coreconnection.cpp b/src/client/coreconnection.cpp index b0c4feb4..79061285 100644 --- a/src/client/coreconnection.cpp +++ b/src/client/coreconnection.cpp @@ -184,6 +184,15 @@ void CoreConnection::onlineStateChanged(bool isOnline) } +QPointer CoreConnection::peer() const +{ + if (_peer) { + return _peer; + } + return _authHandler ? _authHandler->peer() : nullptr; +} + + bool CoreConnection::isEncrypted() const { return _peer && _peer->isSecure(); @@ -463,9 +472,6 @@ void CoreConnection::onHandshakeComplete(RemotePeer *peer, const Protocol::Sessi void CoreConnection::internalSessionStateReceived(const Protocol::SessionState &sessionState) { updateProgress(100, 100); - - Client::setCoreFeatures(Quassel::features()); // mono connection... - setState(Synchronizing); syncToCore(sessionState); } diff --git a/src/client/coreconnection.h b/src/client/coreconnection.h index 4a1766e3..33f91caa 100644 --- a/src/client/coreconnection.h +++ b/src/client/coreconnection.h @@ -73,7 +73,7 @@ public: //! Check if we consider the last connect as reconnect bool wasReconnect() const { return _wasReconnect; } - QPointer peer() { return _peer; } + QPointer peer() const; public slots: bool connectToCore(AccountId = 0); diff --git a/src/client/networkmodel.cpp b/src/client/networkmodel.cpp index 147605c7..582b0084 100644 --- a/src/client/networkmodel.cpp +++ b/src/client/networkmodel.cpp @@ -298,7 +298,7 @@ void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level) void BufferItem::clearActivityLevel() { - if (Client::coreFeatures().testFlag(Quassel::Feature::BufferActivitySync)) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) { // If the core handles activity sync, clear only the highlight flag _activity &= ~BufferInfo::Highlight; } else { @@ -307,7 +307,7 @@ void BufferItem::clearActivityLevel() _firstUnreadMsgId = MsgId(); // FIXME remove with core proto v11 - if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) { _markerLineMsgId = _lastSeenMsgId; } @@ -318,7 +318,7 @@ void BufferItem::clearActivityLevel() void BufferItem::updateActivityLevel(const Message &msg) { // If the core handles activity, and this message is not a highlight, ignore this - if (Client::coreFeatures().testFlag(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) { return; } @@ -344,7 +344,7 @@ void BufferItem::updateActivityLevel(const Message &msg) Message::Types type; // If the core handles activities, ignore types - if (Client::coreFeatures().testFlag(Quassel::Feature::BufferActivitySync)) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) { type = Message::Types(); } else { type = msg.type(); @@ -434,7 +434,7 @@ void BufferItem::setLastSeenMsgId(MsgId msgId) _lastSeenMsgId = msgId; // FIXME remove with core protocol v11 - if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) { if (!isCurrentBuffer()) _markerLineMsgId = msgId; } diff --git a/src/common/internalpeer.cpp b/src/common/internalpeer.cpp index b724dc01..c2512a07 100644 --- a/src/common/internalpeer.cpp +++ b/src/common/internalpeer.cpp @@ -42,7 +42,7 @@ InternalPeer::InternalPeer(QObject *parent) _peer(0), _isOpen(true) { - + setFeatures(Quassel::Features{}); } diff --git a/src/common/message.cpp b/src/common/message.cpp index 911c9353..e5ec65a5 100644 --- a/src/common/message.cpp +++ b/src/common/message.cpp @@ -60,7 +60,7 @@ QDataStream &operator<<(QDataStream &out, const Message &msg) << msg.bufferInfo() << msg.sender().toUtf8(); - if (SignalProxy::current()->targetPeer()->features().testFlag(Quassel::Feature::SenderPrefixes)) + if (SignalProxy::current()->targetPeer()->hasFeature(Quassel::Feature::SenderPrefixes)) out << msg.senderPrefixes().toUtf8(); out << msg.contents().toUtf8(); @@ -91,7 +91,7 @@ QDataStream &operator>>(QDataStream &in, Message &msg) msg._sender = QString::fromUtf8(sender); QByteArray senderPrefixes; - if (SignalProxy::current()->sourcePeer()->features().testFlag(Quassel::Feature::SenderPrefixes)) + if (SignalProxy::current()->sourcePeer()->hasFeature(Quassel::Feature::SenderPrefixes)) in >> senderPrefixes; msg._senderPrefixes = QString::fromUtf8(senderPrefixes); diff --git a/src/common/peer.cpp b/src/common/peer.cpp index 9a9e3f58..84f292e3 100644 --- a/src/common/peer.cpp +++ b/src/common/peer.cpp @@ -57,12 +57,17 @@ void Peer::setClientVersion(const QString &clientVersion) { _clientVersion = clientVersion; } -Quassel::Features Peer::features() const { +bool Peer::hasFeature(Quassel::Feature feature) const { + return _features.isEnabled(feature); +} + +Quassel::Features Peer::features() const +{ return _features; } void Peer::setFeatures(Quassel::Features features) { - _features = features; + _features = std::move(features); } int Peer::id() const { diff --git a/src/common/peer.h b/src/common/peer.h index fbc17914..becf0490 100644 --- a/src/common/peer.h +++ b/src/common/peer.h @@ -51,6 +51,7 @@ public: QString clientVersion() const; void setClientVersion(const QString &clientVersion); + bool hasFeature(Quassel::Feature feature) const; Quassel::Features features() const; void setFeatures(Quassel::Features features); diff --git a/src/common/protocol.h b/src/common/protocol.h index e6266862..38e53c6d 100644 --- a/src/common/protocol.h +++ b/src/common/protocol.h @@ -24,6 +24,8 @@ #include #include +#include "quassel.h" + namespace Protocol { const quint32 magic = 0x42b33f00; @@ -56,18 +58,19 @@ struct HandshakeMessage { struct RegisterClient : public HandshakeMessage { - inline RegisterClient(const QString &clientVersion, const QString &buildDate, bool sslSupported = false, quint32 features = 0) - : clientVersion(clientVersion) - , buildDate(buildDate) - , sslSupported(sslSupported) - , clientFeatures(features) {} + inline RegisterClient(Quassel::Features clientFeatures, const QString &clientVersion, const QString &buildDate, bool sslSupported = false) + : features(std::move(clientFeatures)) + , clientVersion(clientVersion) + , buildDate(buildDate) + , sslSupported(sslSupported) + {} + Quassel::Features features; QString clientVersion; QString buildDate; // this is only used by the LegacyProtocol in compat mode bool sslSupported; - quint32 clientFeatures; }; @@ -82,19 +85,19 @@ struct ClientDenied : public HandshakeMessage struct ClientRegistered : public HandshakeMessage { - inline ClientRegistered(quint32 coreFeatures, bool coreConfigured, const QVariantList &backendInfo, bool sslSupported, const QVariantList &authenticatorInfo) - : coreFeatures(coreFeatures) - , coreConfigured(coreConfigured) - , backendInfo(backendInfo) - , authenticatorInfo(authenticatorInfo) - , sslSupported(sslSupported) + inline ClientRegistered(Quassel::Features coreFeatures, bool coreConfigured, const QVariantList &backendInfo, const QVariantList &authenticatorInfo, bool sslSupported) + : features(std::move(coreFeatures)) + , coreConfigured(coreConfigured) + , backendInfo(backendInfo) + , authenticatorInfo(authenticatorInfo) + , sslSupported(sslSupported) {} - quint32 coreFeatures; + Quassel::Features features; bool coreConfigured; + QVariantList backendInfo; // TODO: abstract this better // The authenticatorInfo should be optional! - QVariantList backendInfo; // TODO: abstract this better QVariantList authenticatorInfo; // this is only used by the LegacyProtocol in compat mode diff --git a/src/common/protocols/datastream/datastreampeer.cpp b/src/common/protocols/datastream/datastreampeer.cpp index e91c9775..6709c695 100644 --- a/src/common/protocols/datastream/datastreampeer.cpp +++ b/src/common/protocols/datastream/datastreampeer.cpp @@ -24,6 +24,7 @@ #include #include "datastreampeer.h" +#include "quassel.h" using namespace Protocol; @@ -116,7 +117,11 @@ void DataStreamPeer::handleHandshakeMessage(const QVariantList &mapData) } if (msgType == "ClientInit") { - handle(RegisterClient(m["ClientVersion"].toString(), m["ClientDate"].toString(), false, m["Features"].toInt())); // UseSsl obsolete + handle(RegisterClient{Quassel::Features{m["FeatureList"].toStringList(), Quassel::LegacyFeatures(m["Features"].toUInt())}, + m["ClientVersion"].toString(), + m["ClientDate"].toString(), + false // UseSsl obsolete + }); } else if (msgType == "ClientInitReject") { @@ -124,7 +129,12 @@ void DataStreamPeer::handleHandshakeMessage(const QVariantList &mapData) } else if (msgType == "ClientInitAck") { - handle(ClientRegistered(m["CoreFeatures"].toUInt(), m["Configured"].toBool(), m["StorageBackends"].toList(), false, m["Authenticators"].toList())); // SupportsSsl obsolete + handle(ClientRegistered{Quassel::Features{m["FeatureList"].toStringList(), Quassel::LegacyFeatures(m["CoreFeatures"].toUInt())}, + m["Configured"].toBool(), + m["StorageBackends"].toList(), + m["Authenticators"].toList(), + false // SupportsSsl obsolete + }); } else if (msgType == "CoreSetupData") { @@ -166,9 +176,10 @@ void DataStreamPeer::handleHandshakeMessage(const QVariantList &mapData) void DataStreamPeer::dispatch(const RegisterClient &msg) { QVariantMap m; m["MsgType"] = "ClientInit"; + m["Features"] = static_cast(msg.features.toLegacyFeatures()); + m["FeatureList"] = msg.features.toStringList(); m["ClientVersion"] = msg.clientVersion; m["ClientDate"] = msg.buildDate; - m["Features"] = msg.clientFeatures; writeMessage(m); } @@ -186,10 +197,17 @@ void DataStreamPeer::dispatch(const ClientDenied &msg) { void DataStreamPeer::dispatch(const ClientRegistered &msg) { QVariantMap m; m["MsgType"] = "ClientInitAck"; - m["CoreFeatures"] = msg.coreFeatures; - m["StorageBackends"] = msg.backendInfo; - m["Authenticators"] = msg.authenticatorInfo; + if (hasFeature(Quassel::Feature::ExtendedFeatures)) { + m["FeatureList"] = msg.features.toStringList(); + } + else { + m["CoreFeatures"] = static_cast(msg.features.toLegacyFeatures()); + } m["LoginEnabled"] = m["Configured"] = msg.coreConfigured; + m["StorageBackends"] = msg.backendInfo; + if (hasFeature(Quassel::Feature::Authenticators)) { + m["Authenticators"] = msg.authenticatorInfo; + } writeMessage(m); } diff --git a/src/common/protocols/legacy/legacypeer.cpp b/src/common/protocols/legacy/legacypeer.cpp index ebbd164e..2f0127eb 100644 --- a/src/common/protocols/legacy/legacypeer.cpp +++ b/src/common/protocols/legacy/legacypeer.cpp @@ -23,6 +23,7 @@ #include #include "legacypeer.h" +#include "quassel.h" /* version.inc is no longer used for this */ const uint protocolVersion = 10; @@ -151,7 +152,10 @@ void LegacyPeer::handleHandshakeMessage(const QVariant &msg) socket()->setProperty("UseCompression", true); } #endif - handle(RegisterClient(m["ClientVersion"].toString(), m["ClientDate"].toString(), m["UseSsl"].toBool(), m["Features"].toInt())); + handle(RegisterClient{Quassel::Features{m["FeatureList"].toStringList(), Quassel::LegacyFeatures(m["Features"].toUInt())}, + m["ClientVersion"].toString(), + m["ClientDate"].toString(), + m["UseSsl"].toBool()}); } else if (msgType == "ClientInitReject") { @@ -170,7 +174,11 @@ void LegacyPeer::handleHandshakeMessage(const QVariant &msg) socket()->setProperty("UseCompression", true); #endif - handle(ClientRegistered(m["CoreFeatures"].toUInt(), m["Configured"].toBool(), m["StorageBackends"].toList(), m["SupportSsl"].toBool(), m["Authenticators"].toList())); + handle(ClientRegistered{Quassel::Features{m["FeatureList"].toStringList(), Quassel::LegacyFeatures(m["CoreFeatures"].toUInt())}, + m["Configured"].toBool(), + m["StorageBackends"].toList(), + m["Authenticators"].toList(), + m["SupportSsl"].toBool()}); } else if (msgType == "CoreSetupData") { @@ -212,9 +220,10 @@ void LegacyPeer::handleHandshakeMessage(const QVariant &msg) void LegacyPeer::dispatch(const RegisterClient &msg) { QVariantMap m; m["MsgType"] = "ClientInit"; + m["Features"] = static_cast(msg.features.toLegacyFeatures()); + m["FeatureList"] = msg.features.toStringList(); m["ClientVersion"] = msg.clientVersion; m["ClientDate"] = msg.buildDate; - m["Features"] = msg.clientFeatures; // FIXME only in compat mode m["ProtocolVersion"] = protocolVersion; @@ -241,9 +250,16 @@ void LegacyPeer::dispatch(const ClientDenied &msg) { void LegacyPeer::dispatch(const ClientRegistered &msg) { QVariantMap m; m["MsgType"] = "ClientInitAck"; - m["CoreFeatures"] = msg.coreFeatures; + if (hasFeature(Quassel::Feature::ExtendedFeatures)) { + m["FeatureList"] = msg.features.toStringList(); + } + else { + m["CoreFeatures"] = static_cast(msg.features.toLegacyFeatures()); + } m["StorageBackends"] = msg.backendInfo; - m["Authenticators"] = msg.authenticatorInfo; + if (hasFeature(Quassel::Feature::Authenticators)) { + m["Authenticators"] = msg.authenticatorInfo; + } // FIXME only in compat mode m["ProtocolVersion"] = protocolVersion; diff --git a/src/common/quassel.cpp b/src/common/quassel.cpp index 16b8887d..dea2cd9b 100644 --- a/src/common/quassel.cpp +++ b/src/common/quassel.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -454,19 +455,6 @@ void Quassel::logFatalMessage(const char *msg) } -Quassel::Features Quassel::features() -{ - Features feats = 0; - for (int i = 1; i <= NumFeatures; i <<= 1) - feats |= (Feature)i; - - // Disable DCC until it is ready - feats &= ~Feature::DccFileTransfer; - - return feats; -} - - const QString &Quassel::coreDumpFileName() { if (_coreDumpFileName.isEmpty()) { @@ -678,3 +666,93 @@ void Quassel::loadTranslation(const QLocale &locale) quasselTranslator->load(QString("%1").arg(locale.name()), translationDirPath()); #endif } + + +// ---- Quassel::Features --------------------------------------------------------------------------------------------- + +Quassel::Features::Features() +{ + QStringList features; + + // TODO Qt5: Use QMetaEnum::fromType() + auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature")); + _features.resize(featureEnum.keyCount(), true); // enable all known features to true +} + + +Quassel::Features::Features(const QStringList &features, LegacyFeatures legacyFeatures) +{ + // TODO Qt5: Use QMetaEnum::fromType() + auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature")); + _features.resize(featureEnum.keyCount(), false); + + for (auto &&feature : features) { + int i = featureEnum.keyToValue(qPrintable(feature)); + if (i >= 0) { + _features[i] = true; + } + else { + _unknownFeatures << feature; + } + } + + if (legacyFeatures) { + // TODO Qt5: Use QMetaEnum::fromType() + auto legacyFeatureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("LegacyFeature")); + for (quint32 mask = 0x0001; mask <= 0x8000; mask <<=1) { + if (static_cast(legacyFeatures) & mask) { + int i = featureEnum.keyToValue(legacyFeatureEnum.valueToKey(mask)); + if (i >= 0) { + _features[i] = true; + } + } + } + } +} + + +bool Quassel::Features::isEnabled(Feature feature) const +{ + size_t i = static_cast(feature); + return i < _features.size() ? _features[i] : false; +} + + +QStringList Quassel::Features::toStringList(bool enabled) const +{ + QStringList result; + // TODO Qt5: Use QMetaEnum::fromType() + auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature")); + for (quint32 i = 0; i < _features.size(); ++i) { + if (_features[i] == enabled) { + result << featureEnum.key(i); + } + } + return result; +} + + +Quassel::LegacyFeatures Quassel::Features::toLegacyFeatures() const +{ + // TODO Qt5: Use LegacyFeatures (flag operators for enum classes not supported in Qt4) + quint32 result{0}; + // TODO Qt5: Use QMetaEnum::fromType() + auto featureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("Feature")); + auto legacyFeatureEnum = Quassel::staticMetaObject.enumerator(Quassel::staticMetaObject.indexOfEnumerator("LegacyFeature")); + + for (quint32 i = 0; i < _features.size(); ++i) { + if (_features[i]) { + int v = legacyFeatureEnum.keyToValue(featureEnum.key(i)); + if (v >= 0) { + result |= v; + } + } + } + return static_cast(result); +} + + +QStringList Quassel::Features::unknownFeatures() const +{ + return _unknownFeatures; +} diff --git a/src/common/quassel.h b/src/common/quassel.h index 8b106771..16e12e9a 100644 --- a/src/common/quassel.h +++ b/src/common/quassel.h @@ -25,16 +25,20 @@ #include #include +#include +#include #include #include +#include #include "abstractcliparser.h" class QFile; -class Quassel +class Quassel : public QObject { - Q_DECLARE_TR_FUNCTIONS(Quassel) + // TODO Qt5: Use Q_GADGET + Q_OBJECT public: enum RunMode { @@ -61,38 +65,72 @@ public: QString organizationDomain; }; - //! A list of features that are optional in core and/or client, but need runtime checking - /** Some features require an uptodate counterpart, but don't justify a protocol break. - * This is what we use this enum for. Add such features to it and check at runtime on the other - * side for their existence. + /** + * This enum defines the optional features supported by cores/clients prior to version 0.13. + * + * Since the number of features declared this way is limited to 16 (due to the enum not having a defined + * width in cores/clients prior to 0.13), and for more robustness when negotiating features on connect, + * the bitfield-based representation was replaced by a string-based representation in 0.13, support for + * which is indicated by having the ExtendedFeatures flag set. Extended features are defined in the Feature + * enum. + * + * @warning Do not alter this enum; new features must be added (only) to the @a Feature enum. * - * This list should be cleaned up after every protocol break, as we can assume them to be present then. + * @sa Feature */ - enum Feature { + enum class LegacyFeature : quint32 { SynchronizedMarkerLine = 0x0001, - SaslAuthentication = 0x0002, - SaslExternal = 0x0004, - HideInactiveNetworks = 0x0008, - PasswordChange = 0x0010, - CapNegotiation = 0x0020, /// IRCv3 capability negotiation, account tracking - VerifyServerSSL = 0x0040, /// IRC server SSL validation - CustomRateLimits = 0x0080, /// IRC server custom message rate limits - DccFileTransfer = 0x0100, /// DCC file transfer support (forcefully disabled for now) - AwayFormatTimestamp = 0x0200, /// Timestamp formatting in away (e.g. %%hh:mm%%) - Authenticators = 0x0400, /// Whether or not the core supports auth backends. - BufferActivitySync = 0x0800, /// Sync buffer activity status - CoreSideHighlights = 0x1000, /// Core-Side highlight configuration and matching - SenderPrefixes = 0x2000, /// Show prefixes for senders in backlog - RemoteDisconnect = 0x4000, /// Allow this peer to be remotely disconnected - - NumFeatures = 0x4000 + SaslAuthentication = 0x0002, + SaslExternal = 0x0004, + HideInactiveNetworks = 0x0008, + PasswordChange = 0x0010, + CapNegotiation = 0x0020, + VerifyServerSSL = 0x0040, + CustomRateLimits = 0x0080, + // DccFileTransfer = 0x0100, // never in use + AwayFormatTimestamp = 0x0200, + Authenticators = 0x0400, + BufferActivitySync = 0x0800, + CoreSideHighlights = 0x1000, + SenderPrefixes = 0x2000, + RemoteDisconnect = 0x4000, + ExtendedFeatures = 0x8000, }; - Q_DECLARE_FLAGS(Features, Feature) + Q_FLAGS(LegacyFeature) + Q_DECLARE_FLAGS(LegacyFeatures, LegacyFeature) - //! The features the current version of Quassel supports (\sa Feature) - /** \return An ORed list of all enum values in Feature + /** + * A list of features that are optional in core and/or client, but need runtime checking. + * + * Some features require an uptodate counterpart, but don't justify a protocol break. + * This is what we use this enum for. Add such features to it and check at runtime on the other + * side for their existence. + * + * For feature negotiation, these enum values are serialized as strings, so order does not matter. However, + * do not rename existing enum values to avoid breaking compatibility. + * + * This list should be cleaned up after every protocol break, as we can assume them to be present then. */ - static Features features(); + enum class Feature : quint32 { + SynchronizedMarkerLine, + SaslAuthentication, + SaslExternal, + HideInactiveNetworks, + PasswordChange, ///< Remote password change + CapNegotiation, ///< IRCv3 capability negotiation, account tracking + VerifyServerSSL, ///< IRC server SSL validation + CustomRateLimits, ///< IRC server custom message rate limits + AwayFormatTimestamp, ///< Timestamp formatting in away (e.g. %%hh:mm%%) + Authenticators, ///< Whether or not the core supports auth backends + BufferActivitySync, ///< Sync buffer activity status + CoreSideHighlights, ///< Core-Side highlight configuration and matching + SenderPrefixes, ///< Show prefixes for senders in backlog + RemoteDisconnect, ///< Allow this peer to be remotely disconnected + ExtendedFeatures, ///< Extended features + }; + Q_ENUMS(Feature) + + class Features; static Quassel *instance(); @@ -217,5 +255,72 @@ private: std::vector _quitHandlers; }; +// -------------------------------------------------------------------------------------------------------------------- + +/** + * Class representing a set of supported core/client features. + * + * @sa Quassel::Feature + */ +class Quassel::Features +{ +public: + /** + * Default constructor. + * + * Creates a Feature instance with all known features (i.e., all values declared in the Quassel::Feature enum) set. + * This is useful for easily creating a Feature instance that represent the current version's capabilities. + */ + Features(); + + /** + * Constructs a Feature instance holding the given list of features. + * + * Both the @a features and the @a legacyFeatures arguments are considered (additively). + * This is useful when receiving a list of features from another peer. + * + * @param features A list of strings matching values in the Quassel::Feature enum. Strings that don't match are + * can be accessed after construction via unknownFeatures(), but are otherwise ignored. + * @param legacyFeatures Holds a bit-wise combination of LegacyFeature flag values, which are each added to the list of + * features represented by this Features instance. + */ + Features(const QStringList &features, LegacyFeatures legacyFeatures); -Q_DECLARE_OPERATORS_FOR_FLAGS(Quassel::Features); + /** + * Check if a given feature is marked as enabled in this Features instance. + * + * @param feature The feature to be queried + * @returns Whether the given feature is marked as enabled + */ + bool isEnabled(Feature feature) const; + + /** + * Provides a list of all features marked as either enabled or disabled (as indicated by the @a enabled argument) as strings. + * + * @param enabled Whether to return the enabled or the disabled features + * @return A string list containing all enabled or disabled features + */ + QStringList toStringList(bool enabled = true) const; + + /** + * Provides a list of all enabled legacy features (i.e. features defined prior to v0.13) as bit-wise combination in a + * LegacyFeatures type. + * + * @note Extended features cannot be represented this way, and are thus ignored even if set. + * @return A LegacyFeatures type holding the bit-wise combination of all legacy features enabled in this Features instance + */ + LegacyFeatures toLegacyFeatures() const; + + /** + * Provides the list of strings that could not be mapped to Quassel::Feature enum values on construction. + * + * Useful for debugging/logging purposes. + * + * @returns A list of strings that could not be mapped to the Feature enum on construction of this Features instance, if any + */ + QStringList unknownFeatures() const; + +private: + std::vector _features; + QStringList _unknownFeatures; +}; diff --git a/src/common/signalproxy.cpp b/src/common/signalproxy.cpp index 1d8c956d..334bfa13 100644 --- a/src/common/signalproxy.cpp +++ b/src/common/signalproxy.cpp @@ -829,7 +829,7 @@ void SignalProxy::updateSecureState() QVariantList SignalProxy::peerData() { QVariantList result; - for (auto peer : _peerMap.values()) { + for (auto &&peer : _peerMap.values()) { QVariantMap data; data["id"] = peer->id(); data["clientVersion"] = peer->clientVersion(); @@ -839,8 +839,8 @@ QVariantList SignalProxy::peerData() { data["remoteAddress"] = peer->address(); data["connectedSince"] = peer->connectedSince(); data["secure"] = peer->isSecure(); - int features = peer->features(); - data["features"] = features; + data["features"] = static_cast(peer->features().toLegacyFeatures()); + data["featureList"] = peer->features().toStringList(); result << data; } return result; diff --git a/src/core/SQL/upgradeSchema.sh b/src/core/SQL/upgradeSchema.sh index 57792e02..22bbff5e 100755 --- a/src/core/SQL/upgradeSchema.sh +++ b/src/core/SQL/upgradeSchema.sh @@ -89,7 +89,7 @@ # # Newer clients need to detect when they're on an older core to disable the # feature. Use 'enum Feature' in 'quassel.h'. In client-side code, test with -# 'if (Client::coreFeatures() & Quassel::FeatureName) { ... }' +# 'if (Client::isCoreFeatureEnabled(Quassel::Feature::FeatureName)) { ... }' # # 9. Test everything! Upgrade, migrate, new setups, new client/old core, # old client/new core, etc. diff --git a/src/core/coreapplication.cpp b/src/core/coreapplication.cpp index 1a689a22..0806a2a8 100644 --- a/src/core/coreapplication.cpp +++ b/src/core/coreapplication.cpp @@ -84,6 +84,7 @@ CoreApplication::CoreApplication(int &argc, char **argv) CoreApplication::~CoreApplication() { delete _internal; + Quassel::destroy(); } diff --git a/src/core/coreauthhandler.cpp b/src/core/coreauthhandler.cpp index c01115f4..a05fa95b 100644 --- a/src/core/coreauthhandler.cpp +++ b/src/core/coreauthhandler.cpp @@ -171,22 +171,23 @@ void CoreAuthHandler::handle(const RegisterClient &msg) return; } + _peer->setFeatures(std::move(msg.features)); + _peer->setBuildDate(msg.buildDate); + _peer->setClientVersion(msg.clientVersion); + QVariantList backends; QVariantList authenticators; bool configured = Core::isConfigured(); if (!configured) { backends = Core::backendInfo(); - authenticators = Core::authenticatorInfo(); + if (_peer->hasFeature(Quassel::Feature::Authenticators)) { + authenticators = Core::authenticatorInfo(); + } } - // useSsl is only used for the legacy protocol - // XXX: FIXME: use client features here: we cannot pass authenticators if the client is too old! - _peer->dispatch(ClientRegistered(Quassel::features(), configured, backends, useSsl, authenticators)); - - _peer->setBuildDate(msg.buildDate); - _peer->setClientVersion(msg.clientVersion); - _peer->setFeatures(Quassel::Features(msg.clientFeatures)); + _peer->dispatch(ClientRegistered(Quassel::Features{}, configured, backends, authenticators, useSsl)); + // useSsl is only used for the legacy protocol if (_legacy && useSsl) startSsl(); @@ -236,6 +237,15 @@ void CoreAuthHandler::handle(const Login &msg) quInfo() << qPrintable(tr("Client %1 initialized and authenticated successfully as \"%2\" (UserId: %3).").arg(socket()->peerAddress().toString(), msg.user, QString::number(uid.toInt()))); + const auto &clientFeatures = _peer->features(); + auto unsupported = clientFeatures.toStringList(false); + if (!unsupported.isEmpty()) { + quInfo() << qPrintable(tr("Client does not support the following features: %1").arg(unsupported.join(", "))); + } + if (!clientFeatures.unknownFeatures().isEmpty()) { + quInfo() << qPrintable(tr("Client supports unknown features: %1").arg(clientFeatures.unknownFeatures().join(", "))); + } + disconnect(socket(), 0, this, 0); disconnect(_peer, 0, this, 0); _peer->setParent(0); // Core needs to take care of this one now! diff --git a/src/qtui/coreconfigwizard.cpp b/src/qtui/coreconfigwizard.cpp index a9453fac..4b8c7c50 100644 --- a/src/qtui/coreconfigwizard.cpp +++ b/src/qtui/coreconfigwizard.cpp @@ -188,7 +188,7 @@ void CoreConfigWizard::prepareCoreSetup(const QString &backend, const QVariantMa // FIXME? We need to be able to set up older cores that don't have auth backend support. // So if the core doesn't support that feature, don't pass those parameters. - if (!(Client::coreFeatures() & Quassel::Authenticators)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::Authenticators)) { coreConnection()->setupCore(Protocol::SetupData(field("adminUser.user").toString(), field("adminUser.password").toString(), backend, properties)); } else { @@ -267,7 +267,7 @@ AdminUserPage::AdminUserPage(QWidget *parent) : QWizardPage(parent) int AdminUserPage::nextId() const { // If the core doesn't support auth backends, skip that page! - if (!(Client::coreFeatures() & Quassel::Authenticators)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::Authenticators)) { return CoreConfigWizard::StorageSelectionPage; } else { diff --git a/src/qtui/coresessionwidget.cpp b/src/qtui/coresessionwidget.cpp index f3f987db..19902363 100644 --- a/src/qtui/coresessionwidget.cpp +++ b/src/qtui/coresessionwidget.cpp @@ -44,8 +44,8 @@ void CoreSessionWidget::setData(QMap map) } ui.labelSecure->setText(map["secure"].toBool() ? tr("Yes") : tr("No")); - auto features = Quassel::Features(map["features"].toInt()); - ui.disconnectButton->setVisible(features.testFlag(Quassel::Feature::RemoteDisconnect)); + auto features = Quassel::Features{map["featureList"].toStringList(), static_cast(map["features"].toUInt())}; + ui.disconnectButton->setVisible(features.isEnabled(Quassel::Feature::RemoteDisconnect)); bool success = false; _peerId = map["id"].toInt(&success); diff --git a/src/qtui/mainwin.cpp b/src/qtui/mainwin.cpp index 923a3849..d41f087b 100644 --- a/src/qtui/mainwin.cpp +++ b/src/qtui/mainwin.cpp @@ -774,7 +774,7 @@ void MainWin::changeActiveBufferView(int bufferViewId) void MainWin::showPasswordChangeDlg() { - if((Client::coreFeatures() & Quassel::PasswordChange)) { + if(Client::isCoreFeatureEnabled(Quassel::Feature::PasswordChange)) { PasswordChangeDlg dlg(this); dlg.exec(); } diff --git a/src/qtui/qtuiapplication.cpp b/src/qtui/qtuiapplication.cpp index bd7ec7ef..77f1ac8f 100644 --- a/src/qtui/qtuiapplication.cpp +++ b/src/qtui/qtuiapplication.cpp @@ -189,6 +189,7 @@ bool QtUiApplication::init() QtUiApplication::~QtUiApplication() { Client::destroy(); + Quassel::destroy(); } diff --git a/src/qtui/qtuimessageprocessor.cpp b/src/qtui/qtuimessageprocessor.cpp index de35336a..40d9c284 100644 --- a/src/qtui/qtuimessageprocessor.cpp +++ b/src/qtui/qtuimessageprocessor.cpp @@ -57,7 +57,7 @@ void QtUiMessageProcessor::reset() void QtUiMessageProcessor::process(Message &msg) { - if (!Client::coreFeatures().testFlag(Quassel::Feature::CoreSideHighlights)) + if (!Client::isCoreFeatureEnabled(Quassel::Feature::CoreSideHighlights)) checkForHighlight(msg); preProcess(msg); Client::messageModel()->insertMessage(msg); @@ -69,7 +69,7 @@ void QtUiMessageProcessor::process(QList &msgs) QList::iterator msgIter = msgs.begin(); QList::iterator msgIterEnd = msgs.end(); while (msgIter != msgIterEnd) { - if (!Client::coreFeatures().testFlag(Quassel::Feature::CoreSideHighlights)) + if (!Client::isCoreFeatureEnabled(Quassel::Feature::CoreSideHighlights)) checkForHighlight(*msgIter); preProcess(*msgIter); ++msgIter; diff --git a/src/qtui/settingspages/bufferviewsettingspage.cpp b/src/qtui/settingspages/bufferviewsettingspage.cpp index 4bfddc4e..235f48e0 100644 --- a/src/qtui/settingspages/bufferviewsettingspage.cpp +++ b/src/qtui/settingspages/bufferviewsettingspage.cpp @@ -40,7 +40,7 @@ BufferViewSettingsPage::BufferViewSettingsPage(QWidget *parent) { ui.setupUi(this); //Hide the hide inactive networks feature on older cores (which won't save the setting) - if (!(Client::coreFeatures() & Quassel::HideInactiveNetworks)) + if (!Client::isCoreFeatureEnabled(Quassel::Feature::HideInactiveNetworks)) ui.hideInactiveNetworks->hide(); ui.renameBufferView->setIcon(QIcon::fromTheme("edit-rename")); diff --git a/src/qtui/settingspages/chatviewsettingspage.cpp b/src/qtui/settingspages/chatviewsettingspage.cpp index 766bcd40..7d3d964f 100644 --- a/src/qtui/settingspages/chatviewsettingspage.cpp +++ b/src/qtui/settingspages/chatviewsettingspage.cpp @@ -34,12 +34,12 @@ ChatViewSettingsPage::ChatViewSettingsPage(QWidget *parent) #endif // FIXME remove with protocol v11 - if (!Client::coreFeatures().testFlag(Quassel::SynchronizedMarkerLine)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) { ui.autoMarkerLine->setEnabled(false); ui.autoMarkerLine->setChecked(true); ui.autoMarkerLine->setToolTip(tr("You need at least version 0.6 of Quassel Core to use this feature")); } - if (!Client::coreFeatures().testFlag(Quassel::Feature::CoreSideHighlights)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::CoreSideHighlights)) { ui.showSenderPrefixes->setEnabled(false); ui.showSenderPrefixes->setToolTip(tr("You need at least version 0.13 of Quassel Core to use this feature")); } diff --git a/src/qtui/settingspages/identityeditwidget.cpp b/src/qtui/settingspages/identityeditwidget.cpp index 9085ae90..9b4c0ba7 100644 --- a/src/qtui/settingspages/identityeditwidget.cpp +++ b/src/qtui/settingspages/identityeditwidget.cpp @@ -86,7 +86,7 @@ IdentityEditWidget::IdentityEditWidget(QWidget *parent) ui.sslCertGroupBox->installEventFilter(this); #endif - if (Client::coreFeatures() & Quassel::AwayFormatTimestamp) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::AwayFormatTimestamp)) { // Core allows formatting %%timestamp%% messages in away strings. Update tooltips. QString strFormatTooltip; QTextStream formatTooltip( &strFormatTooltip, QIODevice::WriteOnly ); diff --git a/src/qtui/settingspages/networkssettingspage.cpp b/src/qtui/settingspages/networkssettingspage.cpp index cc8dda2a..6bb068d2 100644 --- a/src/qtui/settingspages/networkssettingspage.cpp +++ b/src/qtui/settingspages/networkssettingspage.cpp @@ -46,9 +46,9 @@ NetworksSettingsPage::NetworksSettingsPage(QWidget *parent) ui.setupUi(this); // hide SASL options for older cores - if (!(Client::coreFeatures() & Quassel::SaslAuthentication)) + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SaslAuthentication)) ui.sasl->hide(); - if (!(Client::coreFeatures() & Quassel::SaslExternal)) + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SaslExternal)) ui.saslExtInfo->hide(); #ifndef HAVE_SSL ui.saslExtInfo->hide(); @@ -176,7 +176,7 @@ void NetworksSettingsPage::load() reset(); // Handle UI dependent on core feature flags here - if (Client::coreFeatures() & Quassel::CustomRateLimits) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::CustomRateLimits)) { // Custom rate limiting supported, allow toggling ui.useCustomMessageRate->setEnabled(true); // Reset tooltip to default. @@ -358,7 +358,7 @@ void NetworksSettingsPage::setItemState(NetworkId id, QListWidgetItem *item) void NetworksSettingsPage::setNetworkCapStates(NetworkId id) { const Network *net = Client::network(id); - if ((Client::coreFeatures() & Quassel::CapNegotiation) && net) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::CapNegotiation) && net) { // Capability negotiation is supported, network exists. // Check if the network is connected. Don't use net->isConnected() as that won't be true // during capability negotiation when capabilities are added and removed. @@ -575,7 +575,7 @@ void NetworksSettingsPage::displayNetwork(NetworkId id) #ifdef HAVE_SSL // this is only needed when the core supports SASL EXTERNAL - if (Client::coreFeatures() & Quassel::SaslExternal) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::SaslExternal)) { if (_cid) { disconnect(_cid, SIGNAL(sslSettingsUpdated()), this, SLOT(sslUpdated())); delete _cid; @@ -1021,7 +1021,7 @@ NetworkAddDlg::NetworkAddDlg(const QStringList &exist, QWidget *parent) : QDialo // Do NOT call updateSslPort when loading settings, otherwise port settings may be overriden. // If useSSL is later changed to be checked by default, change port's default value, too. - if (Client::coreFeatures() & Quassel::VerifyServerSSL) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::VerifyServerSSL)) { // Synchronize requiring SSL with the use SSL checkbox ui.sslVerify->setEnabled(ui.useSSL->isChecked()); connect(ui.useSSL, SIGNAL(toggled(bool)), ui.sslVerify, SLOT(setEnabled(bool))); @@ -1168,7 +1168,7 @@ ServerEditDlg::ServerEditDlg(const Network::Server &server, QWidget *parent) : Q // Do NOT call updateSslPort when loading settings, otherwise port settings may be overriden. // If useSSL is later changed to be checked by default, change port's default value, too. - if (Client::coreFeatures() & Quassel::VerifyServerSSL) { + if (Client::isCoreFeatureEnabled(Quassel::Feature::VerifyServerSSL)) { // Synchronize requiring SSL with the use SSL checkbox ui.sslVerify->setEnabled(ui.useSSL->isChecked()); connect(ui.useSSL, SIGNAL(toggled(bool)), ui.sslVerify, SLOT(setEnabled(bool)));