15.1.37. tablet_qt/core/networkmanager.h

/*
    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/>.
*/

#pragma once

// #define DEBUG_OFFER_HTTP_TO_SERVER
// ... should NOT be defined in production (which is HTTPS only)

#include <QMap>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QPointer>
#include <QSqlDatabase>
#include <QSslError>
#include <QString>
#include <QUrl>

#include "common/aliases_camcops.h"

class CamcopsApp;
class LogBox;
class QNetworkAccessManager;
class Version;

// Controls network operations, optionally providing a progress display.
class NetworkManager : public QObject
{
    Q_OBJECT

    // ------------------------------------------------------------------------
    // Shorthand
    // ------------------------------------------------------------------------

    using ReplyFuncPtr = void (NetworkManager::*)(QNetworkReply*);
    // ... a pointer to a member function of NetworkManager that takes a
    // QNetworkReply* parameter and returns void

    // ------------------------------------------------------------------------
    // Helper classes
    // ------------------------------------------------------------------------

public:
    // How should we upload?
    enum class UploadMethod {
        Invalid,  // clinician pressed "cancel"
        // clinician mode or single user mode if any current tasks started
        Copy,
        // clinician mode or single user mode if no started current tasks
        MoveKeepingPatients,
        Move  // clinician mode: move all data
    };

    // Types of network error.
    enum ErrorCode {
        NoError,
        IncorrectReplyFormat,
        GenericNetworkError,
        ServerError,
        JsonParseError,
    };

    // ------------------------------------------------------------------------
    // Core
    // ------------------------------------------------------------------------

public:
    NetworkManager(
        CamcopsApp& app,
        DatabaseManager& db,
        TaskFactoryPtr p_task_factory,
        QWidget* parent
    );
    ~NetworkManager();

    // ------------------------------------------------------------------------
    // User interface
    // ------------------------------------------------------------------------

public:
    // Operate in silent mode (without status information)?
    void enableLogging();
    void disableLogging();
    bool isLogging() const;

    // Sets the window title.
    void setTitle(const QString& title);

    // Shows a plain-text status message.
    void statusMessage(const QString& msg) const;

    // Shows an HTML status message.
    void htmlStatusMessage(const QString& html) const;

protected:
    // Ensure we have a logbox.
    void ensureLogBox() const;

    // Delete our logbox.
    void deleteLogBox();

protected slots:
    // "The user pressed cancel on the logbox dialogue."
    void logboxCancelled();

    // "The user pressed OK/Finish on the logbox dialogue."
    void logboxFinished();

    // ------------------------------------------------------------------------
    // Basic connection management
    // ------------------------------------------------------------------------

protected:
    // Ensure we know the user's upload password (ask if not).
    bool ensurePasswordKnown();

    // Disconnect signals/slots from our Qt manager object.
    void disconnectManager();

    // Create a generic network request.
    QNetworkRequest createRequest(
        const QUrl& url,
        bool offer_cancel,
        bool ssl,
        bool ignore_ssl_errors,
        QSsl::SslProtocol ssl_protocol = QSsl::AnyProtocol
    );

    // Returns the URL for the CamCOPS server, as a QUrl.
    QUrl serverUrl(bool& success) const;

    // Returns the URL for the CamCOPS server, as a string.
    QString serverUrlDisplayString() const;

    // Create a request to our server.
    QNetworkRequest createServerRequest(bool& success);

    // Send a message to the server via an HTTP POST, and set up a callback
    // for the results.
    void serverPost(
        Dict dict, ReplyFuncPtr reply_func, bool include_user = true
    );

    // Process the server's reply into our internal data structures,
    // principally m_reply_dict.
    bool processServerReply(QNetworkReply* reply);

    // Formats a human-readable version of "size", e.g. "3 Kb" or similar.
    QString sizeBytes(qint64 size) const;

    // Returns a list of downloaded records from our internal m_reply_dict.
    RecordList getRecordList() const;

    // Does the reply have the correct format from the CamCOPS API?
    bool replyFormatCorrect() const;

    // Did the reply say it was successful?
    bool replyReportsSuccess() const;

    // Wipe internal transmission/reply information.
    void cleanup();

    // Doesn't do very much at present (but in theory converts Qt network
    // errors to our own mapping).
    ErrorCode convertQtNetworkCode(const QNetworkReply::NetworkError error_code
    );

protected slots:
    // We come here when there's an SSL error and we want to ignore it.
    void sslIgnoringErrorHandler(
        QNetworkReply* reply, const QList<QSslError>& errlist
    );

public slots:
    // "User pressed cancel."
    void cancel();

    // "Network operation failed somehow."
    void fail(
        const NetworkManager::ErrorCode error_code
        = NetworkManager::ErrorCode::NoError,
        const QString& error_string = QString()
    );

    // "Network operation succeeded."
    void succeed();

