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

KDE's Doxygen guidelines are available online.