cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / qtui / settingspages / networkssettingspage.cpp
index 468f2b5..dfd1f62 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2020 by the Quassel Project                        *
+ *   Copyright (C) 2005-2022 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -67,6 +67,7 @@ NetworksSettingsPage::NetworksSettingsPage(QWidget* parent)
     disconnectedIcon = icon::get("network-disconnect");
 
     // Status icons
+    infoIcon = icon::get({"emblem-information", "dialog-information"});
     successIcon = icon::get({"emblem-success", "dialog-information"});
     unavailableIcon = icon::get({"emblem-unavailable", "dialog-warning"});
     questionIcon = icon::get({"emblem-question", "dialog-question", "dialog-information"});
@@ -90,7 +91,8 @@ NetworksSettingsPage::NetworksSettingsPage(QWidget* parent)
                                     ui.sendEncoding,         ui.recvEncoding,         ui.serverEncoding,
                                     ui.autoReconnect,        ui.reconnectInterval,    ui.reconnectRetries,
                                     ui.unlimitedRetries,     ui.rejoinOnReconnect,    ui.useCustomMessageRate,
-                                    ui.messageRateBurstSize, ui.messageRateDelay,     ui.unlimitedMessageRate},
+                                    ui.messageRateBurstSize, ui.messageRateDelay,     ui.unlimitedMessageRate,
+                                    ui.enableCapServerTime},
                                    this,
                                    &NetworksSettingsPage::widgetHasChanged);
 
@@ -178,6 +180,20 @@ void NetworksSettingsPage::load()
                                                         "modify message rate limits.")));
     }
 
+    if (!Client::isConnected() || Client::isCoreFeatureEnabled(Quassel::Feature::SkipIrcCaps)) {
+        // Either disconnected or IRCv3 capability skippping supported, enable configuration and
+        // hide warning.  Don't show the warning needlessly when disconnected.
+        ui.enableCapsConfigWidget->setEnabled(true);
+        ui.enableCapsStatusLabel->setText(tr("These features require support from the network"));
+        ui.enableCapsStatusIcon->setPixmap(infoIcon.pixmap(16));
+    }
+    else {
+        // Core does not IRCv3 capability skipping, show warning and disable configuration
+        ui.enableCapsConfigWidget->setEnabled(false);
+        ui.enableCapsStatusLabel->setText(tr("Your Quassel core is too old to configure IRCv3 features"));
+        ui.enableCapsStatusIcon->setPixmap(unavailableIcon.pixmap(16));
+    }
+
     // Hide the SASL EXTERNAL notice until a network's shown.  Stops it from showing while loading
     // backlog from the core.
     sslUpdated();
@@ -342,7 +358,7 @@ void NetworksSettingsPage::setItemState(NetworkId id, QListWidgetItem* item)
 
 void NetworksSettingsPage::resetNetworkCapStates()
 {
-    // Set the status to a blank (invalid) network ID, reseting all UI
+    // Set the status to a blank (invalid) network ID, resetting all UI
     setNetworkCapStates(NetworkId());
 }
 
