Akonadi Calendar

freebusymanager.cpp
1 /*
2  SPDX-FileCopyrightText: 2011 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
3  SPDX-FileCopyrightText: 2004 Cornelius Schumacher <[email protected]>
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 <agentinstance.h>
17 #include <agentmanager.h>
18 #include <contactsearchjob.h>
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/Job>
28 #include <KIO/JobUiDelegate>
29 #include <KLocalizedString>
30 #include <QUrl>
31 
32 #include <KJobWidgets/KJobWidgets>
33 #include <QDir>
34 #include <QFile>
35 #include <QRegularExpression>
36 #include <QStandardPaths>
37 #include <QTemporaryFile>
38 #include <QTextStream>
39 #include <QTimer>
40 #include <QTimerEvent>
41 
42 using namespace Akonadi;
43 using namespace KCalendarCore;
44 
45 /// Free helper functions
46 
47 QUrl replaceVariablesUrl(const QUrl &url, const QString &email)
48 {
49  QString emailName;
50  QString emailHost;
51 
52  const int atPos = email.indexOf(QLatin1Char('@'));
53  if (atPos >= 0) {
54  emailName = email.left(atPos);
55  emailHost = email.mid(atPos + 1);
56  }
57 
58  QString saveStr = url.path();
59  saveStr.replace(QRegularExpression(QStringLiteral("%email%"), QRegularExpression::CaseInsensitiveOption), email);
60  saveStr.replace(QRegularExpression(QStringLiteral("%name%"), QRegularExpression::CaseInsensitiveOption), emailName);
61  saveStr.replace(QRegularExpression(QStringLiteral("%server%"), QRegularExpression::CaseInsensitiveOption), emailHost);
62 
63  QUrl retUrl(url);
64  retUrl.setPath(saveStr);
65  return retUrl;
66 }
67 
68 // We need this function because using KIO::NetAccess::exists()
69 // is useless for the http and https protocols. And getting back
70 // arbitrary data is also useless because a server can respond back
71 // with a "no such document" page. So we need smart checking.
72 FbCheckerJob::FbCheckerJob(const QList<QUrl> &urlsToCheck, QObject *parent)
73  : KJob(parent)
74  , mUrlsToCheck(urlsToCheck)
75 {
76 }
77 
78 void FbCheckerJob::start()
79 {
80  checkNextUrl();
81 }
82 
83 void FbCheckerJob::checkNextUrl()
84 {
85  if (mUrlsToCheck.isEmpty()) {
86  qCDebug(AKONADICALENDAR_LOG) << "No fb file found";
87  setError(KJob::UserDefinedError);
88  emitResult();
89  return;
90  }
91  const QUrl url = mUrlsToCheck.takeFirst();
92 
93  mData.clear();
94  KIO::TransferJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
95  connect(job, &KIO::TransferJob::data, this, &FbCheckerJob::dataReceived);
96  connect(job, &KIO::TransferJob::result, this, &FbCheckerJob::onGetJobFinished);
97 }
98 
99 void FbCheckerJob::dataReceived(KIO::Job *, const QByteArray &data)
100 {
101  mData.append(data);
102 }
103 
104 void FbCheckerJob::onGetJobFinished(KJob *job)
105 {
106  auto transferJob = static_cast<KIO::TransferJob *>(job);
107  if (mData.contains("BEGIN:VCALENDAR")) {
108  qCDebug(AKONADICALENDAR_LOG) << "found freebusy";
109  mValidUrl = transferJob->url();
110  emitResult();
111  } else {
112  checkNextUrl();
113  }
114 }
115 
116 QUrl FbCheckerJob::validUrl() const
117 {
118  return mValidUrl;
119 }
120 
121 /// FreeBusyManagerPrivate::FreeBusyProviderRequest
122 
123 FreeBusyManagerPrivate::FreeBusyProviderRequest::FreeBusyProviderRequest(const QString &provider)
124  : mRequestStatus(NotStarted)
125  , mInterface(nullptr)
126 {
127  mInterface = QSharedPointer<QDBusInterface>(new QDBusInterface(QStringLiteral("org.freedesktop.Akonadi.Resource.") + provider,
128  QStringLiteral("/FreeBusyProvider"),
129  QStringLiteral("org.freedesktop.Akonadi.Resource.FreeBusyProvider")));
130 }
131 
132 /// FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue
133 
134 FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue()
135  : mHandlersCount(0)
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 
144 FreeBusyManagerPrivate::FreeBusyProvidersRequestsQueue::FreeBusyProvidersRequestsQueue(const QDateTime &start, const QDateTime &end)
145  : mHandlersCount(0)
146  , mResultingFreeBusy(nullptr)
147 {
148  mStartTime = start;
149  mEndTime = end;
150  mResultingFreeBusy = KCalendarCore::FreeBusy::Ptr(new KCalendarCore::FreeBusy(start, end));
151 }
152 
153 /// FreeBusyManagerPrivate
154 
155 FreeBusyManagerPrivate::FreeBusyManagerPrivate(FreeBusyManager *q)
156  : QObject()
157  , q_ptr(q)
158  , mTimerID(0)
159  , mUploadingFreeBusy(false)
160  , mBrokenUrl(false)
161  , mParentWidgetForRetrieval(nullptr)
162 {
163  connect(this, &FreeBusyManagerPrivate::freeBusyUrlRetrieved, this, &FreeBusyManagerPrivate::finishProcessRetrieveQueue);
164 }
165 
166 QString FreeBusyManagerPrivate::freeBusyDir() const
167 {
168  return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusy");
169 }
170 
171 void FreeBusyManagerPrivate::checkFreeBusyUrl()
172 {
173  QUrl targetURL(CalendarSettings::self()->freeBusyPublishUrl());
174  mBrokenUrl = targetURL.isEmpty() || !targetURL.isValid();
175 }
176 
177 static QString configFile()
178 {
179  static QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/korganizer/freebusyurls");
180  return file;
181 }
182 
183 void FreeBusyManagerPrivate::fetchFreeBusyUrl(const QString &email)
184 {
185  // First check if there is a specific FB url for this email
186  KConfig cfg(configFile());
187  KConfigGroup group = cfg.group(email);
188  QString url = group.readEntry(QStringLiteral("url"));
189  if (!url.isEmpty()) {
190  qCDebug(AKONADICALENDAR_LOG) << "Found cached url:" << url;
191  QUrl cachedUrl(url);
192  if (Akonadi::CalendarUtils::thatIsMe(email)) {
193  cachedUrl.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
194  cachedUrl.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
195  }
196  Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(cachedUrl, email));
197  return;
198  }
199  // Try with the url configured by preferred email in kcontactmanager
200  auto job = new Akonadi::ContactSearchJob();
201  job->setQuery(Akonadi::ContactSearchJob::Email, email);
202  job->setProperty("contactEmail", QVariant::fromValue(email));
203  connect(job, &Akonadi::ContactSearchJob::result, this, &FreeBusyManagerPrivate::contactSearchJobFinished);
204  job->start();
205 }
206 
207 void FreeBusyManagerPrivate::contactSearchJobFinished(KJob *_job)
208 {
209  const QString email = _job->property("contactEmail").toString();
210 
211  if (_job->error()) {
212  qCritical() << "Error while searching for contact: " << _job->errorString() << ", email = " << email;
213  Q_EMIT freeBusyUrlRetrieved(email, QUrl());
214  return;
215  }
216 
217  auto job = qobject_cast<Akonadi::ContactSearchJob *>(_job);
218  KConfig cfg(configFile());
219  KConfigGroup group = cfg.group(email);
220  QString url = group.readEntry(QStringLiteral("url"));
221 
222  const KContacts::Addressee::List contacts = job->contacts();
223  for (const KContacts::Addressee &contact : contacts) {
224  const QString pref = contact.preferredEmail();
225  if (!pref.isEmpty() && pref != email) {
226  group = cfg.group(pref);
227  url = group.readEntry("url");
228  qCDebug(AKONADICALENDAR_LOG) << "Preferred email of" << email << "is" << pref;
229  if (!url.isEmpty()) {
230  qCDebug(AKONADICALENDAR_LOG) << "Taken url from preferred email:" << url;
231  Q_EMIT freeBusyUrlRetrieved(email, replaceVariablesUrl(QUrl(url), email));
232  return;
233  }
234  }
235  }
236  // None found. Check if we do automatic FB retrieving then
237  if (!CalendarSettings::self()->freeBusyRetrieveAuto()) {
238  // No, so no FB list here
239  qCDebug(AKONADICALENDAR_LOG) << "No automatic retrieving";
240  Q_EMIT freeBusyUrlRetrieved(email, QUrl());
241  return;
242  }
243 
244  // Sanity check: Don't download if it's not a correct email
245  // address (this also avoids downloading for "(empty email)").
246  int emailpos = email.indexOf(QLatin1Char('@'));
247  if (emailpos == -1) {
248  qCWarning(AKONADICALENDAR_LOG) << "No '@' found in" << email;
249  Q_EMIT freeBusyUrlRetrieved(email, QUrl());
250  return;
251  }
252 
253  const QString emailHost = email.mid(emailpos + 1);
254 
255  // Build the URL
256  if (CalendarSettings::self()->freeBusyCheckHostname()) {
257  // Don't try to fetch free/busy data for users not on the specified servers
258  // This tests if the hostnames match, or one is a subset of the other
259  const QString hostDomain = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl()).host();
260  if (hostDomain != emailHost && !hostDomain.endsWith(QLatin1Char('.') + emailHost) && !emailHost.endsWith(QLatin1Char('.') + hostDomain)) {
261  // Host names do not match
262  qCDebug(AKONADICALENDAR_LOG) << "Host '" << hostDomain << "' doesn't match email '" << email << '\'';
263  Q_EMIT freeBusyUrlRetrieved(email, QUrl());
264  return;
265  }
266  }
267 
268  if (CalendarSettings::self()->freeBusyRetrieveUrl().contains(QRegularExpression(QStringLiteral("\\.[xiv]fb$")))) {
269  // user specified a fullpath
270  // do variable string replacements to the URL (MS Outlook style)
271  const QUrl sourceUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
272  QUrl fullpathURL = replaceVariablesUrl(sourceUrl, email);
273 
274  // set the User and Password part of the URL
275  fullpathURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
276  fullpathURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
277 
278  // no need to cache this URL as this is pretty fast to get from the config value.
279  // return the fullpath URL
280  qCDebug(AKONADICALENDAR_LOG) << "Found url. email=" << email << "; url=" << fullpathURL;
281  Q_EMIT freeBusyUrlRetrieved(email, fullpathURL);
282  return;
283  }
284 
285  // else we search for a fb file in the specified URL with known possible extensions
286  QStringList extensions;
287  extensions.reserve(3);
288  extensions << QStringLiteral("xfb");
289  extensions << QStringLiteral("ifb");
290  extensions << QStringLiteral("vfb");
291 
293  QList<QUrl> urlsToCheck;
294  urlsToCheck.reserve(extensions.count());
295  QStringList::ConstIterator extEnd(extensions.constEnd());
296  for (ext = extensions.constBegin(); ext != extEnd; ++ext) {
297  // build a url for this extension
298  const QUrl sourceUrl = QUrl(CalendarSettings::self()->freeBusyRetrieveUrl());
299  QUrl dirURL = replaceVariablesUrl(sourceUrl, email);
300  if (CalendarSettings::self()->freeBusyFullDomainRetrieval()) {
301  dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
302  dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + email + QLatin1Char('.') + (*ext)));
303  } else {
304  // Cut off everything left of the @ sign to get the user name.
305  const QString emailName = email.left(emailpos);
306  dirURL = dirURL.adjusted(QUrl::StripTrailingSlash);
307  dirURL.setPath(QString(dirURL.path() + QLatin1Char('/') + emailName + QLatin1Char('.') + (*ext)));
308  }
309  dirURL.setUserName(CalendarSettings::self()->freeBusyRetrieveUser());
310  dirURL.setPassword(CalendarSettings::self()->freeBusyRetrievePassword());
311  urlsToCheck << dirURL;
312  }
313  KJob *checkerJob = new FbCheckerJob(urlsToCheck, this);
314  checkerJob->setProperty("email", email);
315  connect(checkerJob, &KJob::result, this, &FreeBusyManagerPrivate::fbCheckerJobFinished);
316  checkerJob->start();
317 }
318 
319 void FreeBusyManagerPrivate::fbCheckerJobFinished(KJob *job)
320 {
321  const QString email = job->property("email").toString();
322  if (!job->error()) {
323  auto checkerJob = static_cast<FbCheckerJob *>(job);
324  QUrl dirURL = checkerJob->validUrl();
325  // write the URL to the cache
326  KConfig cfg(configFile());
327  KConfigGroup group = cfg.group(email);
328  group.writeEntry("url", dirURL.toDisplayString()); // prettyURL() does not write user nor password
329  qCDebug(AKONADICALENDAR_LOG) << "Found url email=" << email << "; url=" << dirURL;
330  Q_EMIT freeBusyUrlRetrieved(email, dirURL);
331  } else {
332  qCDebug(AKONADICALENDAR_LOG) << "Returning invalid url";
333  Q_EMIT freeBusyUrlRetrieved(email, QUrl());
334  }
335 }
336 
337 QString FreeBusyManagerPrivate::freeBusyToIcal(const KCalendarCore::FreeBusy::Ptr &freebusy)
338 {
339  return mFormat.createScheduleMessage(freebusy, KCalendarCore::iTIPPublish);
340 }
341 
342 KCalendarCore::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 
355 KCalendarCore::FreeBusy::Ptr FreeBusyManagerPrivate::ownerFreeBusy()
356 {
358  QDateTime end = start.addDays(CalendarSettings::self()->freeBusyPublishDays());
359 
360  KCalendarCore::Event::List events = mCalendar ? mCalendar->rawEvents(start.date(), end.date()) : KCalendarCore::Event::List();
361  KCalendarCore::FreeBusy::Ptr freebusy(new KCalendarCore::FreeBusy(events, start, end));
362  freebusy->setOrganizer(KCalendarCore::Person(Akonadi::CalendarUtils::fullName(), Akonadi::CalendarUtils::email()));
363  return freebusy;
364 }
365 
366 QString FreeBusyManagerPrivate::ownerFreeBusyAsString()
367 {
368  return freeBusyToIcal(ownerFreeBusy());
369 }
370 
371 void 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::sorry(mParentWidgetForRetrieval,
380  i18n("Failed to download free/busy data from: %1\nReason: %2", job->url().toDisplayString(), job->errorText()),
381  i18n("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::sorry(mParentWidgetForRetrieval,
403  i18n("Failed to parse free/busy information that was retrieved from: %1", job->url().toDisplayString()),
404  i18n("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 
413 void FreeBusyManagerPrivate::processFreeBusyUploadResult(KJob *_job)
414 {
415  auto job = static_cast<KIO::FileCopyJob *>(_job);
416  if (job->error()) {
417  KMessageBox::sorry(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()) {
431  QFile::remove(src.toLocalFile());
432  }
433  mUploadingFreeBusy = false;
434 }
435 
436 void FreeBusyManagerPrivate::processRetrieveQueue()
437 {
438  if (mRetrieveQueue.isEmpty()) {
439  return;
440  }
441 
442  QString email = mRetrieveQueue.takeFirst();
443 
444  // First, try to find all agents that are free-busy providers
445  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 
457 void 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 
480 void 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 
529 QStringList FreeBusyManagerPrivate::getFreeBusyProviders() const
530 {
531  QStringList providers;
533  for (const Akonadi::AgentInstance &agent : agents) {
534  if (agent.type().capabilities().contains(QLatin1String("FreeBusyProvider"))) {
535  providers << agent.identifier();
536  }
537  }
538  return providers;
539 }
540 
541 void 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  connect(request.mInterface.data(), SIGNAL(handlesFreeBusy(QString, bool)), this, SLOT(onHandlesFreeBusy(QString, bool)));
551 
552  request.mInterface->call(QStringLiteral("canHandleFreeBusy"), email);
553  request.mRequestStatus = FreeBusyProviderRequest::HandlingRequested;
554  mProvidersRequestsByEmail[email].mRequests << request;
555  }
556 }
557 
558 void FreeBusyManagerPrivate::queryFreeBusyProviders(const QStringList &providers, const QString &email, const QDateTime &start, const QDateTime &end)
559 {
560  if (!mProvidersRequestsByEmail.contains(email)) {
561  mProvidersRequestsByEmail[email] = FreeBusyProvidersRequestsQueue(start, end);
562  }
563 
564  queryFreeBusyProviders(providers, email);
565 }
566 
567 void FreeBusyManagerPrivate::onHandlesFreeBusy(const QString &email, bool handles)
568 {
569  if (!mProvidersRequestsByEmail.contains(email)) {
570  return;
571  }
572 
573  auto iface = dynamic_cast<QDBusInterface *>(sender());
574  if (!iface) {
575  return;
576  }
577 
578  FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
579  QString respondingService = iface->service();
580  qCDebug(AKONADICALENDAR_LOG) << respondingService << "responded to our FreeBusy request:" << handles;
581  int requestIndex = -1;
582 
583  const int requestsSize(queue->mRequests.size());
584  for (int i = 0; i < requestsSize; ++i) {
585  if (queue->mRequests.at(i).mInterface->service() == respondingService) {
586  requestIndex = i;
587  }
588  }
589 
590  if (requestIndex == -1) {
591  return;
592  }
593 
594  disconnect(iface, SIGNAL(handlesFreeBusy(QString, bool)), this, SLOT(onHandlesFreeBusy(QString, bool)));
595 
596  if (!handles) {
597  queue->mRequests.removeAt(requestIndex);
598  // If no more requests are left and no handler responded
599  // then fall back to the URL mechanism
600  if (queue->mRequests.isEmpty() && queue->mHandlersCount == 0) {
601  mProvidersRequestsByEmail.remove(email);
602  fetchFreeBusyUrl(email);
603  }
604  } else {
605  ++queue->mHandlersCount;
606  connect(iface, SIGNAL(freeBusyRetrieved(QString, QString, bool, QString)), this, SLOT(onFreeBusyRetrieved(QString, QString, bool, QString)));
607  iface->call(QStringLiteral("retrieveFreeBusy"), email, queue->mStartTime, queue->mEndTime);
608  queue->mRequests[requestIndex].mRequestStatus = FreeBusyProviderRequest::FreeBusyRequested;
609  }
610 }
611 
612 void FreeBusyManagerPrivate::processMailSchedulerResult(Akonadi::Scheduler::Result result, const QString &errorMsg)
613 {
614  if (result == Scheduler::ResultSuccess) {
615  KMessageBox::information(mParentWidgetForMailling,
616  i18n("The free/busy information was successfully sent."),
617  i18n("Sending Free/Busy"),
618  QStringLiteral("FreeBusyPublishSuccess"));
619  } else {
620  KMessageBox::error(mParentWidgetForMailling, i18n("Unable to publish the free/busy data: %1", errorMsg));
621  }
622 
623  sender()->deleteLater();
624 }
625 
626 void FreeBusyManagerPrivate::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText)
627 {
628  Q_Q(FreeBusyManager);
629  Q_UNUSED(errorText)
630 
631  if (!mProvidersRequestsByEmail.contains(email)) {
632  return;
633  }
634 
635  auto iface = dynamic_cast<QDBusInterface *>(sender());
636  if (!iface) {
637  return;
638  }
639 
640  FreeBusyProvidersRequestsQueue *queue = &mProvidersRequestsByEmail[email];
641  QString respondingService = iface->service();
642  int requestIndex = -1;
643 
644  const int requestsSize(queue->mRequests.size());
645  for (int i = 0; i < requestsSize; ++i) {
646  if (queue->mRequests.at(i).mInterface->service() == respondingService) {
647  requestIndex = i;
648  }
649  }
650 
651  if (requestIndex == -1) {
652  return;
653  }
654 
655  disconnect(iface, SIGNAL(freeBusyRetrieved(QString, QString, bool, QString)), this, SLOT(onFreeBusyRetrieved(QString, QString, bool, QString)));
656 
657  queue->mRequests.removeAt(requestIndex);
658 
659  if (success) {
660  KCalendarCore::FreeBusy::Ptr fb = iCalToFreeBusy(freeBusy.toUtf8());
661  if (!fb) {
662  --queue->mHandlersCount;
663  } else {
664  queue->mResultingFreeBusy->merge(fb);
665  }
666  }
667 
668  if (queue->mRequests.isEmpty()) {
669  if (queue->mHandlersCount == 0) {
670  fetchFreeBusyUrl(email);
671  } else {
672  Q_EMIT q->freeBusyRetrieved(queue->mResultingFreeBusy, email);
673  }
674  mProvidersRequestsByEmail.remove(email);
675  }
676 }
677 
678 /// FreeBusyManager::Singleton
679 
680 namespace Akonadi
681 {
682 class FreeBusyManagerStatic
683 {
684 public:
685  FreeBusyManager instance;
686 };
687 }
688 
689 Q_GLOBAL_STATIC(FreeBusyManagerStatic, sManagerInstance)
690 
691 FreeBusyManager::FreeBusyManager()
692  : d_ptr(new FreeBusyManagerPrivate(this))
693 {
694  setObjectName(QStringLiteral("FreeBusyManager"));
695  connect(CalendarSettings::self(), SIGNAL(configChanged()), SLOT(checkFreeBusyUrl()));
696 }
697 
698 FreeBusyManager::~FreeBusyManager()
699 {
700  delete d_ptr;
701 }
702 
703 FreeBusyManager *FreeBusyManager::self()
704 {
705  return &sManagerInstance->instance;
706 }
707 
708 void 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 */
730 void 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::sorry(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  i18n("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::sorry(parentWidget, i18n("<qt>The target URL '%1' provided is invalid.</qt>", targetURL.toDisplayString()), i18n("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() == QLatin1String("%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 
846 void 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 
874 bool 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 
926 void FreeBusyManager::cancelRetrieval()
927 {
928  Q_D(FreeBusyManager);
929  d->mRetrieveQueue.clear();
930 }
931 
932 KCalendarCore::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.";
941  }
942 
943  if (!f.open(QIODevice::ReadOnly)) {
944  qCDebug(AKONADICALENDAR_LOG) << "Unable to open file" << f.fileName();
946  }
947 
948  QTextStream ts(&f);
949  QString str = ts.readAll();
950 
951  return d->iCalToFreeBusy(str.toUtf8());
952 }
953 
954 bool 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::ReadWrite)) {
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 
996 void FreeBusyManager::timerEvent(QTimerEvent *event)
997 {
998  Q_UNUSED(event)
999  publishFreeBusy();
1000 }
1001 
1002 #include "moc_freebusymanager.cpp"
1003 #include "moc_freebusymanager_p.cpp"
KJOBWIDGETS_EXPORT void setWindow(KJob *job, QWidget *widget)
StripTrailingSlash
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void clear()
AgentInstance::List instances() const
QString toDisplayString(QUrl::FormattingOptions options) const const
void truncate(int position)
QString writableLocation(QStandardPaths::StandardLocation type)
const QUrl & url() const
bool remove()
QObject * sender() const const
void reserve(int alloc)
virtual Event::List events(EventSortField sortField=EventSortUnsorted, SortDirection sortDirection=SortDirectionAscending) const
virtual QString errorString() const
HideProgressInfo
QString host(QUrl::ComponentFormattingOptions options) const const
void setPassword(const QString &password, QUrl::ParsingMode mode)
void setEmail(const QString &email)
void writeEntry(const QString &key, const QVariant &value, WriteConfigFlags pFlags=Normal)
KIOCORE_EXPORT FileCopyJob * file_copy(const QUrl &src, const QUrl &dest, int permissions=-1, JobFlags flags=DefaultFlags)
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
QCA_EXPORT ProviderList providers()
QDateTime toTimeZone(const QTimeZone &timeZone) const const
void setPath(const QString &path, QUrl::ParsingMode mode)
int count(const T &value) const const
QString fromUtf8(const char *str, int size)
QVariant property(const char *name) const const
void setAutoRemove(bool b)
bool isEmpty() const const
void setObjectName(const QString &name)
bool isEmpty() const const
void setScheme(const QString &scheme)
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
QString path(QUrl::ComponentFormattingOptions options) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
Event::Ptr event(const QString &uid, const QDateTime &recurrenceId={}) const override
void deleteLater()
Incidence::Ptr instance(const QString &identifier) const
bool isEmpty() const const
QString toLocalFile() const const
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
void setUserName(const QString &userName, QUrl::ParsingMode mode)
QString email() const
QVariant fromValue(const T &value)
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
virtual QString fileName() const const override
QString i18n(const char *text, const TYPE &arg...)
KConfigGroup group(const QString &group)
QString & replace(int position, int n, QChar after)
void data(KIO::Job *job, const QByteArray &data)
bool isValid() const const
QDateTime currentDateTime()
QString mid(int position, int n) const const
QDate date() const const
AddresseeList List
QUrl adjusted(QUrl::FormattingOptions options) const const
qint64 toSecsSinceEpoch() const const
FreeBusyManager::Singleton.
virtual Q_SCRIPTABLE void start()=0
QDate currentDate()
typedef ConstIterator
int length() const const
static AgentManager * self()
QString left(int n) const const
QSharedPointer< FreeBusy > Ptr
bool setProperty(const char *name, const QVariant &value)
void setHost(const QString &host, QUrl::ParsingMode mode)
void result(KJob *job)
QList::const_iterator constEnd() const const
void information(QWidget *parent, const QString &text, const QString &caption=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
QList::const_iterator constBegin() const const
QueuedConnection
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
T qobject_cast(QObject *object)
QDateTime addDays(qint64 ndays) const const
QString toString() const const
T readEntry(const QString &key, const T &aDefault) const
void killTimer(int id)
QDateTime currentDateTimeUtc()
Q_EMITQ_EMIT
QString errorText() const
QString fullName() const
void sorry(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
int error() const
bool isLocalFile() const const
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Jun 19 2021 23:12:24 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.