service/DavPushXml.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "DavPushXml.h" | ||
| 2 | |||
| 3 | #include "WebPushPayloadCodec.h" | ||
| 4 | |||
| 5 | #include <QDateTime> | ||
| 6 | #include <QRegularExpression> | ||
| 7 | #include <QTimeZone> | ||
| 8 | #include <QXmlStreamReader> | ||
| 9 | #include <QXmlStreamWriter> | ||
| 10 | #include <climits> | ||
| 11 | |||
| 12 | namespace DavPushXml { | ||
| 13 | namespace { | ||
| 14 | |||
| 15 | const QString kDav = QStringLiteral("DAV:"); | ||
| 16 | |||
| 17 | 10 | void setError(QString *error, const QString &text) { | |
| 18 |
2/2✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 2 times.
|
10 | if (error) *error = text; |
| 19 | 10 | } | |
| 20 | |||
| 21 | 822 | bool isPush(const QXmlStreamReader &reader, QStringView name) { | |
| 22 |
6/6✓ Branch 3 → 4 taken 822 times.
✓ Branch 5 → 6 taken 816 times.
✓ Branch 5 → 10 taken 6 times.
✓ Branch 6 → 7 taken 816 times.
✓ Branch 8 → 9 taken 508 times.
✓ Branch 8 → 10 taken 308 times.
|
822 | return reader.namespaceUri() == kNamespace && reader.name() == name; |
| 23 | } | ||
| 24 | |||
| 25 | 2623 | bool isDav(const QXmlStreamReader &reader, QStringView name) { | |
| 26 |
6/6✓ Branch 3 → 4 taken 2623 times.
✓ Branch 5 → 6 taken 2622 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 6 → 7 taken 2622 times.
✓ Branch 8 → 9 taken 1929 times.
✓ Branch 8 → 10 taken 693 times.
|
2623 | return reader.namespaceUri() == kDav && reader.name() == name; |
| 27 | } | ||
| 28 | |||
| 29 | // The push properties found in one successful propstat of one response. | ||
| 30 | struct RawOffer { | ||
| 31 | bool topicSeen = false; | ||
| 32 | QString topic; | ||
| 33 | bool transportsSeen = false; | ||
| 34 | bool webPush = false; | ||
| 35 | bool vapidSeen = false; | ||
| 36 | QString vapidText; | ||
| 37 | QString vapidType; | ||
| 38 | bool triggersSeen = false; | ||
| 39 | int contentUpdateDepth = -1; // -1 = no content-update trigger | ||
| 40 | }; | ||
| 41 | |||
| 42 | 1 | int parseDepth(const QString &text) { | |
| 43 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
|
1 | const QString value = text.trimmed().toLower(); |
| 44 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
|
1 | if (value == QLatin1String("infinity")) return INT_MAX; |
| 45 | 1 | bool ok = false; | |
| 46 |
1/1✓ Branch 9 → 10 taken 1 time.
|
1 | const int depth = value.toInt(&ok); |
| 47 |
2/4✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
|
1 | return ok && depth >= 0 ? depth : -1; |
| 48 | 1 | } | |
| 49 | |||
| 50 | // Reads the children of P:transports / P:supported-triggers into `offer`. | ||
| 51 |
1/1✓ Branch 2 → 3 taken 367 times.
|
367 | void readPushProperty(QXmlStreamReader &reader, RawOffer *offer) { |
| 52 |
3/3✓ Branch 11 → 12 taken 367 times.
✓ Branch 12 → 13 taken 149 times.
✓ Branch 12 → 19 taken 218 times.
|
367 | if (isPush(reader, u"topic")) { |
| 53 | 149 | offer->topicSeen = true; | |
| 54 |
2/2✓ Branch 13 → 14 taken 149 times.
✓ Branch 14 → 15 taken 149 times.
|
149 | offer->topic = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); |
| 55 | 149 | return; | |
| 56 | } | ||
| 57 |
3/3✓ Branch 28 → 29 taken 218 times.
✓ Branch 29 → 30 taken 149 times.
✓ Branch 29 → 79 taken 69 times.
|
218 | if (isPush(reader, u"transports")) { |
| 58 | 149 | offer->transportsSeen = true; | |
| 59 |
2/2✓ Branch 77 → 31 taken 92 times.
✓ Branch 77 → 78 taken 149 times.
|
241 | while (reader.readNextStartElement()) { |
| 60 |
3/3✓ Branch 40 → 41 taken 92 times.
✓ Branch 41 → 42 taken 90 times.
✓ Branch 41 → 75 taken 2 times.
|
92 | if (isPush(reader, u"web-push")) { |
| 61 | 90 | offer->webPush = true; | |
| 62 |
2/2✓ Branch 74 → 43 taken 13 times.
✓ Branch 74 → 76 taken 90 times.
|
103 | while (reader.readNextStartElement()) { |
| 63 |
2/3✓ Branch 52 → 53 taken 13 times.
✓ Branch 53 → 54 taken 13 times.
✗ Branch 53 → 72 not taken.
|
13 | if (isPush(reader, u"vapid-public-key")) { |
| 64 | 13 | offer->vapidSeen = true; | |
| 65 |
2/2✓ Branch 54 → 55 taken 13 times.
✓ Branch 60 → 61 taken 13 times.
|
26 | offer->vapidType = reader.attributes().value(QStringLiteral("type")).toString(); |
| 66 |
2/2✓ Branch 66 → 67 taken 13 times.
✓ Branch 67 → 68 taken 13 times.
|
13 | offer->vapidText = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); |
| 67 | } else { | ||
| 68 | ✗ | reader.skipCurrentElement(); | |
| 69 | } | ||
| 70 | } | ||
| 71 | } else { | ||
| 72 | 2 | reader.skipCurrentElement(); // other (unknown) transports | |
| 73 | } | ||
| 74 | } | ||
| 75 | 149 | return; | |
| 76 | } | ||
| 77 |
2/3✓ Branch 88 → 89 taken 69 times.
✓ Branch 89 → 90 taken 69 times.
✗ Branch 89 → 127 not taken.
|
69 | if (isPush(reader, u"supported-triggers")) { |
| 78 | 69 | offer->triggersSeen = true; | |
| 79 |
2/2✓ Branch 125 → 91 taken 3 times.
✓ Branch 125 → 126 taken 69 times.
|
72 | while (reader.readNextStartElement()) { |
| 80 |
3/3✓ Branch 100 → 101 taken 3 times.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 101 → 123 taken 2 times.
|
3 | if (isPush(reader, u"content-update")) { |
| 81 | 1 | int depth = 0; | |
| 82 |
3/3✓ Branch 119 → 120 taken 2 times.
✓ Branch 120 → 103 taken 1 time.
✓ Branch 120 → 121 taken 1 time.
|
2 | while (reader.readNextStartElement()) { |
| 83 |
4/5✓ Branch 112 → 113 taken 1 time.
✓ Branch 113 → 114 taken 1 time.
✗ Branch 113 → 118 not taken.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
|
1 | if (isDav(reader, u"depth")) depth = parseDepth(reader.readElementText(QXmlStreamReader::SkipChildElements)); |
| 84 | ✗ | else reader.skipCurrentElement(); | |
| 85 | } | ||
| 86 | 1 | offer->contentUpdateDepth = qMax(offer->contentUpdateDepth, depth); | |
| 87 | } else { | ||
| 88 | 2 | reader.skipCurrentElement(); | |
| 89 | } | ||
| 90 | } | ||
| 91 | 69 | return; | |
| 92 | } | ||
| 93 | ✗ | reader.skipCurrentElement(); | |
| 94 | } | ||
| 95 | |||
| 96 | 322 | DavPushOffer classify(const RawOffer &raw) { | |
| 97 | 322 | DavPushOffer offer; | |
| 98 |
5/6✓ Branch 2 → 3 taken 89 times.
✓ Branch 2 → 5 taken 233 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 89 times.
✓ Branch 7 → 8 taken 233 times.
✓ Branch 7 → 22 taken 89 times.
|
322 | if (!raw.topicSeen || raw.topic.isEmpty()) { |
| 99 | 233 | offer.capability = DavPushCapability::Unsupported; | |
| 100 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 233 times.
|
699 | offer.reason = raw.transportsSeen ? QStringLiteral("transports offered without a push topic") |
| 101 |
2/4✓ Branch 17 → 18 taken 233 times.
✗ Branch 17 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 233 times.
|
466 | : QStringLiteral("no WebDAV-Push properties"); |
| 102 | 233 | return offer; | |
| 103 | } | ||
| 104 | 89 | offer.topic = raw.topic; | |
| 105 |
2/2✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 31 taken 88 times.
|
89 | if (!raw.webPush) { |
| 106 | 1 | offer.capability = DavPushCapability::Incompatible; | |
| 107 | 1 | offer.reason = QStringLiteral("no Web Push transport offered"); | |
| 108 | 1 | return offer; | |
| 109 | } | ||
| 110 |
2/2✓ Branch 31 → 32 taken 13 times.
✓ Branch 31 → 67 taken 75 times.
|
88 | if (raw.vapidSeen) { |
| 111 |
1/1✓ Branch 32 → 33 taken 13 times.
|
13 | const auto key = WebPushPayloadCodec::decodeBase64Url(raw.vapidText); |
| 112 |
3/4✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 38 taken 11 times.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
|
13 | const bool typeOk = raw.vapidType.isEmpty() || raw.vapidType == QLatin1String("p256ecdsa"); |
| 113 |
7/10✓ Branch 40 → 41 taken 13 times.
✗ Branch 40 → 49 not taken.
✓ Branch 42 → 43 taken 13 times.
✗ Branch 42 → 49 not taken.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 45 → 49 taken 1 time.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 12 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 59 taken 12 times.
|
13 | if (!typeOk || !key || key->size() != WebPushPayloadCodec::kPublicKeyBytes || key->at(0) != 0x04) { |
| 114 | 1 | offer.capability = DavPushCapability::Incompatible; | |
| 115 | 1 | offer.reason = QStringLiteral("unusable VAPID public key"); | |
| 116 | 1 | return offer; | |
| 117 | } | ||
| 118 | 12 | offer.vapidPublicKey = *key; | |
| 119 | 13 | } | |
| 120 |
2/2✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 76 taken 85 times.
|
87 | if (raw.triggersSeen) { |
| 121 | 2 | offer.profile = DavPushProfile::Draft; | |
| 122 | // Members of a calendar change: content updates need depth >= 1. | ||
| 123 |
2/2✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 77 taken 1 time.
|
2 | if (raw.contentUpdateDepth < 1) { |
| 124 | 1 | offer.capability = DavPushCapability::Incompatible; | |
| 125 | 1 | offer.reason = QStringLiteral("no content-update trigger with depth 1"); | |
| 126 | 1 | return offer; | |
| 127 | } | ||
| 128 | } else { | ||
| 129 | 85 | offer.profile = DavPushProfile::NextcloudLegacy; | |
| 130 | } | ||
| 131 | 86 | offer.capability = DavPushCapability::Supported; | |
| 132 | 86 | return offer; | |
| 133 | ✗ | } | |
| 134 | |||
| 135 | 302 | int statusCode(const QString &line) { | |
| 136 |
2/2✓ Branch 2 → 3 taken 302 times.
✓ Branch 6 → 7 taken 302 times.
|
604 | const QStringList parts = line.simplified().split(QLatin1Char(' ')); |
| 137 |
2/3✓ Branch 9 → 10 taken 302 times.
✗ Branch 9 → 13 not taken.
✓ Branch 11 → 12 taken 302 times.
|
604 | return parts.size() >= 2 ? parts.at(1).toInt() : 0; |
| 138 | 302 | } | |
| 139 | |||
| 140 | // One DAV:propstat: its push properties and status. | ||
| 141 | 391 | void readPropstat(QXmlStreamReader &reader, RawOffer *merged) { | |
| 142 | 391 | RawOffer found; | |
| 143 | 391 | int status = 0; | |
| 144 |
3/3✓ Branch 39 → 40 taken 1084 times.
✓ Branch 40 → 3 taken 693 times.
✓ Branch 40 → 41 taken 391 times.
|
1084 | while (reader.readNextStartElement()) { |
| 145 |
3/3✓ Branch 12 → 13 taken 693 times.
✓ Branch 13 → 14 taken 391 times.
✓ Branch 13 → 23 taken 302 times.
|
693 | if (isDav(reader, u"prop")) { |
| 146 |
3/3✓ Branch 21 → 22 taken 1920 times.
✓ Branch 22 → 15 taken 1529 times.
✓ Branch 22 → 39 taken 391 times.
|
1920 | while (reader.readNextStartElement()) { |
| 147 |
4/4✓ Branch 16 → 17 taken 1529 times.
✓ Branch 18 → 19 taken 367 times.
✓ Branch 18 → 20 taken 1162 times.
✓ Branch 19 → 21 taken 367 times.
|
1529 | if (reader.namespaceUri() == kNamespace) readPushProperty(reader, &found); |
| 148 |
1/1✓ Branch 20 → 21 taken 1162 times.
|
1162 | else reader.skipCurrentElement(); |
| 149 | } | ||
| 150 |
2/3✓ Branch 32 → 33 taken 302 times.
✓ Branch 33 → 34 taken 302 times.
✗ Branch 33 → 38 not taken.
|
302 | } else if (isDav(reader, u"status")) { |
| 151 |
2/2✓ Branch 34 → 35 taken 302 times.
✓ Branch 35 → 36 taken 302 times.
|
302 | status = statusCode(reader.readElementText(QXmlStreamReader::SkipChildElements)); |
| 152 | } else { | ||
| 153 | ✗ | reader.skipCurrentElement(); | |
| 154 | } | ||
| 155 | } | ||
| 156 | // S106-070: only a propstat whose DAV:status says 2xx counts — a missing | ||
| 157 | // or unparseable status (RFC 4918 makes it mandatory) used to be success. | ||
| 158 |
4/4✓ Branch 41 → 42 taken 302 times.
✓ Branch 41 → 43 taken 89 times.
✓ Branch 42 → 43 taken 68 times.
✓ Branch 42 → 44 taken 234 times.
|
391 | if (status < 200 || status >= 300) return; // 404, missing, garbage: not offered |
| 159 |
2/2✓ Branch 44 → 45 taken 89 times.
✓ Branch 44 → 46 taken 145 times.
|
234 | if (found.topicSeen) { merged->topicSeen = true; merged->topic = found.topic; } |
| 160 |
2/2✓ Branch 46 → 47 taken 89 times.
✓ Branch 46 → 49 taken 145 times.
|
234 | if (found.transportsSeen) { |
| 161 | 89 | merged->transportsSeen = true; | |
| 162 | 89 | merged->webPush = found.webPush; | |
| 163 | 89 | merged->vapidSeen = found.vapidSeen; | |
| 164 | 89 | merged->vapidText = found.vapidText; | |
| 165 | 89 | merged->vapidType = found.vapidType; | |
| 166 | } | ||
| 167 |
2/2✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 232 times.
|
234 | if (found.triggersSeen) { |
| 168 | 2 | merged->triggersSeen = true; | |
| 169 | 2 | merged->contentUpdateDepth = found.contentUpdateDepth; | |
| 170 | } | ||
| 171 | 391 | } | |
| 172 | |||
| 173 | } // namespace | ||
| 174 | |||
| 175 | 219 | QByteArray discoveryProperties() { | |
| 176 | 219 | return "<P:transports/><P:topic/><P:supported-triggers/>"; | |
| 177 | } | ||
| 178 | |||
| 179 | 193 | std::optional<QHash<QString, DavPushOffer>> parseDiscovery(const QByteArray &multistatus, QString *error) { | |
| 180 | // S106-071: the same DTD/entity refusal as parsePushMessage (defense in | ||
| 181 | // depth; QXmlStreamReader does not expand them, readNextStartElement | ||
| 182 | // skipped them silently). | ||
| 183 |
4/6✓ Branch 4 → 5 taken 192 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 7 → 8 taken 192 times.
✗ Branch 7 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 192 times.
|
385 | if (multistatus.contains("<!DOCTYPE") || multistatus.contains("<!ENTITY") || |
| 184 |
2/2✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 21 taken 192 times.
|
385 | multistatus.contains("<!doctype")) { |
| 185 | 1 | setError(error, QStringLiteral("DTDs and entities are not allowed")); | |
| 186 | 1 | return std::nullopt; | |
| 187 | } | ||
| 188 |
1/1✓ Branch 21 → 22 taken 192 times.
|
192 | QXmlStreamReader reader(multistatus); |
| 189 | 192 | QHash<QString, DavPushOffer> offers; | |
| 190 |
7/8✓ Branch 22 → 23 taken 192 times.
✓ Branch 23 → 24 taken 192 times.
✗ Branch 23 → 35 not taken.
✓ Branch 33 → 34 taken 192 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 191 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 45 taken 191 times.
|
384 | if (!reader.readNextStartElement() || !isDav(reader, u"multistatus")) { |
| 191 | 1 | setError(error, QStringLiteral("not a DAV multistatus")); | |
| 192 | 1 | return std::nullopt; | |
| 193 | } | ||
| 194 |
3/3✓ Branch 106 → 107 taken 515 times.
✓ Branch 107 → 46 taken 324 times.
✓ Branch 107 → 108 taken 191 times.
|
515 | while (reader.readNextStartElement()) { |
| 195 |
2/3✓ Branch 55 → 56 taken 324 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 59 taken 324 times.
|
324 | if (!isDav(reader, u"response")) { |
| 196 | ✗ | reader.skipCurrentElement(); | |
| 197 | ✗ | continue; | |
| 198 | } | ||
| 199 | 324 | QString href; | |
| 200 | 324 | RawOffer raw; | |
| 201 |
3/3✓ Branch 95 → 96 taken 1037 times.
✓ Branch 96 → 60 taken 713 times.
✓ Branch 96 → 97 taken 324 times.
|
1037 | while (reader.readNextStartElement()) { |
| 202 |
8/9✓ Branch 69 → 70 taken 713 times.
✓ Branch 70 → 71 taken 322 times.
✓ Branch 70 → 74 taken 391 times.
✓ Branch 72 → 73 taken 322 times.
✗ Branch 72 → 74 not taken.
✓ Branch 75 → 76 taken 322 times.
✓ Branch 75 → 82 taken 391 times.
✓ Branch 76 → 77 taken 322 times.
✓ Branch 77 → 78 taken 322 times.
|
713 | if (isDav(reader, u"href") && href.isEmpty()) href = reader.readElementText().trimmed(); |
| 203 |
3/4✓ Branch 91 → 92 taken 391 times.
✓ Branch 92 → 93 taken 391 times.
✗ Branch 92 → 94 not taken.
✓ Branch 93 → 95 taken 391 times.
|
391 | else if (isDav(reader, u"propstat")) readPropstat(reader, &raw); |
| 204 | ✗ | else reader.skipCurrentElement(); | |
| 205 | } | ||
| 206 |
4/4✓ Branch 98 → 99 taken 322 times.
✓ Branch 98 → 103 taken 2 times.
✓ Branch 99 → 100 taken 322 times.
✓ Branch 100 → 101 taken 322 times.
|
324 | if (!href.isEmpty()) offers.insert(href, classify(raw)); |
| 207 | 324 | } | |
| 208 |
3/3✓ Branch 108 → 109 taken 191 times.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 122 taken 190 times.
|
191 | if (reader.hasError()) { |
| 209 |
2/2✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
|
2 | setError(error, QStringLiteral("invalid discovery XML: %1").arg(reader.errorString())); |
| 210 | 1 | return std::nullopt; | |
| 211 | } | ||
| 212 | 190 | return offers; | |
| 213 | 192 | } | |
| 214 | |||
| 215 | 83 | QByteArray registerBody(const QString &pushResource, const QByteArray &publicKey, const QByteArray &authSecret) { | |
| 216 | 83 | QByteArray body; | |
| 217 |
1/1✓ Branch 2 → 3 taken 83 times.
|
83 | QXmlStreamWriter writer(&body); |
| 218 |
1/1✓ Branch 3 → 4 taken 83 times.
|
83 | writer.writeStartDocument(); |
| 219 |
1/1✓ Branch 5 → 6 taken 83 times.
|
83 | writer.writeDefaultNamespace(kNamespace); |
| 220 |
1/1✓ Branch 11 → 12 taken 83 times.
|
83 | writer.writeNamespace(kDav, QStringLiteral("D")); |
| 221 |
1/1✓ Branch 19 → 20 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("push-register")); |
| 222 |
1/1✓ Branch 27 → 28 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("subscription")); |
| 223 |
1/1✓ Branch 35 → 36 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("web-push-subscription")); |
| 224 |
1/1✓ Branch 44 → 45 taken 83 times.
|
166 | writer.writeTextElement(kNamespace, QStringLiteral("push-resource"), pushResource); |
| 225 |
1/1✓ Branch 56 → 57 taken 83 times.
|
166 | writer.writeTextElement(kNamespace, QStringLiteral("content-encoding"), QStringLiteral("aes128gcm")); |
| 226 |
1/1✓ Branch 66 → 67 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("subscription-public-key")); |
| 227 |
1/1✓ Branch 77 → 78 taken 83 times.
|
166 | writer.writeAttribute(QStringLiteral("type"), QStringLiteral("p256dh")); |
| 228 |
2/2✓ Branch 82 → 83 taken 83 times.
✓ Branch 84 → 85 taken 83 times.
|
83 | writer.writeCharacters(WebPushPayloadCodec::encodeBase64Url(publicKey)); |
| 229 |
1/1✓ Branch 86 → 87 taken 83 times.
|
83 | writer.writeEndElement(); |
| 230 |
2/2✓ Branch 87 → 88 taken 83 times.
✓ Branch 94 → 95 taken 83 times.
|
166 | writer.writeTextElement(kNamespace, QStringLiteral("auth-secret"), WebPushPayloadCodec::encodeBase64Url(authSecret)); |
| 231 |
1/1✓ Branch 98 → 99 taken 83 times.
|
83 | writer.writeEndElement(); // web-push-subscription |
| 232 |
1/1✓ Branch 99 → 100 taken 83 times.
|
83 | writer.writeEndElement(); // subscription |
| 233 |
1/1✓ Branch 105 → 106 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("trigger")); |
| 234 |
1/1✓ Branch 113 → 114 taken 83 times.
|
83 | writer.writeStartElement(kNamespace, QStringLiteral("content-update")); |
| 235 |
1/1✓ Branch 125 → 126 taken 83 times.
|
166 | writer.writeTextElement(kDav, QStringLiteral("depth"), QStringLiteral("1")); |
| 236 |
1/1✓ Branch 130 → 131 taken 83 times.
|
83 | writer.writeEndElement(); |
| 237 |
1/1✓ Branch 131 → 132 taken 83 times.
|
83 | writer.writeEndElement(); |
| 238 |
1/1✓ Branch 132 → 133 taken 83 times.
|
83 | writer.writeEndElement(); // push-register |
| 239 |
1/1✓ Branch 133 → 134 taken 83 times.
|
83 | writer.writeEndDocument(); |
| 240 | 83 | return body; | |
| 241 | 83 | } | |
| 242 | |||
| 243 | 17 | std::optional<PushMessage> parsePushMessage(const QByteArray &xml, QString *error) { | |
| 244 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 14 taken 16 times.
|
17 | if (xml.size() > kMaxMessageBytes) { |
| 245 |
1/1✓ Branch 8 → 9 taken 1 time.
|
2 | setError(error, QStringLiteral("push message exceeds %1 bytes").arg(kMaxMessageBytes)); |
| 246 | 1 | return std::nullopt; | |
| 247 | } | ||
| 248 |
1/1✓ Branch 14 → 15 taken 16 times.
|
16 | QXmlStreamReader reader(xml); |
| 249 | 16 | PushMessage message; | |
| 250 | 16 | int depth = 0; | |
| 251 | 16 | bool rootSeen = false; | |
| 252 | 16 | QStringList path; | |
| 253 |
3/3✓ Branch 171 → 172 taken 140 times.
✓ Branch 172 → 16 taken 128 times.
✓ Branch 172 → 173 taken 12 times.
|
140 | while (!reader.atEnd()) { |
| 254 |
1/1✓ Branch 16 → 17 taken 128 times.
|
128 | const auto token = reader.readNext(); |
| 255 |
3/4✓ Branch 17 → 18 taken 127 times.
✓ Branch 17 → 19 taken 1 time.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 26 taken 127 times.
|
128 | if (token == QXmlStreamReader::DTD || token == QXmlStreamReader::EntityReference) { |
| 256 | 1 | setError(error, QStringLiteral("DTDs and entities are not allowed")); | |
| 257 | 1 | return std::nullopt; | |
| 258 | } | ||
| 259 |
2/2✓ Branch 26 → 27 taken 65 times.
✓ Branch 26 → 167 taken 62 times.
|
127 | if (token == QXmlStreamReader::StartElement) { |
| 260 |
2/2✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 35 taken 64 times.
|
65 | if (++depth > kMaxDepth) { |
| 261 | 1 | setError(error, QStringLiteral("push message nested too deeply")); | |
| 262 | 1 | return std::nullopt; | |
| 263 | } | ||
| 264 |
2/2✓ Branch 35 → 36 taken 15 times.
✓ Branch 35 → 55 taken 49 times.
|
64 | if (!rootSeen) { |
| 265 |
3/3✓ Branch 45 → 46 taken 15 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 54 taken 13 times.
|
15 | if (!isPush(reader, u"push-message")) { |
| 266 | 2 | setError(error, QStringLiteral("root element is not push-message")); | |
| 267 | 2 | return std::nullopt; | |
| 268 | } | ||
| 269 | 13 | rootSeen = true; | |
| 270 |
7/7✓ Branch 55 → 56 taken 24 times.
✓ Branch 55 → 68 taken 25 times.
✓ Branch 65 → 66 taken 24 times.
✓ Branch 66 → 67 taken 10 times.
✓ Branch 66 → 68 taken 14 times.
✓ Branch 69 → 70 taken 10 times.
✓ Branch 69 → 76 taken 39 times.
|
73 | } else if (depth == 2 && isPush(reader, u"topic")) { |
| 271 |
2/2✓ Branch 70 → 71 taken 10 times.
✓ Branch 71 → 72 taken 10 times.
|
10 | message.topic = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); |
| 272 | 10 | --depth; | |
| 273 | 10 | continue; | |
| 274 |
7/7✓ Branch 76 → 77 taken 14 times.
✓ Branch 76 → 89 taken 25 times.
✓ Branch 86 → 87 taken 14 times.
✓ Branch 87 → 88 taken 9 times.
✓ Branch 87 → 89 taken 5 times.
✓ Branch 90 → 91 taken 9 times.
✓ Branch 90 → 92 taken 30 times.
|
53 | } else if (depth == 2 && isPush(reader, u"content-update")) { |
| 275 | 9 | message.contentUpdate = true; | |
| 276 |
7/7✓ Branch 92 → 93 taken 5 times.
✓ Branch 92 → 105 taken 25 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 103 → 104 taken 3 times.
✓ Branch 103 → 105 taken 2 times.
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 108 taken 27 times.
|
35 | } else if (depth == 2 && isPush(reader, u"property-update")) { |
| 277 | 3 | message.propertyUpdate = true; | |
| 278 |
11/14✓ Branch 108 → 109 taken 10 times.
✓ Branch 108 → 125 taken 17 times.
✓ Branch 110 → 111 taken 10 times.
✓ Branch 112 → 113 taken 7 times.
✓ Branch 112 → 125 taken 3 times.
✓ Branch 122 → 123 taken 7 times.
✓ Branch 123 → 124 taken 7 times.
✗ Branch 123 → 125 not taken.
✓ Branch 126 → 127 taken 10 times.
✓ Branch 126 → 128 taken 17 times.
✓ Branch 128 → 129 taken 7 times.
✓ Branch 128 → 135 taken 20 times.
✗ Branch 227 → 228 not taken.
✗ Branch 227 → 229 not taken.
|
34 | } else if (depth == 3 && path.value(1) == QLatin1String("content-update") && isDav(reader, u"sync-token")) { |
| 279 |
2/2✓ Branch 129 → 130 taken 7 times.
✓ Branch 130 → 131 taken 7 times.
|
7 | message.syncToken = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); |
| 280 | 7 | --depth; | |
| 281 | 7 | continue; | |
| 282 |
11/14✓ Branch 135 → 136 taken 3 times.
✓ Branch 135 → 152 taken 17 times.
✓ Branch 137 → 138 taken 3 times.
✓ Branch 139 → 140 taken 2 times.
✓ Branch 139 → 152 taken 1 time.
✓ Branch 149 → 150 taken 2 times.
✓ Branch 150 → 151 taken 2 times.
✗ Branch 150 → 152 not taken.
✓ Branch 153 → 154 taken 3 times.
✓ Branch 153 → 155 taken 17 times.
✓ Branch 155 → 156 taken 2 times.
✓ Branch 155 → 157 taken 18 times.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
|
22 | } else if (depth == 4 && path.value(1) == QLatin1String("property-update") && isPush(reader, u"transports")) { |
| 283 | 2 | message.transportsChanged = true; | |
| 284 | } | ||
| 285 |
1/1✓ Branch 158 → 159 taken 45 times.
|
45 | const bool own = reader.namespaceUri() == kNamespace; |
| 286 |
5/5✓ Branch 160 → 161 taken 42 times.
✓ Branch 160 → 163 taken 3 times.
✓ Branch 161 → 162 taken 42 times.
✓ Branch 162 → 164 taken 42 times.
✓ Branch 164 → 165 taken 45 times.
|
45 | path.append(own ? reader.name().toString() : QString()); |
| 287 |
2/2✓ Branch 167 → 168 taken 28 times.
✓ Branch 167 → 171 taken 34 times.
|
62 | } else if (token == QXmlStreamReader::EndElement) { |
| 288 | 28 | --depth; | |
| 289 |
1/2✓ Branch 169 → 170 taken 28 times.
✗ Branch 169 → 171 not taken.
|
28 | if (!path.isEmpty()) path.removeLast(); |
| 290 | } | ||
| 291 | } | ||
| 292 |
6/7✓ Branch 173 → 174 taken 12 times.
✓ Branch 174 → 175 taken 11 times.
✓ Branch 174 → 176 taken 1 time.
✗ Branch 175 → 176 not taken.
✓ Branch 175 → 177 taken 11 times.
✓ Branch 178 → 179 taken 1 time.
✓ Branch 178 → 191 taken 11 times.
|
12 | if (reader.hasError() || !rootSeen) { |
| 293 |
2/2✓ Branch 183 → 184 taken 1 time.
✓ Branch 184 → 185 taken 1 time.
|
2 | setError(error, QStringLiteral("invalid push message XML: %1").arg(reader.errorString())); |
| 294 | 1 | return std::nullopt; | |
| 295 | } | ||
| 296 |
6/6✓ Branch 192 → 193 taken 3 times.
✓ Branch 192 → 195 taken 8 times.
✓ Branch 193 → 194 taken 1 time.
✓ Branch 193 → 195 taken 2 times.
✓ Branch 196 → 197 taken 1 time.
✓ Branch 196 → 204 taken 10 times.
|
11 | if (message.topic.isEmpty() && !message.transportsChanged) { |
| 297 | 1 | setError(error, QStringLiteral("push message without topic")); | |
| 298 | 1 | return std::nullopt; | |
| 299 | } | ||
| 300 | 10 | return message; | |
| 301 | 16 | } | |
| 302 | |||
| 303 | 140 | std::optional<qint64> parseHttpDate(const QString &value) { | |
| 304 | 6 | static const QRegularExpression pattern(QStringLiteral( | |
| 305 |
4/7✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 15 taken 137 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
|
146 | R"(^(?:[A-Za-z]{3},\s*)?(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+(GMT|UTC|Z|[+-]\d{4})\+?$)")); |
| 306 | 3 | static const QStringList months{QStringLiteral("jan"), QStringLiteral("feb"), QStringLiteral("mar"), | |
| 307 | 3 | QStringLiteral("apr"), QStringLiteral("may"), QStringLiteral("jun"), | |
| 308 | 3 | QStringLiteral("jul"), QStringLiteral("aug"), QStringLiteral("sep"), | |
| 309 |
5/10✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 73 taken 137 times.
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 73 not taken.
✓ Branch 58 → 59 taken 36 times.
✓ Branch 58 → 60 taken 3 times.
✗ Branch 168 → 169 not taken.
✗ Branch 168 → 170 not taken.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 215 not taken.
|
182 | QStringLiteral("oct"), QStringLiteral("nov"), QStringLiteral("dec")}; |
| 310 |
2/2✓ Branch 74 → 75 taken 140 times.
✓ Branch 75 → 76 taken 140 times.
|
140 | const auto match = pattern.match(value.trimmed()); |
| 311 |
3/3✓ Branch 77 → 78 taken 140 times.
✓ Branch 78 → 79 taken 68 times.
✓ Branch 78 → 82 taken 72 times.
|
140 | if (!match.hasMatch()) return std::nullopt; |
| 312 |
2/2✓ Branch 82 → 83 taken 72 times.
✓ Branch 83 → 84 taken 72 times.
|
72 | const int month = months.indexOf(match.captured(2).toLower()) + 1; |
| 313 |
5/5✓ Branch 87 → 88 taken 72 times.
✓ Branch 88 → 89 taken 72 times.
✓ Branch 89 → 90 taken 72 times.
✓ Branch 90 → 91 taken 72 times.
✓ Branch 91 → 92 taken 72 times.
|
72 | const QDate date(match.captured(3).toInt(), month, match.captured(1).toInt()); |
| 314 |
7/7✓ Branch 94 → 95 taken 72 times.
✓ Branch 95 → 96 taken 72 times.
✓ Branch 96 → 97 taken 72 times.
✓ Branch 97 → 98 taken 72 times.
✓ Branch 98 → 99 taken 72 times.
✓ Branch 99 → 100 taken 72 times.
✓ Branch 100 → 101 taken 72 times.
|
72 | const QTime time(match.captured(4).toInt(), match.captured(5).toInt(), match.captured(6).toInt()); |
| 315 |
8/10✓ Branch 104 → 105 taken 72 times.
✗ Branch 104 → 109 not taken.
✓ Branch 105 → 106 taken 72 times.
✓ Branch 106 → 107 taken 71 times.
✓ Branch 106 → 109 taken 1 time.
✓ Branch 107 → 108 taken 71 times.
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 71 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 115 taken 71 times.
|
72 | if (month == 0 || !date.isValid() || !time.isValid()) return std::nullopt; |
| 316 | 71 | qint64 offsetSeconds = 0; | |
| 317 |
1/1✓ Branch 115 → 116 taken 71 times.
|
71 | const QString zone = match.captured(7); |
| 318 |
7/8✓ Branch 118 → 119 taken 71 times.
✓ Branch 119 → 120 taken 8 times.
✓ Branch 119 → 124 taken 63 times.
✓ Branch 122 → 123 taken 8 times.
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 125 taken 8 times.
✓ Branch 126 → 127 taken 63 times.
✓ Branch 126 → 145 taken 8 times.
|
71 | if (zone.startsWith(QLatin1Char('+')) || zone.startsWith(QLatin1Char('-'))) { |
| 319 |
2/2✓ Branch 127 → 128 taken 63 times.
✓ Branch 128 → 129 taken 63 times.
|
63 | const int hours = zone.mid(1, 2).toInt(); |
| 320 |
2/2✓ Branch 130 → 131 taken 63 times.
✓ Branch 131 → 132 taken 63 times.
|
63 | const int minutes = zone.mid(3, 2).toInt(); |
| 321 |
2/4✓ Branch 133 → 134 taken 63 times.
✗ Branch 133 → 135 not taken.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 138 taken 63 times.
|
63 | if (hours > 23 || minutes > 59) return std::nullopt; |
| 322 |
2/3✓ Branch 140 → 141 taken 63 times.
✗ Branch 141 → 142 not taken.
✓ Branch 141 → 143 taken 63 times.
|
63 | offsetSeconds = (hours * 3600 + minutes * 60) * (zone.startsWith(QLatin1Char('-')) ? -1 : 1); |
| 323 | } | ||
| 324 |
3/3✓ Branch 145 → 146 taken 71 times.
✓ Branch 146 → 147 taken 71 times.
✓ Branch 147 → 148 taken 71 times.
|
71 | return QDateTime(date, time, QTimeZone::utc()).toSecsSinceEpoch() - offsetSeconds; |
| 325 | 179 | } | |
| 326 | |||
| 327 | } // namespace DavPushXml | ||
| 328 |