service/CardDavClient.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QHash> | ||
| 4 | #include <QList> | ||
| 5 | #include <QNetworkReply> | ||
| 6 | #include <QObject> | ||
| 7 | #include <functional> | ||
| 8 | |||
| 9 | #include "data/ContactModels.h" | ||
| 10 | #include "service/CardDavXml.h" | ||
| 11 | #include "service/DavResponse.h" | ||
| 12 | |||
| 13 | class QNetworkAccessManager; | ||
| 14 | class QDomElement; | ||
| 15 | |||
| 16 | |||
| 17 | // CardDAV client (Sprint 97 S97.1.3 CONTACT-DISCOVERY, S97.1.5 | ||
| 18 | // CONTACT-SYNC-INCR, write requests for the ContactOutbox). | ||
| 19 | // | ||
| 20 | // Discovery like CalDAV (RFC 6352 §7.1, RFC 6764): the configured URL | ||
| 21 | // itself, then /.well-known/carddav → current-user-principal → | ||
| 22 | // addressbook-home-set → the books in the home (PROPFIND Depth 1 with | ||
| 23 | // display name, CTag, sync-token, privileges, supported-address-data and | ||
| 24 | // max-resource-size); the Nextcloud path /remote.php/dav/addressbooks/ | ||
| 25 | // users/<user>/ stays as the last fallback. | ||
| 26 | // | ||
| 27 | // Sync never downloads a whole book in one answer: fetchBook() reads the | ||
| 28 | // book's properties (one request; an unchanged sync-token or CTag ends the | ||
| 29 | // run there), listChanges() asks sync-collection for ETags only (or | ||
| 30 | // listEtags() PROPFINDs them on servers without RFC 6578), and multiget() | ||
| 31 | // fetches changed cards in batches. Every answer stays within | ||
| 32 | // DavNetworkLimits; a batch that is too large is reported as such so the | ||
| 33 | // caller can split it. | ||
| 34 | class CardDavClient : public QObject { | ||
| 35 | Q_OBJECT | ||
| 36 | |||
| 37 | public: | ||
| 38 | CardDavClient(const QString &serverUrl, const QString &username, const QString &password, | ||
| 39 | QObject *parent = nullptr); | ||
| 40 | |||
| 41 | void discoverAddressBooks(); | ||
| 42 | void fetchBook(const QString &bookPath); | ||
| 43 | void listChanges(const QString &bookPath, const QString &syncToken); | ||
| 44 | void listEtags(const QString &bookPath); | ||
| 45 | void multiget(const QString &bookPath, const QStringList &hrefs); | ||
| 46 | |||
| 47 | // Raw resource requests; each returns a request id echoed in | ||
| 48 | // resourceResponse(). An invalid If-Match tag is refused locally (status 0). | ||
| 49 | quint64 putResource(const QString &url, const QByteArray &body, const QString &ifMatch, bool createOnly); | ||
| 50 | quint64 deleteResource(const QString &url, const QString &ifMatch); | ||
| 51 | quint64 getResource(const QString &url); | ||
| 52 | // <book>/<name>.vcf for a new card; `name` is a fresh UUID, never a | ||
| 53 | // foreign UID (S97.1.4). | ||
| 54 | QString newResourceUrl(const QString &bookPath, const QString &name) const; | ||
| 55 | QString resolveDavUrl(const QString &href) const; | ||
| 56 | |||
| 57 | void setNetworkAccessManager(QNetworkAccessManager *nam); | ||
| 58 | // Sprint 96: the own push registration of the book the next PUT/DELETE | ||
| 59 | // writes to ("" = none). Sent as Push-Dont-Notify, never "*". | ||
| 60 | 13 | void setPushDontNotify(const QString ®istrationUrl) { m_pushDontNotify = registrationUrl; } | |
| 61 | 1 | QNetworkReply::NetworkError lastNetworkError() const { return m_lastNetworkError; } | |
| 62 | |||
| 63 | signals: | ||
| 64 | void addressBooksDiscovered(const QList<AddressBookInfo> &books); | ||
| 65 | void bookFetched(const QString &bookPath, const AddressBookInfo &book); | ||
| 66 | void changesListed(const QString &bookPath, const DavCardChanges &changes); | ||
| 67 | // The server has no sync-collection (invalidToken = false) or no longer | ||
| 68 | // accepts the token (true). | ||
| 69 | void collectionSyncRejected(const QString &bookPath, bool invalidToken); | ||
| 70 | void etagsListed(const QString &bookPath, const QHash<QString, QString> &etags); | ||
| 71 | void cardsFetched(const QString &bookPath, const QList<DavCardResource> &cards, const QStringList &missing); | ||
| 72 | // tooLarge: the answer hit DavNetworkLimits — split the batch. | ||
| 73 | void multigetFailed(const QString &bookPath, const QStringList &hrefs, const QString &error, bool tooLarge); | ||
| 74 | void syncFailed(const QString &error); | ||
| 75 | void resourceResponse(const DavResponse &response); | ||
| 76 | #ifdef MAILJD_E2E_TESTING | ||
| 77 | // Metadata only: operation class and an opaque per-client request token. | ||
| 78 | void e2eTransportPhase(const QString &phase, quint64 requestToken); | ||
| 79 | #endif | ||
| 80 | |||
| 81 | private: | ||
| 82 | enum class DiscoveryStep { ConfiguredCollection, WellKnown, Principal, Home, NextcloudFallback }; | ||
| 83 | |||
| 84 | QNetworkReply *send(const QByteArray &verb, const QString &url, const QByteArray &depth, const QByteArray &body, | ||
| 85 | const QString &operation); | ||
| 86 | void requestDiscovery(const QString &url, DiscoveryStep step); | ||
| 87 | void advanceDiscovery(const QString &reason); | ||
| 88 | void onDiscoverReply(QNetworkReply *reply); | ||
| 89 | QList<AddressBookInfo> booksIn(const QByteArray &xml) const; | ||
| 90 | AddressBookInfo bookFrom(const QDomElement &response) const; | ||
| 91 | // Common failure handling; false when the reply failed (signal emitted). | ||
| 92 | bool replyOk(QNetworkReply *reply, const QString &what); | ||
| 93 | quint64 sendResourceRequest(const QByteArray &method, const QString &url, const QByteArray &body, | ||
| 94 | const QList<QPair<QByteArray, QByteArray>> &headers); | ||
| 95 | QByteArray authHeader() const; | ||
| 96 | #ifdef MAILJD_E2E_TESTING | ||
| 97 | void reportE2eTransportPhase(const QString &operation); | ||
| 98 | #endif | ||
| 99 | |||
| 100 | QNetworkAccessManager *m_nam = nullptr; | ||
| 101 | QString m_serverUrl; | ||
| 102 | QString m_username; | ||
| 103 | QString m_password; | ||
| 104 | DiscoveryStep m_discoveryStep = DiscoveryStep::ConfiguredCollection; | ||
| 105 | int m_discoveryRedirects = 0; | ||
| 106 | QString m_discoveryError; | ||
| 107 | QNetworkReply::NetworkError m_lastNetworkError = QNetworkReply::NoError; | ||
| 108 | quint64 m_nextResourceRequest = 0; | ||
| 109 | bool m_pushPropertiesRejected = false; // retry fetchBook without push properties | ||
| 110 | QString m_pushDontNotify; | ||
| 111 | #ifdef MAILJD_E2E_TESTING | ||
| 112 | quint64 m_e2eRequestToken = 0; | ||
| 113 | #endif | ||
| 114 | }; | ||
| 115 | |||
| 116 |
4/7✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 14 not taken.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 14 → 15 not taken.
|
1 | Q_DECLARE_METATYPE(DavCardChanges) |
| 117 |