@@ -356,11 +372,15 @@ void NetworksSettingsPage::setNetworkCapStates(NetworkId id)
         if (net->connectionState() != Network::Disconnected) {
             // Network exists and is connected, check available capabilities...
             // [SASL]
-            if (net->saslMaybeSupports(IrcCap::SaslMech::PLAIN)) {
-                setSASLStatus(CapSupportStatus::MaybeSupported);
+            // Quassel switches between SASL PLAIN and SASL EXTERNAL based on the existence of an
+            // SSL certificate on the identity - check EXTERNAL if CertID exists, PLAIN if not
+            bool usingSASLExternal = displayedNetworkHasCertId();
+            if ((usingSASLExternal && net->saslMaybeSupports(IrcCap::SaslMech::EXTERNAL))
+                    || (!usingSASLExternal && net->saslMaybeSupports(IrcCap::SaslMech::PLAIN))) {
+                setCapSASLStatus(CapSupportStatus::MaybeSupported, usingSASLExternal);
             }
             else {
-                setSASLStatus(CapSupportStatus::MaybeUnsupported);
+                setCapSASLStatus(CapSupportStatus::MaybeUnsupported, usingSASLExternal);
             }
 
             // Add additional capability-dependent interface updates here
@@ -368,7 +388,7 @@ void NetworksSettingsPage::setNetworkCapStates(NetworkId id)
         else {
             // Network is disconnected
             // [SASL]
-            setSASLStatus(CapSupportStatus::Disconnected);
+            setCapSASLStatus(CapSupportStatus::Disconnected);
 
             // Add additional capability-dependent interface updates here
         }
@@ -377,7 +397,7 @@ void NetworksSettingsPage::setNetworkCapStates(NetworkId id)
         // Capability negotiation is not supported and/or network doesn't exist.
         // Don't assume anything and reset all capability-dependent interface elements to neutral.
         // [SASL]
-        setSASLStatus(CapSupportStatus::Unknown);
+        setCapSASLStatus(CapSupportStatus::Unknown);
 
         // Add additional capability-dependent interface updates here
     }
@@ -574,12 +594,21 @@ void NetworksSettingsPage::displayNetwork(NetworkId id)
         // this is only needed when the core supports SASL EXTERNAL
         if (Client::isCoreFeatureEnabled(Quassel::Feature::SaslExternal)) {
             if (_cid) {
+                // Clean up existing CertIdentity
                 disconnect(_cid, &CertIdentity::sslSettingsUpdated, this, &NetworksSettingsPage::sslUpdated);
                 delete _cid;
+                _cid = nullptr;
+            }
+            auto *identity = Client::identity(info.identity);
+            if (identity) {
+                // Connect new CertIdentity
+                _cid = new CertIdentity(*identity, this);
+                _cid->enableEditSsl(true);
+                connect(_cid, &CertIdentity::sslSettingsUpdated, this, &NetworksSettingsPage::sslUpdated);
+            }
+            else {
+                qWarning() << "NetworksSettingsPage::displayNetwork can't find Identity for IdentityId:" << info.identity;
             }
-            _cid = new CertIdentity(*Client::identity(info.identity), this);
-            _cid->enableEditSsl(true);
-            connect(_cid, &CertIdentity::sslSettingsUpdated, this, &NetworksSettingsPage::sslUpdated);
         }
 
         ui.identityList->setCurrentIndex(ui.identityList->findData(info.identity.toInt()));
@@ -626,6 +655,8 @@ void NetworksSettingsPage::displayNetwork(NetworkId id)
         ui.messageRateBurstSize->setValue(info.messageRateBurstSize);
         // Convert milliseconds (integer) into seconds (double)
         ui.messageRateDelay->setValue(info.messageRateDelay / 1000.0f);
+        // Skipped IRCv3 capabilities
+        ui.enableCapServerTime->setChecked(!info.skipCaps.contains(IrcCap::SERVER_TIME));
     }
     else {
         // just clear widgets
@@ -678,6 +709,14 @@ void NetworksSettingsPage::saveToNetworkInfo(NetworkInfo& info)
     // Convert seconds (double) into milliseconds (integer)
     info.messageRateDelay = static_cast<quint32>((ui.messageRateDelay->value() * 1000));
     info.unlimitedMessageRate = ui.unlimitedMessageRate->isChecked();
+    // Skipped IRCv3 capabilities
+    if (ui.enableCapServerTime->isChecked()) {
+        // Capability enabled, remove it from the skip list
+        info.skipCaps.removeAll(IrcCap::SERVER_TIME);
+    } else if (!info.skipCaps.contains(IrcCap::SERVER_TIME)) {
+        // Capability disabled and not in the skip list, add it
+        info.skipCaps.append(IrcCap::SERVER_TIME);
+    }
 }
 
 void NetworksSettingsPage::clientNetworkCapsUpdated()
@@ -695,11 +734,12 @@ void NetworksSettingsPage::clientNetworkCapsUpdated()
     }
 }
 
