From e44af7ec7a71b20b27f9ff86ad97a559bd736687 Mon Sep 17 00:00:00 2001 From: Scharel Date: Mon, 12 Apr 2021 18:58:39 +0200 Subject: [PATCH] Worked on Nextcloud API capabilities (not tested\!) --- qml/harbour-nextcloudnotes.qml | 12 ++-- qml/pages/AboutPage.qml | 2 +- qml/pages/EditPage.qml | 2 +- qml/pages/GPLLicense.qml | 2 +- qml/pages/LoginPage.qml | 4 +- qml/pages/MITLicense.qml | 2 +- qml/pages/NotePage.qml | 2 +- qml/pages/NotesPage.qml | 10 +-- qml/pages/SettingsPage.qml | 2 +- qml/pages/SyntaxPage.qml | 2 +- qml/pages/UnencryptedDialog.qml | 2 +- src/harbour-nextcloudnotes.cpp | 2 + src/nextcloudapi.cpp | 92 ++++++++++++++++++++++++-- src/nextcloudapi.h | 79 +++++++++++++--------- translations/harbour-nextcloudnotes.ts | 22 +++--- 15 files changed, 164 insertions(+), 73 deletions(-) diff --git a/qml/harbour-nextcloudnotes.qml b/qml/harbour-nextcloudnotes.qml index 80d6a7c..c761498 100644 --- a/qml/harbour-nextcloudnotes.qml +++ b/qml/harbour-nextcloudnotes.qml @@ -30,13 +30,10 @@ ApplicationWindow } onSortByChanged: { - if (sortBy == "none") - notesProxyModel.invalidate() - else - notesProxyModel.sortRole = notesModel.roleFromName(sortBy) + if (sortBy == "none") notesModel.invalidate() } onFavoritesOnTopChanged: { - notesProxyModel.favoritesOnTop = favoritesOnTop + notesModel.favoritesOnTop = favoritesOnTop } function createAccount(username, password, url, name) { @@ -138,7 +135,7 @@ ApplicationWindow } Nextcloud { - id: nextcloud + id: notesApi server: account.url username: account.username password: account.passowrd @@ -155,5 +152,6 @@ ApplicationWindow initialPage: Component { NotesPage { } } cover: Qt.resolvedUrl("cover/CoverPage.qml") - allowedOrientations: defaultAllowedOrientations + + allowedOrientations: debug ? Orientation.All : defaultAllowedOrientations } diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml index 25830b5..809691c 100644 --- a/qml/pages/AboutPage.qml +++ b/qml/pages/AboutPage.qml @@ -89,5 +89,5 @@ Page { VerticalScrollDecorator {} } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/EditPage.qml b/qml/pages/EditPage.qml index 5a6084e..b0ee9e2 100644 --- a/qml/pages/EditPage.qml +++ b/qml/pages/EditPage.qml @@ -179,5 +179,5 @@ Dialog { VerticalScrollDecorator {} } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/GPLLicense.qml b/qml/pages/GPLLicense.qml index 4db6145..5e31981 100644 --- a/qml/pages/GPLLicense.qml +++ b/qml/pages/GPLLicense.qml @@ -111,5 +111,5 @@ NO WARRANTY } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/LoginPage.qml b/qml/pages/LoginPage.qml index 76dff4c..8bb3cb6 100644 --- a/qml/pages/LoginPage.qml +++ b/qml/pages/LoginPage.qml @@ -1,7 +1,7 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 import Nemo.Configuration 1.0 -import NextcloudNotes 1.0 +import NextcloudApi 1.0 Dialog { id: loginDialog @@ -308,5 +308,5 @@ Dialog { } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/MITLicense.qml b/qml/pages/MITLicense.qml index 476ff57..67a6343 100644 --- a/qml/pages/MITLicense.qml +++ b/qml/pages/MITLicense.qml @@ -44,5 +44,5 @@ SOFTWARE.

" } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/NotePage.qml b/qml/pages/NotePage.qml index 85f14f5..cbff9ab 100644 --- a/qml/pages/NotePage.qml +++ b/qml/pages/NotePage.qml @@ -261,5 +261,5 @@ Dialog { VerticalScrollDecorator {} } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/NotesPage.qml b/qml/pages/NotesPage.qml index 1ba99dd..3857a76 100644 --- a/qml/pages/NotesPage.qml +++ b/qml/pages/NotesPage.qml @@ -38,7 +38,7 @@ Page { MenuItem { text: notesApi.networkAccessible && !notesApi.busy ? qsTr("Reload") : qsTr("Updating...") enabled: account !== null && notesApi.networkAccessible && !notesApi.busy - onClicked: notes.getAllNotes() + onClicked: notesApi.getAllNotes() } MenuLabel { visible: account !== null @@ -59,7 +59,7 @@ Page { EnterKey.iconSource: "image://theme/icon-m-enter-close" EnterKey.onClicked: focus = false onTextChanged: { - notesProxyModel.searchFilter = text + notesModel.searchFilter = text } } Label { @@ -84,7 +84,7 @@ Page { currentIndex: -1 - model: notesProxyModel + model: notesModel delegate: BackgroundItem { id: note @@ -238,7 +238,7 @@ Page { ViewPlaceholder { id: noSearchPlaceholder - enabled: notesList.count === 0 && notesProxyModel.searchFilter !== "" //notesProxyModel.filterRegExp !== "" + enabled: notesList.count === 0 && notesModel.searchFilter !== "" //notesModel.filterRegExp !== "" text: qsTr("No result") hintText: qsTr("Try another query") } @@ -266,5 +266,5 @@ Page { VerticalScrollDecorator { flickable: notesList } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 09575ca..d7ef555 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -231,5 +231,5 @@ Page { VerticalScrollDecorator {} } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/SyntaxPage.qml b/qml/pages/SyntaxPage.qml index 8b6aba2..3e563a4 100644 --- a/qml/pages/SyntaxPage.qml +++ b/qml/pages/SyntaxPage.qml @@ -394,5 +394,5 @@ Use HTML Tags to format your text." } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/qml/pages/UnencryptedDialog.qml b/qml/pages/UnencryptedDialog.qml index 20ec87b..95faeac 100644 --- a/qml/pages/UnencryptedDialog.qml +++ b/qml/pages/UnencryptedDialog.qml @@ -39,5 +39,5 @@ Dialog { } } - allowedOrientations: defaultAllowedOrientations + allowedOrientations: appWindow.allowedOrientations } diff --git a/src/harbour-nextcloudnotes.cpp b/src/harbour-nextcloudnotes.cpp index efb26c1..aa372f0 100644 --- a/src/harbour-nextcloudnotes.cpp +++ b/src/harbour-nextcloudnotes.cpp @@ -17,6 +17,7 @@ int main(int argc, char *argv[]) qDebug() << app->applicationDisplayName() << app->applicationVersion(); + NotesProxyModel* notesProxyModel = new NotesProxyModel; AccountHash* accountHash = new AccountHash; qmlRegisterType("NextcloudApi", 1, 0, "Nextcloud"); @@ -26,6 +27,7 @@ int main(int argc, char *argv[]) #else view->rootContext()->setContextProperty("debug", QVariant(false)); #endif + view->rootContext()->setContextProperty("notesModel", notesProxyModel); view->rootContext()->setContextProperty("accountHash", accountHash); view->setSource(SailfishApp::pathTo("qml/harbour-nextcloudnotes.qml")); diff --git a/src/nextcloudapi.cpp b/src/nextcloudapi.cpp index 5376050..986631c 100644 --- a/src/nextcloudapi.cpp +++ b/src/nextcloudapi.cpp @@ -15,6 +15,9 @@ NextcloudApi::NextcloudApi(QObject *parent) : QObject(parent) m_status_needsDbUpgrade = false; m_status_extendedSupport = false; + // Add capabilities functions for server apps + m_appCapabilities["notes"] = &NextcloudApi::updateNoteCapabilities; + // Login Flow V2 poll timer m_loginPollTimer.setInterval(LOGIN_FLOWV2_POLL_INTERVALL); connect(&m_loginPollTimer, SIGNAL(timeout()), this, SLOT(pollLoginUrl())); @@ -118,6 +121,15 @@ void NextcloudApi::setPort(int port) { } } +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); + } +} + void NextcloudApi::setUsername(QString username) { if (username != m_url.userName()) { m_url.setUserName(username); @@ -142,13 +154,23 @@ void NextcloudApi::setPassword(QString password) { } } -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); +bool NextcloudApi::notesAppInstalled() const { + QJsonObject notes = m_capabilities.value("notes").toObject(); + return !notes.isEmpty(); +} + +QStringList NextcloudApi::notesAppApiVersions() const { + QStringList versions; + QJsonObject notes = m_capabilities.value("notes").toObject(); + if (!notes.isEmpty()) { + QJsonArray apiVersion = notes.value("api_version").toArray(); + QJsonArray::const_iterator i; + for (i = apiVersion.constBegin(); i != apiVersion.constEnd(); ++i) { + if (i->isString()) + versions << i->toString(); + } } + return versions; } const QString NextcloudApi::errorMessage(int error) const { @@ -288,6 +310,18 @@ bool NextcloudApi::deleteAppPassword() { return del(DEL_APPPASSWORD_ENDPOINT, true); } +bool NextcloudApi::getUserList() { + return false; // TODO +} + +bool NextcloudApi::getUserMetaData(const QString& user) { + return false; // TODO +} + +bool NextcloudApi::getCapabilities() { + return false; // TODO +} + void NextcloudApi::verifyUrl(QUrl url) { emit urlValidChanged( url.isValid()&& @@ -608,9 +642,53 @@ void NextcloudApi::setUserMetaStatus(ApiCallStatus status, bool *changed) { } bool NextcloudApi::updateCapabilities(const QJsonObject &json) { + QJsonObject ocs = json.value("ocs").toObject(); + QJsonObject data = ocs.value("data").toObject(); + QJsonObject preCapabilities = m_capabilities; + QJsonObject newCapabilities = data.value("capabilities").toObject(); + if (!newCapabilities.isEmpty()) { + setCababilitiesStatus(ApiSuccess); + if (newCapabilities != preCapabilities) { + m_capabilities = newCapabilities; + QStringList apps = newCapabilities.keys(); + QStringList::const_iterator app; + for (app = apps.constBegin(); app != apps.constEnd(); ++app) { + if (m_appCapabilities.contains(*app)) { + qDebug() << "Updating \"" << *app << "\" capabilities"; + (this->*m_appCapabilities[*app])(newCapabilities.value(*app).toObject(), preCapabilities.value(*app).toObject()); + } + else { + qDebug() << "Capabilities for " << *app << " not implemented!"; + } + } + } + return true; + } + setCababilitiesStatus(ApiFailed); + return false; +} + +void NextcloudApi::updateNoteCapabilities(const QJsonObject &newObject, const QJsonObject &preObject) { + qDebug() << "Updating \"notes\" capabilities"; + if (newObject.isEmpty() != preObject.isEmpty()) + emit notesAppInstalledChanged(notesAppInstalled()); + + QStringList preVersions; + QJsonArray preApiVersion = preObject.value("api_version").toArray(); + QJsonArray::const_iterator i; + for (i = preApiVersion.constBegin(); i != preApiVersion.constEnd(); ++i) { + if (i->isString()) + preVersions << i->toString(); + } + if (preVersions != notesAppApiVersions()) + emit notesAppApiVersionsChanged(notesAppApiVersions()); } void NextcloudApi::setCababilitiesStatus(ApiCallStatus status, bool *changed) { - + if (status != m_capabilitiesStatus) { + m_capabilitiesStatus = status; + if (changed) *changed = true; + emit capabilitiesStatusChanged(m_capabilitiesStatus); + } } diff --git a/src/nextcloudapi.h b/src/nextcloudapi.h index 671577e..b3902ce 100644 --- a/src/nextcloudapi.h +++ b/src/nextcloudapi.h @@ -16,21 +16,24 @@ // Nextcloud instance information const QString STATUS_ENDPOINT("/status.php"); -// Capabilites and users +// OCS APIs endpoints +// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html const QString CAPABILITIES_ENDPOINT("/ocs/v2.php/cloud/capabilities"); const QString LIST_USERS_ENDPOINT("/ocs/v2.php/cloud/users"); const QString USER_METADATA_ENDPOINT("/ocs/v2.php/cloud/users/%1"); -const QString USER_NOTIFICATION_ENDPOINT("/ocs/v2.php/cloud/capabilities"); +const QString USER_NOTIFICATION_ENDPOINT("/ocs/v2.php/apps/notifications/api/v2/notifications"); +const QString DIRECT_DOWNLOAD_ENDPOINT("/ocs/v2.php/apps/dav/api/v1/direct"); -// Login and authentication +// Login Flow endpoints +// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html const QString GET_APPPASSWORD_ENDPOINT("/ocs/v2.php/core/getapppassword"); const QString DEL_APPPASSWORD_ENDPOINT("/ocs/v2.php/core/apppassword"); const QString LOGIN_FLOWV2_ENDPOINT("/index.php/login/v2"); const int LOGIN_FLOWV2_MIN_VERSION = 16; const int LOGIN_FLOWV2_POLL_INTERVALL = 5000; -// Diredct Download -const QString DIRECT_DOWNLOAD_ENDPOINT("/ocs/v2.php/apps/dav/api/v1/direct"); +class NextcloudApi; +typedef void (NextcloudApi::*updateAppCapabilities)(const QJsonObject &newObject, const QJsonObject &preObject); class NextcloudApi : public QObject { @@ -38,9 +41,9 @@ class NextcloudApi : public QObject // Generic API properties 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(QUrl url READ url WRITE setUrl NOTIFY urlChanged) // complete nextcloud 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 and vice versa + // the following 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) @@ -49,10 +52,10 @@ 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) + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) // when all needed properties are set + Q_PROPERTY(bool urlValid READ urlValid NOTIFY urlValidChanged) // if the property url is valid + Q_PROPERTY(bool networkAccessible READ networkAccessible NOTIFY networkAccessibleChanged) // when the device has connectivity + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) // when an API call is currently running // Nextcloud status (status.php), these properties will be automatically updated on changes of the generic properties Q_PROPERTY(ApiCallStatus statusStatus READ statusStatus NOTIFY statusStatusChanged) @@ -66,17 +69,21 @@ class NextcloudApi : public QObject Q_PROPERTY(bool statusExtendedSupport READ statusExtendedSupport NOTIFY statusExtendedSupportChanged) // Login status + Q_PROPERTY(LoginStatus loginStatus READ loginStatus NOTIFY loginStatusChanged) Q_PROPERTY(bool loginFlowV2Possible READ loginFlowV2Possible NOTIFY loginFlowV2PossibleChanged) - Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged) - Q_PROPERTY(ApiCallStatus loginStatus READ loginStatus NOTIFY loginStatusChanged) + Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged) // will be set after initiateFlowV2Login() has been called. The URL needs to be opened in a browser or webview // User(s) status Q_PROPERTY(ApiCallStatus userListStatus READ userListStatus NOTIFY userListStatusChanged) - Q_PROPERTY(QStringList userList READ userList NOTIFY userListChanged) Q_PROPERTY(ApiCallStatus userMetaStatus READ userMetaStatus NOTIFY userMetaStatusChanged) + Q_PROPERTY(QStringList userList READ userList NOTIFY userListChanged) + // TODO property for user metadata // Nextcloud capabilities Q_PROPERTY(ApiCallStatus capabilitiesStatus READ capabilitiesStatus NOTIFY capabilitiesStatusChanged) + Q_PROPERTY(bool notesAppInstalled READ notesAppInstalled NOTIFY notesAppInstalledChanged) + Q_PROPERTY(QStringList notesAppApiVersions READ notesAppApiVersions NOTIFY notesAppApiVersionsChanged) + // TODO other property for server capabilities public: explicit NextcloudApi(QObject *parent = nullptr); @@ -148,17 +155,19 @@ public: bool statusExtendedSupport() const { return m_status_extendedSupport; } // Login status + LoginStatus loginStatus() const { return m_loginStatus; } bool loginFlowV2Possible() const { return QVersionNumber::fromString(statusVersion()) >= QVersionNumber(LOGIN_FLOWV2_MIN_VERSION); } QUrl loginUrl() const { return m_loginUrl; } - LoginStatus loginStatus() const { return m_loginStatus; } // User(s) status ApiCallStatus userListStatus() const { return m_userListStatus; } - QStringList userList() const { return m_userList; } ApiCallStatus userMetaStatus() const { return m_userMetaStatus; } + QStringList userList() const { return m_userList; } // Nextcloud capabilities ApiCallStatus capabilitiesStatus() const { return m_capabilitiesStatus; } + bool notesAppInstalled() const; + QStringList notesAppApiVersions() const; enum ErrorCodes { NoError, @@ -229,6 +238,8 @@ signals: // Nextcloud capabilities void capabilitiesStatusChanged(ApiCallStatus status); + void notesAppInstalledChanged(bool installed); + void notesAppApiVersionsChanged(QStringList versions); // API helper updates void getFinished(QNetworkReply* reply); @@ -265,11 +276,18 @@ private: QString m_status_productname; bool m_status_extendedSupport; - // Nextcloud capabilities - bool updateCapabilities(const QJsonObject &json); - void setCababilitiesStatus(ApiCallStatus status, bool *changed = NULL); - ApiCallStatus m_capabilitiesStatus; - QJsonObject m_capabilities; + // Nextcloud Login Flow v2 + // https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 + bool updateLoginFlow(const QJsonObject &json); + bool updateLoginCredentials(const QJsonObject &json); + bool updateAppPassword(const QJsonObject &json); + bool deleteAppPassword(const QJsonObject &json); + void setLoginStatus(LoginStatus status, bool *changed = NULL); + LoginStatus m_loginStatus; + QTimer m_loginPollTimer; + QUrl m_loginUrl; + QUrl m_pollUrl; + QString m_pollToken; // Nextcloud users list bool updateUserList(const QJsonObject &json); @@ -283,18 +301,13 @@ private: ApiCallStatus m_userMetaStatus; QHash m_userMeta; - // Nextcloud Login Flow v2 - // https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 - bool updateLoginFlow(const QJsonObject &json); - bool updateLoginCredentials(const QJsonObject &json); - bool updateAppPassword(const QJsonObject &json); - bool deleteAppPassword(const QJsonObject &json); - void setLoginStatus(LoginStatus status, bool *changed = NULL); - LoginStatus m_loginStatus; - QTimer m_loginPollTimer; - QUrl m_loginUrl; - QUrl m_pollUrl; - QString m_pollToken; + // Nextcloud capabilities + bool updateCapabilities(const QJsonObject &json); + QMap m_appCapabilities; + void updateNoteCapabilities(const QJsonObject &newObject, const QJsonObject &preObject = QJsonObject()); + void setCababilitiesStatus(ApiCallStatus status, bool *changed = NULL); + ApiCallStatus m_capabilitiesStatus; + QJsonObject m_capabilities; }; #endif // NEXTCLOUDAPI_H diff --git a/translations/harbour-nextcloudnotes.ts b/translations/harbour-nextcloudnotes.ts index 5152263..170c864 100644 --- a/translations/harbour-nextcloudnotes.ts +++ b/translations/harbour-nextcloudnotes.ts @@ -275,32 +275,32 @@ NextcloudApi - + No error - + No network connection available - + Failed to communicate with the Nextcloud server - + An error occured while establishing an encrypted connection - + Could not authenticate to the Nextcloud instance - + Unknown error @@ -912,27 +912,27 @@ You can also use other markdown syntax inside them. harbour-nextcloudnotes - + Notes - + Offline - + Synced - + API error - + File error