KDAV

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

KDE's Doxygen guidelines are available online.