-void NetworksSettingsPage::setSASLStatus(const CapSupportStatus saslStatus)
+void NetworksSettingsPage::setCapSASLStatus(const CapSupportStatus saslStatus, bool usingSASLExternal)
 {
-    if (_saslStatusSelected != saslStatus) {
+    if (_capSaslStatusSelected != saslStatus || _capSaslStatusUsingExternal != usingSASLExternal) {
         // Update the cached copy of SASL status used with the Details dialog
-        _saslStatusSelected = saslStatus;
+        _capSaslStatusSelected = saslStatus;
+        _capSaslStatusUsingExternal = usingSASLExternal;
 
         // Update the user interface
         switch (saslStatus) {
@@ -715,14 +755,23 @@ void NetworksSettingsPage::setSASLStatus(const CapSupportStatus saslStatus)
             ui.saslStatusIcon->setPixmap(questionIcon.pixmap(16));
             break;
         case CapSupportStatus::MaybeUnsupported:
-            // The network doesn't advertise support for SASL PLAIN.  Here be dragons.
+            // The network doesn't advertise support for SASL PLAIN/EXTERNAL.  Here be dragons.
             ui.saslStatusLabel->setText(QString("<i>%1</i>").arg(tr("Not currently supported by network")));
             ui.saslStatusIcon->setPixmap(unavailableIcon.pixmap(16));
             break;
         case CapSupportStatus::MaybeSupported:
-            // The network advertises support for SASL PLAIN.  Encourage using it!
+            // The network advertises support for SASL PLAIN/EXTERNAL.  Encourage using it!
             // Unfortunately we don't know for sure if it's desired or functional.
-            ui.saslStatusLabel->setText(QString("<i>%1</i>").arg(tr("Supported by network")));
+            if (usingSASLExternal) {
+                // SASL EXTERNAL is used
+                // With SASL v3.1, it's not possible to reliably tell if SASL EXTERNAL is supported,
+                // or just SASL PLAIN.  Use less assertive phrasing.
+                ui.saslStatusLabel->setText(QString("<i>%1</i>").arg(tr("May be supported by network")));
+            }
+            else {
+                // SASL PLAIN is used
+                ui.saslStatusLabel->setText(QString("<i>%1</i>").arg(tr("Supported by network")));
+            }
             ui.saslStatusIcon->setPixmap(successIcon.pixmap(16));
             break;
         }
@@ -731,25 +780,29 @@ void NetworksSettingsPage::setSASLStatus(const CapSupportStatus saslStatus)
 
 void NetworksSettingsPage::sslUpdated()
 {
-    if (_cid && !_cid->sslKey().isNull()) {
-        ui.saslContents->setDisabled(true);
+    if (displayedNetworkHasCertId()) {
+        ui.saslPlainContents->setDisabled(true);
         ui.saslExtInfo->setHidden(false);
     }
     else {
-        ui.saslContents->setDisabled(false);
+        ui.saslPlainContents->setDisabled(false);
         // Directly re-enabling causes the widgets to ignore the parent "Use SASL Authentication"
         // state to indicate whether or not it's disabled.  To workaround this, keep track of
         // whether or not "Use SASL Authentication" is enabled, then quickly uncheck/recheck the
         // group box.
         if (!ui.sasl->isChecked()) {
-            // SASL is not enabled, uncheck/recheck the group box to re-disable saslContents.
-            // Leaving saslContents disabled doesn't work as that prevents it from re-enabling if
+            // SASL is not enabled, uncheck/recheck the group box to re-disable saslPlainContents.
+            // Leaving saslPlainContents disabled doesn't work as that prevents it from re-enabling if
             // sasl is later checked.
             ui.sasl->setChecked(true);
             ui.sasl->setChecked(false);
         }
         ui.saslExtInfo->setHidden(true);
     }
+    // Update whether SASL PLAIN or SASL EXTERNAL is used to detect SASL status
+    if (currentId != 0) {
+        setNetworkCapStates(currentId);
+    }
 }
 
 /*** Network list ***/
@@ -933,7 +986,7 @@ void NetworksSettingsPage::on_saslStatusDetails_clicked()
         bool useWarningIcon = false;
 
         // Determine which explanation to show
-        switch (_saslStatusSelected) {
+        switch (_capSaslStatusSelected) {
         case CapSupportStatus::Unknown:
             saslStatusHeader = tr("Could not check if SASL supported by network");
             saslStatusExplanation = tr("Quassel could not check if \"%1\" supports SASL.  This may "
@@ -949,17 +1002,41 @@ void NetworksSettingsPage::on_saslStatusDetails_clicked()
                                         .arg(netName);
             break;
         case CapSupportStatus::MaybeUnsupported:
-            saslStatusHeader = tr("SASL not currently supported by network");
-            saslStatusExplanation = tr("The network \"%1\" does not currently support SASL.  "
-                                       "However, support might be added later on.")
-                                        .arg(netName);
+            if (displayedNetworkHasCertId()) {
+                // SASL EXTERNAL is used
+                saslStatusHeader = tr("SASL EXTERNAL not currently supported by network");
+                saslStatusExplanation = tr("The network \"%1\" does not currently support SASL "
+                                           "EXTERNAL for SSL certificate authentication.  However, "
+                                           "support might be added later on.")
+                                           .arg(netName);
+            }
+            else {
+                // SASL PLAIN is used
+                saslStatusHeader = tr("SASL not currently supported by network");
+                saslStatusExplanation = tr("The network \"%1\" does not currently support SASL.  "
+                                           "However, support might be added later on.")
+                                           .arg(netName);
+            }
             useWarningIcon = true;
             break;
         case CapSupportStatus::MaybeSupported:
-            saslStatusHeader = tr("SASL supported by network");
-            saslStatusExplanation = tr("The network \"%1\" supports SASL.  In most cases, you "
-                                       "should use SASL instead of NickServ identification.")
-                                        .arg(netName);
+            if (displayedNetworkHasCertId()) {
+                // SASL EXTERNAL is used
+                // With SASL v3.1, it's not possible to reliably tell if SASL EXTERNAL is supported,
+                // or just SASL PLAIN.  Caution about this in the details dialog.
+                saslStatusHeader = tr("SASL EXTERNAL may be supported by network");
+                saslStatusExplanation = tr("The network \"%1\" may support SASL EXTERNAL for SSL "
+                                           "certificate authentication.  In most cases, you should "
+                                           "use SASL instead of NickServ identification.")
+                                            .arg(netName);
+            }
+            else {
+                // SASL PLAIN is used
+                saslStatusHeader = tr("SASL supported by network");
+                saslStatusExplanation = tr("The network \"%1\" supports SASL.  In most cases, you "
+                                           "should use SASL instead of NickServ identification.")
+                                            .arg(netName);
+            }
             break;
         }
 
@@ -982,6 +1059,73 @@ void NetworksSettingsPage::on_saslStatusDetails_clicked()
     }
 }
 
+void NetworksSettingsPage::on_enableCapsStatusDetails_clicked()
+{
+    if (!Client::isConnected() || Client::isCoreFeatureEnabled(Quassel::Feature::SkipIrcCaps)) {
+        // Either disconnected or IRCv3 capability skippping supported
+
+        // Try to get a list of currently enabled features
+        QStringList sortedCapsEnabled;
+        // Check if a network is selected
+        if (ui.networkList->selectedItems().count()) {
+            // Get the underlying Network from the selected network
+            NetworkId netid = ui.networkList->selectedItems()[0]->data(Qt::UserRole).value<NetworkId>();
+            const Network* net = Client::network(netid);
+            if (net && Client::isCoreFeatureEnabled(Quassel::Feature::CapNegotiation)) {
+                // Capability negotiation is supported, network exists.
+                // If the network is disconnected, the list of enabled capabilities will be empty,
+                // no need to check for that specifically.
+                // Sorting isn't required, but it looks nicer.
+                sortedCapsEnabled = net->capsEnabled();
+                sortedCapsEnabled.sort();
+            }
+        }
+
+        // Try to explain IRCv3 network features in a friendly way, including showing the currently
+        // enabled features if available
+        auto messageText = QString("<p>%1</p></br><p>%2</p>")
+                .arg(tr("Quassel makes use of newer IRC features when supported by the IRC network."
+                        "  If desired, you can disable unwanted or problematic features here."),
+                     tr("The <a href=\"https://ircv3.net/irc/\">IRCv3 website</a> provides more "
+                        "technical details on the IRCv3 capabilities powering these features."));
+
+        if (!sortedCapsEnabled.isEmpty()) {
+            // Format the capabilities within <code></code> blocks
+            auto formattedCaps = QString("<code>%1</code>")
+                    .arg(sortedCapsEnabled.join("</code>, <code>"));
+
+            // Add the currently enabled capabilities to the list
+            // This creates a new QString, but this code is not performance-critical.
+            messageText = messageText.append(QString("<p><i>%1</i></p>").arg(
+                                                 tr("Currently enabled IRCv3 capabilities for this "
+                                                    "network: %1").arg(formattedCaps)));
+        }
+
+        QMessageBox::information(this, tr("Configuring network features"), messageText);
+    }
+    else {
+        // Core does not IRCv3 capability skipping, show warning
+        QMessageBox::warning(this, tr("Configuring network features unsupported"),
+                             QString("<p><b>%1</b></p></br><p>%2</p>")
+                             .arg(tr("Your Quassel core is too old to configure IRCv3 network features"),
+                                  tr("You need a Quassel core v0.14.0 or newer to control what network "
+                                     "features Quassel will use.")));
+    }
+}
+
+void NetworksSettingsPage::on_enableCapsAdvanced_clicked()
+{
+    if (currentId == 0)
+        return;
+
+    CapsEditDlg dlg(networkInfos[currentId].skipCapsToString(), this);
+    if (dlg.exec() == QDialog::Accepted) {
+        networkInfos[currentId].skipCapsFromString(dlg.skipCapsString());
+        displayNetwork(currentId);
+        widgetHasChanged();
+    }
+}
+
 IdentityId NetworksSettingsPage::defaultIdentity() const
 {
     IdentityId defaultId = 0;
@@ -993,6 +1137,12 @@ IdentityId NetworksSettingsPage::defaultIdentity() const
     return defaultId;
 }
 
+bool NetworksSettingsPage::displayedNetworkHasCertId() const
+{
+    // Check if the CertIdentity exists and that it has a non-null SSL key set
+    return (_cid && !_cid->sslKey().isNull());
+}
+
 /**************************************************************************
  * NetworkAddDlg
  *************************************************************************/
@@ -1006,7 +1156,7 @@ NetworkAddDlg::NetworkAddDlg(QStringList exist, QWidget* parent)
 
     // Whenever useSSL is toggled, update the port number if not changed from the default
     connect(ui.useSSL, &QAbstractButton::toggled, this, &NetworkAddDlg::updateSslPort);
-    // Do NOT call updateSslPort when loading settings, otherwise port settings may be overriden.
+    // Do NOT call updateSslPort when loading settings, otherwise port settings may be overridden.
     // If useSSL is later changed to be checked by default, change port's default value, too.
 
     if (Client::isCoreFeatureEnabled(Quassel::Feature::VerifyServerSSL)) {
@@ -1156,7 +1306,7 @@ ServerEditDlg::ServerEditDlg(const Network::Server& server, QWidget* parent)
 
     // Whenever useSSL is toggled, update the port number if not changed from the default
     connect(ui.useSSL, &QAbstractButton::toggled, this, &ServerEditDlg::updateSslPort);
-    // Do NOT call updateSslPort when loading settings, otherwise port settings may be overriden.
+    // Do NOT call updateSslPort when loading settings, otherwise port settings may be overridden.
     // If useSSL is later changed to be checked by default, change port's default value, too.
 
     if (Client::isCoreFeatureEnabled(Quassel::Feature::VerifyServerSSL)) {
@@ -1213,6 +1363,47 @@ void ServerEditDlg::updateSslPort(bool isChecked)
     }
 }
 
+/**************************************************************************
+ * CapsEditDlg
+ *************************************************************************/
+
+CapsEditDlg::CapsEditDlg(const QString& oldSkipCapsString, QWidget* parent)
+    : QDialog(parent)
+    , oldSkipCapsString(oldSkipCapsString)
+{
+    ui.setupUi(this);
+
+    // Connect to the reset button to reset the text
+    // This provides an explicit way to "get back to defaults" in case someone changes settings to
+    // experiment
+    QPushButton* defaultsButton = ui.buttonBox->button(QDialogButtonBox::RestoreDefaults);
+    connect(defaultsButton, &QPushButton::clicked, this, &CapsEditDlg::defaultSkipCaps);
+
+    if (oldSkipCapsString.isEmpty()) {
+        // Disable Reset button
+        on_skipCapsEdit_textChanged("");
+    }
+    else {
+        ui.skipCapsEdit->setText(oldSkipCapsString);
+    }
+}
+
+
+QString CapsEditDlg::skipCapsString() const
+{
+    return ui.skipCapsEdit->text();
+}
+
+void CapsEditDlg::defaultSkipCaps()
+{
+    ui.skipCapsEdit->setText("");
+}
+
+void CapsEditDlg::on_skipCapsEdit_textChanged(const QString& text)
+{
+    ui.buttonBox->button(QDialogButtonBox::RestoreDefaults)->setDisabled(text.isEmpty());
+}
+
 /**************************************************************************
  * SaveNetworksDlg
  *************************************************************************/