Akonadi Calendar

freebusymanager.cpp
1/*
2 SPDX-FileCopyrightText: 2011 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3 SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "freebusymanager.h"
9using namespace Qt::Literals::StringLiterals;
10
11#include "calendarsettings.h"
12#include "freebusydownloadjob_p.h"
13#include "freebusymanager_p.h"
14#include "mailscheduler_p.h"
15#include "publishdialog.h"
16#include "utils_p.h"
17
18#include <Akonadi/AgentInstance>
19#include <Akonadi/AgentManager>
20#include <Akonadi/ContactSearchJob>
21
22#include <KCalendarCore/Event>
23#include <KCalendarCore/FreeBusy>
24#include <KCalendarCore/Person>
25
26#include "akonadicalendar_debug.h"
27#include <KMessageBox>
28
29#include <KIO/FileCopyJob>
30#include <KIO/Job>
31#include <KIO/TransferJob>
32#include <KLocalizedString>
33#include <QUrl>
34
35#include <KJobWidgets>
36#include <QDir>
37#include <QFile>
38#include <QRegularExpression>
39#include <QStandardPaths>
40#include <QTemporaryFile>
41#include <QTextStream>
42#include <QTimer>
43#include <QTimerEvent>
44
45using namespace Akonadi;
46using namespace KCalendarCore;
47
48/// Free helper functions
49
50QUrl replaceVariablesUrl(const QUrl &url, const QString &email)
51{
52 QString emailName;
53 QString emailHost;
54
55 const int atPos = email.indexOf(QLatin1Char('@'));
56 if (atPos >= 0) {
57 emailName = email.left(atPos);
58 emailHost = email.mid(atPos + 1);
59 }
60
61 QString saveStr = url.path();
62 saveStr.replace(QStringLiteral("%email%"), email, Qt::CaseInsensitive);
63 saveStr.replace(QStringLiteral("%name%"), emailName, Qt::CaseInsensitive);
64 saveStr.replace(QStringLiteral("%server%"), emailHost, Qt::CaseInsensitive);
65
66 QUrl retUrl(url);
67 retUrl.setPath(saveStr);
68 return retUrl;
69}
70
71// We need this function because using KIO::NetAccess::exists()
72// is useless for the http and https protocols. And getting back
73// arbitrary data is also useless because a server can respond back
74// with a "no such document" page. So we need smart checking.
75FbCheckerJob::FbCheckerJob(const QList<QUrl> &urlsToCheck, QObject *parent)
76 : KJob(parent)
77 , mUrlsToCheck(urlsToCheck)
78{
79}
80
81void FbCheckerJob::start()
82{
83 checkNextUrl();
84}
85
86void FbCheckerJob::checkNextUrl()
87{
88 if (mUrlsToCheck.isEmpty()) {
89 qCDebug(AKONADICALENDAR_LOG) << "No fb file found";
90 setError(KJob::UserDefinedError);
91 emitResult();
92 return;
93 }
94 const QUrl url = mUrlsToCheck.takeFirst();
95
96 mData.clear();
97 KIO::TransferJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
98 connect(job, &KIO::TransferJob::data, this, &FbCheckerJob::dataReceived);
99 connect(job, &KIO::TransferJob::result, this, &FbCheckerJob::onGetJobFinished);
100}
101
102void FbCheckerJob::dataReceived(KIO::Job *, const QByteArray &data)
103{
104 mData.append(data);
105}
106
107void FbCheckerJob::onGetJobFinished(KJob *job)
108{
109 auto transferJob = static_cast<KIO::TransferJob *>(job);
110 if (mData.contains("BEGIN:VCALENDAR")) {
111 qCDebug(AKONADICALENDAR_LOG) << "found freebusy";
112 mValidUrl = transferJob->url();
113 emitResult();
114 } else {
115 checkNextUrl();
116 }
117}
118
119QUrl FbCheckerJob::validUrl() const
120{
121 return mValidUrl;
122}
123
124/// FreeBusyManagerPrivate::FreeBusyProviderRequest
125
126FreeBusyManagerPrivate::FreeBusyProviderRequest::FreeBusyProviderRequest(const QString &provider)
127 : mRequestStatus(NotStarted)
128 , mInterface(nullptr)
129{
130 mInterface = QSharedPointer<QDBusInterface>(new QDBusInterface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + provider,
131 QStringLiteral("/FreeBusyProvider"),
132 QStringLiteral("org.freedesktop.Akonadi.Resource.FreeBusyProvider")));
133}
134
135/// FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue
136
137FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue()
138 : mResultingFreeBusy(nullptr)
139{
140 // Set the start of the period to today 00:00:00
141 mStartTime = QDateTime(QDate::currentDate(), QTime());
142 mEndTime = mStartTime.addDays(14);
143 mResultingFreeBusy = KCalendarCore::FreeBusy::Ptr(new KCalendarCore::FreeBusy(mStartTime, mEndTime));
144}
145
146FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue(const QDateTime &start, const QDateTime &end)
147 : mResultingFreeBusy(nullptr)
148{
149 mStartTime = start;
150 mEndTime = end;
151 mResultingFreeBusy = KCalendarCore::FreeBusy::Ptr(new KCalendarCore::FreeBusy(start, end));
152}
153
154/// FreeBusyManagerPrivate
155
156FreeBusyManagerPrivate::FreeBusyManagerPrivate(FreeBusyManager *q)
157 : QObject()
158 , q_ptr(q)
159 , mParentWidgetForRetrieval(nullptr)
160{
161 connect(this, &FreeBusyManagerPrivate::freeBusyUrlRetrieved, this, &FreeBusyManagerPrivate::finishProcessRetrieveQueue);
162}
163
164QString FreeBusyManagerPrivate::freeBusyDir() const
165{
166 return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusy");
167}
168
169void FreeBusyManagerPrivate::checkFreeBusyUrl()
170{
171 const QUrl targetURL(CalendarSettings::self()->freeBusyPublishUrl());
172 mBrokenUrl = targetURL.isEmpty() || !targetURL.isValid();
173}
174
175static QString configFile()
176{
177 static QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusyurls");
178 return file;
179}
180
181void FreeBusyManagerPrivate::fetchFreeBusyUrl(const QString &email)
182{
183 // First check if there is a specific FB url for this email
184 KConfig cfg(configFile());
185 KConfigGroup group = cfg.group(email);
186 QString url = group.readEntry(QStringLiteral("url"));
187 if (!url.isEmpty()) {
188 qCDebug(AKONADICALENDAR_LOG) << "Found cached url:" << url;
189 QUrl cachedUrl(url);
190 if (Akonadi::CalendarUtils::thatIsMe(email)) {
191 cachedUrl.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
192 cachedUrl.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
193 }
194 Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(cachedUrl, email));
195 return;
196 }
197 // Try with the url configured by preferred email in kcontactmanager
198 auto job = new Akonadi::ContactSearchJob();
199 job->setQuery(Akonadi::ContactSearchJob::Email, email);
200 job->setProperty("contactEmail", QVariant::fromValue(email));
201 connect(job, &Akonadi::ContactSearchJob::result, this, &FreeBusyManagerPrivate::contactSearchJobFinished);
202 job->start();
203}
204
205void FreeBusyManagerPrivate::contactSearchJobFinished(KJob *_job)
206{
207 const QString email = _job->property("contactEmail").toString();
208
209 if (_job->error()) {
210 qCritical() << "Error while searching for contact: " << _job->errorString() << ", email = " << email;
211 Q_EMIT freeBusyUrlRetrieved(email, QUrl());
212 return;
213 }
214
215 auto job = qobject_cast<Akonadi::ContactSearchJob *>(_job);
216 KConfig cfg(configFile());
217 KConfigGroup group = cfg.group(email);
218 QString url = group.readEntry(QStringLiteral("url"));
219
220 const KContacts::Addressee::List contacts = job->contacts();
221 for (const KContacts::Addressee &contact : contacts) {
222 const QString pref = contact.preferredEmail();
223 if (!pref.isEmpty() && pref != email) {
224 group = cfg.group(pref);
225 url = group.readEntry("url");
226 qCDebug(AKONADICALENDAR_LOG) << "Preferred email of" << email << "is" << pref;
227 if (!url.isEmpty()) {
228 qCDebug(AKONADICALENDAR_LOG) << "Taken url from preferred email:" << url;
229 Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(QUrl(url), email));
230 return;
231 }
232 }
233 }
234 // None found. Check if we do automatic FB retrieving then
235 if (!CalendarSettings::self()->freeBusyRetrieveAuto()) {
236 // No, so no FB list here
237 qCDebug(AKONADICALENDAR_LOG) << "No automatic retrieving";
238 Q_EMIT freeBusyUrlRetrieved(email, QUrl());
239 return;
240 }
241
242 // Sanity check: Don't download if it's not a correct email
243 // address (this also avoids downloading for "(empty email)").
244 int emailpos = email.indexOf(QLatin1Char('@'));
245 if (emailpos == -1) {
246 qCWarning(AKONADICALENDAR_LOG) << "No '@' found in" << email;
247 Q_EMIT freeBusyUrlRetrieved(email, QUrl());
248 return;
249 }
250
251 const QString emailHost = email.mid(emailpos + 1);
252
253 // Build the URL
254 if (CalendarSettings::self()->freeBusyCheckHostname()) {
255 // Don't try to fetch free/busy data for users not on the specified servers
256 // This tests if the hostnames match, or one is a subset of the other
257 const QString hostDomain = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl()).host();
258 if (hostDomain != emailHost && !hostDomain.endsWith(QLatin1Char('.') + emailHost) && !emailHost.endsWith(QLatin1Char('.') + hostDomain)) {
259 // Host names do not match
260 qCDebug(AKONADICALENDAR_LOG) << "Host '" << hostDomain << "' doesn't match email '" << email << '\'';
261 Q_EMIT freeBusyUrlRetrieved(email, QUrl());
262 return;
263 }
264 }
265
266 if (CalendarSettings::self()->freeBusyRetrieveUrl().contains(QRegularExpression(QStringLiteral("\\.[xiv]fb$")))) {
267 // user specified a fullpath
268 // do variable string replacements to the URL (MS Outlook style)
269 const QUrl sourceUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
270 QUrl fullpathURL = replaceVariablesUrl(sourceUrl, email);
271
272 // set the User and Password part of the URL
273 fullpathURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
274 fullpathURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
275
276 // no need to cache this URL as this is pretty fast to get from the config value.
277 // return the fullpath URL
278 qCDebug(AKONADICALENDAR_LOG) << "Found url. email=" << email << "; url=" << fullpathURL;
279 Q_EMIT freeBusyUrlRetrieved(email, fullpathURL);
280 return;
281 }
282
283 // else we search for a fb file in the specified URL with known possible extensions
284 QStringList extensions;
285 extensions.reserve(3);
286 extensions << QStringLiteral("xfb");
287 extensions << QStringLiteral("ifb");
288 extensions << QStringLiteral("vfb");
289
291 QList<QUrl> urlsToCheck;
292 urlsToCheck.reserve(extensions.count());
293 QStringList::ConstIterator extEnd(extensions.constEnd());
294 for (ext = extensions.constBegin(); ext != extEnd; ++ext) {
295 // build a url for this extension
296 const QUrl sourceUrl = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
297 QUrl dirURL = replaceVariablesUrl(sourceUrl, email);
298 if (CalendarSettings::self()->freeBusyFullDomainRetrieval()) {
299 dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
300 dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + email + QLatin1Char('.') + (*ext)));
301 } else {
302 // Cut off everything left of the @ sign to get the user name.
303 const QString emailName = email.left(emailpos);
304 dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
305 dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + emailName + QLatin1Char('.') + (*ext)));
306 }
307 dirURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
308 dirURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
309 urlsToCheck << dirURL;
310 }
311 KJob *checkerJob = new FbCheckerJob(urlsToCheck, this);
312 checkerJob->setProperty("email", email);
313 connect(checkerJob, &KJob::result, this, &FreeBusyManagerPrivate::fbCheckerJobFinished);
314 checkerJob->start();
315}
316
317void FreeBusyManagerPrivate::fbCheckerJobFinished(KJob *job)
318{
319 const QString email = job->property("email").toString();
320 if (!job->error()) {
321 auto checkerJob = static_cast<FbCheckerJob *>(job);
322 const QUrl dirURL = checkerJob->validUrl();
323 if (dirURL.isValid()) {
324 // write the URL to the cache
325 KConfig cfg(configFile());
326 KConfigGroup group = cfg.group(email);
327 group.writeEntry("url", dirURL.toDisplayString()); // prettyURL() does not write user nor password
328 qCDebug(AKONADICALENDAR_LOG) << "Found url email=" << email << "; url=" << dirURL;
329 }
330 Q_EMIT freeBusyUrlRetrieved(email, dirURL);
331 } else {
332 qCDebug(AKONADICALENDAR_LOG) << "Returning invalid url";
333 Q_EMIT freeBusyUrlRetrieved(email, QUrl());
334 }
335}
336
337QString FreeBusyManagerPrivate::freeBusyToIcal(const KCalendarCore::FreeBusy::Ptr &freebusy)
338{
339 return mFormat.createScheduleMessage(freebusy, KCalendarCore::iTIPPublish);
340}
341
342KCalendarCore::FreeBusy::Ptr FreeBusyManagerPrivate::iCalToFreeBusy(const QByteArray &freeBusyData)
343{
344 const QString freeBusyVCal(QString::fromUtf8(freeBusyData));
345 KCalendarCore::FreeBusy::Ptr fb = mFormat.parseFreeBusy(freeBusyVCal);
346
347 if (!fb) {
348 qCDebug(AKONADICALENDAR_LOG) << "Error parsing free/busy";
349 qCDebug(AKONADICALENDAR_LOG) << freeBusyVCal;
350 }
351
352 return fb;
353}
354
355KCalendarCore::FreeBusy::Ptr FreeBusyManagerPrivate::ownerFreeBusy()
356{
358 const QDateTime end = start.addDays(CalendarSettings::self()->freeBusyPublishDays());
359
360 KCalendarCore::Event::List events = mCalendar ? mCalendar->rawEvents(start.date(), end.date()) : KCalendarCore::Event::List();
362 freebusy->setOrganizer(KCalendarCore::Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
363 return freebusy;
364}
365
366QString FreeBusyManagerPrivate::ownerFreeBusyAsString()
367{
368 return freeBusyToIcal(ownerFreeBusy());
369}
370
371void FreeBusyManagerPrivate::processFreeBusyDownloadResult(KJob *_job)
372{
373 Q_Q(FreeBusyManager);
374
375 auto job = qobject_cast<FreeBusyDownloadJob *>(_job);
376 Q_ASSERT(job);
377 if (job->error()) {
378 qCritical() << "Error downloading freebusy" << _job->errorString();
379 KMessageBox::error(mParentWidgetForRetrieval,
380 i18n("Failed to download free/busy data from: %1\nReason: %2", job->url().toDisplayString(), job->errorText()),
381 i18nc("@title:window", "Free/Busy Retrieval Error"));
382
383 // TODO: Ask for a retry? (i.e. queue the email again when the user wants it).
384
385 // Make sure we don't fill up the map with unneeded data on failures.
386 mFreeBusyUrlEmailMap.take(job->url());
387 } else {
388 KCalendarCore::FreeBusy::Ptr fb = iCalToFreeBusy(job->rawFreeBusyData());
389
390 Q_ASSERT(mFreeBusyUrlEmailMap.contains(job->url()));
391 const QString email = mFreeBusyUrlEmailMap.take(job->url());
392
393 if (fb) {
394 KCalendarCore::Person p = fb->organizer();
395 p.setEmail(email);
396 fb->setOrganizer(p);
397 q->saveFreeBusy(fb, p);
398 qCDebug(AKONADICALENDAR_LOG) << "Freebusy retrieved for " << email;
399 Q_EMIT q->freeBusyRetrieved(fb, email);
400 } else {
401 qCritical() << "Error downloading freebusy, invalid fb.";
402 KMessageBox::error(mParentWidgetForRetrieval,
403 i18n("Failed to parse free/busy information that was retrieved from: %1", job->url().toDisplayString()),
404 i18nc("@title:window", "Free/Busy Retrieval Error"));
405 }
406 }
407
408 // When downloading failed or finished, start a job for the next one in the
409 // queue if needed.
410 processRetrieveQueue();
411}
412
413void FreeBusyManagerPrivate::processFreeBusyUploadResult(KJob *_job)
414{
415 auto job = static_cast<KIO::FileCopyJob *>(_job);
416 if (job->error()) {
417 KMessageBox::error(nullptr,
418 i18n("<qt><p>The software could not upload your free/busy list to "
419 "the URL '%1'. There might be a problem with the access "
420 "rights, or you specified an incorrect URL. The system said: "
421 "<em>%2</em>.</p>"
422 "<p>Please check the URL or contact your system administrator."
423 "</p></qt>",
424 job->destUrl().toString(),
425 job->errorString()));
426 }
427 // Delete temp file
428 QUrl src = job->srcUrl();
429 Q_ASSERT(src.isLocalFile());
430 if (src.isLocalFile()) {
432 }
433 mUploadingFreeBusy = false;
434}
435
436void FreeBusyManagerPrivate::processRetrieveQueue()
437{
438 if (mRetrieveQueue.isEmpty()) {
439 return;
440 }
441
442 const QString email = mRetrieveQueue.takeFirst();
443
444 // First, try to find all agents that are free-busy providers
445 const QStringList providers = getFreeBusyProviders();
446 qCDebug(AKONADICALENDAR_LOG) << "Got the following FreeBusy providers: " << providers;
447
448 // If some free-busy providers were found let's query them first and ask them
449 // if they manage the free-busy information for the email address we have.
450 if (!providers.isEmpty()) {
451 queryFreeBusyProviders(providers, email);
452 } else {
453 fetchFreeBusyUrl(email);
454 }
455}
456
457void FreeBusyManagerPrivate::finishProcessRetrieveQueue(const QString &email, const QUrl &freeBusyUrlForEmail)
458{
459 Q_Q(FreeBusyManager);
460
461 if (!freeBusyUrlForEmail.isValid()) {
462 qCDebug(AKONADICALENDAR_LOG) << "Invalid FreeBusy URL" << freeBusyUrlForEmail.toDisplayString() << email;
463 return;
464 }
465
466 if (mFreeBusyUrlEmailMap.contains(freeBusyUrlForEmail)) {
467 qCDebug(AKONADICALENDAR_LOG) << "Download already in progress for " << freeBusyUrlForEmail;
468 return;
469 }
470
471 mFreeBusyUrlEmailMap.insert(freeBusyUrlForEmail, email);
472
473 auto job = new FreeBusyDownloadJob(freeBusyUrlForEmail, mParentWidgetForRetrieval);
474 q->connect(job, &FreeBusyDownloadJob::result, this, [this](KJob *job) {
475 processFreeBusyDownloadResult(job);
476 });
477 job->start();
478}
479
480void FreeBusyManagerPrivate::uploadFreeBusy()
481{
482 Q_Q(FreeBusyManager);
483
484 // user has automatic uploading disabled, bail out
485 if (!CalendarSettings::self()->freeBusyPublishAuto() || CalendarSettings::self()->freeBusyPublishUrl().isEmpty()) {
486 return;
487 }
488
489 if (mTimerID != 0) {
490 // A timer is already running, so we don't need to do anything
491 return;
492 }
493
494 const QDateTime currentDateTime = QDateTime::currentDateTime();
495 int now = static_cast<int>(currentDateTime.toSecsSinceEpoch());
496 int eta = static_cast<int>(mNextUploadTime.toSecsSinceEpoch()) - now;
497
498 if (!mUploadingFreeBusy) {
499 // Not currently uploading
500 if (mNextUploadTime.isNull() || currentDateTime > mNextUploadTime) {
501 // No uploading have been done in this session, or delay time is over
502 q->publishFreeBusy();
503 return;
504 }
505
506 // We're in the delay time and no timer is running. Start one
507 if (eta <= 0) {
508 // Sanity check failed - better do the upload
509 q->publishFreeBusy();
510 return;
511 }
512 } else {
513 // We are currently uploading the FB list. Start the timer
514 if (eta <= 0) {
515 qCDebug(AKONADICALENDAR_LOG) << "This shouldn't happen! eta <= 0";
516 eta = 10; // whatever
517 }
518 }
519
520 // Start the timer
521 mTimerID = q->startTimer(eta * 1000);
522
523 if (mTimerID == 0) {
524 // startTimer failed - better do the upload
525 q->publishFreeBusy();
526 }
527}
528
529QStringList FreeBusyManagerPrivate::getFreeBusyProviders() const
530{
533 for (const Akonadi::AgentInstance &agent : agents) {
534 if (agent.type().capabilities().contains("FreeBusyProvider"_L1)) {
535 providers << agent.identifier();
536 }
537 }
538 return providers;
539}
540
541void FreeBusyManagerPrivate::queryFreeBusyProviders(const QStringList &providers, const QString &email)
542{
543 if (!mProvidersRequestsByEmail.contains(email)) {
544 mProvidersRequestsByEmail[email] = FreeBusyProvidersRequestsQueue();
545 }
546
547 for (const QString &provider : providers) {
548 FreeBusyProviderRequest request(provider);
549
550 // clang-format off
551 connect(request.mInterface.data(), SIGNAL(handlesFreeBusy(QString,bool)), this, SLOT(onHandlesFreeBusy(QString,bool)));
552 // clang-format on
553 request.mInterface->call(QStringLiteral("canHandleFreeBusy"), email);
554 request.mRequestStatus = FreeBusyProviderRequest::HandlingRequested;
555 mProvidersRequestsByEmail[email].mRequests << request;
556 }
557}
558
559void FreeBusyManagerPrivate::queryFreeBusyProviders(const QStringList &providers, const QString &email, const QDateTime &start, const QDateTime &end)
560{
561 if (!mProvidersRequestsByEmail.contains(email)) {
562 mProvidersRequestsByEmail[email] = FreeBusyProvidersRequestsQueue(start, end);
563 }
564
565 queryFreeBusyProviders(providers, email);
566}
567
568void FreeBusyManagerPrivate::onHandlesFreeBusy(const QString &email, bool handles)
569{
570 if (!mProvidersRequestsByEmail.contains(email)) {
571 return;
572 }
573
574 auto iface = qobject_cast<QDBusInterface *>(sender());
575 if (!iface) {
576 return;
577 }
578
579 FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
580 QString respondingService = iface->service();
581 qCDebug(AKONADICALENDAR_LOG) << respondingService << "responded to our FreeBusy request:" << handles;
582 int requestIndex = -1;
583
584 const int requestsSize(queue->mRequests.size());
585 for (int i = 0; i < requestsSize; ++i) {
586 if (queue->mRequests.at(i).mInterface->service() == respondingService) {
587 requestIndex = i;
588 }
589 }
590
591 if (requestIndex == -1) {
592 return;
593 }
594 // clang-format off
595 disconnect(iface, SIGNAL(handlesFreeBusy(QString,bool)), this, SLOT(onHandlesFreeBusy(QString,bool)));
596 // clang-format on
597 if (!handles) {
598 queue->mRequests.removeAt(requestIndex);
599 // If no more requests are left and no handler responded
600 // then fall back to the URL mechanism
601 if (queue->mRequests.isEmpty() && queue->mHandlersCount == 0) {
602 mProvidersRequestsByEmail.remove(email);
603 fetchFreeBusyUrl(email);
604 }
605 } else {
606 ++queue->mHandlersCount;
607 // clang-format off
608 connect(iface, SIGNAL(freeBusyRetrieved(QString,QString,bool,QString)), this, SLOT(onFreeBusyRetrieved(QString,QString,bool,QString)));
609 // clang-format on
610 iface->call(QStringLiteral("retrieveFreeBusy"), email, queue->mStartTime, queue->mEndTime);
611 queue->mRequests[requestIndex].mRequestStatus = FreeBusyProviderRequest::FreeBusyRequested;
612 }
613}
614
615void FreeBusyManagerPrivate::processMailSchedulerResult(Akonadi::Scheduler::Result result, const QString &errorMsg)
616{
617 if (result == Scheduler::ResultSuccess) {
618 KMessageBox::information(mParentWidgetForMailling,
619 i18n("The free/busy information was successfully sent."),
620 i18nc("@title:window", "Sending Free/Busy"),
621 QStringLiteral("FreeBusyPublishSuccess"));
622 } else {
623 KMessageBox::error(mParentWidgetForMailling, i18n("Unable to publish the free/busy data: %1", errorMsg));
624 }
625
626 sender()->deleteLater();
627}
628
629void FreeBusyManagerPrivate::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText)
630{
631 Q_Q(FreeBusyManager);
632 Q_UNUSED(errorText)
633
634 if (!mProvidersRequestsByEmail.contains(email)) {
635 return;
636 }
637
638 auto iface = dynamic_cast<QDBusInterface *>(sender());
639 if (!iface) {
640 return;
641 }
642
643 FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
644 QString respondingService = iface->service();
645 int requestIndex = -1;
646
647 const int requestsSize(queue->mRequests.size());
648 for (int i = 0; i < requestsSize; ++i) {
649 if (queue->mRequests.at(i).mInterface->service() == respondingService) {
650 requestIndex = i;
651 }
652 }
653
654 if (requestIndex == -1) {
655 return;
656 }
657 // clang-format off
658 disconnect(iface, SIGNAL(freeBusyRetrieved(QString,QString,bool,QString)), this, SLOT(onFreeBusyRetrieved(QString,QString,bool,QString)));
659 // clang-format on
660 queue->mRequests.removeAt(requestIndex);
661
662 if (success) {
663 KCalendarCore::FreeBusy::Ptr fb = iCalToFreeBusy(freeBusy.toUtf8());
664 if (!fb) {
665 --queue->mHandlersCount;
666 } else {
667 queue->mResultingFreeBusy->merge(fb);
668 }
669 }
670
671 if (queue->mRequests.isEmpty()) {
672 if (queue->mHandlersCount == 0) {
673 fetchFreeBusyUrl(email);
674 } else {
675 Q_EMIT q->freeBusyRetrieved(queue->mResultingFreeBusy, email);
676 }
677 mProvidersRequestsByEmail.remove(email);
678 }
679}
680
681/// FreeBusyManager::Singleton
682
683namespace Akonadi
684{
685class FreeBusyManagerStatic
686{
687public:
688 FreeBusyManager instance;
689};
690}
691
692Q_GLOBAL_STATIC(FreeBusyManagerStatic, sManagerInstance)
693
694FreeBusyManager::FreeBusyManager()
695 : d_ptr(new FreeBusyManagerPrivate(this))
696{
697 setObjectName("FreeBusyManager"_L1);
698 connect(CalendarSettings::self(), SIGNAL(configChanged()), SLOT(checkFreeBusyUrl()));
699}
700
701FreeBusyManager::~FreeBusyManager() = default;
702
703FreeBusyManager *FreeBusyManager::self()
704{
705 return &sManagerInstance->instance;
706}
707
708void FreeBusyManager::setCalendar(const Akonadi::ETMCalendar::Ptr &c)
709{
710 Q_D(FreeBusyManager);
711
712 if (d->mCalendar) {
713 disconnect(d->mCalendar.data(), SIGNAL(calendarChanged()));
714 }
715
716 d->mCalendar = c;
717 if (d->mCalendar) {
718 d->mFormat.setTimeZone(d->mCalendar->timeZone());
719 connect(d->mCalendar.data(), SIGNAL(calendarChanged()), SLOT(uploadFreeBusy()));
720 }
721
722 // Lets see if we need to update our published
723 QTimer::singleShot(0, this, SLOT(uploadFreeBusy()));
724}
725
726/*!
727 This method is called when the user has selected to publish its
728 free/busy list or when the delay have passed.
729*/
730void FreeBusyManager::publishFreeBusy(QWidget *parentWidget)
731{
732 Q_D(FreeBusyManager);
733 // Already uploading? Skip this one then.
734 if (d->mUploadingFreeBusy) {
735 return;
736 }
737
738 // No calendar set yet? Don't upload to prevent losing published information that
739 // might still be valid.
740 if (!d->mCalendar) {
741 return;
742 }
743
744 QUrl targetURL(CalendarSettings::self()->freeBusyPublishUrl());
745 if (targetURL.isEmpty()) {
746 KMessageBox::error(parentWidget,
747 i18n("<qt><p>No URL configured for uploading your free/busy list. "
748 "Please set it in KOrganizer's configuration dialog, on the "
749 "\"Free/Busy\" page.</p>"
750 "<p>Contact your system administrator for the exact URL and the "
751 "account details.</p></qt>"),
752 i18nc("@title:window", "No Free/Busy Upload URL"));
753 return;
754 }
755
756 if (d->mBrokenUrl) {
757 // Url is invalid, don't try again
758 return;
759 }
760 if (!targetURL.isValid()) {
761 KMessageBox::error(parentWidget,
762 i18n("<qt>The target URL '%1' provided is invalid.</qt>", targetURL.toDisplayString()),
763 i18nc("@title:window", "Invalid URL"));
764 d->mBrokenUrl = true;
765 return;
766 }
767 targetURL.setUserName(CalendarSettings::self()->freeBusyPublishUser());
768 targetURL.setPassword(CalendarSettings::self()->freeBusyPublishPassword());
769
770 d->mUploadingFreeBusy = true;
771
772 // If we have a timer running, it should be stopped now
773 if (d->mTimerID != 0) {
774 killTimer(d->mTimerID);
775 d->mTimerID = 0;
776 }
777
778 // Save the time of the next free/busy uploading
779 d->mNextUploadTime = QDateTime::currentDateTime();
780 if (CalendarSettings::self()->freeBusyPublishDelay() > 0) {
781 d->mNextUploadTime = d->mNextUploadTime.addSecs(CalendarSettings::self()->freeBusyPublishDelay() * 60);
782 }
783
784 QString messageText = d->ownerFreeBusyAsString();
785
786 // We need to massage the list a bit so that Outlook understands
787 // it.
788 messageText.replace(QRegularExpression(QStringLiteral("ORGANIZER\\s*:MAILTO:")), QStringLiteral("ORGANIZER:"));
789
790 // Create a local temp file and save the message to it
791 QTemporaryFile tempFile;
792 tempFile.setAutoRemove(false);
793 if (tempFile.open()) {
794 QTextStream textStream(&tempFile);
795 textStream << messageText;
796 textStream.flush();
797
798#if 0
799 QString defaultEmail = KOCore()
800 ::self()->email();
801 QString emailHost = defaultEmail.mid(defaultEmail.indexOf('@') + 1);
802
803 // Put target string together
804 QUrl targetURL;
805 if (CalendarSettings::self()->publishKolab()) {
806 // we use Kolab
807 QString server;
808 if (CalendarSettings::self()->publishKolabServer() == "%SERVER%"_L1
809 || CalendarSettings::self()->publishKolabServer().isEmpty()) {
810 server = emailHost;
811 } else {
812 server = CalendarSettings::self()->publishKolabServer();
813 }
814
815 targetURL.setScheme("webdavs");
816 targetURL.setHost(server);
817
818 QString fbname = CalendarSettings::self()->publishUserName();
819 int at = fbname.indexOf('@');
820 if (at > 1 && fbname.length() > (uint)at) {
821 fbname.truncate(at);
822 }
823 targetURL.setPath("/freebusy/" + fbname + ".ifb");
824 targetURL.setUserName(CalendarSettings::self()->publishUserName());
825 targetURL.setPassword(CalendarSettings::self()->publishPassword());
826 } else {
827 // we use something else
828 targetURL = CalendarSettings::self()->+publishAnyURL().replace("%SERVER%", emailHost);
829 targetURL.setUserName(CalendarSettings::self()->publishUserName());
830 targetURL.setPassword(CalendarSettings::self()->publishPassword());
831 }
832#endif
833
834 QUrl src;
835 src.setPath(tempFile.fileName());
836
837 qCDebug(AKONADICALENDAR_LOG) << targetURL;
838
839 KIO::Job *job = KIO::file_copy(src, targetURL, -1, KIO::Overwrite | KIO::HideProgressInfo);
840
841 KJobWidgets::setWindow(job, parentWidget);
842
843 // FIXME slot doesn't exist
844 // connect(job, SIGNAL(result(KJob*)), SLOT(slotUploadFreeBusyResult(KJob*)));
845 }
846}
847
848void FreeBusyManager::mailFreeBusy(int daysToPublish, QWidget *parentWidget)
849{
850 Q_D(FreeBusyManager);
851 // No calendar set yet?
852 if (!d->mCalendar) {
853 return;
854 }
855
856 QDateTime start = QDateTime::currentDateTimeUtc().toTimeZone(d->mCalendar->timeZone());
857 QDateTime end = start.addDays(daysToPublish);
858
859 KCalendarCore::Event::List events = d->mCalendar->rawEvents(start.date(), end.date());
860
861 FreeBusy::Ptr freebusy(new FreeBusy(events, start, end));
862 freebusy->setOrganizer(Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
863
864 QPointer<PublishDialog> publishdlg = new PublishDialog();
865 if (publishdlg->exec() == QDialog::Accepted) {
866 // Send the mail
867 auto scheduler = new MailScheduler(/*factory=*/nullptr, this);
868 connect(scheduler, &Scheduler::transactionFinished, d, &FreeBusyManagerPrivate::processMailSchedulerResult);
869 d->mParentWidgetForMailling = parentWidget;
870
871 scheduler->publish(freebusy, publishdlg->addresses());
872 }
873 delete publishdlg;
874}
875
876bool FreeBusyManager::retrieveFreeBusy(const QString &email, bool forceDownload, QWidget *parentWidget)
877{
878 Q_D(FreeBusyManager);
879
880 qCDebug(AKONADICALENDAR_LOG) << email;
881 if (email.isEmpty()) {
882 qCDebug(AKONADICALENDAR_LOG) << "Email is empty";
883 return false;
884 }
885
886 d->mParentWidgetForRetrieval = parentWidget;
887
888 if (Akonadi::CalendarUtils::thatIsMe(email)) {
889 // Don't download our own free-busy list from the net
890 qCDebug(AKONADICALENDAR_LOG) << "freebusy of owner, not downloading";
891 Q_EMIT freeBusyRetrieved(d->ownerFreeBusy(), email);
892 return true;
893 }
894
895 // Check for cached copy of free/busy list
896 KCalendarCore::FreeBusy::Ptr fb = loadFreeBusy(email);
897 if (fb) {
898 qCDebug(AKONADICALENDAR_LOG) << "Found a cached copy for " << email;
899 Q_EMIT freeBusyRetrieved(fb, email);
900 return true;
901 }
902
903 // Don't download free/busy if the user does not want it.
904 if (!CalendarSettings::self()->freeBusyRetrieveAuto() && !forceDownload) {
905 qCDebug(AKONADICALENDAR_LOG) << "Not downloading freebusy";
906 return false;
907 }
908
909 d->mRetrieveQueue.append(email);
910
911 if (d->mRetrieveQueue.count() > 1) {
912 // TODO: true should always emit
913 qCWarning(AKONADICALENDAR_LOG) << "Returning true without Q_EMIT, is this correct?";
914 return true;
915 }
916
917 // queued, because "true" means the download was initiated. So lets
918 // return before starting stuff
920 d,
921 [d] {
922 d->processRetrieveQueue();
923 },
925 return true;
926}
927
928void FreeBusyManager::cancelRetrieval()
929{
930 Q_D(FreeBusyManager);
931 d->mRetrieveQueue.clear();
932}
933
934KCalendarCore::FreeBusy::Ptr FreeBusyManager::loadFreeBusy(const QString &email)
935{
936 Q_D(FreeBusyManager);
937 const QString fbd = d->freeBusyDir();
938
939 QFile f(fbd + QLatin1Char('/') + email + QStringLiteral(".ifb"));
940 if (!f.exists()) {
941 qCDebug(AKONADICALENDAR_LOG) << f.fileName() << "doesn't exist.";
942 return {};
943 }
944
945 if (!f.open(QIODevice::ReadOnly)) {
946 qCDebug(AKONADICALENDAR_LOG) << "Unable to open file" << f.fileName();
947 return {};
948 }
949
950 QTextStream ts(&f);
951 QString str = ts.readAll();
952
953 return d->iCalToFreeBusy(str.toUtf8());
954}
955
956bool FreeBusyManager::saveFreeBusy(const KCalendarCore::FreeBusy::Ptr &freebusy, const KCalendarCore::Person &person)
957{
958 Q_D(FreeBusyManager);
959 qCDebug(AKONADICALENDAR_LOG) << person.fullName();
960
961 QString fbd = d->freeBusyDir();
962
963 QDir freeBusyDirectory(fbd);
964 if (!freeBusyDirectory.exists()) {
965 qCDebug(AKONADICALENDAR_LOG) << "Directory" << fbd << " does not exist!";
966 qCDebug(AKONADICALENDAR_LOG) << "Creating directory:" << fbd;
967
968 if (!freeBusyDirectory.mkpath(fbd)) {
969 qCDebug(AKONADICALENDAR_LOG) << "Could not create directory:" << fbd;
970 return false;
971 }
972 }
973
974 QString filename(fbd);
975 filename += QLatin1Char('/');
976 filename += person.email();
977 filename += QStringLiteral(".ifb");
978 QFile f(filename);
979
980 qCDebug(AKONADICALENDAR_LOG) << "filename:" << filename;
981
982 freebusy->clearAttendees();
983 freebusy->setOrganizer(person);
984
985 QString messageText = d->mFormat.createScheduleMessage(freebusy, KCalendarCore::iTIPPublish);
986
987 if (!f.open(QIODevice::WriteOnly)) {
988 qCDebug(AKONADICALENDAR_LOG) << "acceptFreeBusy: Can't open:" << filename << "for writing";
989 return false;
990 }
991 QTextStream t(&f);
992 t << messageText;
993 f.close();
994
995 return true;
996}
997
998void FreeBusyManager::timerEvent(QTimerEvent *event)
999{
1000 Q_UNUSED(event)
1001 publishFreeBusy();
1002}
1003
1004#include "moc_freebusymanager.cpp"
1005#include "moc_freebusymanager_p.cpp"
static AgentManager * self()
AgentInstance::List instances() const
QSharedPointer< FreeBusy > Ptr
QString email() const
void setEmail(const QString &email)
QString fullName() const
KConfigGroup group(const QString &group)
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
AddresseeList List
const QUrl & url() const
void data(KIO::Job *job, const QByteArray &data)
virtual QString errorString() const
int error() const
void result(KJob *job)
virtual Q_SCRIPTABLE void start()=0
QString errorText() const
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
FreeBusyManager::Singleton.
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT FileCopyJob * file_copy(const QUrl &src, const QUrl &dest, int permissions=-1, JobFlags flags=DefaultFlags)
HideProgressInfo
void setWindow(QObject *job, QWidget *widget)
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
const QList< QKeySequence > & end()
QCA_EXPORT ProviderList providers()
QDate currentDate()
QDateTime currentDateTime()
QDateTime currentDateTimeUtc()
qint64 toSecsSinceEpoch() const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
bool remove()
typedef ConstIterator
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype count() const const
bool isEmpty() const const
void reserve(qsizetype size)
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
void killTimer(int id)
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
QString writableLocation(StandardLocation type)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QByteArray toUtf8() const const
void truncate(qsizetype position)
CaseInsensitive
QueuedConnection
virtual QString fileName() const const override
void setAutoRemove(bool b)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
StripTrailingSlash
QUrl adjusted(FormattingOptions options) const const
void clear()
QString host(ComponentFormattingOptions options) const const
bool isLocalFile() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
void setHost(const QString &host, ParsingMode mode)
void setPassword(const QString &password, ParsingMode mode)
void setPath(const QString &path, ParsingMode mode)
void setScheme(const QString &scheme)
void setUserName(const QString &userName, ParsingMode mode)
QString toDisplayString(FormattingOptions options) const const
QString toLocalFile() const const
QVariant fromValue(T &&value)
QString toString() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 19 2024 11:52:11 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.