1 /*
2     SPDX-FileCopyrightText: 2010 Grégory Oestreicher <greg@kamago.net>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "davprincipalhomesetsfetchjob.h"
8 #include "davjobbase_p.h"
9 
10 #include "daverror.h"
11 #include "davmanager_p.h"
12 #include "davprotocolbase_p.h"
13 #include "protocolinfo.h"
14 #include "utils_p.h"
15 
16 #include <KIO/DavJob>
17 #include <KIO/Job>
18 
19 using namespace KDAV;
20 
21 namespace KDAV
22 {
23 class DavPrincipalHomeSetsFetchJobPrivate : public DavJobBasePrivate
24 {
25 public:
26     void davJobFinished(KJob *job);
27     /**
28      * Start the fetch process.
29      *
30      * There may be two rounds necessary if the first request
31      * does not returns the home sets, but only the current-user-principal
32      * or the principal-URL. The bool flag is here to prevent requesting
33      * those last two on each request, as they are only fetched in
34      * the first round.
35      *
36      * @param fetchHomeSetsOnly If set to true the request will not include
37      *        the current-user-principal and principal-URL props.
38      */
39     void fetchHomeSets(bool fetchHomeSetsOnly);
40 
41     DavUrl mUrl;
42     QStringList mHomeSets;
43 };
44 }
45 
DavPrincipalHomeSetsFetchJob(const DavUrl & url,QObject * parent)46 DavPrincipalHomeSetsFetchJob::DavPrincipalHomeSetsFetchJob(const DavUrl &url, QObject *parent)
47     : DavJobBase(new DavPrincipalHomeSetsFetchJobPrivate, parent)
48 {
49     Q_D(DavPrincipalHomeSetsFetchJob);
50     d->mUrl = url;
51 }
52 
start()53 void DavPrincipalHomeSetsFetchJob::start()
54 {
55     Q_D(DavPrincipalHomeSetsFetchJob);
56     d->fetchHomeSets(false);
57 }
58 
fetchHomeSets(bool homeSetsOnly)59 void DavPrincipalHomeSetsFetchJobPrivate::fetchHomeSets(bool homeSetsOnly)
60 {
61     QDomDocument document;
62 
63     QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
64     document.appendChild(propfindElement);
65 
66     QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
67     propfindElement.appendChild(propElement);
68 
69     const QString homeSet = ProtocolInfo::principalHomeSet(mUrl.protocol());
70     const QString homeSetNS = ProtocolInfo::principalHomeSetNS(mUrl.protocol());
71     propElement.appendChild(document.createElementNS(homeSetNS, homeSet));
72 
73     if (!homeSetsOnly) {
74         propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("current-user-principal")));
75         propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-URL")));
76     }
77 
78     KIO::DavJob *job = DavManager::self()->createPropFindJob(mUrl.url(), document.toString(), QStringLiteral("0"));
79     job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
80     QObject::connect(job, &KIO::DavJob::result, q_ptr, [this](KJob *job) {
81         davJobFinished(job);
82     });
83 }
84 
homeSets() const85 QStringList DavPrincipalHomeSetsFetchJob::homeSets() const
86 {
87     Q_D(const DavPrincipalHomeSetsFetchJob);
88     return d->mHomeSets;
89 }
90 
davJobFinished(KJob * job)91 void DavPrincipalHomeSetsFetchJobPrivate::davJobFinished(KJob *job)
92 {
93     KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
94     const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode"));
95     const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt();
96 
97     // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
98     if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
99         QString err;
100         if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
101             err = KIO::buildErrorString(davJob->error(), davJob->errorText());
102         } else {
103             err = davJob->errorText();
104         }
105 
106         setLatestResponseCode(responseCode);
107         setError(ERR_PROBLEM_WITH_REQUEST);
108         setJobErrorText(davJob->errorText());
109         setJobError(davJob->error());
110         setErrorTextFromDavError();
111 
112         emitResult();
113         return;
114     }
115 
116     /*
117      * Extract information from a document like the following (if no homeset is defined) :
118      *
119      * <D:multistatus xmlns:D="DAV:">
120      *  <D:response xmlns:D="DAV:">
121      *   <D:href xmlns:D="DAV:">/dav/</D:href>
122      *   <D:propstat xmlns:D="DAV:">
123      *    <D:status xmlns:D="DAV:">HTTP/1.1 200 OK</D:status>
124      *    <D:prop xmlns:D="DAV:">
125      *     <D:current-user-principal xmlns:D="DAV:">
126      *      <D:href xmlns:D="DAV:">/principals/users/gdacoin/</D:href>
127      *     </D:current-user-principal>
128      *    </D:prop>
129      *   </D:propstat>
130      *   <D:propstat xmlns:D="DAV:">
131      *    <D:status xmlns:D="DAV:">HTTP/1.1 404 Not Found</D:status>
132      *    <D:prop xmlns:D="DAV:">
133      *     <principal-URL xmlns="DAV:"/>
134      *     <calendar-home-set xmlns="urn:ietf:params:xml:ns:caldav"/>
135      *    </D:prop>
136      *   </D:propstat>
137      *  </D:response>
138      * </D:multistatus>
139      *
140      * Or like this (if the homeset is defined):
141      *
142      *  <?xml version="1.0" encoding="utf-8" ?>
143      *  <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
144      *    <response>
145      *      <href>/principals/users/greg%40kamago.net/</href>
146      *      <propstat>
147      *        <prop>
148      *          <C:calendar-home-set>
149      *            <href>/greg%40kamago.net/</href>
150      *          </C:calendar-home-set>
151      *        </prop>
152      *        <status>HTTP/1.1 200 OK</status>
153      *      </propstat>
154      *    </response>
155      *  </multistatus>
156      */
157 
158     const QString homeSet = ProtocolInfo::principalHomeSet(mUrl.protocol());
159     const QString homeSetNS = ProtocolInfo::principalHomeSetNS(mUrl.protocol());
160     QString nextRoundHref; // The content of the href element that will be used if no homeset was found.
161     // This is either given by current-user-principal or by principal-URL.
162 
163     QDomDocument document;
164     document.setContent(davJob->responseData(), true);
165     const QDomElement multistatusElement = document.documentElement();
166 
167     QDomElement responseElement = Utils::firstChildElementNS(multistatusElement, QStringLiteral("DAV:"), QStringLiteral("response"));
168     while (!responseElement.isNull()) {
169         QDomElement propstatElement;
170 
171         // check for the valid propstat, without giving up on first error
172         {
173             const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
174             for (int i = 0; i < propstats.length(); ++i) {
175                 const QDomElement propstatCandidate = propstats.item(i).toElement();
176                 const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
177                 if (statusElement.text().contains(QLatin1String("200"))) {
178                     propstatElement = propstatCandidate;
179                 }
180             }
181         }
182 
183         if (propstatElement.isNull()) {
184             responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
185             continue;
186         }
187 
188         // extract home sets
189         const QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
190         const QDomElement homeSetElement = Utils::firstChildElementNS(propElement, homeSetNS, homeSet);
191 
192         if (!homeSetElement.isNull()) {
193             QDomElement hrefElement = Utils::firstChildElementNS(homeSetElement, QStringLiteral("DAV:"), QStringLiteral("href"));
194 
195             while (!hrefElement.isNull()) {
196                 const QString href = hrefElement.text();
197                 if (!mHomeSets.contains(href)) {
198                     mHomeSets << href;
199                 }
200 
201                 hrefElement = Utils::nextSiblingElementNS(hrefElement, QStringLiteral("DAV:"), QStringLiteral("href"));
202             }
203         } else {
204             // Trying to get the principal url, given either by current-user-principal or principal-URL
205             QDomElement urlHolder = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-principal"));
206             if (urlHolder.isNull()) {
207                 urlHolder = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("principal-URL"));
208             }
209 
210             if (!urlHolder.isNull()) {
211                 // Getting the href that will be used for the next round
212                 QDomElement hrefElement = Utils::firstChildElementNS(urlHolder, QStringLiteral("DAV:"), QStringLiteral("href"));
213                 if (!hrefElement.isNull()) {
214                     nextRoundHref = hrefElement.text();
215                 }
216             }
217         }
218 
219         responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
220     }
221 
222     /*
223      * Now either we got one or more homesets, or we got an href for the next round
224      * or nothing can be found by this job.
225      * If we have homesets, we're done here and can notify the caller.
226      * Else we must ensure that we have an href for the next round.
227      */
228     if (!mHomeSets.isEmpty() || nextRoundHref.isEmpty()) {
229         emitResult();
230     } else {
231         QUrl nextRoundUrl(mUrl.url());
232 
233         if (nextRoundHref.startsWith(QLatin1Char('/'))) {
234             // nextRoundHref is only a path, use request url to complete
235             nextRoundUrl.setPath(nextRoundHref, QUrl::TolerantMode);
236         } else {
237             // href is a complete url
238             nextRoundUrl = QUrl::fromUserInput(nextRoundHref);
239             nextRoundUrl.setUserName(mUrl.url().userName());
240             nextRoundUrl.setPassword(mUrl.url().password());
241         }
242 
243         mUrl.setUrl(nextRoundUrl);
244         // And one more round, fetching only homesets
245         fetchHomeSets(true);
246     }
247 }
248