File indexing completed on 2024-05-12 05:10:44

0001 /*
0002   SPDX-FileCopyrightText: 2011 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003   SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org>
0004 
0005   SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "freebusymanager.h"
0009 #include "calendarsettings.h"
0010 #include "freebusydownloadjob_p.h"
0011 #include "freebusymanager_p.h"
0012 #include "mailscheduler_p.h"
0013 #include "publishdialog.h"
0014 #include "utils_p.h"
0015 
0016 #include <Akonadi/AgentInstance>
0017 #include <Akonadi/AgentManager>
0018 #include <Akonadi/ContactSearchJob>
0019 
0020 #include <KCalendarCore/Event>
0021 #include <KCalendarCore/FreeBusy>
0022 #include <KCalendarCore/Person>
0023 
0024 #include "akonadicalendar_debug.h"
0025 #include <KMessageBox>
0026 
0027 #include <KIO/FileCopyJob>
0028 #include <KIO/Job>
0029 #include <KIO/TransferJob>
0030 #include <KLocalizedString>
0031 #include <QUrl>
0032 
0033 #include <KJobWidgets>
0034 #include <QDir>
0035 #include <QFile>
0036 #include <QRegularExpression>
0037 #include <QStandardPaths>
0038 #include <QTemporaryFile>
0039 #include <QTextStream>
0040 #include <QTimer>
0041 #include <QTimerEvent>
0042 
0043 using namespace Akonadi;
0044 using namespace KCalendarCore;
0045 
0046 /// Free helper functions
0047 
0048 QUrl replaceVariablesUrl(const QUrl &url, const QString &email)
0049 {
0050     QString emailName;
0051     QString emailHost;
0052 
0053     const int atPos = email.indexOf(QLatin1Char('@'));
0054     if (atPos >= 0) {
0055         emailName = email.left(atPos);
0056         emailHost = email.mid(atPos + 1);
0057     }
0058 
0059     QString saveStr = url.path();
0060     saveStr.replace(QStringLiteral("%email%"), email, Qt::CaseInsensitive);
0061     saveStr.replace(QStringLiteral("%name%"), emailName, Qt::CaseInsensitive);
0062     saveStr.replace(QStringLiteral("%server%"), emailHost, Qt::CaseInsensitive);
0063 
0064     QUrl retUrl(url);
0065     retUrl.setPath(saveStr);
0066     return retUrl;
0067 }
0068 
0069 // We need this function because using KIO::NetAccess::exists()
0070 // is useless for the http and https protocols. And getting back
0071 // arbitrary data is also useless because a server can respond back
0072 // with a "no such document" page.  So we need smart checking.
0073 FbCheckerJob::FbCheckerJob(const QList<QUrl> &urlsToCheck, QObject *parent)
0074     : KJob(parent)
0075     , mUrlsToCheck(urlsToCheck)
0076 {
0077 }
0078 
0079 void FbCheckerJob::start()
0080 {
0081     checkNextUrl();
0082 }
0083 
0084 void FbCheckerJob::checkNextUrl()
0085 {
0086     if (mUrlsToCheck.isEmpty()) {
0087         qCDebug(AKONADICALENDAR_LOG) << "No fb file found";
0088         setError(KJob::UserDefinedError);
0089         emitResult();
0090         return;
0091     }
0092     const QUrl url = mUrlsToCheck.takeFirst();
0093 
0094     mData.clear();
0095     KIO::TransferJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
0096     connect(job, &KIO::TransferJob::data, this, &FbCheckerJob::dataReceived);
0097     connect(job, &KIO::TransferJob::result, this, &FbCheckerJob::onGetJobFinished);
0098 }
0099 
0100 void FbCheckerJob::dataReceived(KIO::Job *, const QByteArray &data)
0101 {
0102     mData.append(data);
0103 }
0104 
0105 void FbCheckerJob::onGetJobFinished(KJob *job)
0106 {
0107     auto transferJob = static_cast<KIO::TransferJob *>(job);
0108     if (mData.contains("BEGIN:VCALENDAR")) {
0109         qCDebug(AKONADICALENDAR_LOG) << "found freebusy";
0110         mValidUrl = transferJob->url();
0111         emitResult();
0112     } else {
0113         checkNextUrl();
0114     }
0115 }
0116 
0117 QUrl FbCheckerJob::validUrl() const
0118 {
0119     return mValidUrl;
0120 }
0121 
0122 /// FreeBusyManagerPrivate::FreeBusyProviderRequest
0123 
0124 FreeBusyManagerPrivate::FreeBusyProviderRequest::FreeBusyProviderRequest(const QString &provider)
0125     : mRequestStatus(NotStarted)
0126     , mInterface(nullptr)
0127 {
0128     mInterface = QSharedPointer<QDBusInterface>(new QDBusInterface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + provider,
0129                                                                    QStringLiteral("/FreeBusyProvider"),
0130                                                                    QStringLiteral("org.freedesktop.Akonadi.Resource.FreeBusyProvider")));
0131 }
0132 
0133 /// FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue
0134 
0135 FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue()
0136     : mResultingFreeBusy(nullptr)
0137 {
0138     // Set the start of the period to today 00:00:00
0139     mStartTime = QDateTime(QDate::currentDate(), QTime());
0140     mEndTime = mStartTime.addDays(14);
0141     mResultingFreeBusy = KCalendarCore::FreeBusy::Ptr(new KCalendarCore::FreeBusy(mStartTime, mEndTime));
0142 }
0143 
0144 FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue(const QDateTime &start, const QDateTime &end)
0145     : mResultingFreeBusy(nullptr)
0146 {
0147     mStartTime = start;
0148     mEndTime = end;
0149     mResultingFreeBusy = KCalendarCore::FreeBusy::Ptr(new KCalendarCore::FreeBusy(start, end));
0150 }
0151 
0152 /// FreeBusyManagerPrivate
0153 
0154 FreeBusyManagerPrivate::FreeBusyManagerPrivate(FreeBusyManager *q)
0155     : QObject()
0156     , q_ptr(q)
0157     , mParentWidgetForRetrieval(nullptr)
0158 {
0159     connect(this, &FreeBusyManagerPrivate::freeBusyUrlRetrieved, this, &FreeBusyManagerPrivate::finishProcessRetrieveQueue);
0160 }
0161 
0162 QString FreeBusyManagerPrivate::freeBusyDir() const
0163 {
0164     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusy");
0165 }
0166 
0167 void FreeBusyManagerPrivate::checkFreeBusyUrl()
0168 {
0169     const QUrl targetURL(CalendarSettings::self()->freeBusyPublishUrl());
0170     mBrokenUrl = targetURL.isEmpty() || !targetURL.isValid();
0171 }
0172 
0173 static QString configFile()
0174 {
0175     static QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusyurls");
0176     return file;
0177 }
0178 
0179 void FreeBusyManagerPrivate::fetchFreeBusyUrl(const QString &email)
0180 {
0181     // First check if there is a specific FB url for this email
0182     KConfig cfg(configFile());
0183     KConfigGroup group = cfg.group(email);
0184     QString url = group.readEntry(QStringLiteral("url"));
0185     if (!url.isEmpty()) {
0186         qCDebug(AKONADICALENDAR_LOG) << "Found cached url:" << url;
0187         QUrl cachedUrl(url);
0188         if (Akonadi::CalendarUtils::thatIsMe(email)) {
0189             cachedUrl.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
0190             cachedUrl.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
0191         }
0192         Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(cachedUrl, email));
0193         return;
0194     }
0195     // Try with the url configured by preferred email in kcontactmanager
0196     auto job = new Akonadi::ContactSearchJob();
0197     job->setQuery(Akonadi::ContactSearchJob::Email, email);
0198     job->setProperty("contactEmail", QVariant::fromValue(email));
0199     connect(job, &Akonadi::ContactSearchJob::result, this, &FreeBusyManagerPrivate::contactSearchJobFinished);
0200     job->start();
0201 }
0202 
0203 void FreeBusyManagerPrivate::contactSearchJobFinished(KJob *_job)
0204 {
0205     const QString email = _job->property("contactEmail").toString();
0206 
0207     if (_job->error()) {
0208         qCritical() << "Error while searching for contact: " << _job->errorString() << ", email = " << email;
0209         Q_EMIT freeBusyUrlRetrieved(email, QUrl());
0210         return;
0211     }
0212 
0213     auto job = qobject_cast<Akonadi::ContactSearchJob *>(_job);
0214     KConfig cfg(configFile());
0215     KConfigGroup group = cfg.group(email);
0216     QString url = group.readEntry(QStringLiteral("url"));
0217 
0218     const KContacts::Addressee::List contacts = job->contacts();
0219     for (const KContacts::Addressee &contact : contacts) {
0220         const QString pref = contact.preferredEmail();
0221         if (!pref.isEmpty() && pref != email) {
0222             group = cfg.group(pref);
0223             url = group.readEntry("url");
0224             qCDebug(AKONADICALENDAR_LOG) << "Preferred email of" << email << "is" << pref;
0225             if (!url.isEmpty()) {
0226                 qCDebug(AKONADICALENDAR_LOG) << "Taken url from preferred email:" << url;
0227                 Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(QUrl(url), email));
0228                 return;
0229             }
0230         }
0231     }
0232     // None found. Check if we do automatic FB retrieving then
0233     if (!CalendarSettings::self()->freeBusyRetrieveAuto()) {
0234         // No, so no FB list here
0235         qCDebug(AKONADICALENDAR_LOG) << "No automatic retrieving";
0236         Q_EMIT freeBusyUrlRetrieved(email, QUrl());
0237         return;
0238     }
0239 
0240     // Sanity check: Don't download if it's not a correct email
0241     // address (this also avoids downloading for "(empty email)").
0242     int emailpos = email.indexOf(QLatin1Char('@'));
0243     if (emailpos == -1) {
0244         qCWarning(AKONADICALENDAR_LOG) << "No '@' found in" << email;
0245         Q_EMIT freeBusyUrlRetrieved(email, QUrl());
0246         return;
0247     }
0248 
0249     const QString emailHost = email.mid(emailpos + 1);
0250 
0251     // Build the URL
0252     if (CalendarSettings::self()->freeBusyCheckHostname()) {
0253         // Don't try to fetch free/busy data for users not on the specified servers
0254         // This tests if the hostnames match, or one is a subset of the other
0255         const QString hostDomain = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl()).host();
0256         if (hostDomain != emailHost && !hostDomain.endsWith(QLatin1Char('.') + emailHost) && !emailHost.endsWith(QLatin1Char('.') + hostDomain)) {
0257             // Host names do not match
0258             qCDebug(AKONADICALENDAR_LOG) << "Host '" << hostDomain << "' doesn't match email '" << email << '\'';
0259             Q_EMIT freeBusyUrlRetrieved(email, QUrl());
0260             return;
0261         }
0262     }
0263 
0264     if (CalendarSettings::self()->freeBusyRetrieveUrl().contains(QRegularExpression(QStringLiteral("\\.[xiv]fb$")))) {
0265         // user specified a fullpath
0266         // do variable string replacements to the URL (MS Outlook style)
0267         const QUrl sourceUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
0268         QUrl fullpathURL = replaceVariablesUrl(sourceUrl, email);
0269 
0270         // set the User and Password part of the URL
0271         fullpathURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
0272         fullpathURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
0273 
0274         // no need to cache this URL as this is pretty fast to get from the config value.
0275         // return the fullpath URL
0276         qCDebug(AKONADICALENDAR_LOG) << "Found url. email=" << email << "; url=" << fullpathURL;
0277         Q_EMIT freeBusyUrlRetrieved(email, fullpathURL);
0278         return;
0279     }
0280 
0281     // else we search for a fb file in the specified URL with known possible extensions
0282     QStringList extensions;
0283     extensions.reserve(3);
0284     extensions << QStringLiteral("xfb");
0285     extensions << QStringLiteral("ifb");
0286     extensions << QStringLiteral("vfb");
0287 
0288     QStringList::ConstIterator ext;
0289     QList<QUrl> urlsToCheck;
0290     urlsToCheck.reserve(extensions.count());
0291     QStringList::ConstIterator extEnd(extensions.constEnd());
0292     for (ext = extensions.constBegin(); ext != extEnd; ++ext) {
0293         // build a url for this extension
0294         const QUrl sourceUrl = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
0295         QUrl dirURL = replaceVariablesUrl(sourceUrl, email);
0296         if (CalendarSettings::self()->freeBusyFullDomainRetrieval()) {
0297             dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
0298             dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + email + QLatin1Char('.') + (*ext)));
0299         } else {
0300             // Cut off everything left of the @ sign to get the user name.
0301             const QString emailName = email.left(emailpos);
0302             dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
0303             dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + emailName + QLatin1Char('.') + (*ext)));
0304         }
0305         dirURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
0306         dirURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
0307         urlsToCheck << dirURL;
0308     }
0309     KJob *checkerJob = new FbCheckerJob(urlsToCheck, this);
0310     checkerJob->setProperty("email", email);
0311     connect(checkerJob, &KJob::result, this, &FreeBusyManagerPrivate::fbCheckerJobFinished);
0312     checkerJob->start();
0313 }
0314 
0315 void FreeBusyManagerPrivate::fbCheckerJobFinished(KJob *job)
0316 {
0317     const QString email = job->property("email").toString();
0318     if (!job->error()) {
0319         auto checkerJob = static_cast<FbCheckerJob *>(job);
0320         const QUrl dirURL = checkerJob->validUrl();
0321         if (dirURL.isValid()) {
0322             // write the URL to the cache
0323             KConfig cfg(configFile());
0324             KConfigGroup group = cfg.group(email);
0325             group.writeEntry("url", dirURL.toDisplayString()); // prettyURL() does not write user nor password
0326             qCDebug(AKONADICALENDAR_LOG) << "Found url email=" << email << "; url=" << dirURL;
0327         }
0328         Q_EMIT freeBusyUrlRetrieved(email, dirURL);
0329     } else {
0330         qCDebug(AKONADICALENDAR_LOG) << "Returning invalid url";
0331         Q_EMIT freeBusyUrlRetrieved(email, QUrl());
0332     }
0333 }
0334 
0335 QString FreeBusyManagerPrivate::freeBusyToIcal(const KCalendarCore::FreeBusy::Ptr &freebusy)
0336 {
0337     return mFormat.createScheduleMessage(freebusy, KCalendarCore::iTIPPublish);
0338 }
0339 
0340 KCalendarCore::FreeBusy::Ptr FreeBusyManagerPrivate::iCalToFreeBusy(const QByteArray &freeBusyData)
0341 {
0342     const QString freeBusyVCal(QString::fromUtf8(freeBusyData));
0343     KCalendarCore::FreeBusy::Ptr fb = mFormat.parseFreeBusy(freeBusyVCal);
0344 
0345     if (!fb) {
0346         qCDebug(AKONADICALENDAR_LOG) << "Error parsing free/busy";
0347         qCDebug(AKONADICALENDAR_LOG) << freeBusyVCal;
0348     }
0349 
0350     return fb;
0351 }
0352 
0353 KCalendarCore::FreeBusy::Ptr FreeBusyManagerPrivate::ownerFreeBusy()
0354 {
0355     const QDateTime start = QDateTime::currentDateTimeUtc();
0356     const QDateTime end = start.addDays(CalendarSettings::self()->freeBusyPublishDays());
0357 
0358     KCalendarCore::Event::List events = mCalendar ? mCalendar->rawEvents(start.date(), end.date()) : KCalendarCore::Event::List();
0359     KCalendarCore::FreeBusy::Ptr freebusy(new KCalendarCore::FreeBusy(events, start, end));
0360     freebusy->setOrganizer(KCalendarCore::Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
0361     return freebusy;
0362 }
0363 
0364 QString FreeBusyManagerPrivate::ownerFreeBusyAsString()
0365 {
0366     return freeBusyToIcal(ownerFreeBusy());
0367 }
0368 
0369 void FreeBusyManagerPrivate::processFreeBusyDownloadResult(KJob *_job)
0370 {
0371     Q_Q(FreeBusyManager);
0372 
0373     auto job = qobject_cast<FreeBusyDownloadJob *>(_job);
0374     Q_ASSERT(job);
0375     if (job->error()) {
0376         qCritical() << "Error downloading freebusy" << _job->errorString();
0377         KMessageBox::error(mParentWidgetForRetrieval,
0378                            i18n("Failed to download free/busy data from: %1\nReason: %2", job->url().toDisplayString(), job->errorText()),
0379                            i18nc("@title:window", "Free/Busy Retrieval Error"));
0380 
0381         // TODO: Ask for a retry? (i.e. queue  the email again when the user wants it).
0382 
0383         // Make sure we don't fill up the map with unneeded data on failures.
0384         mFreeBusyUrlEmailMap.take(job->url());
0385     } else {
0386         KCalendarCore::FreeBusy::Ptr fb = iCalToFreeBusy(job->rawFreeBusyData());
0387 
0388         Q_ASSERT(mFreeBusyUrlEmailMap.contains(job->url()));
0389         const QString email = mFreeBusyUrlEmailMap.take(job->url());
0390 
0391         if (fb) {
0392             KCalendarCore::Person p = fb->organizer();
0393             p.setEmail(email);
0394             fb->setOrganizer(p);
0395             q->saveFreeBusy(fb, p);
0396             qCDebug(AKONADICALENDAR_LOG) << "Freebusy retrieved for " << email;
0397             Q_EMIT q->freeBusyRetrieved(fb, email);
0398         } else {
0399             qCritical() << "Error downloading freebusy, invalid fb.";
0400             KMessageBox::error(mParentWidgetForRetrieval,
0401                                i18n("Failed to parse free/busy information that was retrieved from: %1", job->url().toDisplayString()),
0402                                i18nc("@title:window", "Free/Busy Retrieval Error"));
0403         }
0404     }
0405 
0406     // When downloading failed or finished, start a job for the next one in the
0407     // queue if needed.
0408     processRetrieveQueue();
0409 }
0410 
0411 void FreeBusyManagerPrivate::processFreeBusyUploadResult(KJob *_job)
0412 {
0413     auto job = static_cast<KIO::FileCopyJob *>(_job);
0414     if (job->error()) {
0415         KMessageBox::error(nullptr,
0416                            i18n("<qt><p>The software could not upload your free/busy list to "
0417                                 "the URL '%1'. There might be a problem with the access "
0418                                 "rights, or you specified an incorrect URL. The system said: "
0419                                 "<em>%2</em>.</p>"
0420                                 "<p>Please check the URL or contact your system administrator."
0421                                 "</p></qt>",
0422                                 job->destUrl().toString(),
0423                                 job->errorString()));
0424     }
0425     // Delete temp file
0426     QUrl src = job->srcUrl();
0427     Q_ASSERT(src.isLocalFile());
0428     if (src.isLocalFile()) {
0429         QFile::remove(src.toLocalFile());
0430     }
0431     mUploadingFreeBusy = false;
0432 }
0433 
0434 void FreeBusyManagerPrivate::processRetrieveQueue()
0435 {
0436     if (mRetrieveQueue.isEmpty()) {
0437         return;
0438     }
0439 
0440     const QString email = mRetrieveQueue.takeFirst();
0441 
0442     // First, try to find all agents that are free-busy providers
0443     const QStringList providers = getFreeBusyProviders();
0444     qCDebug(AKONADICALENDAR_LOG) << "Got the following FreeBusy providers: " << providers;
0445 
0446     // If some free-busy providers were found let's query them first and ask them
0447     // if they manage the free-busy information for the email address we have.
0448     if (!providers.isEmpty()) {
0449         queryFreeBusyProviders(providers, email);
0450     } else {
0451         fetchFreeBusyUrl(email);
0452     }
0453 }
0454 
0455 void FreeBusyManagerPrivate::finishProcessRetrieveQueue(const QString &email, const QUrl &freeBusyUrlForEmail)
0456 {
0457     Q_Q(FreeBusyManager);
0458 
0459     if (!freeBusyUrlForEmail.isValid()) {
0460         qCDebug(AKONADICALENDAR_LOG) << "Invalid FreeBusy URL" << freeBusyUrlForEmail.toDisplayString() << email;
0461         return;
0462     }
0463 
0464     if (mFreeBusyUrlEmailMap.contains(freeBusyUrlForEmail)) {
0465         qCDebug(AKONADICALENDAR_LOG) << "Download already in progress for " << freeBusyUrlForEmail;
0466         return;
0467     }
0468 
0469     mFreeBusyUrlEmailMap.insert(freeBusyUrlForEmail, email);
0470 
0471     auto job = new FreeBusyDownloadJob(freeBusyUrlForEmail, mParentWidgetForRetrieval);
0472     q->connect(job, &FreeBusyDownloadJob::result, this, [this](KJob *job) {
0473         processFreeBusyDownloadResult(job);
0474     });
0475     job->start();
0476 }
0477 
0478 void FreeBusyManagerPrivate::uploadFreeBusy()
0479 {
0480     Q_Q(FreeBusyManager);
0481 
0482     // user has automatic uploading disabled, bail out
0483     if (!CalendarSettings::self()->freeBusyPublishAuto() || CalendarSettings::self()->freeBusyPublishUrl().isEmpty()) {
0484         return;
0485     }
0486 
0487     if (mTimerID != 0) {
0488         // A timer is already running, so we don't need to do anything
0489         return;
0490     }
0491 
0492     const QDateTime currentDateTime = QDateTime::currentDateTime();
0493     int now = static_cast<int>(currentDateTime.toSecsSinceEpoch());
0494     int eta = static_cast<int>(mNextUploadTime.toSecsSinceEpoch()) - now;
0495 
0496     if (!mUploadingFreeBusy) {
0497         // Not currently uploading
0498         if (mNextUploadTime.isNull() || currentDateTime > mNextUploadTime) {
0499             // No uploading have been done in this session, or delay time is over
0500             q->publishFreeBusy();
0501             return;
0502         }
0503 
0504         // We're in the delay time and no timer is running. Start one
0505         if (eta <= 0) {
0506             // Sanity check failed - better do the upload
0507             q->publishFreeBusy();
0508             return;
0509         }
0510     } else {
0511         // We are currently uploading the FB list. Start the timer
0512         if (eta <= 0) {
0513             qCDebug(AKONADICALENDAR_LOG) << "This shouldn't happen! eta <= 0";
0514             eta = 10; // whatever
0515         }
0516     }
0517 
0518     // Start the timer
0519     mTimerID = q->startTimer(eta * 1000);
0520 
0521     if (mTimerID == 0) {
0522         // startTimer failed - better do the upload
0523         q->publishFreeBusy();
0524     }
0525 }
0526 
0527 QStringList FreeBusyManagerPrivate::getFreeBusyProviders() const
0528 {
0529     QStringList providers;
0530     const Akonadi::AgentInstance::List agents = Akonadi::AgentManager::self()->instances();
0531     for (const Akonadi::AgentInstance &agent : agents) {
0532         if (agent.type().capabilities().contains(QLatin1StringView("FreeBusyProvider"))) {
0533             providers << agent.identifier();
0534         }
0535     }
0536     return providers;
0537 }
0538 
0539 void FreeBusyManagerPrivate::queryFreeBusyProviders(const QStringList &providers, const QString &email)
0540 {
0541     if (!mProvidersRequestsByEmail.contains(email)) {
0542         mProvidersRequestsByEmail[email] = FreeBusyProvidersRequestsQueue();
0543     }
0544 
0545     for (const QString &provider : providers) {
0546         FreeBusyProviderRequest request(provider);
0547 
0548         // clang-format off
0549         connect(request.mInterface.data(), SIGNAL(handlesFreeBusy(QString,bool)), this, SLOT(onHandlesFreeBusy(QString,bool)));
0550         // clang-format on
0551         request.mInterface->call(QStringLiteral("canHandleFreeBusy"), email);
0552         request.mRequestStatus = FreeBusyProviderRequest::HandlingRequested;
0553         mProvidersRequestsByEmail[email].mRequests << request;
0554     }
0555 }
0556 
0557 void FreeBusyManagerPrivate::queryFreeBusyProviders(const QStringList &providers, const QString &email, const QDateTime &start, const QDateTime &end)
0558 {
0559     if (!mProvidersRequestsByEmail.contains(email)) {
0560         mProvidersRequestsByEmail[email] = FreeBusyProvidersRequestsQueue(start, end);
0561     }
0562 
0563     queryFreeBusyProviders(providers, email);
0564 }
0565 
0566 void FreeBusyManagerPrivate::onHandlesFreeBusy(const QString &email, bool handles)
0567 {
0568     if (!mProvidersRequestsByEmail.contains(email)) {
0569         return;
0570     }
0571 
0572     auto iface = qobject_cast<QDBusInterface *>(sender());
0573     if (!iface) {
0574         return;
0575     }
0576 
0577     FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
0578     QString respondingService = iface->service();
0579     qCDebug(AKONADICALENDAR_LOG) << respondingService << "responded to our FreeBusy request:" << handles;
0580     int requestIndex = -1;
0581 
0582     const int requestsSize(queue->mRequests.size());
0583     for (int i = 0; i < requestsSize; ++i) {
0584         if (queue->mRequests.at(i).mInterface->service() == respondingService) {
0585             requestIndex = i;
0586         }
0587     }
0588 
0589     if (requestIndex == -1) {
0590         return;
0591     }
0592     // clang-format off
0593     disconnect(iface, SIGNAL(handlesFreeBusy(QString,bool)), this, SLOT(onHandlesFreeBusy(QString,bool)));
0594     // clang-format on
0595     if (!handles) {
0596         queue->mRequests.removeAt(requestIndex);
0597         // If no more requests are left and no handler responded
0598         // then fall back to the URL mechanism
0599         if (queue->mRequests.isEmpty() && queue->mHandlersCount == 0) {
0600             mProvidersRequestsByEmail.remove(email);
0601             fetchFreeBusyUrl(email);
0602         }
0603     } else {
0604         ++queue->mHandlersCount;
0605         // clang-format off
0606         connect(iface, SIGNAL(freeBusyRetrieved(QString,QString,bool,QString)), this, SLOT(onFreeBusyRetrieved(QString,QString,bool,QString)));
0607         // clang-format on
0608         iface->call(QStringLiteral("retrieveFreeBusy"), email, queue->mStartTime, queue->mEndTime);
0609         queue->mRequests[requestIndex].mRequestStatus = FreeBusyProviderRequest::FreeBusyRequested;
0610     }
0611 }
0612 
0613 void FreeBusyManagerPrivate::processMailSchedulerResult(Akonadi::Scheduler::Result result, const QString &errorMsg)
0614 {
0615     if (result == Scheduler::ResultSuccess) {
0616         KMessageBox::information(mParentWidgetForMailling,
0617                                  i18n("The free/busy information was successfully sent."),
0618                                  i18nc("@title:window", "Sending Free/Busy"),
0619                                  QStringLiteral("FreeBusyPublishSuccess"));
0620     } else {
0621         KMessageBox::error(mParentWidgetForMailling, i18n("Unable to publish the free/busy data: %1", errorMsg));
0622     }
0623 
0624     sender()->deleteLater();
0625 }
0626 
0627 void FreeBusyManagerPrivate::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText)
0628 {
0629     Q_Q(FreeBusyManager);
0630     Q_UNUSED(errorText)
0631 
0632     if (!mProvidersRequestsByEmail.contains(email)) {
0633         return;
0634     }
0635 
0636     auto iface = dynamic_cast<QDBusInterface *>(sender());
0637     if (!iface) {
0638         return;
0639     }
0640 
0641     FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
0642     QString respondingService = iface->service();
0643     int requestIndex = -1;
0644 
0645     const int requestsSize(queue->mRequests.size());
0646     for (int i = 0; i < requestsSize; ++i) {
0647         if (queue->mRequests.at(i).mInterface->service() == respondingService) {
0648             requestIndex = i;
0649         }
0650     }
0651 
0652     if (requestIndex == -1) {
0653         return;
0654     }
0655     // clang-format off
0656     disconnect(iface, SIGNAL(freeBusyRetrieved(QString,QString,bool,QString)), this, SLOT(onFreeBusyRetrieved(QString,QString,bool,QString)));
0657     // clang-format on
0658     queue->mRequests.removeAt(requestIndex);
0659 
0660     if (success) {
0661         KCalendarCore::FreeBusy::Ptr fb = iCalToFreeBusy(freeBusy.toUtf8());
0662         if (!fb) {
0663             --queue->mHandlersCount;
0664         } else {
0665             queue->mResultingFreeBusy->merge(fb);
0666         }
0667     }
0668 
0669     if (queue->mRequests.isEmpty()) {
0670         if (queue->mHandlersCount == 0) {
0671             fetchFreeBusyUrl(email);
0672         } else {
0673             Q_EMIT q->freeBusyRetrieved(queue->mResultingFreeBusy, email);
0674         }
0675         mProvidersRequestsByEmail.remove(email);
0676     }
0677 }
0678 
0679 /// FreeBusyManager::Singleton
0680 
0681 namespace Akonadi
0682 {
0683 class FreeBusyManagerStatic
0684 {
0685 public:
0686     FreeBusyManager instance;
0687 };
0688 }
0689 
0690 Q_GLOBAL_STATIC(FreeBusyManagerStatic, sManagerInstance)
0691 
0692 FreeBusyManager::FreeBusyManager()
0693     : d_ptr(new FreeBusyManagerPrivate(this))
0694 {
0695     setObjectName(QLatin1StringView("FreeBusyManager"));
0696     connect(CalendarSettings::self(), SIGNAL(configChanged()), SLOT(checkFreeBusyUrl()));
0697 }
0698 
0699 FreeBusyManager::~FreeBusyManager() = default;
0700 
0701 FreeBusyManager *FreeBusyManager::self()
0702 {
0703     return &sManagerInstance->instance;
0704 }
0705 
0706 void FreeBusyManager::setCalendar(const Akonadi::ETMCalendar::Ptr &c)
0707 {
0708     Q_D(FreeBusyManager);
0709 
0710     if (d->mCalendar) {
0711         disconnect(d->mCalendar.data(), SIGNAL(calendarChanged()));
0712     }
0713 
0714     d->mCalendar = c;
0715     if (d->mCalendar) {
0716         d->mFormat.setTimeZone(d->mCalendar->timeZone());
0717         connect(d->mCalendar.data(), SIGNAL(calendarChanged()), SLOT(uploadFreeBusy()));
0718     }
0719 
0720     // Lets see if we need to update our published
0721     QTimer::singleShot(0, this, SLOT(uploadFreeBusy()));
0722 }
0723 
0724 /*!
0725   This method is called when the user has selected to publish its
0726   free/busy list or when the delay have passed.
0727 */
0728 void FreeBusyManager::publishFreeBusy(QWidget *parentWidget)
0729 {
0730     Q_D(FreeBusyManager);
0731     // Already uploading? Skip this one then.
0732     if (d->mUploadingFreeBusy) {
0733         return;
0734     }
0735 
0736     // No calendar set yet? Don't upload to prevent losing published information that
0737     // might still be valid.
0738     if (!d->mCalendar) {
0739         return;
0740     }
0741 
0742     QUrl targetURL(CalendarSettings::self()->freeBusyPublishUrl());
0743     if (targetURL.isEmpty()) {
0744         KMessageBox::error(parentWidget,
0745                            i18n("<qt><p>No URL configured for uploading your free/busy list. "
0746                                 "Please set it in KOrganizer's configuration dialog, on the "
0747                                 "\"Free/Busy\" page.</p>"
0748                                 "<p>Contact your system administrator for the exact URL and the "
0749                                 "account details.</p></qt>"),
0750                            i18nc("@title:window", "No Free/Busy Upload URL"));
0751         return;
0752     }
0753 
0754     if (d->mBrokenUrl) {
0755         // Url is invalid, don't try again
0756         return;
0757     }
0758     if (!targetURL.isValid()) {
0759         KMessageBox::error(parentWidget,
0760                            i18n("<qt>The target URL '%1' provided is invalid.</qt>", targetURL.toDisplayString()),
0761                            i18nc("@title:window", "Invalid URL"));
0762         d->mBrokenUrl = true;
0763         return;
0764     }
0765     targetURL.setUserName(CalendarSettings::self()->freeBusyPublishUser());
0766     targetURL.setPassword(CalendarSettings::self()->freeBusyPublishPassword());
0767 
0768     d->mUploadingFreeBusy = true;
0769 
0770     // If we have a timer running, it should be stopped now
0771     if (d->mTimerID != 0) {
0772         killTimer(d->mTimerID);
0773         d->mTimerID = 0;
0774     }
0775 
0776     // Save the time of the next free/busy uploading
0777     d->mNextUploadTime = QDateTime::currentDateTime();
0778     if (CalendarSettings::self()->freeBusyPublishDelay() > 0) {
0779         d->mNextUploadTime = d->mNextUploadTime.addSecs(CalendarSettings::self()->freeBusyPublishDelay() * 60);
0780     }
0781 
0782     QString messageText = d->ownerFreeBusyAsString();
0783 
0784     // We need to massage the list a bit so that Outlook understands
0785     // it.
0786     messageText.replace(QRegularExpression(QStringLiteral("ORGANIZER\\s*:MAILTO:")), QStringLiteral("ORGANIZER:"));
0787 
0788     // Create a local temp file and save the message to it
0789     QTemporaryFile tempFile;
0790     tempFile.setAutoRemove(false);
0791     if (tempFile.open()) {
0792         QTextStream textStream(&tempFile);
0793         textStream << messageText;
0794         textStream.flush();
0795 
0796 #if 0
0797         QString defaultEmail = KOCore()
0798                                ::self()->email();
0799         QString emailHost = defaultEmail.mid(defaultEmail.indexOf('@') + 1);
0800 
0801         // Put target string together
0802         QUrl targetURL;
0803         if (CalendarSettings::self()->publishKolab()) {
0804             // we use Kolab
0805             QString server;
0806             if (CalendarSettings::self()->publishKolabServer() == QLatin1StringView("%SERVER%")
0807                 || CalendarSettings::self()->publishKolabServer().isEmpty()) {
0808                 server = emailHost;
0809             } else {
0810                 server = CalendarSettings::self()->publishKolabServer();
0811             }
0812 
0813             targetURL.setScheme("webdavs");
0814             targetURL.setHost(server);
0815 
0816             QString fbname = CalendarSettings::self()->publishUserName();
0817             int at = fbname.indexOf('@');
0818             if (at > 1 && fbname.length() > (uint)at) {
0819                 fbname.truncate(at);
0820             }
0821             targetURL.setPath("/freebusy/" + fbname + ".ifb");
0822             targetURL.setUserName(CalendarSettings::self()->publishUserName());
0823             targetURL.setPassword(CalendarSettings::self()->publishPassword());
0824         } else {
0825             // we use something else
0826             targetURL = CalendarSettings::self()->+publishAnyURL().replace("%SERVER%", emailHost);
0827             targetURL.setUserName(CalendarSettings::self()->publishUserName());
0828             targetURL.setPassword(CalendarSettings::self()->publishPassword());
0829         }
0830 #endif
0831 
0832         QUrl src;
0833         src.setPath(tempFile.fileName());
0834 
0835         qCDebug(AKONADICALENDAR_LOG) << targetURL;
0836 
0837         KIO::Job *job = KIO::file_copy(src, targetURL, -1, KIO::Overwrite | KIO::HideProgressInfo);
0838 
0839         KJobWidgets::setWindow(job, parentWidget);
0840 
0841         // FIXME slot doesn't exist
0842         // connect(job, SIGNAL(result(KJob*)), SLOT(slotUploadFreeBusyResult(KJob*)));
0843     }
0844 }
0845 
0846 void FreeBusyManager::mailFreeBusy(int daysToPublish, QWidget *parentWidget)
0847 {
0848     Q_D(FreeBusyManager);
0849     // No calendar set yet?
0850     if (!d->mCalendar) {
0851         return;
0852     }
0853 
0854     QDateTime start = QDateTime::currentDateTimeUtc().toTimeZone(d->mCalendar->timeZone());
0855     QDateTime end = start.addDays(daysToPublish);
0856 
0857     KCalendarCore::Event::List events = d->mCalendar->rawEvents(start.date(), end.date());
0858 
0859     FreeBusy::Ptr freebusy(new FreeBusy(events, start, end));
0860     freebusy->setOrganizer(Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
0861 
0862     QPointer<PublishDialog> publishdlg = new PublishDialog();
0863     if (publishdlg->exec() == QDialog::Accepted) {
0864         // Send the mail
0865         auto scheduler = new MailScheduler(/*factory=*/nullptr, this);
0866         connect(scheduler, &Scheduler::transactionFinished, d, &FreeBusyManagerPrivate::processMailSchedulerResult);
0867         d->mParentWidgetForMailling = parentWidget;
0868 
0869         scheduler->publish(freebusy, publishdlg->addresses());
0870     }
0871     delete publishdlg;
0872 }
0873 
0874 bool FreeBusyManager::retrieveFreeBusy(const QString &email, bool forceDownload, QWidget *parentWidget)
0875 {
0876     Q_D(FreeBusyManager);
0877 
0878     qCDebug(AKONADICALENDAR_LOG) << email;
0879     if (email.isEmpty()) {
0880         qCDebug(AKONADICALENDAR_LOG) << "Email is empty";
0881         return false;
0882     }
0883 
0884     d->mParentWidgetForRetrieval = parentWidget;
0885 
0886     if (Akonadi::CalendarUtils::thatIsMe(email)) {
0887         // Don't download our own free-busy list from the net
0888         qCDebug(AKONADICALENDAR_LOG) << "freebusy of owner, not downloading";
0889         Q_EMIT freeBusyRetrieved(d->ownerFreeBusy(), email);
0890         return true;
0891     }
0892 
0893     // Check for cached copy of free/busy list
0894     KCalendarCore::FreeBusy::Ptr fb = loadFreeBusy(email);
0895     if (fb) {
0896         qCDebug(AKONADICALENDAR_LOG) << "Found a cached copy for " << email;
0897         Q_EMIT freeBusyRetrieved(fb, email);
0898         return true;
0899     }
0900 
0901     // Don't download free/busy if the user does not want it.
0902     if (!CalendarSettings::self()->freeBusyRetrieveAuto() && !forceDownload) {
0903         qCDebug(AKONADICALENDAR_LOG) << "Not downloading freebusy";
0904         return false;
0905     }
0906 
0907     d->mRetrieveQueue.append(email);
0908 
0909     if (d->mRetrieveQueue.count() > 1) {
0910         // TODO: true should always emit
0911         qCWarning(AKONADICALENDAR_LOG) << "Returning true without Q_EMIT, is this correct?";
0912         return true;
0913     }
0914 
0915     // queued, because "true" means the download was initiated. So lets
0916     // return before starting stuff
0917     QMetaObject::invokeMethod(
0918         d,
0919         [d] {
0920             d->processRetrieveQueue();
0921         },
0922         Qt::QueuedConnection);
0923     return true;
0924 }
0925 
0926 void FreeBusyManager::cancelRetrieval()
0927 {
0928     Q_D(FreeBusyManager);
0929     d->mRetrieveQueue.clear();
0930 }
0931 
0932 KCalendarCore::FreeBusy::Ptr FreeBusyManager::loadFreeBusy(const QString &email)
0933 {
0934     Q_D(FreeBusyManager);
0935     const QString fbd = d->freeBusyDir();
0936 
0937     QFile f(fbd + QLatin1Char('/') + email + QStringLiteral(".ifb"));
0938     if (!f.exists()) {
0939         qCDebug(AKONADICALENDAR_LOG) << f.fileName() << "doesn't exist.";
0940         return {};
0941     }
0942 
0943     if (!f.open(QIODevice::ReadOnly)) {
0944         qCDebug(AKONADICALENDAR_LOG) << "Unable to open file" << f.fileName();
0945         return {};
0946     }
0947 
0948     QTextStream ts(&f);
0949     QString str = ts.readAll();
0950 
0951     return d->iCalToFreeBusy(str.toUtf8());
0952 }
0953 
0954 bool FreeBusyManager::saveFreeBusy(const KCalendarCore::FreeBusy::Ptr &freebusy, const KCalendarCore::Person &person)
0955 {
0956     Q_D(FreeBusyManager);
0957     qCDebug(AKONADICALENDAR_LOG) << person.fullName();
0958 
0959     QString fbd = d->freeBusyDir();
0960 
0961     QDir freeBusyDirectory(fbd);
0962     if (!freeBusyDirectory.exists()) {
0963         qCDebug(AKONADICALENDAR_LOG) << "Directory" << fbd << " does not exist!";
0964         qCDebug(AKONADICALENDAR_LOG) << "Creating directory:" << fbd;
0965 
0966         if (!freeBusyDirectory.mkpath(fbd)) {
0967             qCDebug(AKONADICALENDAR_LOG) << "Could not create directory:" << fbd;
0968             return false;
0969         }
0970     }
0971 
0972     QString filename(fbd);
0973     filename += QLatin1Char('/');
0974     filename += person.email();
0975     filename += QStringLiteral(".ifb");
0976     QFile f(filename);
0977 
0978     qCDebug(AKONADICALENDAR_LOG) << "filename:" << filename;
0979 
0980     freebusy->clearAttendees();
0981     freebusy->setOrganizer(person);
0982 
0983     QString messageText = d->mFormat.createScheduleMessage(freebusy, KCalendarCore::iTIPPublish);
0984 
0985     if (!f.open(QIODevice::ReadWrite)) {
0986         qCDebug(AKONADICALENDAR_LOG) << "acceptFreeBusy: Can't open:" << filename << "for writing";
0987         return false;
0988     }
0989     QTextStream t(&f);
0990     t << messageText;
0991     f.close();
0992 
0993     return true;
0994 }
0995 
0996 void FreeBusyManager::timerEvent(QTimerEvent *event)
0997 {
0998     Q_UNUSED(event)
0999     publishFreeBusy();
1000 }
1001 
1002 #include "moc_freebusymanager.cpp"
1003 #include "moc_freebusymanager_p.cpp"