KDAV

davcollectionsfetchjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2010 Tobias Koenig <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "davcollectionsfetchjob.h"
8 #include "davjobbase_p.h"
9 
10 #include "davmanager_p.h"
11 #include "davprincipalhomesetsfetchjob.h"
12 #include "davprotocolbase_p.h"
13 #include "utils_p.h"
14 #include "daverror.h"
15 
16 #include "libkdav_debug.h"
17 #include <KIO/DavJob>
18 #include <KIO/Job>
19 
20 #include <QColor>
21 #include <QBuffer>
22 #include <QtXmlPatterns/QXmlQuery>
23 
24 using namespace KDAV;
25 
26 namespace KDAV {
27 class DavCollectionsFetchJobPrivate : public DavJobBasePrivate
28 {
29 public:
30  void principalFetchFinished(KJob *job);
31  void collectionsFetchFinished(KJob *job);
32  void doCollectionsFetch(const QUrl &url);
33  void subjobFinished();
34 
35  DavUrl mUrl;
36  DavCollection::List mCollections;
37  uint mSubJobCount = 0;
38 
39  Q_DECLARE_PUBLIC(DavCollectionsFetchJob)
40 };
41 }
42 
44  : DavJobBase(new DavCollectionsFetchJobPrivate, parent)
45 {
47  d->mUrl = url;
48 }
49 
51 {
53  if (DavManager::davProtocol(d->mUrl.protocol())->supportsPrincipals()) {
55  connect(job, &DavPrincipalHomeSetsFetchJob::result, this, [d](KJob *job) { d->principalFetchFinished(job); });
56  job->start();
57  } else {
58  d->doCollectionsFetch(d->mUrl.url());
59  }
60 }
61 
63 {
64  Q_D(const DavCollectionsFetchJob);
65  return d->mCollections;
66 }
67 
69 {
70  Q_D(const DavCollectionsFetchJob);
71  return d->mUrl;
72 }
73 
74 void DavCollectionsFetchJobPrivate::doCollectionsFetch(const QUrl &url)
75 {
76  ++mSubJobCount;
77 
78  const QDomDocument collectionQuery = DavManager::davProtocol(mUrl.protocol())->collectionsQuery()->buildQuery();
79 
80  KIO::DavJob *job = DavManager::self()->createPropFindJob(url, collectionQuery);
81  QObject::connect(job, &KIO::DavJob::result, q_ptr, [this](KJob *job) { collectionsFetchFinished(job); });
82  job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
83 }
84 
85 void DavCollectionsFetchJobPrivate::principalFetchFinished(KJob *job)
86 {
88 
89  if (davJob->error()) {
90  if (davJob->latestResponseCode()) {
91  // If we have a HTTP response code then this may mean that
92  // the URL was not a principal URL. Retry as if it were a calendar URL.
93  qCDebug(KDAV_LOG) << job->errorText();
94  doCollectionsFetch(mUrl.url());
95  } else {
96  // Just give up here.
97  setDavError(davJob->davError());
98  setErrorTextFromDavError();
99  emitResult();
100  }
101 
102  return;
103  }
104 
105  const QStringList homeSets = davJob->homeSets();
106  qCDebug(KDAV_LOG) << "Found" << homeSets.size() << "homesets";
107  qCDebug(KDAV_LOG) << homeSets;
108 
109  if (homeSets.isEmpty()) {
110  // Same as above, retry as if it were a calendar URL.
111  doCollectionsFetch(mUrl.url());
112  return;
113  }
114 
115  for (const QString &homeSet : homeSets) {
116  QUrl url = mUrl.url();
117 
118  if (homeSet.startsWith(QLatin1Char('/'))) {
119  // homeSet is only a path, use request url to complete
120  url.setPath(homeSet, QUrl::TolerantMode);
121  } else {
122  // homeSet is a complete url
123  QUrl tmpUrl(homeSet);
124  tmpUrl.setUserName(url.userName());
125  tmpUrl.setPassword(url.password());
126  url = tmpUrl;
127  }
128 
129  doCollectionsFetch(url);
130  }
131 }
132 
133 void DavCollectionsFetchJobPrivate::collectionsFetchFinished(KJob *job)
134 {
136  KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
137  const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode"));
138  const int responseCode = responseCodeStr.isEmpty()
139  ? 0
140  : responseCodeStr.toInt();
141 
142  // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
143  if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
144  if (davJob->url() != mUrl.url()) {
145  // Retry as if the initial URL was a calendar URL.
146  // We can end up here when retrieving a homeset on
147  // which a PROPFIND resulted in an error
148  doCollectionsFetch(mUrl.url());
149  --mSubJobCount;
150  return;
151  }
152 
153  setLatestResponseCode(responseCode);
154  setError(ERR_PROBLEM_WITH_REQUEST);
155  setJobErrorText(davJob->errorText());
156  setJobError(davJob->error());
157  setErrorTextFromDavError();
158  } else {
159  // For use in the collectionDiscovered() signal
160  QUrl _jobUrl = mUrl.url();
161  _jobUrl.setUserInfo(QString());
162  const QString jobUrl = _jobUrl.toDisplayString();
163 
164  // Validate that we got a valid PROPFIND response
165  QDomElement rootElement = davJob->response().documentElement();
166  if (rootElement.tagName().compare(QLatin1String("multistatus"), Qt::CaseInsensitive) != 0) {
167  setError(ERR_COLLECTIONFETCH);
168  setErrorTextFromDavError();
169  subjobFinished();
170  return;
171  }
172 
173  QByteArray resp(davJob->response().toByteArray());
174  QBuffer buffer(&resp);
175  buffer.open(QIODevice::ReadOnly);
176 
177  QXmlQuery xquery;
178  if (!xquery.setFocus(&buffer)) {
179  setError(ERR_COLLECTIONFETCH_XQUERY_SETFOCUS);
180  setErrorTextFromDavError();
181  subjobFinished();
182  return;
183  }
184 
185  xquery.setQuery(DavManager::davProtocol(mUrl.protocol())->collectionsXQuery());
186  if (!xquery.isValid()) {
187  setError(ERR_COLLECTIONFETCH_XQUERY_INVALID);
188  setErrorTextFromDavError();
189  subjobFinished();
190  return;
191  }
192 
193  QString responsesStr;
194  xquery.evaluateTo(&responsesStr);
195  responsesStr.prepend(QLatin1String("<responses>"));
196  responsesStr.append(QLatin1String("</responses>"));
197 
198  QDomDocument document;
199  if (!document.setContent(responsesStr, true)) {
200  setError(ERR_COLLECTIONFETCH);
201  setErrorTextFromDavError();
202  subjobFinished();
203  return;
204  }
205 
206  if (!q->error()) {
207  /*
208  * Extract information from a document like the following:
209  *
210  * <responses>
211  * <response xmlns="DAV:">
212  * <href xmlns="DAV:">/caldav.php/test1.user/home/</href>
213  * <propstat xmlns="DAV:">
214  * <prop xmlns="DAV:">
215  * <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
216  * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
217  * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
218  * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
219  * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
220  * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
221  * </C:supported-calendar-component-set>
222  * <resourcetype xmlns="DAV:">
223  * <collection xmlns="DAV:"/>
224  * <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
225  * <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
226  * </resourcetype>
227  * <displayname xmlns="DAV:">Test1 User</displayname>
228  * <current-user-privilege-set xmlns="DAV:">
229  * <privilege xmlns="DAV:">
230  * <read xmlns="DAV:"/>
231  * </privilege>
232  * </current-user-privilege-set>
233  * <getctag xmlns="http://calendarserver.org/ns/">12345</getctag>
234  * </prop>
235  * <status xmlns="DAV:">HTTP/1.1 200 OK</status>
236  * </propstat>
237  * </response>
238  * </responses>
239  */
240 
241  const QDomElement responsesElement = document.documentElement();
242 
243  QDomElement responseElement = Utils::firstChildElementNS(responsesElement, QStringLiteral("DAV:"), QStringLiteral("response"));
244  while (!responseElement.isNull()) {
245  QDomElement propstatElement;
246 
247  // check for the valid propstat, without giving up on first error
248  {
249  const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
250  for (int i = 0; i < propstats.length(); ++i) {
251  const QDomElement propstatCandidate = propstats.item(i).toElement();
252  const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
253  if (statusElement.text().contains(QLatin1String("200"))) {
254  propstatElement = propstatCandidate;
255  }
256  }
257  }
258 
259  if (propstatElement.isNull()) {
260  responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
261  continue;
262  }
263 
264  // extract url
265  const QDomElement hrefElement = Utils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
266  if (hrefElement.isNull()) {
267  responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
268  continue;
269  }
270 
271  QString href = hrefElement.text();
272  if (!href.endsWith(QLatin1Char('/'))) {
273  href.append(QLatin1Char('/'));
274  }
275 
276  QUrl url = davJob->url();
277  url.setUserInfo(QString());
278  if (href.startsWith(QLatin1Char('/'))) {
279  // href is only a path, use request url to complete
280  url.setPath(href, QUrl::TolerantMode);
281  } else {
282  // href is a complete url
283  url = QUrl::fromUserInput(href);
284  }
285 
286  // don't add this resource if it has already been detected
287  bool alreadySeen = false;
288  for (const DavCollection &seen : qAsConst(mCollections)) {
289  if (seen.url().toDisplayString() == url.toDisplayString()) {
290  alreadySeen = true;
291  }
292  }
293  if (alreadySeen) {
294  responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
295  continue;
296  }
297 
298  // extract display name
299  const QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
300  const QDomElement displaynameElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("displayname"));
301  const QString displayName = displaynameElement.text();
302 
303  // Extract CTag
304  const QDomElement CTagElement = Utils::firstChildElementNS(propElement, QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag"));
305  QString CTag;
306  if (!CTagElement.isNull()) {
307  CTag = CTagElement.text();
308  }
309 
310  // extract calendar color if provided
311  const QDomElement colorElement = Utils::firstChildElementNS(propElement, QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color"));
312  QColor color;
313  if (!colorElement.isNull()) {
314  QString colorValue = colorElement.text();
315  if (QColor::isValidColor(colorValue)) {
316  // Color is either #RRGGBBAA or #RRGGBB but QColor expects #AARRGGBB
317  // so we put the AA in front if the string's length is 9.
318  if (colorValue.size() == 9) {
319  QString fixedColorValue = QStringLiteral("#") + colorValue.mid(7, 2) + colorValue.mid(1, 6);
320  color.setNamedColor(fixedColorValue);
321  } else {
322  color.setNamedColor(colorValue);
323  }
324  }
325  }
326 
327  // extract allowed content types
328  const DavCollection::ContentTypes contentTypes = DavManager::davProtocol(mUrl.protocol())->collectionContentTypes(propstatElement);
329 
330  auto _url = url;
331  _url.setUserInfo(mUrl.url().userInfo());
332  DavCollection collection(DavUrl(_url, mUrl.protocol()), displayName, contentTypes);
333 
334  collection.setCTag(CTag);
335  if (color.isValid()) {
336  collection.setColor(color);
337  }
338 
339  // extract privileges
340  const QDomElement currentPrivsElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-privilege-set"));
341  if (currentPrivsElement.isNull()) {
342  // Assume that we have all privileges
343  collection.setPrivileges(KDAV::All);
344  } else {
345  Privileges privileges = Utils::extractPrivileges(currentPrivsElement);
346  collection.setPrivileges(privileges);
347  }
348 
349  qCDebug(KDAV_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges();
350  mCollections << collection;
351  Q_EMIT q->collectionDiscovered(mUrl.protocol(), url.toDisplayString(), jobUrl);
352 
353  responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
354  }
355  }
356  }
357 
358  subjobFinished();
359 }
360 
361 void DavCollectionsFetchJobPrivate::subjobFinished()
362 {
363  if (--mSubJobCount == 0) {
364  emitResult();
365  }
366 }
QString url(QUrl::FormattingOptions options) const const
A helper class to store information about DAV collection.
Definition: davcollection.h:35
void setFocus(const QXmlItem &item)
QString & append(QChar ch)
QDomNode item(int index) const const
QString toDisplayString(QUrl::FormattingOptions options) const const
QDomNodeList elementsByTagNameNS(const QString &nsURI, const QString &localName) const const
Error davError() const
Returns a instance of the KDAV:Error to be able to translate the error.
Definition: davjobbase.cpp:86
void emitResult()
QString userName(QUrl::ComponentFormattingOptions options) const const
const QUrl & url() const
QStringList homeSets() const
Returns the found home sets.
int latestResponseCode() const
Get the latest response code.
Definition: davjobbase.cpp:30
QString & prepend(QChar ch)
void setCTag(const QString &ctag)
Sets this collection CTag.
int size() const const
void setError(int errorCode)
void setPassword(const QString &password, QUrl::ParsingMode mode)
void setPrivileges(Privileges privs)
Sets the privileges on this collection.
A helper class to combine URL and protocol of a DAV URL.
Definition: davurl.h:25
TolerantMode
QDomElement documentElement() const const
QString queryMetaData(const QString &key)
void setNamedColor(const QString &name)
bool isValidColor(const QString &name)
QUrl fromUserInput(const QString &userInput)
int size() const const
QDomElement toElement() const const
void setPath(const QString &path, QUrl::ParsingMode mode)
A job that fetches all DAV collection.
The KDAV namespace.
Definition: davcollection.h:22
void setQuery(const QString &sourceCode, const QUrl &documentURI)
QString text() const const
A job that fetches home sets for a principal.
base class for the jobs used by the resource.
Definition: davjobbase.h:26
CaseInsensitive
bool isEmpty() const const
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
DavCollection::List collections() const
Returns the list of fetched DAV collections.
void setColor(const QColor &color)
Sets the color for this collection.
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString password(QUrl::ComponentFormattingOptions options) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
void setUserName(const QString &userName, QUrl::ParsingMode mode)
bool isNull() const const
DavCollectionsFetchJob(const DavUrl &url, QObject *parent=nullptr)
Creates a new DAV collections fetch job.
QString mid(int position, int n) const const
void evaluateTo(QXmlResultItems *result) const const
virtual Q_SCRIPTABLE void start()=0
void start() override
Starts the job.
QString tagName() const const
void result(KJob *job)
int length() const const
Privileges privileges() const
Returns the privileges on this collection.
DavUrl davUrl() const
Return the DavUrl used by this job.
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
T qobject_cast(QObject *object)
void setUserInfo(const QString &userInfo, QUrl::ParsingMode mode)
int compare(const QString &other, Qt::CaseSensitivity cs) const const
Q_EMITQ_EMIT
QString errorText() const
QDomDocument & response()
QByteArray toByteArray(int indent) const const
bool isValid() const const
int error() const
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Wed Oct 28 2020 23:01:59 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.