15.1.361. tablet_qt/menulib/serversettingswindow.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/



#include "serversettingswindow.h"

#include <QString>

#include "common/aliases_camcops.h"
#include "common/varconst.h"
#include "core/camcopsapp.h"
#include "lib/convert.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "questionnairelib/commonoptions.h"
#include "questionnairelib/namevalueoptions.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/questionnairefunc.h"
#include "questionnairelib/quhorizontalline.h"
#include "questionnairelib/qulineedit.h"
#include "questionnairelib/qulineeditinteger.h"
#include "questionnairelib/qulineeditint64.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/qutext.h"
#include "widgets/openablewidget.h"

// ============================================================================
// Server configuration
// ============================================================================

ServerSettingsWindow::ServerSettingsWindow(CamcopsApp& app) :
    m_app(app)
{
}


OpenableWidget* ServerSettingsWindow::editor()
{
    // The general options here: have the Questionnaire save directly to the
    // StoredVar but in a way that's not permanent and allows "recall/reload"
    // upon cancel; or temporary local storage with writing to the StoredVar
    // on OK. The latter is a generally better principle. Since C++ lambda
    // functions don't extend the life of things they reference (see "dangling
    // references" in the spec), the best way is by having the CamcopsApp
    // (whose lifespan is guaranteed to be long enough) to do the storage,
    // and having a fieldref to a cached storedvar.
    // A third option is to use a database transaction (and forgo or invalidate
    // any copies of storedvars maintained in memory).

    const QString DEPRECATED("(†) ");

    m_app.clearCachedVars();  // ... in case any are left over

    FieldRefPtr address_fr = m_app.storedVarFieldRef(varconst::SERVER_ADDRESS);
    const QString address_t = tr("Server address");
    const QString address_h = tr("host name or IP address");

    FieldRefPtr port_fr = m_app.storedVarFieldRef(varconst::SERVER_PORT);
    const QString port_t = tr("Server port for HTTPS");
    const QString port_h = tr("default 443");

    FieldRefPtr path_fr = m_app.storedVarFieldRef(varconst::SERVER_PATH);
    const QString path_t = tr("Path on server");
    const QString path_h = tr("no leading /; e.g. camcops/database");

    FieldRefPtr timeout_fr = m_app.storedVarFieldRef(varconst::SERVER_TIMEOUT_MS);
    const QString timeout_t = tr("Network timeout (ms)");
    const QString timeout_h = tr("e.g. 50000");

#ifdef DEBUG_OFFER_HTTP_TO_SERVER
    FieldRefPtr https_fr = m_app.storedVarFieldRef(varconst::DEBUG_USE_HTTPS_TO_SERVER);
    const QString https_t = tr("Use HTTPS to server?");
    const QString https_h = tr("You should <b>only</b> disable this for debugging!");
#endif

    FieldRefPtr ssl_fr = m_app.storedVarFieldRef(
                varconst::VALIDATE_SSL_CERTIFICATES);
    const QString ssl_t = tr("Validate HTTPS (TLS/SSL) certificates?");
    const QString ssl_h = tr("Should always be YES for security-conscious "
                             "systems.");

    FieldRefPtr ssl_proto_fr = m_app.storedVarFieldRef(varconst::SSL_PROTOCOL);
    const QString ssl_proto_t = tr("HTTPS (TLS/SSL) protocol?");
    const QString ssl_proto_h = tr("Stick with the default unless your server "
                                   "can’t cope with it.");
    const NameValueOptions options_ssl_protocol{
        // https://doc.qt.io/qt-6/qssl.html#SslProtocol-enum
        {tr("Known secure [default]"), convert::SSLPROTODESC_SECUREPROTOCOLS},
        {tr("TLS v1.2"), convert::SSLPROTODESC_TLSV1_2},
        {tr("TLS v1.2 or later"), convert::SSLPROTODESC_TLSV1_2_OR_LATER},
        {tr("TLS v1.3"), convert::SSLPROTODESC_TLSV1_3},
        {tr("TLS v1.3 or later"), convert::SSLPROTODESC_TLSV1_3_OR_LATER},
        {tr("DTLS v1.2"), convert::SSLPROTODESC_DTLSV1_2},
        {tr("DTLS v1.2 or later"), convert::SSLPROTODESC_DTLSV1_2_OR_LATER},
        {DEPRECATED + tr("Any supported protocol"), convert::SSLPROTODESC_ANYPROTOCOL},
    };
    const QString ssl_proto_explanation = DEPRECATED + "Insecure, deprecated.";

    FieldRefPtr storepw_fr = m_app.storedVarFieldRef(varconst::STORE_SERVER_PASSWORD);
    const QString storepw_t = tr("Store user’s server password?");
    const QString storepw_h = tr(
                "NO = fractionally more secure; YES = more convenient/"
                "fractionally less secure, but still AES-256-encrypted.");

    FieldRefPtr uploadmethod_fr = m_app.storedVarFieldRef(
                varconst::UPLOAD_METHOD);
    const QString uploadmethod_t = tr("Upload method");
    const NameValueOptions options_upload_method{
        {tr("Multi-step (original)"), varconst::UPLOAD_METHOD_MULTISTEP},
        {tr("Always one-step (faster)"), varconst::UPLOAD_METHOD_ONESTEP},
        {tr("One-step if small enough (default)"), varconst::UPLOAD_METHOD_BYSIZE},
    };

    FieldRefPtr maxsizeonestep_fr = m_app.storedVarFieldRef(
                varconst::MAX_DBSIZE_FOR_ONESTEP_UPLOAD);
    const QString maxsizeonestep_t = tr(
            "Maximum (approximate) database size for one-step upload (bytes)");
    const QString maxsizeonestep_h = tr("e.g. 2000000 for ~2Mb");

    QuPagePtr page(new QuPage{
        questionnairefunc::defaultGridRawPointer({
            {
                stringfunc::makeTitle(address_t, address_h, true),
                (new QuLineEdit(address_fr))->setHint(
                    stringfunc::makeHint(address_t, address_h))
                    ->setWidgetInputMethodHints(Qt::ImhNoAutoUppercase |
                                                Qt::ImhNoPredictiveText)
            },
            {
                stringfunc::makeTitle(port_t, port_h, true),
                new QuLineEditInteger(port_fr,
                    uiconst::IP_PORT_MIN, uiconst::IP_PORT_MAX)
            },
            {
                stringfunc::makeTitle(path_t, path_h, true),
                (new QuLineEdit(path_fr))->setHint(
                    stringfunc::makeHint(path_t, path_h))
                    ->setWidgetInputMethodHints(Qt::ImhNoAutoUppercase |
                                                Qt::ImhNoPredictiveText)
            },
            {
                stringfunc::makeTitle(timeout_t, timeout_h, true),
                new QuLineEditInteger(timeout_fr,
                    uiconst::NETWORK_TIMEOUT_MS_MIN,
                    uiconst::NETWORK_TIMEOUT_MS_MAX)
            },
        }, 1, 1),

#ifdef DEBUG_OFFER_HTTP_TO_SERVER
       new QuText(stringfunc::makeTitle(https_t, https_h)),
       (new QuMcq(https_fr, CommonOptions::yesNoBoolean()))
                       ->setHorizontal(true)
                       ->setAsTextButton(true),
#endif

        new QuText(stringfunc::makeTitle(ssl_t, ssl_h)),
        (new QuMcq(ssl_fr, CommonOptions::yesNoBoolean()))
                       ->setHorizontal(true)
                       ->setAsTextButton(true),

        new QuText(stringfunc::makeTitle(ssl_proto_t, ssl_proto_h)),
        (new QuMcq(ssl_proto_fr, options_ssl_protocol))
                       ->setHorizontal(true)
                       ->setAsTextButton(true),
        new QuText(ssl_proto_explanation),

        new QuHorizontalLine(),

        new QuText(stringfunc::makeTitle(storepw_t, storepw_h)),
        (new QuMcq(storepw_fr, CommonOptions::yesNoBoolean()))
                       ->setHorizontal(true)
                       ->setAsTextButton(true),

        new QuHorizontalLine(),

        new QuText(stringfunc::makeTitle(uploadmethod_t)),
        (new QuMcq(uploadmethod_fr, options_upload_method))
                       ->setHorizontal(true)
                       ->setAsTextButton(true),

        questionnairefunc::defaultGridRawPointer({
            {
                stringfunc::makeTitle(maxsizeonestep_t, maxsizeonestep_h, true),
                (new QuLineEditInt64(maxsizeonestep_fr))->setHint(
                    stringfunc::makeHint(maxsizeonestep_t, maxsizeonestep_h))
            }
        }, 1, 1),
    });
    page->setTitle(tr("Configure server settings"));
    page->setType(QuPage::PageType::Config);
    page->registerValidator(
        std::bind(&ServerSettingsWindow::validateServerSettings,
                  this, std::placeholders::_1,
                  std::placeholders::_2)
    );

    auto questionnaire = new Questionnaire(m_app, {page});
    questionnaire->setFinishButtonIconToTick();
    connect(questionnaire, &Questionnaire::completed,
            this, &ServerSettingsWindow::serverSettingsSaved);
    connect(questionnaire, &Questionnaire::cancelled,
            &m_app, &CamcopsApp::clearCachedVars);
    return questionnaire;
}


bool ServerSettingsWindow::validateServerSettings(QStringList& errors,
                                                  const QuPage* page)
{
    Q_UNUSED(page)
    // Note that we are using cached server variables.
    const QString hostname = m_app.getCachedVar(varconst::SERVER_ADDRESS).toString();
    if (hostname.contains("/")) {
        errors.append("No forward slashes ('/') permitted in server hostname");
        return false;
    }
    return true;
}


void ServerSettingsWindow::serverSettingsSaved()
{
    // User has edited server settings and then clicked OK.
    const bool server_details_changed = (
        m_app.cachedVarChanged(varconst::SERVER_ADDRESS)
        || m_app.cachedVarChanged(varconst::SERVER_PORT)
        || m_app.cachedVarChanged(varconst::SERVER_PATH)
    );
    if (server_details_changed) {
        uifunc::alert(
            tr("Server details have changed. You should consider "
               "re-registering with the server."),
            tr("Registration advised")
        );
    }
    if (!m_app.storingServerPassword()) {
        // Wipe the stored password
        m_app.setCachedVar(varconst::SERVER_USERPASSWORD_OBSCURED, "");
    }
    m_app.saveCachedVars();
}