    // ------------------------------------------------------------------------
    // Testing
    // ------------------------------------------------------------------------

public:
    // Tests HTTP GET.
    void testHttpGet(const QString& url, bool offer_cancel = true);

    // Tests HTTPS GET.
    void testHttpsGet(
        const QString& url,
        bool offer_cancel = true,
        bool ignore_ssl_errors = false
    );

protected:
    // Callback for the tests.
    void testReplyFinished(QNetworkReply* reply);

    // ------------------------------------------------------------------------
    // Registering a device with the server.
    // ------------------------------------------------------------------------

public:
    // Register with the CamCOPS server.
    void registerWithServer();  // "register" is a C++ keyword

    // Fetch all information without registration (i.e. fetch ID descriptions,
    // table details, extra strings...).
    void fetchAllServerInfo();

    // Fetch ID number type description/information (and group ID policies)
    // from the server.
    void fetchIdDescriptions();

    // Fetch extra strings from the server.
    void fetchExtraStrings();

protected:
    // Regular entry point for phases under registerWithServer().
    void registerNext(QNetworkReply* reply = nullptr);

    // Parse reply to fetchIdDescriptions().
    void fetchIdDescriptionsSub1(QNetworkReply* reply);

    // Parse reply to fetchExtraStrings().
    void fetchExtraStringsSub1(QNetworkReply* reply);

    // Parse reply to fetchAllServerInfo().
    void fetchAllServerInfoSub1(QNetworkReply* reply);

    // Store ID/policy information from the server.
    void storeServerIdentificationInfo();

    // Store "which tables are allowed" information from the server.
    void storeAllowedTables();

    // Store extra strings from the server.
    void storeExtraStrings();

    // ------------------------------------------------------------------------
    // Upload
    // ------------------------------------------------------------------------

public:
    // Upload to the server.
    void upload(UploadMethod method);

protected:
    // Upload core (called repeatedly at different phases):
    void uploadNext(QNetworkReply* reply);

    // Does a "no-op"-type request to ensure our device is registered.
    void checkDeviceRegistered();

    // Check our user/device is permitted to upload.
    void checkUploadUser();

    // Fetch server's version/ID policies/ID descriptions.
    void uploadFetchServerIdInfo();

    // Does this server version support validation of patient details being
    // uploaded?
    bool serverSupportsValidatePatients() const;

    // Validate patients for upload.
    void uploadValidatePatients();

    // Fetch details of which tables the server will accept.
    void uploadFetchAllowedTables();

    // Start the actual upload.
    void startUpload();

    // Ask the server to begin a preservation "transaction".
    void startPreservation();

    // Send details of all empty tables (in a quick way).
    void sendEmptyTables(const QStringList& tablenames);

    // Send a table in one go.
    void sendTableWhole(const QString& tablename);

    // Sent a table, record-wise (for giant tables).
    void sendTableRecordwise(const QString& tablename);

    // "Here are my PKs, record modification dates, etc. Which ones do you
    // want to receive full data for?" (Used to speed up the upload of giant
    // tables.)
    void requestRecordwisePkPrune();

    // Called repeatedly during record-wise upload.
    void sendNextRecord();

    // Tell the server the upload has finished (asking it to "commit" our
    // ongoing transaction.)
    void endUpload();

    // Is our internal patient info complete (e.g. compliant with the server's
    // ID policies)?
    bool isPatientInfoComplete();

    // For those patients the user has flagged individually to move off, copy
    // the move-off status to those patients' tasks.
    bool applyPatientMoveOffTabletFlagsToTasks();

    // Trawl our tables, populating our internal catalogues
    // (m_upload_empty_tables, m_upload_tables_to_send_recordwise,
    // m_upload_tables_to_send_whole, m_upload_tables_to_wipe).
    bool catalogueTablesForUpload();

    // Check the server version (a) matches what we had stored, and (b) is
    // new enough for us to upload at all.
    bool isServerVersionOK() const;

    // Server version matches what we had stored.
    bool serverVersionMatchesStored() const;

    // Server version is new enough for us to upload at all.
    bool serverVersionNewEnough() const;

    // Version returned by the server.
    Version serverVersionFromReply() const;

    // Do our ID policies match those of the server?
    bool arePoliciesOK() const;

    // Do our ID number description match those of the server?
    bool areDescriptionsOK() const;

    // Which ID number types are in use?
    QVector<int> whichIdnumsUsedOnTablet() const;

    // Based on the server's reply to requestRecordwisePkPrune(), restrict
    // which records we will send.
    bool pruneRecordwisePks();

    // Wipe all tables marked to be wiped.
    void wipeTables();

    // Tell the user about the failure of a local SQL query.
    void queryFail(const QString& sql);

    // Tell the user about an SQL query failure whilst clearing the move-off
    // flag.
    void queryFailClearingMoveOffFlag(const QString& tablename);

    // Clear the move-off flag for all records in a table.
    bool clearMoveOffTabletFlag(const QString& tablename);

