service/CardDavClient.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "CardDavClient.h" | ||
| 2 | |||
| 3 | #include <QDomDocument> | ||
| 4 | #include <QLoggingCategory> | ||
| 5 | #include <QNetworkAccessManager> | ||
| 6 | #include <QNetworkRequest> | ||
| 7 | #include <QSet> | ||
| 8 | #include <QTimer> | ||
| 9 | |||
| 10 | #include "DavNetworkLimits.h" | ||
| 11 | #include "DavPushXml.h" | ||
| 12 | #include "DavXmlHelper.h" | ||
| 13 | |||
| 14 |
4/7✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 8 taken 281 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 9 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
290 | Q_LOGGING_CATEGORY(lcCardDav, "mailjd.carddav") |
| 15 | |||
| 16 | using namespace DavXmlHelper; | ||
| 17 | |||
| 18 | namespace { | ||
| 19 | |||
| 20 | const QByteArray kBookProperties = | ||
| 21 | "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | ||
| 22 | "<d:propfind xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" " | ||
| 23 | "xmlns:cs=\"http://calendarserver.org/ns/\"><d:prop>" | ||
| 24 | "<d:resourcetype/><d:displayname/><cs:getctag/><d:sync-token/><d:current-user-privilege-set/>" | ||
| 25 | "<card:supported-address-data/><card:max-resource-size/><d:supported-report-set/>" | ||
| 26 | "</d:prop></d:propfind>"; | ||
| 27 | |||
| 28 | } // namespace | ||
| 29 | |||
| 30 | 234 | CardDavClient::CardDavClient(const QString &serverUrl, const QString &username, const QString &password, | |
| 31 | 234 | QObject *parent) | |
| 32 | 234 | : QObject(parent), m_serverUrl(DavNetworkLimits::requestServerUrl(serverUrl)), m_username(username), | |
| 33 |
1/1✓ Branch 3 → 4 taken 234 times.
|
468 | m_password(password) { |
| 34 |
3/3✓ Branch 9 → 10 taken 234 times.
✓ Branch 10 → 11 taken 31 times.
✓ Branch 10 → 12 taken 203 times.
|
234 | if (m_serverUrl.endsWith('/')) |
| 35 |
1/1✓ Branch 11 → 12 taken 31 times.
|
31 | m_serverUrl.chop(1); |
| 36 |
3/6✓ Branch 12 → 13 taken 234 times.
✓ Branch 13 → 14 taken 234 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 234 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
|
234 | m_nam = new QNetworkAccessManager(this); |
| 37 |
1/1✓ Branch 16 → 17 taken 234 times.
|
234 | m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy); |
| 38 | 234 | } | |
| 39 | |||
| 40 | 232 | void CardDavClient::setNetworkAccessManager(QNetworkAccessManager *nam) { | |
| 41 |
3/6✓ Branch 2 → 3 taken 232 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 232 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 232 times.
✗ Branch 7 → 10 not taken.
|
232 | if (m_nam && m_nam->parent() == this) |
| 42 |
1/2✓ Branch 8 → 9 taken 232 times.
✗ Branch 8 → 10 not taken.
|
232 | delete m_nam; |
| 43 | 232 | m_nam = nam; | |
| 44 |
2/2✓ Branch 10 → 11 taken 231 times.
✓ Branch 10 → 13 taken 1 time.
|
232 | if (m_nam) { |
| 45 | 231 | m_nam->setParent(this); | |
| 46 | 231 | m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy); | |
| 47 | } | ||
| 48 | 232 | } | |
| 49 | |||
| 50 | 428 | QByteArray CardDavClient::authHeader() const { | |
| 51 | // No credentials means an intentionally anonymous DAV request. | ||
| 52 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 427 times.
|
428 | if (m_username.isEmpty()) |
| 53 | 1 | return {}; | |
| 54 | // T-504: Refuse to send credentials over unencrypted connections | ||
| 55 |
5/5✓ Branch 6 → 7 taken 427 times.
✓ Branch 7 → 8 taken 427 times.
✓ Branch 8 → 9 taken 427 times.
✓ Branch 13 → 14 taken 47 times.
✓ Branch 13 → 25 taken 380 times.
|
427 | if (QUrl(m_serverUrl).scheme().toLower() == QLatin1String("http")) { |
| 56 |
5/5✓ Branch 14 → 15 taken 47 times.
✓ Branch 18 → 19 taken 47 times.
✓ Branch 19 → 20 taken 47 times.
✓ Branch 23 → 16 taken 47 times.
✓ Branch 23 → 24 taken 47 times.
|
94 | qCWarning(lcCardDav) << "Refusing Basic Auth over HTTP — use HTTPS"; |
| 57 | 47 | return {}; | |
| 58 | } | ||
| 59 |
5/5✓ Branch 28 → 29 taken 380 times.
✓ Branch 29 → 30 taken 380 times.
✓ Branch 30 → 31 taken 380 times.
✓ Branch 32 → 33 taken 380 times.
✓ Branch 33 → 34 taken 380 times.
|
1140 | return "Basic " + QByteArray((m_username + QStringLiteral(":") + m_password).toUtf8()).toBase64(); |
| 60 | } | ||
| 61 | |||
| 62 | #ifdef MAILJD_E2E_TESTING | ||
| 63 | 351 | void CardDavClient::reportE2eTransportPhase(const QString &operation) { | |
| 64 |
2/2✓ Branch 6 → 7 taken 351 times.
✓ Branch 7 → 8 taken 351 times.
|
1053 | emit e2eTransportPhase(QStringLiteral("request-dispatch:%1").arg(operation), ++m_e2eRequestToken); |
| 65 | 351 | } | |
| 66 | #endif | ||
| 67 | |||
| 68 | 11069 | QString CardDavClient::resolveDavUrl(const QString &href) const { | |
| 69 | 11069 | const QString resolved = CardDavXml::resolveHref(m_serverUrl, href); | |
| 70 |
2/2✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 16 taken 11066 times.
|
11069 | if (resolved.isEmpty()) |
| 71 |
5/5✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 7 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
|
6 | qCWarning(lcCardDav) << "Rejected cross-origin DAV URL"; |
| 72 | 11069 | return resolved; | |
| 73 | ✗ | } | |
| 74 | |||
| 75 | 1 | QString CardDavClient::newResourceUrl(const QString &bookPath, const QString &name) const { | |
| 76 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | QString book = resolveDavUrl(bookPath); |
| 77 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
|
1 | if (book.isEmpty()) |
| 78 | ✗ | return {}; | |
| 79 |
2/3✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 14 not taken.
|
1 | if (!book.endsWith(QLatin1Char('/'))) |
| 80 |
1/1✓ Branch 12 → 13 taken 1 time.
|
1 | book += QLatin1Char('/'); |
| 81 |
4/4✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
|
1 | return book + QString::fromUtf8(QUrl::toPercentEncoding(name)) + QStringLiteral(".vcf"); |
| 82 | 1 | } | |
| 83 | |||
| 84 | 234 | QNetworkReply *CardDavClient::send(const QByteArray &verb, const QString &url, const QByteArray &depth, | |
| 85 | const QByteArray &body, const QString &operation) { | ||
| 86 |
2/2✓ Branch 2 → 3 taken 234 times.
✓ Branch 3 → 4 taken 234 times.
|
234 | QNetworkRequest request{QUrl(url)}; |
| 87 |
3/3✓ Branch 5 → 6 taken 234 times.
✓ Branch 6 → 7 taken 234 times.
✓ Branch 7 → 8 taken 234 times.
|
234 | request.setRawHeader("Authorization", authHeader()); |
| 88 |
2/2✓ Branch 10 → 11 taken 234 times.
✓ Branch 11 → 12 taken 234 times.
|
234 | request.setRawHeader("Depth", depth); |
| 89 |
3/3✓ Branch 13 → 14 taken 234 times.
✓ Branch 14 → 15 taken 234 times.
✓ Branch 15 → 16 taken 234 times.
|
234 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 90 | #ifdef MAILJD_E2E_TESTING | ||
| 91 |
1/1✓ Branch 18 → 19 taken 222 times.
|
222 | reportE2eTransportPhase(operation); |
| 92 | #else | ||
| 93 | Q_UNUSED(operation); | ||
| 94 | #endif | ||
| 95 |
2/2✓ Branch 18 → 19 taken 12 times.
✓ Branch 19 → 20 taken 222 times.
|
234 | QNetworkReply *reply = m_nam->sendCustomRequest(request, verb, body); |
| 96 |
2/2✓ Branch 19 → 20 taken 12 times.
✓ Branch 20 → 21 taken 222 times.
|
234 | DavNetworkLimits::apply(reply); |
| 97 | 234 | return reply; | |
| 98 | 234 | } | |
| 99 | |||
| 100 | 137 | bool CardDavClient::replyOk(QNetworkReply *reply, const QString &what) { | |
| 101 |
1/1✓ Branch 2 → 3 taken 137 times.
|
137 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 102 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 19 taken 137 times.
|
137 | if (!limitError.isEmpty()) { |
| 103 | ✗ | qCWarning(lcCardDav) << what << "failed:" << limitError; | |
| 104 | ✗ | emit syncFailed(limitError); | |
| 105 | ✗ | return false; | |
| 106 | } | ||
| 107 |
3/3✓ Branch 19 → 20 taken 137 times.
✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 40 taken 129 times.
|
137 | if (reply->error() != QNetworkReply::NoError) { |
| 108 |
8/8✓ Branch 21 → 22 taken 8 times.
✓ Branch 25 → 26 taken 8 times.
✓ Branch 26 → 27 taken 8 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 28 → 29 taken 8 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 34 → 23 taken 8 times.
✓ Branch 34 → 35 taken 8 times.
|
16 | qCWarning(lcCardDav) << what << "failed:" << reply->errorString(); |
| 109 |
1/1✓ Branch 35 → 36 taken 8 times.
|
8 | m_lastNetworkError = reply->error(); |
| 110 |
2/2✓ Branch 36 → 37 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
|
8 | emit syncFailed(reply->errorString()); |
| 111 | 8 | return false; | |
| 112 | } | ||
| 113 | 129 | return true; | |
| 114 | 137 | } | |
| 115 | |||
| 116 | // ─── discovery ───────────────────────────────────────────── | ||
| 117 | |||
| 118 | 21 | void CardDavClient::discoverAddressBooks() { | |
| 119 | 21 | m_discoveryError.clear(); | |
| 120 | 21 | m_discoveryRedirects = 0; | |
| 121 | 21 | m_lastNetworkError = QNetworkReply::NoError; | |
| 122 | 21 | requestDiscovery(m_serverUrl, DiscoveryStep::ConfiguredCollection); | |
| 123 | 21 | } | |
| 124 | |||
| 125 | 31 | void CardDavClient::requestDiscovery(const QString &url, DiscoveryStep step) { | |
| 126 | 31 | m_discoveryStep = step; | |
| 127 |
1/1✓ Branch 2 → 3 taken 31 times.
|
31 | const QString resolved = resolveDavUrl(url); |
| 128 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 31 times.
|
31 | if (resolved.isEmpty()) { |
| 129 | ✗ | advanceDiscovery(QStringLiteral("Cross-origin discovery URL rejected")); | |
| 130 | ✗ | return; | |
| 131 | } | ||
| 132 | 31 | QByteArray body; | |
| 133 |
1/1✓ Branch 12 → 13 taken 31 times.
|
31 | QByteArray depth = "1"; |
| 134 |
2/2✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 16 taken 26 times.
|
31 | if (step == DiscoveryStep::WellKnown) { |
| 135 |
1/1✓ Branch 14 → 15 taken 5 times.
|
5 | depth = "0"; |
| 136 |
1/1✓ Branch 15 → 20 taken 5 times.
|
5 | body = "<d:propfind xmlns:d=\"DAV:\"><d:prop><d:current-user-principal/></d:prop></d:propfind>"; |
| 137 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 25 times.
|
26 | } else if (step == DiscoveryStep::Principal) { |
| 138 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | depth = "0"; |
| 139 | body = "<d:propfind xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\"><d:prop>" | ||
| 140 |
1/1✓ Branch 18 → 20 taken 1 time.
|
1 | "<card:addressbook-home-set/></d:prop></d:propfind>"; |
| 141 | } else { | ||
| 142 | 25 | body = kBookProperties; | |
| 143 | } | ||
| 144 |
2/2✓ Branch 20 → 21 taken 31 times.
✓ Branch 21 → 22 taken 31 times.
|
31 | QNetworkRequest request{QUrl(resolved)}; |
| 145 |
3/3✓ Branch 23 → 24 taken 31 times.
✓ Branch 24 → 25 taken 31 times.
✓ Branch 25 → 26 taken 31 times.
|
31 | request.setRawHeader("Authorization", authHeader()); |
| 146 |
2/2✓ Branch 28 → 29 taken 31 times.
✓ Branch 29 → 30 taken 31 times.
|
31 | request.setRawHeader("Depth", depth); |
| 147 |
3/3✓ Branch 31 → 32 taken 31 times.
✓ Branch 32 → 33 taken 31 times.
✓ Branch 33 → 34 taken 31 times.
|
31 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 148 | // /.well-known/carddav answers with a redirect; Qt would turn a followed | ||
| 149 | // 301/302 into a GET, so discovery follows redirects itself. | ||
| 150 |
1/1✓ Branch 37 → 38 taken 31 times.
|
31 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); |
| 151 | #ifdef MAILJD_E2E_TESTING | ||
| 152 |
1/1✓ Branch 42 → 43 taken 1 time.
|
1 | reportE2eTransportPhase(QStringLiteral("PROPFIND_DISCOVERY")); |
| 153 | #endif | ||
| 154 |
4/4✓ Branch 39 → 40 taken 30 times.
✓ Branch 40 → 41 taken 30 times.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
|
31 | auto *reply = m_nam->sendCustomRequest(request, "PROPFIND", body); |
| 155 |
2/2✓ Branch 42 → 43 taken 30 times.
✓ Branch 48 → 49 taken 1 time.
|
31 | DavNetworkLimits::apply(reply); |
| 156 |
2/2✓ Branch 43 → 44 taken 30 times.
✓ Branch 49 → 50 taken 1 time.
|
62 | connect(reply, &QNetworkReply::finished, this, [this, reply] { onDiscoverReply(reply); }); |
| 157 | 31 | } | |
| 158 | |||
| 159 | 9 | void CardDavClient::advanceDiscovery(const QString &reason) { | |
| 160 |
1/2✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 5 not taken.
|
9 | if (!reason.isEmpty()) |
| 161 | 9 | m_discoveryError = reason; | |
| 162 |
1/1✓ Branch 5 → 6 taken 9 times.
|
9 | QUrl origin(m_serverUrl); |
| 163 |
1/1✓ Branch 9 → 10 taken 9 times.
|
9 | origin.setPath(QStringLiteral("/")); |
| 164 |
1/1✓ Branch 13 → 14 taken 9 times.
|
9 | origin.setQuery(QString()); |
| 165 |
1/1✓ Branch 16 → 17 taken 9 times.
|
9 | origin.setFragment(QString()); |
| 166 |
2/2✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 33 taken 5 times.
|
9 | if (m_discoveryStep == DiscoveryStep::ConfiguredCollection) { |
| 167 |
4/4✓ Branch 22 → 23 taken 4 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
|
4 | requestDiscovery(origin.resolved(QUrl(QStringLiteral("/.well-known/carddav"))).toString(), |
| 168 | DiscoveryStep::WellKnown); | ||
| 169 | 4 | return; | |
| 170 | } | ||
| 171 |
2/2✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 59 taken 2 times.
|
5 | if (m_discoveryStep != DiscoveryStep::NextcloudFallback) { |
| 172 | // T-505: the user name is percent-encoded (no path traversal). | ||
| 173 | const QString fallback = | ||
| 174 |
1/1✓ Branch 44 → 45 taken 3 times.
|
12 | origin.resolved(QUrl(QStringLiteral("/remote.php/dav/addressbooks/users/%1/") |
| 175 |
4/4✓ Branch 40 → 41 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
✓ Branch 43 → 44 taken 3 times.
|
6 | .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_username))))) |
| 176 |
1/1✓ Branch 46 → 47 taken 3 times.
|
3 | .toString(); |
| 177 |
1/1✓ Branch 56 → 57 taken 3 times.
|
3 | requestDiscovery(fallback, DiscoveryStep::NextcloudFallback); |
| 178 | 3 | return; | |
| 179 | 3 | } | |
| 180 |
3/7✗ Branch 60 → 61 not taken.
✓ Branch 60 → 64 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 2 times.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
|
2 | emit syncFailed(m_discoveryError.isEmpty() ? QStringLiteral("CardDAV discovery failed") : m_discoveryError); |
| 181 | 9 | } | |
| 182 | |||
| 183 | 31 | void CardDavClient::onDiscoverReply(QNetworkReply *reply) { | |
| 184 |
1/1✓ Branch 2 → 3 taken 31 times.
|
31 | reply->deleteLater(); |
| 185 |
1/1✓ Branch 3 → 4 taken 31 times.
|
31 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 186 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 31 times.
|
31 | if (!limitError.isEmpty()) { |
| 187 | ✗ | advanceDiscovery(limitError); | |
| 188 | ✗ | return; | |
| 189 | } | ||
| 190 |
2/2✓ Branch 8 → 9 taken 31 times.
✓ Branch 9 → 10 taken 31 times.
|
31 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 191 |
5/8✓ Branch 11 → 12 taken 30 times.
✓ Branch 11 → 15 taken 1 time.
✓ Branch 12 → 13 taken 30 times.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 30 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 55 taken 30 times.
|
31 | if (status == 301 || status == 302 || status == 307 || status == 308) { |
| 192 |
4/4✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
|
2 | const QString target = resolveDavUrl(QString::fromUtf8(reply->rawHeader("Location")).trimmed()); |
| 193 |
3/6✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 30 not taken.
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 46 not taken.
|
1 | if (!target.isEmpty() && m_discoveryRedirects < 3) { |
| 194 | 1 | ++m_discoveryRedirects; | |
| 195 |
7/7✓ Branch 32 → 33 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 43 → 34 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
|
2 | qCInfo(lcCardDav) << "Discovery redirected (" << status << ")"; |
| 196 |
1/1✓ Branch 44 → 45 taken 1 time.
|
1 | requestDiscovery(target, m_discoveryStep); |
| 197 | 1 | return; | |
| 198 | } | ||
| 199 | ✗ | advanceDiscovery(QStringLiteral("CardDAV discovery redirect refused")); | |
| 200 | ✗ | return; | |
| 201 | 1 | } | |
| 202 |
3/3✓ Branch 55 → 56 taken 30 times.
✓ Branch 56 → 57 taken 5 times.
✓ Branch 56 → 64 taken 25 times.
|
30 | if (reply->error() != QNetworkReply::NoError) { |
| 203 | // A rejected password stays the reported cause. | ||
| 204 |
2/2✓ Branch 57 → 58 taken 4 times.
✓ Branch 57 → 60 taken 1 time.
|
5 | if (m_lastNetworkError != QNetworkReply::AuthenticationRequiredError) |
| 205 |
1/1✓ Branch 58 → 59 taken 4 times.
|
4 | m_lastNetworkError = reply->error(); |
| 206 |
2/2✓ Branch 60 → 61 taken 5 times.
✓ Branch 61 → 62 taken 5 times.
|
5 | advanceDiscovery(reply->errorString()); |
| 207 | 5 | return; | |
| 208 | } | ||
| 209 |
1/1✓ Branch 64 → 65 taken 25 times.
|
25 | const QByteArray data = reply->readAll(); |
| 210 |
1/1✓ Branch 65 → 66 taken 25 times.
|
25 | QDomDocument doc; |
| 211 |
3/3✓ Branch 66 → 67 taken 25 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 67 → 75 taken 22 times.
|
25 | if (!DavXmlHelper::parseInto(doc, data)) { |
| 212 |
1/1✓ Branch 71 → 72 taken 3 times.
|
3 | advanceDiscovery(QStringLiteral("Invalid XML response")); |
| 213 | 3 | return; | |
| 214 | } | ||
| 215 |
4/4✓ Branch 75 → 76 taken 21 times.
✓ Branch 75 → 77 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 76 → 113 taken 20 times.
|
22 | if (m_discoveryStep == DiscoveryStep::WellKnown || m_discoveryStep == DiscoveryStep::Principal) { |
| 216 | 1 | const QString property = m_discoveryStep == DiscoveryStep::WellKnown ? QStringLiteral("current-user-principal") | |
| 217 |
6/6✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 81 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 84 → 86 taken 1 time.
✓ Branch 86 → 87 taken 1 time.
✓ Branch 86 → 88 taken 1 time.
|
4 | : QStringLiteral("addressbook-home-set"); |
| 218 |
2/2✓ Branch 88 → 89 taken 2 times.
✓ Branch 89 → 90 taken 2 times.
|
2 | const QString href = hrefOfProperty(doc.documentElement(), property); |
| 219 |
2/3✗ Branch 92 → 93 not taken.
✓ Branch 92 → 94 taken 2 times.
✓ Branch 94 → 95 taken 2 times.
|
2 | const QString resolved = href.isEmpty() ? QString() : resolveDavUrl(href); |
| 220 |
1/2✗ Branch 96 → 97 not taken.
✓ Branch 96 → 104 taken 2 times.
|
2 | if (resolved.isEmpty()) { |
| 221 | ✗ | advanceDiscovery(QStringLiteral("CardDAV discovery property missing")); | |
| 222 | ✗ | return; | |
| 223 | } | ||
| 224 |
3/3✓ Branch 104 → 105 taken 1 time.
✓ Branch 104 → 106 taken 1 time.
✓ Branch 107 → 108 taken 2 times.
|
2 | requestDiscovery(resolved, m_discoveryStep == DiscoveryStep::WellKnown ? DiscoveryStep::Principal |
| 225 | : DiscoveryStep::Home); | ||
| 226 | 2 | return; | |
| 227 | 2 | } | |
| 228 |
1/1✓ Branch 113 → 114 taken 20 times.
|
20 | const QList<AddressBookInfo> books = booksIn(data); |
| 229 |
2/2✓ Branch 115 → 116 taken 1 time.
✓ Branch 115 → 123 taken 19 times.
|
20 | if (books.isEmpty()) { |
| 230 |
1/1✓ Branch 119 → 120 taken 1 time.
|
1 | advanceDiscovery(QStringLiteral("No address books found at discovery URL")); |
| 231 | 1 | return; | |
| 232 | } | ||
| 233 |
7/7✓ Branch 123 → 124 taken 19 times.
✓ Branch 127 → 128 taken 19 times.
✓ Branch 128 → 129 taken 19 times.
✓ Branch 130 → 131 taken 19 times.
✓ Branch 131 → 132 taken 19 times.
✓ Branch 135 → 125 taken 19 times.
✓ Branch 135 → 136 taken 19 times.
|
38 | qCInfo(lcCardDav) << "Discovered" << books.size() << "address books"; |
| 234 |
1/1✓ Branch 136 → 137 taken 19 times.
|
19 | emit addressBooksDiscovered(books); |
| 235 | 44 | } | |
| 236 | |||
| 237 | 75 | AddressBookInfo CardDavClient::bookFrom(const QDomElement &response) const { | |
| 238 | 75 | return CardDavXml::book(response); | |
| 239 | } | ||
| 240 | |||
| 241 | 20 | QList<AddressBookInfo> CardDavClient::booksIn(const QByteArray &xml) const { | |
| 242 | 20 | return CardDavXml::parseBooks(xml); | |
| 243 | } | ||
| 244 | |||
| 245 | // ─── sync ────────────────────────────────────────────────── | ||
| 246 | |||
| 247 | 84 | void CardDavClient::fetchBook(const QString &bookPath) { | |
| 248 |
1/1✓ Branch 2 → 3 taken 84 times.
|
84 | const QString url = resolveDavUrl(bookPath); |
| 249 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 84 times.
|
84 | if (url.isEmpty()) { |
| 250 | ✗ | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); | |
| 251 | ✗ | return; | |
| 252 | } | ||
| 253 | // Sprint 96 S96.6.2: the WebDAV-Push offer travels with the book's | ||
| 254 | // properties (no extra request). A server that rejects the unknown | ||
| 255 | // properties gets the same PROPFIND once more without them. | ||
| 256 | 84 | const bool push = !m_pushPropertiesRejected; | |
| 257 | 84 | QByteArray body = kBookProperties; | |
| 258 |
2/2✓ Branch 13 → 14 taken 83 times.
✓ Branch 13 → 31 taken 1 time.
|
84 | if (push) |
| 259 |
3/3✓ Branch 14 → 15 taken 83 times.
✓ Branch 15 → 16 taken 83 times.
✓ Branch 18 → 19 taken 83 times.
|
166 | body.replace("<d:prop>", "<d:prop>" + DavPushXml::discoveryProperties()) |
| 260 |
4/4✓ Branch 19 → 20 taken 83 times.
✓ Branch 20 → 21 taken 83 times.
✓ Branch 21 → 22 taken 83 times.
✓ Branch 24 → 25 taken 83 times.
|
83 | .replace("<d:propfind ", "<d:propfind xmlns:P=\"" + DavPushXml::kNamespace.toLatin1() + "\" "); |
| 261 |
3/3✓ Branch 34 → 35 taken 84 times.
✓ Branch 35 → 36 taken 84 times.
✓ Branch 36 → 37 taken 84 times.
|
84 | QNetworkReply *reply = send("PROPFIND", url, "0", body, QStringLiteral("PROPFIND_BOOK")); |
| 262 |
1/3✓ Branch 42 → 43 taken 84 times.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
|
84 | connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, push] { |
| 263 |
1/1✓ Branch 2 → 3 taken 84 times.
|
84 | reply->deleteLater(); |
| 264 |
2/2✓ Branch 3 → 4 taken 84 times.
✓ Branch 4 → 5 taken 84 times.
|
84 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 265 |
5/6✓ Branch 6 → 7 taken 83 times.
✓ Branch 6 → 23 taken 1 time.
✓ Branch 7 → 8 taken 82 times.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 23 taken 82 times.
|
84 | if (push && (status == 400 || status == 422)) { |
| 266 |
7/7✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 11 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
|
2 | qCInfo(lcCardDav) << "PROPFIND with push properties rejected (" << status << "), retrying without"; |
| 267 | 1 | m_pushPropertiesRejected = true; | |
| 268 |
1/1✓ Branch 21 → 22 taken 1 time.
|
1 | fetchBook(bookPath); |
| 269 | 9 | return; | |
| 270 | } | ||
| 271 |
3/3✓ Branch 26 → 27 taken 83 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 31 taken 75 times.
|
166 | if (!replyOk(reply, QStringLiteral("PROPFIND (book)"))) |
| 272 | 8 | return; | |
| 273 |
1/1✓ Branch 31 → 32 taken 75 times.
|
75 | const QByteArray data = reply->readAll(); |
| 274 |
1/1✓ Branch 32 → 33 taken 75 times.
|
75 | QDomDocument doc; |
| 275 |
2/3✓ Branch 33 → 34 taken 75 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 42 taken 75 times.
|
75 | if (!DavXmlHelper::parseInto(doc, data)) { |
| 276 | ✗ | emit syncFailed(QStringLiteral("Invalid XML response")); | |
| 277 | ✗ | return; | |
| 278 | } | ||
| 279 |
1/1✓ Branch 45 → 46 taken 75 times.
|
75 | const auto responses = findElementsByLocalNameDoc(doc, QStringLiteral("response")); |
| 280 |
1/2✗ Branch 49 → 50 not taken.
✓ Branch 49 → 57 taken 75 times.
|
75 | if (responses.isEmpty()) { |
| 281 | ✗ | emit syncFailed(QStringLiteral("Empty PROPFIND response")); | |
| 282 | ✗ | return; | |
| 283 | } | ||
| 284 |
3/3✓ Branch 57 → 58 taken 75 times.
✓ Branch 58 → 59 taken 75 times.
✓ Branch 59 → 60 taken 75 times.
|
75 | AddressBookInfo book = bookFrom(responses.at(0).toElement()); |
| 285 |
2/2✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 70 taken 74 times.
|
75 | if (!push) { |
| 286 | 1 | book.push.capability = DavPushCapability::Unsupported; | |
| 287 | 1 | book.push.reason = QStringLiteral("server rejects the push discovery properties"); | |
| 288 | } else { | ||
| 289 | 74 | QString pushError; | |
| 290 |
1/1✓ Branch 70 → 71 taken 74 times.
|
74 | const auto offers = DavPushXml::parseDiscovery(data, &pushError); |
| 291 |
1/2✗ Branch 72 → 73 not taken.
✓ Branch 72 → 74 taken 74 times.
|
74 | if (!offers) { |
| 292 | ✗ | book.push.capability = DavPushCapability::ProbeError; | |
| 293 | ✗ | book.push.reason = pushError; | |
| 294 | } else { | ||
| 295 | // Depth 0: the one response is this book (its href may be spelled | ||
| 296 | // differently from the stored path). | ||
| 297 |
1/2✓ Branch 77 → 78 taken 74 times.
✗ Branch 77 → 82 not taken.
|
74 | book.push = offers->value(book.path, offers->size() == 1 ? offers->cbegin().value() : DavPushOffer{}); |
| 298 | } | ||
| 299 | 74 | } | |
| 300 | 75 | book.path = bookPath; | |
| 301 |
1/1✓ Branch 92 → 93 taken 75 times.
|
75 | emit bookFetched(bookPath, book); |
| 302 |
3/6✓ Branch 96 → 97 taken 75 times.
✗ Branch 96 → 98 not taken.
✓ Branch 100 → 101 taken 75 times.
✗ Branch 100 → 102 not taken.
✓ Branch 104 → 105 taken 75 times.
✗ Branch 104 → 107 not taken.
|
75 | }); |
| 303 | 84 | } | |
| 304 | |||
| 305 | 55 | void CardDavClient::listChanges(const QString &bookPath, const QString &syncToken) { | |
| 306 |
1/1✓ Branch 2 → 3 taken 55 times.
|
55 | const QString url = resolveDavUrl(bookPath); |
| 307 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 55 times.
|
55 | if (url.isEmpty()) { |
| 308 | ✗ | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); | |
| 309 | ✗ | return; | |
| 310 | } | ||
| 311 | const QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | ||
| 312 | 55 | "<d:sync-collection xmlns:d=\"DAV:\"><d:sync-token>" + | |
| 313 |
3/3✓ Branch 12 → 13 taken 55 times.
✓ Branch 13 → 14 taken 55 times.
✓ Branch 14 → 15 taken 55 times.
|
110 | syncToken.toHtmlEscaped().toUtf8() + |
| 314 | "</d:sync-token><d:sync-level>1</d:sync-level>" | ||
| 315 |
1/1✓ Branch 15 → 16 taken 55 times.
|
55 | "<d:prop><d:getetag/></d:prop></d:sync-collection>"; |
| 316 |
3/3✓ Branch 22 → 23 taken 55 times.
✓ Branch 23 → 24 taken 55 times.
✓ Branch 24 → 25 taken 55 times.
|
55 | QNetworkReply *reply = send("REPORT", url, "0", body, QStringLiteral("REPORT_SYNC_COLLECTION")); |
| 317 | 55 | const bool full = syncToken.isEmpty(); | |
| 318 |
1/5✓ Branch 32 → 33 taken 55 times.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
|
55 | connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, url, full] { |
| 319 |
1/1✓ Branch 2 → 3 taken 55 times.
|
55 | reply->deleteLater(); |
| 320 |
2/2✓ Branch 3 → 4 taken 55 times.
✓ Branch 4 → 5 taken 55 times.
|
55 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 321 |
8/12✓ Branch 6 → 7 taken 55 times.
✓ Branch 8 → 9 taken 55 times.
✗ Branch 8 → 12 not taken.
✓ Branch 9 → 10 taken 55 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 51 times.
✓ Branch 13 → 14 taken 55 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 43 taken 51 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
|
55 | if (DavNetworkLimits::failureReason(reply).isEmpty() && reply->error() != QNetworkReply::NoError) { |
| 322 | // RFC 6578 §3.2: an expired token is 403/409 with valid-sync-token; | ||
| 323 | // 400/404/405/415/501 mean the report is not supported. | ||
| 324 |
1/1✓ Branch 16 → 17 taken 4 times.
|
4 | const QByteArray error = reply->readAll(); |
| 325 |
6/10✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 24 taken 1 time.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 24 not taken.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 24 not taken.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 28 taken 1 time.
|
4 | if (!full && (status == 403 || status == 409) && error.contains("valid-sync-token")) { |
| 326 |
1/1✓ Branch 26 → 27 taken 3 times.
|
3 | emit collectionSyncRejected(bookPath, true); |
| 327 | 3 | return; | |
| 328 | } | ||
| 329 |
7/14✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 35 not taken.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 35 not taken.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 35 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 35 not taken.
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 35 not taken.
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 35 not taken.
✓ Branch 34 → 35 taken 1 time.
✗ Branch 34 → 37 not taken.
|
1 | if (status == 400 || status == 403 || status == 404 || status == 405 || status == 409 || status == 415 || |
| 330 | status == 501) { | ||
| 331 |
1/1✓ Branch 35 → 36 taken 1 time.
|
1 | emit collectionSyncRejected(bookPath, false); |
| 332 | 1 | return; | |
| 333 | } | ||
| 334 | 4 | } | |
| 335 |
2/3✓ Branch 46 → 47 taken 51 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 51 times.
|
102 | if (!replyOk(reply, QStringLiteral("REPORT (sync-collection)"))) |
| 336 | ✗ | return; | |
| 337 |
1/1✓ Branch 51 → 52 taken 51 times.
|
51 | const QByteArray data = reply->readAll(); |
| 338 | 51 | DavCardChanges changes; | |
| 339 | 51 | changes.full = full; | |
| 340 | 5325 | const auto resolve = [this](const QString &href) { return resolveDavUrl(href); }; | |
| 341 |
2/3✓ Branch 53 → 54 taken 51 times.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 63 taken 51 times.
|
51 | if (!CardDavXml::parseChanges(data, resolve, &changes)) { |
| 342 | ✗ | emit syncFailed(QStringLiteral("Invalid XML response")); | |
| 343 | ✗ | return; | |
| 344 | } | ||
| 345 |
2/2✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 67 taken 50 times.
|
51 | if (changes.syncToken.isEmpty()) { |
| 346 |
1/1✓ Branch 65 → 66 taken 1 time.
|
1 | emit collectionSyncRejected(bookPath, false); |
| 347 | 1 | return; | |
| 348 | } | ||
| 349 |
10/10✓ Branch 67 → 68 taken 50 times.
✓ Branch 71 → 72 taken 50 times.
✓ Branch 72 → 73 taken 50 times.
✓ Branch 73 → 74 taken 31 times.
✓ Branch 73 → 75 taken 19 times.
✓ Branch 76 → 77 taken 50 times.
✓ Branch 77 → 78 taken 50 times.
✓ Branch 79 → 80 taken 50 times.
✓ Branch 86 → 69 taken 50 times.
✓ Branch 86 → 87 taken 50 times.
|
100 | qCInfo(lcCardDav) << "sync-collection" << (full ? "full" : "delta") << "changed" << changes.changed.size() |
| 350 |
2/2✓ Branch 80 → 81 taken 50 times.
✓ Branch 82 → 83 taken 50 times.
|
50 | << "removed" << changes.removed.size(); |
| 351 |
1/1✓ Branch 87 → 88 taken 50 times.
|
50 | emit changesListed(bookPath, changes); |
| 352 |
4/4✓ Branch 90 → 91 taken 50 times.
✓ Branch 90 → 92 taken 1 time.
✓ Branch 94 → 95 taken 50 times.
✓ Branch 94 → 97 taken 1 time.
|
52 | }); |
| 353 | 55 | } | |
| 354 | |||
| 355 | 3 | void CardDavClient::listEtags(const QString &bookPath) { | |
| 356 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | const QString url = resolveDavUrl(bookPath); |
| 357 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 3 times.
|
3 | if (url.isEmpty()) { |
| 358 | ✗ | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); | |
| 359 | ✗ | return; | |
| 360 | } | ||
| 361 | const QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | ||
| 362 |
1/1✓ Branch 12 → 13 taken 3 times.
|
3 | "<d:propfind xmlns:d=\"DAV:\"><d:prop><d:resourcetype/><d:getetag/></d:prop></d:propfind>"; |
| 363 |
3/3✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
|
3 | QNetworkReply *reply = send("PROPFIND", url, "1", body, QStringLiteral("PROPFIND_ETAGS")); |
| 364 |
1/1✓ Branch 24 → 25 taken 3 times.
|
3 | connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath] { |
| 365 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | reply->deleteLater(); |
| 366 |
2/3✓ Branch 6 → 7 taken 3 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3 times.
|
6 | if (!replyOk(reply, QStringLiteral("PROPFIND (etags)"))) |
| 367 | ✗ | return; | |
| 368 | 3 | QHash<QString, QString> etags; | |
| 369 | 4 | const auto resolve = [this](const QString &href) { return resolveDavUrl(href); }; | |
| 370 |
3/4✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 24 taken 3 times.
|
3 | if (!CardDavXml::parseEtags(reply->readAll(), resolve, &etags)) { |
| 371 | ✗ | emit syncFailed(QStringLiteral("Invalid XML response")); | |
| 372 | ✗ | return; | |
| 373 | } | ||
| 374 |
1/1✓ Branch 24 → 25 taken 3 times.
|
3 | emit etagsListed(bookPath, etags); |
| 375 |
1/2✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 30 not taken.
|
3 | }); |
| 376 | 3 | } | |
| 377 | |||
| 378 | 92 | void CardDavClient::multiget(const QString &bookPath, const QStringList &hrefs) { | |
| 379 |
1/1✓ Branch 2 → 3 taken 92 times.
|
92 | const QString url = resolveDavUrl(bookPath); |
| 380 |
3/6✓ Branch 4 → 5 taken 92 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 92 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 17 taken 92 times.
|
92 | if (url.isEmpty() || hrefs.isEmpty()) { |
| 381 | ✗ | emit multigetFailed(bookPath, hrefs, QStringLiteral("Cross-origin URL rejected"), false); | |
| 382 | ✗ | return; | |
| 383 | } | ||
| 384 | QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | ||
| 385 | "<card:addressbook-multiget xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\">" | ||
| 386 |
1/1✓ Branch 17 → 18 taken 92 times.
|
92 | "<d:prop><d:getetag/><card:address-data/></d:prop>"; |
| 387 |
2/2✓ Branch 37 → 20 taken 5314 times.
✓ Branch 37 → 38 taken 92 times.
|
5406 | for (const QString &href : hrefs) |
| 388 |
7/7✓ Branch 21 → 22 taken 5314 times.
✓ Branch 23 → 24 taken 5314 times.
✓ Branch 24 → 25 taken 5314 times.
✓ Branch 25 → 26 taken 5314 times.
✓ Branch 26 → 27 taken 5314 times.
✓ Branch 27 → 28 taken 5314 times.
✓ Branch 28 → 29 taken 5314 times.
|
5314 | body += "<d:href>" + QUrl(href).path(QUrl::FullyEncoded).toHtmlEscaped().toUtf8() + "</d:href>"; |
| 389 |
1/1✓ Branch 38 → 39 taken 92 times.
|
92 | body += "</card:addressbook-multiget>"; |
| 390 |
3/3✓ Branch 42 → 43 taken 92 times.
✓ Branch 43 → 44 taken 92 times.
✓ Branch 44 → 45 taken 92 times.
|
92 | QNetworkReply *reply = send("REPORT", url, "1", body, QStringLiteral("REPORT_MULTIGET")); |
| 391 |
1/3✓ Branch 51 → 52 taken 92 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
|
92 | connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, hrefs] { |
| 392 |
1/1✓ Branch 2 → 3 taken 92 times.
|
92 | reply->deleteLater(); |
| 393 |
1/1✓ Branch 3 → 4 taken 92 times.
|
92 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 394 |
6/7✓ Branch 5 → 6 taken 90 times.
✓ Branch 5 → 8 taken 2 times.
✓ Branch 6 → 7 taken 90 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 90 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 43 taken 90 times.
|
92 | if (!limitError.isEmpty() || reply->error() != QNetworkReply::NoError) { |
| 395 |
1/1✓ Branch 12 → 13 taken 2 times.
|
2 | const bool tooLarge = limitError.contains(QLatin1String("too large")); |
| 396 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 2 times.
|
2 | if (limitError.isEmpty()) |
| 397 | ✗ | m_lastNetworkError = reply->error(); | |
| 398 |
7/7✓ Branch 17 → 18 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 35 → 19 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
|
4 | qCWarning(lcCardDav) << "REPORT (multiget) of" << hrefs.size() << "cards failed:" |
| 399 |
2/4✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 2 times.
✗ Branch 28 → 30 not taken.
✓ Branch 30 → 31 taken 2 times.
|
2 | << (limitError.isEmpty() ? reply->errorString() : limitError); |
| 400 |
2/4✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
✓ Branch 40 → 41 taken 2 times.
|
2 | emit multigetFailed(bookPath, hrefs, limitError.isEmpty() ? reply->errorString() : limitError, tooLarge); |
| 401 | 2 | return; | |
| 402 | } | ||
| 403 | 90 | QList<DavCardResource> cards; | |
| 404 | 90 | QSet<QString> answered; // decoded paths: servers re-encode hrefs differently | |
| 405 | 5303 | const auto resolve = [this](const QString &href) { return resolveDavUrl(href); }; | |
| 406 |
3/4✓ Branch 45 → 46 taken 90 times.
✓ Branch 46 → 47 taken 90 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 57 taken 90 times.
|
90 | if (!CardDavXml::parseCards(reply->readAll(), resolve, &cards, &answered)) { |
| 407 | ✗ | emit multigetFailed(bookPath, hrefs, QStringLiteral("Invalid XML response"), false); | |
| 408 | ✗ | return; | |
| 409 | } | ||
| 410 | 90 | QStringList missing; | |
| 411 |
2/2✓ Branch 70 → 59 taken 5304 times.
✓ Branch 70 → 71 taken 90 times.
|
5394 | for (const QString &href : hrefs) |
| 412 |
4/4✓ Branch 60 → 61 taken 5304 times.
✓ Branch 62 → 63 taken 5304 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 5302 times.
|
10608 | if (!answered.contains(QUrl(href).path())) |
| 413 |
1/1✓ Branch 67 → 68 taken 2 times.
|
2 | missing << href; |
| 414 |
1/1✓ Branch 71 → 72 taken 90 times.
|
90 | emit cardsFetched(bookPath, cards, missing); |
| 415 |
4/6✓ Branch 75 → 76 taken 90 times.
✗ Branch 75 → 77 not taken.
✓ Branch 79 → 80 taken 90 times.
✗ Branch 79 → 81 not taken.
✓ Branch 83 → 84 taken 90 times.
✓ Branch 83 → 86 taken 2 times.
|
92 | }); |
| 416 | 92 | } | |
| 417 | |||
| 418 | // ─── raw resource requests ───────────────────────────────── | ||
| 419 | |||
| 420 | 169 | quint64 CardDavClient::sendResourceRequest(const QByteArray &method, const QString &url, const QByteArray &body, | |
| 421 | const QList<QPair<QByteArray, QByteArray>> &headers) { | ||
| 422 | 169 | const quint64 id = ++m_nextResourceRequest; | |
| 423 |
3/3✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 164 times.
✓ Branch 5 → 6 taken 164 times.
|
169 | const QString resolved = url.isEmpty() ? QString() : resolveDavUrl(url); |
| 424 |
2/2✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 13 taken 163 times.
|
169 | if (resolved.isEmpty()) { |
| 425 | // Answer asynchronously like a network reply would. | ||
| 426 |
1/5✓ Branch 10 → 11 taken 6 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 83 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
|
6 | QTimer::singleShot(0, this, [this, id, method, url] { |
| 427 | 6 | DavResponse response; | |
| 428 | 6 | response.request = id; | |
| 429 | 6 | response.method = method; | |
| 430 | 6 | response.url = url; | |
| 431 | 6 | response.error = QStringLiteral("Rejected or empty resource URL"); | |
| 432 | 6 | response.refusedLocally = true; | |
| 433 |
1/1✓ Branch 10 → 11 taken 6 times.
|
6 | emit resourceResponse(response); |
| 434 | 6 | }); | |
| 435 | 6 | return id; | |
| 436 | } | ||
| 437 |
2/2✓ Branch 13 → 14 taken 163 times.
✓ Branch 14 → 15 taken 163 times.
|
163 | QNetworkRequest request{QUrl(resolved)}; |
| 438 |
3/3✓ Branch 16 → 17 taken 163 times.
✓ Branch 17 → 18 taken 163 times.
✓ Branch 18 → 19 taken 163 times.
|
163 | request.setRawHeader("Authorization", authHeader()); |
| 439 |
2/2✓ Branch 22 → 23 taken 88 times.
✓ Branch 22 → 29 taken 75 times.
|
163 | if (!body.isEmpty()) |
| 440 |
3/3✓ Branch 23 → 24 taken 88 times.
✓ Branch 24 → 25 taken 88 times.
✓ Branch 25 → 26 taken 88 times.
|
88 | request.setRawHeader("Content-Type", "text/vcard; charset=utf-8"); |
| 441 | // S96.3.7/S96.6.2: own writes name our push registration of the book. | ||
| 442 |
8/8✓ Branch 30 → 31 taken 75 times.
✓ Branch 30 → 33 taken 88 times.
✓ Branch 32 → 33 taken 18 times.
✓ Branch 32 → 36 taken 57 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 105 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 48 taken 162 times.
|
163 | if ((method == "PUT" || method == "DELETE") && !m_pushDontNotify.isEmpty()) |
| 443 |
5/5✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
|
1 | request.setRawHeader("Push-Dont-Notify", "\"" + m_pushDontNotify.toUtf8() + "\""); |
| 444 |
2/2✓ Branch 54 → 50 taken 106 times.
✓ Branch 54 → 55 taken 163 times.
|
269 | for (const auto &header : headers) |
| 445 |
1/1✓ Branch 51 → 52 taken 106 times.
|
106 | request.setRawHeader(header.first, header.second); |
| 446 | #ifdef MAILJD_E2E_TESTING | ||
| 447 |
3/3✓ Branch 58 → 59 taken 128 times.
✓ Branch 59 → 60 taken 128 times.
✓ Branch 60 → 61 taken 128 times.
|
128 | reportE2eTransportPhase(QString::fromLatin1(method) + QStringLiteral("_RESOURCE")); |
| 448 | #endif | ||
| 449 | // S106-058: a resource request never follows a redirect — QNAM re-sent a | ||
| 450 | // followed 301/302 PUT/DELETE as GET, and the GET's status/ETag was taken | ||
| 451 | // for the write result (the edit was lost, a DELETE looked done). The 3xx | ||
| 452 | // is the answer. | ||
| 453 |
2/2✓ Branch 56 → 57 taken 35 times.
✓ Branch 66 → 67 taken 128 times.
|
163 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); |
| 454 |
6/6✓ Branch 59 → 60 taken 13 times.
✓ Branch 59 → 62 taken 22 times.
✓ Branch 60 → 61 taken 13 times.
✓ Branch 69 → 70 taken 75 times.
✓ Branch 69 → 72 taken 53 times.
✓ Branch 70 → 71 taken 75 times.
|
238 | QNetworkReply *reply = method == "PUT" ? m_nam->put(request, body) |
| 455 |
6/6✓ Branch 63 → 64 taken 3 times.
✓ Branch 63 → 66 taken 19 times.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 73 → 74 taken 15 times.
✓ Branch 73 → 76 taken 38 times.
✓ Branch 74 → 75 taken 15 times.
|
75 | : method == "DELETE" ? m_nam->deleteResource(request) |
| 456 |
2/2✓ Branch 66 → 67 taken 19 times.
✓ Branch 76 → 77 taken 38 times.
|
57 | : m_nam->get(request); |
| 457 |
2/2✓ Branch 69 → 70 taken 35 times.
✓ Branch 79 → 80 taken 128 times.
|
163 | DavNetworkLimits::apply(reply); |
| 458 |
2/6✓ Branch 72 → 73 taken 35 times.
✓ Branch 82 → 83 taken 128 times.
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
|
163 | connect(reply, &QNetworkReply::finished, this, [this, reply, id, method, resolved] { |
| 459 |
1/1✓ Branch 2 → 3 taken 154 times.
|
154 | reply->deleteLater(); |
| 460 | 154 | DavResponse response; | |
| 461 | 154 | response.request = id; | |
| 462 | 154 | response.method = method; | |
| 463 | 154 | response.url = resolved; | |
| 464 |
1/1✓ Branch 5 → 6 taken 154 times.
|
154 | response.error = DavNetworkLimits::failureReason(reply); |
| 465 |
1/2✓ Branch 9 → 10 taken 154 times.
✗ Branch 9 → 45 not taken.
|
154 | if (response.error.isEmpty()) { |
| 466 |
2/2✓ Branch 10 → 11 taken 154 times.
✓ Branch 11 → 12 taken 154 times.
|
154 | response.status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 467 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 18 taken 154 times.
|
154 | if (response.status == 0) |
| 468 | ✗ | response.error = reply->errorString(); | |
| 469 |
1/1✓ Branch 18 → 19 taken 154 times.
|
154 | response.body = reply->readAll(); |
| 470 |
2/2✓ Branch 25 → 26 taken 154 times.
✓ Branch 26 → 27 taken 154 times.
|
308 | response.etag = QString::fromUtf8(reply->rawHeader("ETag")); |
| 471 |
1/1✓ Branch 34 → 35 taken 154 times.
|
308 | const QByteArray location = reply->rawHeader("Location"); |
| 472 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 43 taken 153 times.
|
154 | if (!location.isEmpty()) |
| 473 |
2/2✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
|
1 | response.location = resolveDavUrl(QString::fromUtf8(location)); |
| 474 | 154 | } | |
| 475 |
8/8✓ Branch 45 → 46 taken 154 times.
✓ Branch 49 → 50 taken 154 times.
✓ Branch 50 → 51 taken 154 times.
✓ Branch 51 → 52 taken 154 times.
✓ Branch 52 → 53 taken 154 times.
✓ Branch 53 → 54 taken 154 times.
✓ Branch 57 → 47 taken 154 times.
✓ Branch 57 → 58 taken 154 times.
|
308 | qCDebug(lcCardDav) << method << "resource ->" << response.status << response.error; |
| 476 |
1/1✓ Branch 58 → 59 taken 154 times.
|
154 | emit resourceResponse(response); |
| 477 | 154 | }); | |
| 478 | 163 | return id; | |
| 479 | 169 | } | |
| 480 | |||
| 481 | 92 | quint64 CardDavClient::putResource(const QString &url, const QByteArray &body, const QString &ifMatch, | |
| 482 | bool createOnly) { | ||
| 483 | 92 | QList<QPair<QByteArray, QByteArray>> headers; | |
| 484 |
2/2✓ Branch 2 → 3 taken 22 times.
✓ Branch 2 → 11 taken 70 times.
|
92 | if (createOnly) { |
| 485 |
4/4✓ Branch 3 → 4 taken 22 times.
✓ Branch 4 → 5 taken 22 times.
✓ Branch 5 → 6 taken 22 times.
✓ Branch 6 → 7 taken 22 times.
|
22 | headers.append(qMakePair(QByteArray("If-None-Match"), QByteArray("*"))); |
| 486 |
3/3✓ Branch 11 → 12 taken 70 times.
✓ Branch 12 → 13 taken 66 times.
✓ Branch 12 → 21 taken 4 times.
|
70 | } else if (DavEntityTag::isValid(ifMatch)) { |
| 487 |
4/4✓ Branch 13 → 14 taken 66 times.
✓ Branch 14 → 15 taken 66 times.
✓ Branch 15 → 16 taken 66 times.
✓ Branch 16 → 17 taken 66 times.
|
66 | headers.append(qMakePair(QByteArray("If-Match"), ifMatch.toUtf8())); |
| 488 | } else { | ||
| 489 |
5/5✓ Branch 21 → 22 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 30 → 23 taken 4 times.
✓ Branch 30 → 31 taken 4 times.
|
8 | qCWarning(lcCardDav) << "Refusing PUT without a valid entity-tag"; |
| 490 |
2/2✓ Branch 34 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
|
4 | return sendResourceRequest("PUT", QString(), {}, {}); |
| 491 | } | ||
| 492 |
2/2✓ Branch 42 → 43 taken 88 times.
✓ Branch 43 → 44 taken 88 times.
|
88 | return sendResourceRequest("PUT", url, body, headers); |
| 493 | 92 | } | |
| 494 | |||
| 495 | 19 | quint64 CardDavClient::deleteResource(const QString &url, const QString &ifMatch) { | |
| 496 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 25 taken 18 times.
|
19 | if (!DavEntityTag::isValid(ifMatch)) { |
| 497 |
5/5✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
|
2 | qCWarning(lcCardDav) << "Refusing DELETE without a valid entity-tag"; |
| 498 |
2/2✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | return sendResourceRequest("DELETE", QString(), {}, {}); |
| 499 | } | ||
| 500 |
5/7✓ Branch 26 → 27 taken 18 times.
✓ Branch 30 → 31 taken 18 times.
✓ Branch 31 → 32 taken 18 times.
✓ Branch 37 → 38 taken 18 times.
✓ Branch 37 → 39 taken 18 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
|
54 | return sendResourceRequest("DELETE", url, {}, {qMakePair(QByteArray("If-Match"), ifMatch.toUtf8())}); |
| 501 | 36 | } | |
| 502 | |||
| 503 |
2/2✓ Branch 4 → 5 taken 58 times.
✓ Branch 5 → 6 taken 58 times.
|
58 | quint64 CardDavClient::getResource(const QString &url) { return sendResourceRequest("GET", url, {}, {}); } |
| 504 |