From 538bc6c54f06dc742cbaf045ecedd8ed0923f7a4 Mon Sep 17 00:00:00 2001 From: Scharel Clemens Date: Mon, 25 Jan 2021 22:42:38 +0100 Subject: [PATCH] Some work on Generic Nextcloud API class --- src/nextcloudapi.cpp | 291 +++++++++++++++++++++++++++++++++++++++++++ src/nextcloudapi.h | 37 +++--- 2 files changed, 311 insertions(+), 17 deletions(-) diff --git a/src/nextcloudapi.cpp b/src/nextcloudapi.cpp index 87d8847..39bf22d 100644 --- a/src/nextcloudapi.cpp +++ b/src/nextcloudapi.cpp @@ -1,6 +1,297 @@ #include "nextcloudapi.h" +#include NextcloudApi::NextcloudApi(QObject *parent) : QObject(parent) { + // Initial status + setStatus(NextcloudStatus::NextcloudUnknown); + setCababilitiesStatus(CapabilitiesStatus::CapabilitiesUnknown); + setLoginStatus(LoginStatus::LoginUnknown); + m_status_installed = false; + m_status_maintenance = false; + m_status_needsDbUpgrade = false; + m_status_extendedSupport = false; + m_running_requests = 0; + // Login Flow V2 poll timer + m_loginPollTimer.setInterval(LOGIN_FLOWV2_POLL_INTERVALL); + connect(&m_loginPollTimer, SIGNAL(timeout()), this, SLOT(pollLoginUrl())); + + // Verify URL + connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(verifyUrl(QUrl))); + + // Listen to signals of the QNetworkAccessManager class + connect(&m_manager, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), this, SLOT(requireAuthentication(QNetworkReply*,QAuthenticator*))); + connect(&m_manager, SIGNAL(networkAccessibleChanged(QNetworkAccessManager::NetworkAccessibility)), this, SLOT(onNetworkAccessibleChanged(QNetworkAccessManager::NetworkAccessibility))); + connect(&m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); + connect(&m_manager, SIGNAL(sslErrors(QNetworkReply*,QList)), this, SLOT(sslError(QNetworkReply*,QList))); + + // Prepare the QNetworkRequest classes + m_request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); + m_request.setHeader(QNetworkRequest::UserAgentHeader, QGuiApplication::applicationDisplayName() + " " + QGuiApplication::applicationVersion() + " - " + QSysInfo::machineHostName()); + m_request.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/x-www-form-urlencoded").toUtf8()); + m_request.setRawHeader("OCS-APIRequest", "true"); + m_request.setRawHeader("Accept", "application/json"); + m_authenticatedRequest = m_request; + m_authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/json").toUtf8()); +} + +NextcloudApi::~NextcloudApi() { +} + +void NextcloudApi::setVerifySsl(bool verify) { + if (verify != (m_request.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer) || + verify != (m_authenticatedRequest.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer)) { + m_request.sslConfiguration().setPeerVerifyMode(verify ? QSslSocket::VerifyPeer : QSslSocket::VerifyNone); + m_authenticatedRequest.sslConfiguration().setPeerVerifyMode(verify ? QSslSocket::VerifyPeer : QSslSocket::VerifyNone); + emit verifySslChanged(verify); + } +} + +void NextcloudApi::setUrl(QUrl url) { + if (url != m_url) { + QString oldServer = server(); + setScheme(url.scheme()); + setHost(url.host()); + setPort(url.port()); + setUsername(url.userName()); + setPassword(url.password()); + setPath(url.path()); + if (server() != oldServer) + emit serverChanged(server()); + emit urlChanged(m_url); + } +} + +QString NextcloudApi::server() const { + QUrl server; + server.setScheme(m_url.scheme()); + server.setHost(m_url.host()); + if (m_url.port() > 0) + server.setPort(m_url.port()); + server.setPath(m_url.path()); + return server.toString(); +} + +void NextcloudApi::setServer(QString serverUrl) { + QUrl url(serverUrl.trimmed()); + if (url != server()) { + setScheme(url.scheme()); + setHost(url.host()); + setPort(url.port()); + setPath(url.path()); + } +} + +void NextcloudApi::setScheme(QString scheme) { + if (scheme != m_url.scheme() && (scheme == "http" || scheme == "https")) { + m_url.setScheme(scheme); + emit schemeChanged(m_url.scheme()); + emit serverChanged(server()); + emit urlChanged(m_url); + } +} + +void NextcloudApi::setHost(QString host) { + if (host != m_url.host()) { + m_url.setHost(host); + emit hostChanged(m_url.host()); + emit serverChanged(server()); + emit urlChanged(m_url); + } +} + +void NextcloudApi::setPort(int port) { + if (port != m_url.port() && port >= 1 && port <= 65535) { + m_url.setPort(port); + emit portChanged(m_url.port()); + emit serverChanged(server()); + emit urlChanged(m_url); + } +} + +void NextcloudApi::setUsername(QString username) { + if (username != m_url.userName()) { + m_url.setUserName(username); + QString concatenated = username + ":" + password(); + QByteArray data = concatenated.toLocal8Bit().toBase64(); + QString headerData = "Basic " + data; + m_authenticatedRequest.setRawHeader("Authorization", headerData.toLocal8Bit()); + emit usernameChanged(m_url.userName()); + emit urlChanged(m_url); + } +} + +void NextcloudApi::setPassword(QString password) { + if (password != m_url.password()) { + m_url.setPassword(password); + QString concatenated = username() + ":" + password; + QByteArray data = concatenated.toLocal8Bit().toBase64(); + QString headerData = "Basic " + data; + m_authenticatedRequest.setRawHeader("Authorization", headerData.toLocal8Bit()); + emit passwordChanged(m_url.password()); + emit urlChanged(m_url); + } +} + +void NextcloudApi::setPath(QString path) { + if (path != m_url.path()) { + m_url.setPath(path); + emit pathChanged(m_url.path()); + emit serverChanged(server()); + emit urlChanged(m_url); + } +} + +const QString NextcloudApi::errorMessage(int error) const { + QString message; + switch (error) { + case NoError: + message = tr("No error"); + break; + case NoConnectionError: + message = tr("No network connection available"); + break; + case CommunicationError: + message = tr("Failed to communicate with the Nextcloud server"); + break; + case SslHandshakeError: + message = tr("An error occured while establishing an encrypted connection"); + break; + case AuthenticationError: + message = tr("Could not authenticate to the Nextcloud instance"); + break; + default: + message = tr("Unknown error"); + break; + } + return message; +} + +bool NextcloudApi::get(const QString& endpoint, bool authenticated) { + QUrl url = server(); + url.setPath(url.path() + endpoint); + qDebug() << "GET" << url.toDisplayString(); + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; + request.setUrl(url); + m_manager.get(request); + m_running_requests ++; + return true; + } + else { + qDebug() << "GET URL not valid" << url.toDisplayString(); + } + return false; +} + +bool NextcloudApi::post(const QString& endpoint, const QByteArray& data, bool authenticated) { + QUrl url = server(); + url.setPath(url.path() + endpoint); + qDebug() << "POST" << url.toDisplayString(); + qDebug() << data; + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; + request.setUrl(url); + m_manager.post(request, data); + m_running_requests ++; + return true; + } + else { + qDebug() << "POST URL not valid" << url.toDisplayString(); + } + return false; +} + +bool NextcloudApi::put(const QString& endpoint, const QByteArray& data, bool authenticated) { + QUrl url = server(); + url.setPath(url.path() + endpoint); + qDebug() << "PUT" << url.toDisplayString(); + qDebug() << data; + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; + request.setUrl(url); + m_manager.put(request, data); + m_running_requests ++; + return true; + } + else { + qDebug() << "PUT URL not valid" << url.toDisplayString(); + } + return false; +} + +bool NextcloudApi::del(const QString& endpoint, bool authenticated) { + QUrl url = server(); + url.setPath(url.path() + endpoint); + qDebug() << "DEL" << url.toDisplayString(); + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; + request.setUrl(url); + m_manager.deleteResource(request); + m_running_requests ++; + return true; + } + else { + qDebug() << "DEL URL not valid" << url.toDisplayString(); + } + return false; +} + +bool NextcloudApi::getStatus() { + if (get(STATUS_ENDPOINT, false)) { + setStatus(NextcloudStatus::NextcloudBusy); + return true; + } + else { + setStatus(NextcloudStatus::NextcloudFailed); + } + return false; +} + +bool NextcloudApi::initiateFlowV2Login() { + if (m_loginStatus == LoginStatus::LoginFlowV2Initiating || m_loginStatus == LoginStatus::LoginFlowV2Polling) { + abortFlowV2Login(); + } + if (post(LOGIN_FLOWV2_ENDPOINT, QByteArray(), false)) { + setLoginStatus(LoginStatus::LoginFlowV2Initiating); + return true; + } + else { + setLoginStatus(LoginStatus::LoginFlowV2Failed); + abortFlowV2Login(); + } + return false; +} + +void NextcloudApi::abortFlowV2Login() { + m_loginPollTimer.stop(); + m_loginUrl.clear(); + m_pollUrl.clear(); + m_pollToken.clear(); + setLoginStatus(LoginStatus::LoginUnknown); +} + +bool NextcloudApi::verifyLogin() { + return get(USER_CAPABILITIES_ENDPOINT.arg(username()), true); +} + +bool NextcloudApi::getAppPassword() { + return get(GET_APPPASSWORD_ENDPOINT, true); +} + +bool NextcloudApi::deleteAppPassword() { + return del(DEL_APPPASSWORD_ENDPOINT, true); +} + +bool NextcloudApi::pollLoginUrl() { + if (post(m_pollUrl.path(), QByteArray("token=").append(m_pollToken), false)) { + setLoginStatus(LoginStatus::LoginFlowV2Polling); + return true; + } + else { + setLoginStatus(LoginStatus::LoginFlowV2Failed); + abortFlowV2Login(); + } + return false; } diff --git a/src/nextcloudapi.h b/src/nextcloudapi.h index 2834bf8..eb77a26 100644 --- a/src/nextcloudapi.h +++ b/src/nextcloudapi.h @@ -34,7 +34,7 @@ class NextcloudApi : public QObject Q_PROPERTY(bool verifySsl READ verifySsl WRITE setVerifySsl NOTIFY verifySslChanged) // to allow selfsigned certificates Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) // complete API URL = ://:@[:]/ Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged) // url without username and password = ://[:]/ - // the following six properties will update the url and server properties + // the following six properties will update the url and server properties and vice versa Q_PROPERTY(QString scheme READ scheme WRITE setScheme NOTIFY schemeChanged) Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) @@ -43,12 +43,13 @@ class NextcloudApi : public QObject Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) // Networking status information + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) Q_PROPERTY(bool urlValid READ urlValid NOTIFY urlValidChanged) Q_PROPERTY(bool networkAccessible READ networkAccessible NOTIFY networkAccessibleChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) // Nextcloud status (status.php), these properties will be automatically updated on changes of the generic properties - Q_PROPERTY(NextcloudStatus ncStatusStatus READ ncStatusStatus NOTIFY ncStatusStatusChanged) + Q_PROPERTY(NextcloudStatus statusStatus READ statusStatus NOTIFY statusStatusChanged) Q_PROPERTY(bool statusInstalled READ statusInstalled NOTIFY statusInstalledChanged) Q_PROPERTY(bool statusMaintenance READ statusMaintenance NOTIFY statusMaintenanceChanged) Q_PROPERTY(bool statusNeedsDbUpgrade READ statusNeedsDbUpgrade NOTIFY statusNeedsDbUpgradeChanged) @@ -126,12 +127,13 @@ public: void setPassword(QString password); // Class status information + bool ready() const { return urlValid() && networkAccessible() && !busy() && statusInstalled() && !statusMaintenance() && loginStatus() == LoginSuccess; } bool urlValid() const { return m_url.isValid(); } bool networkAccessible() const { return m_manager.networkAccessible() == QNetworkAccessManager::Accessible; } - bool busy() const; + bool busy() const { return m_running_requests > 0; } // Nextcloud status (status.php) - NextcloudStatus ncStatusStatus() const { return m_statusStatus; } + NextcloudStatus statusStatus() const { return m_statusStatus; } bool statusInstalled() const { return m_status_installed; } bool statusMaintenance() const { return m_status_maintenance; } bool statusNeedsDbUpgrade() const { return m_status_needsDbUpgrade; } @@ -159,20 +161,20 @@ public: Q_INVOKABLE const QString errorMessage(int error) const; public slots: - // Callable functions - Q_INVOKABLE bool getNcStatus(); - Q_INVOKABLE bool initiateFlowV2Login(); - Q_INVOKABLE void abortFlowV2Login(); - Q_INVOKABLE void verifyLogin(); - Q_INVOKABLE void getAppPassword(); - Q_INVOKABLE void deleteAppPassword(); - // API helper functions Q_INVOKABLE bool get(const QString& endpoint, bool authenticated = true); - Q_INVOKABLE bool post(const QString& endpoint, bool authenticated = true); - Q_INVOKABLE bool put(const QString& endpoint, bool authenticated = true); + Q_INVOKABLE bool post(const QString& endpoint, const QByteArray& data, bool authenticated = true); + Q_INVOKABLE bool put(const QString& endpoint, const QByteArray& data, bool authenticated = true); Q_INVOKABLE bool del(const QString& endpoint, bool authenticated = true); + // Callable functions + Q_INVOKABLE bool getStatus(); + Q_INVOKABLE bool initiateFlowV2Login(); + Q_INVOKABLE void abortFlowV2Login(); + Q_INVOKABLE bool verifyLogin(); + Q_INVOKABLE bool getAppPassword(); + Q_INVOKABLE bool deleteAppPassword(); + signals: // Generic API properties void verifySslChanged(bool verify); @@ -194,7 +196,7 @@ signals: void capabilitiesStatusChanged(CapabilitiesStatus status); // Nextcloud status (status.php) - void ncStatusStatusChanged(NextcloudStatus status); + void statusChanged(NextcloudStatus status); void statusInstalledChanged(bool installed); void statusMaintenanceChanged(bool maintenance); void statusNeedsDbUpgradeChanged(bool needsDbUpgrade); @@ -219,7 +221,7 @@ private slots: void requireAuthentication(QNetworkReply * reply, QAuthenticator * authenticator); void onNetworkAccessibleChanged(QNetworkAccessManager::NetworkAccessibility accessible); void replyFinished(QNetworkReply* reply); - void pollLoginUrl(); + bool pollLoginUrl(); void sslError(QNetworkReply* reply, const QList &errors); private: @@ -227,10 +229,11 @@ private: QNetworkAccessManager m_manager; QNetworkRequest m_request; QNetworkRequest m_authenticatedRequest; + uint m_running_requests; // Nextcloud status.php void updateStatus(const QJsonObject &status); - void setNextcloudStatus(NextcloudStatus status, bool *changed = NULL); + void setStatus(NextcloudStatus status, bool *changed = NULL); NextcloudStatus m_statusStatus; bool m_status_installed; bool m_status_maintenance;