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