File indexing completed on 2024-12-22 04:56:56

0001 /*
0002     SPDX-FileCopyrightText: 2011 Grégory Oestreicher <greg@kamago.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "davfreebusyhandler.h"
0008 
0009 #include "settings.h"
0010 #include <KDAV/DavCollectionsFetchJob>
0011 #include <KDAV/DavPrincipalSearchJob>
0012 #include <QDomElement>
0013 
0014 #include "davresource_debug.h"
0015 #include <KCalendarCore/ICalFormat>
0016 #include <KIO/Job>
0017 #include <KIO/StoredTransferJob>
0018 #include <KLocalizedString>
0019 
0020 static QDomElement firstChildElementNS(const QDomElement &parent, const QString &namespaceUri, const QString &tagName)
0021 {
0022     for (QDomNode child = parent.firstChild(); !child.isNull(); child = child.nextSibling()) {
0023         if (child.isElement()) {
0024             const QDomElement elt = child.toElement();
0025             if (tagName.isEmpty() || (elt.tagName() == tagName && elt.namespaceURI() == namespaceUri)) {
0026                 return elt;
0027             }
0028         }
0029     }
0030 
0031     return {};
0032 }
0033 
0034 DavFreeBusyHandler::DavFreeBusyHandler(QObject *parent)
0035     : QObject(parent)
0036 {
0037 }
0038 
0039 void DavFreeBusyHandler::canHandleFreeBusy(const QString &email)
0040 {
0041     const KDAV::DavUrl::List urls = Settings::self()->configuredDavUrls();
0042     for (const KDAV::DavUrl &url : urls) {
0043         if (url.protocol() == KDAV::CalDav) {
0044             ++mRequestsTracker[email].handlingJobCount;
0045             auto job = new KDAV::DavPrincipalSearchJob(url, KDAV::DavPrincipalSearchJob::EmailAddress, email);
0046             job->setProperty("email", QVariant::fromValue(email));
0047             job->setProperty("url", QVariant::fromValue(url.url().toString()));
0048             job->fetchProperty(QStringLiteral("schedule-inbox-URL"), QStringLiteral("urn:ietf:params:xml:ns:caldav"));
0049             connect(job, &KDAV::DavPrincipalSearchJob::result, this, &DavFreeBusyHandler::onPrincipalSearchJobFinished);
0050             job->start();
0051         }
0052     }
0053 }
0054 
0055 void DavFreeBusyHandler::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end)
0056 {
0057     if (!mPrincipalScheduleOutbox.contains(email)) {
0058         Q_EMIT freeBusyRetrieved(email, QString(), false, i18n("No schedule-outbox found for %1", email));
0059         return;
0060     }
0061 
0062     KCalendarCore::FreeBusy::Ptr fb(new KCalendarCore::FreeBusy(start, end));
0063     KCalendarCore::Attendee att(QString(), email);
0064     fb->addAttendee(att);
0065 
0066     KCalendarCore::ICalFormat formatter;
0067     const QByteArray fbData = formatter.createScheduleMessage(fb, KCalendarCore::iTIPRequest).toUtf8();
0068 
0069     const QStringList principalScheduleOutboxFromEmail = mPrincipalScheduleOutbox[email];
0070     for (const QString &outbox : principalScheduleOutboxFromEmail) {
0071         ++mRequestsTracker[email].retrievalJobCount;
0072         const uint requestId = mNextRequestId++;
0073 
0074         QUrl url(outbox);
0075         KIO::StoredTransferJob *job = KIO::storedHttpPost(fbData, url);
0076         job->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/calendar"));
0077         job->setProperty("email", QVariant::fromValue(email));
0078         job->setProperty("request-id", QVariant::fromValue(requestId));
0079         connect(job, &KDAV::DavPrincipalSearchJob::result, this, &DavFreeBusyHandler::onRetrieveFreeBusyJobFinished);
0080         job->start();
0081     }
0082 }
0083 
0084 void DavFreeBusyHandler::onPrincipalSearchJobFinished(KJob *job)
0085 {
0086     const QString email = job->property("email").toString();
0087     int handlingJobCount = --mRequestsTracker[email].handlingJobCount;
0088 
0089     if (job->error()) {
0090         if (handlingJobCount == 0 && !mRequestsTracker[email].handlingJobSuccessful) {
0091             Q_EMIT handlesFreeBusy(email, false);
0092         }
0093         return;
0094     }
0095 
0096     auto davJob = qobject_cast<KDAV::DavPrincipalSearchJob *>(job);
0097     const QList<KDAV::DavPrincipalSearchJob::Result> results = davJob->results();
0098 
0099     if (results.isEmpty()) {
0100         if (handlingJobCount == 0 && !mRequestsTracker[email].handlingJobSuccessful) {
0101             Q_EMIT handlesFreeBusy(email, false);
0102         }
0103         return;
0104     }
0105 
0106     mRequestsTracker[email].handlingJobSuccessful = true;
0107 
0108     for (const KDAV::DavPrincipalSearchJob::Result &result : std::as_const(results)) {
0109         qCDebug(DAVRESOURCE_LOG) << result.value;
0110         QUrl url(davJob->property("url").toString());
0111         if (result.value.startsWith(QLatin1Char('/'))) {
0112             // href is only a path, use request url to complete
0113             url.setPath(result.value, QUrl::TolerantMode);
0114         } else {
0115             // href is a complete url
0116             url = QUrl::fromUserInput(result.value);
0117         }
0118 
0119         if (!mPrincipalScheduleOutbox[email].contains(url.url())) {
0120             mPrincipalScheduleOutbox[email] << url.url();
0121         }
0122     }
0123 
0124     if (handlingJobCount == 0) {
0125         Q_EMIT handlesFreeBusy(email, true);
0126     }
0127 }
0128 
0129 void DavFreeBusyHandler::onRetrieveFreeBusyJobFinished(KJob *job)
0130 {
0131     const QString email = job->property("email").toString();
0132     uint requestId = job->property("request-id").toUInt();
0133     int retrievalJobCount = --mRequestsTracker[email].retrievalJobCount;
0134 
0135     if (job->error()) {
0136         if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
0137             Q_EMIT freeBusyRetrieved(email, QString(), false, job->errorString());
0138         }
0139         return;
0140     }
0141 
0142     /*
0143      * Extract info from a document like the following:
0144      * <?xml version="1.0" encoding="utf-8" ?>
0145      * <C:schedule-response xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
0146      *   <C:response>
0147      *     <C:recipient>
0148      *       <D:href>mailto:wilfredo@example.com<D:href>
0149      *     </C:recipient>
0150      *     <C:request-status>2.0;Success</C:request-status>
0151      *     <C:calendar-data>BEGIN:VCALENDAR
0152      * VERSION:2.0
0153      * PRODID:-//Example Corp.//CalDAV Server//EN
0154      * METHOD:REPLY
0155      * BEGIN:VFREEBUSY
0156      * UID:4FD3AD926350
0157      * DTSTAMP:20090602T200733Z
0158      * DTSTART:20090602T000000Z
0159      * DTEND:20090604T000000Z
0160      * ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
0161      * ATTENDEE;CN="Wilfredo Sanchez Vega":mailto:wilfredo@example.com
0162      * FREEBUSY;FBTYPE=BUSY:20090602T110000Z/20090602T120000Z
0163      * FREEBUSY;FBTYPE=BUSY:20090603T170000Z/20090603T180000Z
0164      * END:VFREEBUSY
0165      * END:VCALENDAR
0166      *     </C:calendar-data>
0167      *   </C:response>
0168      *   <C:response>
0169      *     <C:recipient>
0170      *       <D:href>mailto:mike@example.org<D:href>
0171      *     </C:recipient>
0172      *     <C:request-status>3.7;Invalid calendar user</C:request-status>
0173      *   </C:response>
0174      * </C:schedule-response>
0175      */
0176 
0177     auto postJob = qobject_cast<KIO::StoredTransferJob *>(job);
0178     QDomDocument response;
0179     response.setContent(postJob->data(), true);
0180 
0181     QDomElement scheduleResponse = response.documentElement();
0182 
0183     // We are only expecting one response tag
0184     QDomElement responseElement = firstChildElementNS(scheduleResponse, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("response"));
0185     if (responseElement.isNull()) {
0186         if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
0187             Q_EMIT freeBusyRetrieved(email, QString(), false, i18n("Invalid response from the server"));
0188         }
0189         return;
0190     }
0191 
0192     // We can load directly the calendar-data and use its content to create
0193     // an incidence base that will give us everything we need to test
0194     // the success
0195     QDomElement calendarDataElement = firstChildElementNS(responseElement, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-data"));
0196     if (calendarDataElement.isNull()) {
0197         if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
0198             Q_EMIT freeBusyRetrieved(email, QString(), false, i18n("Invalid response from the server"));
0199         }
0200         return;
0201     }
0202 
0203     const QString rawData = calendarDataElement.text();
0204 
0205     KCalendarCore::ICalFormat format;
0206     KCalendarCore::FreeBusy::Ptr fb = format.parseFreeBusy(rawData);
0207     if (fb.isNull()) {
0208         if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
0209             Q_EMIT freeBusyRetrieved(email, QString(), false, i18n("Unable to parse free-busy data received"));
0210         }
0211         return;
0212     }
0213 
0214     // We're safe now
0215     mRequestsTracker[email].retrievalJobSuccessful = true;
0216 
0217     //   fb->clearAttendees();
0218     if (mRequestsTracker[email].resultingFreeBusy[requestId].isNull()) {
0219         mRequestsTracker[email].resultingFreeBusy[requestId] = fb;
0220     } else {
0221         mRequestsTracker[email].resultingFreeBusy[requestId]->merge(fb);
0222     }
0223 
0224     if (retrievalJobCount == 0) {
0225         const QString fbStr = format.createScheduleMessage(mRequestsTracker[email].resultingFreeBusy[requestId], KCalendarCore::iTIPRequest);
0226         Q_EMIT freeBusyRetrieved(email, fbStr, true, QString());
0227     }
0228 }
0229 
0230 #include "moc_davfreebusyhandler.cpp"