    // Delete local records of any BLOBs that have become orphaned.
    bool pruneDeadBlobs();

    // Does the server support the newer one-step upload feature?
    bool serverSupportsOneStepUpload() const;

    // Should we use the one-step upload feature, because (a) the user wants
    // it, and (b) the server supports it?
    bool shouldUseOneStepUpload() const;

    // Perform a one-step upload (via a big JSON dump).
    void uploadOneStep();

    // Provide (as a JSON string) a mapping from table name to PK name.
    QString getPkInfoAsJson();

    // ------------------------------------------------------------------------
    // Single-user mode
    // ------------------------------------------------------------------------

public:
    // In single-user mode, send the server a proquint access key and receive
    // patient details, user details, and schedule information.
    void registerPatient();

    // Update task schedules for the single user.
    void updateTaskSchedulesAndPatientDetails();

protected:
    // Parse reply to registerPatient().
    void registerPatientSub1(QNetworkReply* reply);

    // Store the username/password that the server has given us.
    void setUserDetails();

    // From the server's reply, including patient details, create a local
    // patient record (and select it as our sole patient).
    bool createSinglePatient();

    // From the server's reply, set our local variables regarding the
    // intellectual property context in which we're operating.
    bool setIpUseInfo();

    // Parse reply to updateTaskSchedules().
    void receivedTaskSchedulesAndPatientDetails(QNetworkReply* reply);

    // Store the task schedules.
    void storeTaskSchedulesAndPatientDetails();

    // Copy complete status for anonymous tasks when updating tasks
    void updateCompleteStatusForAnonymousTasks(
        TaskSchedulePtrList old_schedules, TaskSchedulePtrList new_schedules
    );

    // ------------------------------------------------------------------------
    // Signals
    // ------------------------------------------------------------------------
signals:
    // "Operation was cancelled."
    void cancelled(
        const NetworkManager::ErrorCode error_code, const QString& error_string
    );

    // "Operation has finished, successfully or not; user has acknowledged."
    void finished();

    // ------------------------------------------------------------------------
    // Translatable text
    // ------------------------------------------------------------------------

protected:
    // Provides text to say "please re-fetch server information".
    static QString txtPleaseRefetchServerInfo();

    // ------------------------------------------------------------------------
    // Data
    // ------------------------------------------------------------------------

protected:
    // Our app.
    CamcopsApp& m_app;

    // The data database.
    DatabaseManager& m_db;

    // Our app's task factory.
    TaskFactoryPtr m_p_task_factory;

    // Parent widget.
    QWidget* m_parent;

    // Window title.
    QString m_title;

    // Offer a cancel button?
    bool m_offer_cancel;

    // Suppress all status messages?
    bool m_silent;

    // Our logbox (triggered when a status message is displayed)
    mutable QPointer<LogBox> m_logbox;

    // Our Qt network manager
    QNetworkAccessManager* m_mgr;

    // Temporary storage of information going to the server:
    QString m_tmp_password;
    QString m_tmp_session_id;
    QString m_tmp_session_token;

    // Incoming information.
    // We store these here to save passing around large objects, and for
    // convenience:
    QByteArray m_reply_data;
    Dict m_reply_dict;  // the main repository of information received

    // How will we upload?
    UploadMethod m_upload_method;

    // Sequencing of the upload steps
    enum class NextUploadStage {
        Invalid,
        CheckUser,
        FetchServerIdInfo,
        StoreExtraStrings,
        ValidatePatients,  // v2.3.0
        FetchAllowedTables,
        CheckPoliciesThenStartUpload,
        StartPreservation,
        Uploading,
        Finished,
    };

    // Internal calculations for uploading.
    NextUploadStage m_upload_next_stage;
    QVector<int> m_upload_patient_ids_to_move_off;
    QStringList m_upload_empty_tables;
    QStringList m_upload_tables_to_send_whole;
    QStringList m_upload_tables_to_send_recordwise;
    QString m_upload_recordwise_table_in_progress;
    QStringList m_upload_recordwise_fieldnames;
    int m_upload_current_record_index;
    bool m_recordwise_prune_req_sent;
    bool m_recordwise_pks_pruned;
    QVector<int> m_upload_recordwise_pks_to_send;
    int m_upload_n_records;
    // ... cached as m_upload_recordwise_pks_to_send shrinks during upload
    QStringList m_upload_tables_to_wipe;
    QString m_upload_patient_info_json;

    // Possible states during single-user-mode patient registration.
    enum class NextRegisterStage {
        Invalid,
        Register,
        StoreServerIdentification,
        GetAllowedTables,
        StoreAllowedTables,
        GetExtraStrings,
        StoreExtraStrings,
        GetTaskSchedules,
        StoreTaskSchedules,
        Finished,
    };

    // Current registration stage.
    NextRegisterStage m_register_next_stage;
};