diff --git a/agents/newmailnotifier/newmailnotifieragent.cpp b/agents/newmailnotifier/newmailnotifieragent.cpp index 5239cd302..d87db720a 100644 --- a/agents/newmailnotifier/newmailnotifieragent.cpp +++ b/agents/newmailnotifier/newmailnotifieragent.cpp @@ -1,479 +1,479 @@ /* Copyright (c) 2013-2019 Laurent Montel Copyright (c) 2010 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "newmailnotifieragent.h" #include #include "specialnotifierjob.h" #include "newmailnotifieradaptor.h" #include "newmailnotifieragentsettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "newmailnotifier_debug.h" #include #include #include #include using namespace Akonadi; NewMailNotifierAgent::NewMailNotifierAgent(const QString &id) : AgentBase(id) { Kdelibs4ConfigMigrator migrate(QStringLiteral("newmailnotifieragent")); migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi_newmailnotifier_agentrc") << QStringLiteral("akonadi_newmailnotifier_agent.notifyrc")); migrate.migrate(); KLocalizedString::setApplicationDomain("akonadi_newmailnotifier_agent"); Akonadi::AttributeFactory::registerAttribute(); new NewMailNotifierAdaptor(this); NewMailNotifierAgentSettings::instance(KSharedConfig::openConfig()); mIdentityManager = KIdentityManagement::IdentityManager::self(); connect(mIdentityManager, QOverload<>::of(&KIdentityManagement::IdentityManager::changed), this, &NewMailNotifierAgent::slotIdentitiesChanged); slotIdentitiesChanged(); mDefaultIconName = QStringLiteral("kmail"); KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/NewMailNotifierAgent"), this, QDBusConnection::ExportAdaptors); QString service = QStringLiteral("org.freedesktop.Akonadi.NewMailNotifierAgent"); if (Akonadi::ServerManager::hasInstanceIdentifier()) { service += QLatin1Char('.') + Akonadi::ServerManager::instanceIdentifier(); } KDBusConnectionPool::threadConnection().registerService(service); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceStatusChanged, this, &NewMailNotifierAgent::slotInstanceStatusChanged); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceRemoved, this, &NewMailNotifierAgent::slotInstanceRemoved); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceAdded, this, &NewMailNotifierAgent::slotInstanceAdded); connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceNameChanged, this, &NewMailNotifierAgent::slotInstanceNameChanged); changeRecorder()->setMimeTypeMonitored(KMime::Message::mimeType()); changeRecorder()->itemFetchScope().setCacheOnly(true); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->fetchCollection(true); changeRecorder()->setChangeRecordingEnabled(false); changeRecorder()->ignoreSession(Akonadi::Session::defaultSession()); changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); changeRecorder()->setCollectionMonitored(Collection::root(), true); mTimer.setInterval(5 * 1000); connect(&mTimer, &QTimer::timeout, this, &NewMailNotifierAgent::slotShowNotifications); if (isActive()) { mTimer.setSingleShot(true); } } void NewMailNotifierAgent::slotIdentitiesChanged() { mListEmails = mIdentityManager->allEmails(); } void NewMailNotifierAgent::doSetOnline(bool online) { if (!online) { clearAll(); } } void NewMailNotifierAgent::setEnableAgent(bool enabled) { NewMailNotifierAgentSettings::setEnabled(enabled); NewMailNotifierAgentSettings::self()->save(); if (!enabled) { clearAll(); } } bool NewMailNotifierAgent::enabledAgent() const { return NewMailNotifierAgentSettings::enabled(); } void NewMailNotifierAgent::clearAll() { mNewMails.clear(); mInstanceNameInProgress.clear(); } bool NewMailNotifierAgent::excludeSpecialCollection(const Akonadi::Collection &collection) const { if (collection.hasAttribute()) { return true; } if (collection.hasAttribute()) { if (collection.attribute()->ignoreNewMail()) { return true; } } if (!collection.contentMimeTypes().contains(KMime::Message::mimeType())) { return true; } SpecialMailCollections::Type type = SpecialMailCollections::self()->specialCollectionType(collection); switch (type) { case SpecialMailCollections::Invalid: //Not a special collection case SpecialMailCollections::Inbox: return false; default: return true; } } void NewMailNotifierAgent::itemsRemoved(const Item::List &items) { if (!isActive()) { return; } QHash< Akonadi::Collection, QList >::iterator end(mNewMails.end()); for (QHash< Akonadi::Collection, QList >::iterator it = mNewMails.begin(); it != end; ++it) { QList idList = it.value(); bool itemFound = false; for (const Item &item : items) { const int numberOfItemsRemoved = idList.removeAll(item.id()); if (numberOfItemsRemoved > 0) { itemFound = true; } } if (itemFound) { if (mNewMails[it.key()].isEmpty()) { mNewMails.remove(it.key()); } else { mNewMails[it.key()] = idList; } } } } void NewMailNotifierAgent::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) { Q_UNUSED(removedFlags); if (!isActive()) { return; } for (const Akonadi::Item &item : items) { QHash< Akonadi::Collection, QList >::iterator end(mNewMails.end()); for (QHash< Akonadi::Collection, QList >::iterator it = mNewMails.begin(); it != end; ++it) { QList idList = it.value(); if (idList.contains(item.id()) && addedFlags.contains("\\SEEN")) { idList.removeAll(item.id()); if (idList.isEmpty()) { mNewMails.remove(it.key()); break; } else { (*it) = idList; } } } } } void NewMailNotifierAgent::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) { if (!isActive()) { return; } for (const Akonadi::Item &item : items) { if (ignoreStatusMail(item)) { continue; } if (excludeSpecialCollection(collectionSource)) { continue; // outbox, sent-mail, trash, drafts or templates. } if (mNewMails.contains(collectionSource)) { QList idListFrom = mNewMails[ collectionSource ]; const int removeItems = idListFrom.removeAll(item.id()); if (removeItems > 0) { if (idListFrom.isEmpty()) { mNewMails.remove(collectionSource); } else { mNewMails[ collectionSource ] = idListFrom; } if (!excludeSpecialCollection(collectionDestination)) { QList idListTo = mNewMails[ collectionDestination ]; idListTo.append(item.id()); mNewMails[ collectionDestination ] = idListTo; } } } } } bool NewMailNotifierAgent::ignoreStatusMail(const Akonadi::Item &item) { Akonadi::MessageStatus status; status.setStatusFromFlags(item.flags()); if (status.isRead() || status.isSpam() || status.isIgnored()) { return true; } return false; } void NewMailNotifierAgent::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (!isActive()) { return; } if (excludeSpecialCollection(collection)) { return; // outbox, sent-mail, trash, drafts or templates. } if (ignoreStatusMail(item)) { return; } if (!mTimer.isActive()) { mTimer.start(); } mNewMails[ collection ].append(item.id()); } void NewMailNotifierAgent::slotShowNotifications() { if (mNewMails.isEmpty()) { return; } if (!isActive()) { return; } if (!mInstanceNameInProgress.isEmpty()) { //Restart timer until all is done. mTimer.start(); return; } QString message; if (NewMailNotifierAgentSettings::verboseNotification()) { bool hasUniqMessage = true; Akonadi::Item::Id item = -1; QString currentPath; QStringList texts; const int numberOfCollection(mNewMails.count()); if (numberOfCollection > 1) { hasUniqMessage = false; } QHash< Akonadi::Collection, QList >::const_iterator end(mNewMails.constEnd()); for (QHash< Akonadi::Collection, QList >::const_iterator it = mNewMails.constBegin(); it != end; ++it) { - Akonadi::EntityDisplayAttribute *attr = it.key().attribute(); + const Akonadi::EntityDisplayAttribute *attr = it.key().attribute(); QString displayName; if (attr && !attr->displayName().isEmpty()) { displayName = attr->displayName(); } else { displayName = it.key().name(); } if (hasUniqMessage) { const int numberOfValue(it.value().count()); if (numberOfValue == 0) { //You can have an unique folder with 0 message return; } else if (numberOfValue == 1) { item = it.value().at(0); currentPath = displayName; break; } else { hasUniqMessage = false; } } QString resourceName; if (!mCacheResourceName.contains(it.key().resource())) { const Akonadi::AgentInstance::List lst = Akonadi::AgentManager::self()->instances(); for (const Akonadi::AgentInstance &instance : lst) { if (instance.identifier() == it.key().resource()) { mCacheResourceName.insert(instance.identifier(), instance.name()); resourceName = instance.name(); break; } } } else { resourceName = mCacheResourceName.value(it.key().resource()); } const int numberOfEmails(it.value().count()); if (numberOfEmails > 0) { texts.append(i18ncp("%2 = name of mail folder; %3 = name of Akonadi POP3/IMAP/etc resource (as user named it)", "One new email in %2 from \"%3\"", "%1 new emails in %2 from \"%3\"", numberOfEmails, displayName, resourceName)); } } if (hasUniqMessage) { SpecialNotifierJob *job = new SpecialNotifierJob(mListEmails, currentPath, item, this); job->setDefaultIconName(mDefaultIconName); connect(job, &SpecialNotifierJob::displayNotification, this, &NewMailNotifierAgent::slotDisplayNotification); connect(job, &SpecialNotifierJob::say, this, &NewMailNotifierAgent::slotSay); mNewMails.clear(); return; } else { message = texts.join(QStringLiteral("
")); } } else { message = i18n("New mail arrived"); } qCDebug(NEWMAILNOTIFIER_LOG) << message; slotDisplayNotification(QPixmap(), message); mNewMails.clear(); } void NewMailNotifierAgent::slotDisplayNotification(const QPixmap &pixmap, const QString &message) { if (pixmap.isNull()) { KNotification::event(QStringLiteral("new-email"), QString(), message, mDefaultIconName, nullptr, NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping : KNotification::CloseOnTimeout, QStringLiteral("akonadi_newmailnotifier_agent")); } else { KNotification::event(QStringLiteral("new-email"), message, pixmap, nullptr, NewMailNotifierAgentSettings::keepPersistentNotification() ? KNotification::Persistent | KNotification::SkipGrouping : KNotification::CloseOnTimeout, QStringLiteral("akonadi_newmailnotifier_agent")); } } void NewMailNotifierAgent::slotInstanceNameChanged(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); int resourceNameRemoved = mCacheResourceName.remove(identifier); if (resourceNameRemoved > 0) { mCacheResourceName.insert(identifier, instance.name()); } } void NewMailNotifierAgent::slotInstanceStatusChanged(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); switch (instance.status()) { case Akonadi::AgentInstance::Broken: case Akonadi::AgentInstance::Idle: mInstanceNameInProgress.removeAll(identifier); break; case Akonadi::AgentInstance::Running: if (!excludeAgentType(instance)) { if (!mInstanceNameInProgress.contains(identifier)) { mInstanceNameInProgress.append(identifier); } } break; case Akonadi::AgentInstance::NotConfigured: //Nothing break; } } bool NewMailNotifierAgent::excludeAgentType(const Akonadi::AgentInstance &instance) { if (instance.type().mimeTypes().contains(KMime::Message::mimeType())) { const QStringList capabilities(instance.type().capabilities()); if (capabilities.contains(QLatin1String("Resource")) && !capabilities.contains(QLatin1String("Virtual")) && !capabilities.contains(QLatin1String("MailTransport"))) { return false; } else { return true; } } return true; } void NewMailNotifierAgent::slotInstanceRemoved(const Akonadi::AgentInstance &instance) { if (!isActive()) { return; } const QString identifier(instance.identifier()); mInstanceNameInProgress.removeAll(identifier); } void NewMailNotifierAgent::slotInstanceAdded(const Akonadi::AgentInstance &instance) { mCacheResourceName.insert(instance.identifier(), instance.name()); } void NewMailNotifierAgent::printDebug() { qCDebug(NEWMAILNOTIFIER_LOG) << "instance in progress: " << mInstanceNameInProgress << "\n notifier enabled : " << NewMailNotifierAgentSettings::enabled() << "\n check in progress : " << !mInstanceNameInProgress.isEmpty(); } bool NewMailNotifierAgent::isActive() const { return isOnline() && NewMailNotifierAgentSettings::enabled(); } void NewMailNotifierAgent::slotSay(const QString &message) { if (!mTextToSpeech) { mTextToSpeech = new QTextToSpeech(this); } if (mTextToSpeech->availableEngines().isEmpty()) { qCWarning(NEWMAILNOTIFIER_LOG) << "No texttospeech engine available"; } else { mTextToSpeech->say(message); } } AKONADI_AGENT_MAIN(NewMailNotifierAgent) diff --git a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp index ad66e333e..bf2f78456 100644 --- a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp +++ b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp @@ -1,240 +1,240 @@ /* Copyright (c) 2013-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "newmailnotifierselectcollectionwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "newmailnotifier_debug.h" #include #include #include #include #include NewMailNotifierCollectionProxyModel::NewMailNotifierCollectionProxyModel(QObject *parent) : QIdentityProxyModel(parent) { } QVariant NewMailNotifierCollectionProxyModel::data(const QModelIndex &index, int role) const { if (role == Qt::CheckStateRole) { if (index.isValid()) { const Akonadi::Collection collection = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); if (mNotificationCollection.contains(collection)) { return mNotificationCollection.value(collection) ? Qt::Checked : Qt::Unchecked; } else { - Akonadi::NewMailNotifierAttribute *attr = collection.attribute(); + const Akonadi::NewMailNotifierAttribute *attr = collection.attribute(); if (!attr || !attr->ignoreNewMail()) { return Qt::Checked; } return Qt::Unchecked; } } } return QIdentityProxyModel::data(index, role); } bool NewMailNotifierCollectionProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role == Qt::CheckStateRole) { if (index.isValid()) { const Akonadi::Collection collection = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); mNotificationCollection[collection] = (value == Qt::Checked); emit dataChanged(index, index); return true; } } return QIdentityProxyModel::setData(index, value, role); } Qt::ItemFlags NewMailNotifierCollectionProxyModel::flags(const QModelIndex &index) const { if (index.isValid()) { return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable; } else { return QIdentityProxyModel::flags(index); } } QHash NewMailNotifierCollectionProxyModel::notificationCollection() const { return mNotificationCollection; } NewMailNotifierSelectCollectionWidget::NewMailNotifierSelectCollectionWidget(QWidget *parent) : QWidget(parent) { Akonadi::AttributeFactory::registerAttribute(); QVBoxLayout *vbox = new QVBoxLayout(this); QLabel *label = new QLabel(i18n("Select which folders to monitor for new message notifications:")); vbox->addWidget(label); // Create a new change recorder. mChangeRecorder = new Akonadi::ChangeRecorder(this); mChangeRecorder->setMimeTypeMonitored(KMime::Message::mimeType()); mChangeRecorder->fetchCollection(true); mChangeRecorder->setAllMonitored(true); mModel = new Akonadi::EntityTreeModel(mChangeRecorder, this); // Set the model to show only collections, not items. mModel->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation); connect(mModel, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched); Akonadi::CollectionFilterProxyModel *mimeTypeProxy = new Akonadi::CollectionFilterProxyModel(this); mimeTypeProxy->setExcludeVirtualCollections(true); mimeTypeProxy->setDynamicSortFilter(true); mimeTypeProxy->addMimeTypeFilters(QStringList() << KMime::Message::mimeType()); mimeTypeProxy->setSourceModel(mModel); mNewMailNotifierProxyModel = new NewMailNotifierCollectionProxyModel(this); mNewMailNotifierProxyModel->setSourceModel(mimeTypeProxy); mCollectionFilter = new QSortFilterProxyModel(this); mCollectionFilter->setRecursiveFilteringEnabled(true); mCollectionFilter->setSourceModel(mNewMailNotifierProxyModel); mCollectionFilter->setDynamicSortFilter(true); mCollectionFilter->setFilterCaseSensitivity(Qt::CaseInsensitive); mCollectionFilter->setSortRole(Qt::DisplayRole); mCollectionFilter->setSortCaseSensitivity(Qt::CaseSensitive); mCollectionFilter->setSortLocaleAware(true); KLineEdit *searchLine = new KLineEdit(this); searchLine->setPlaceholderText(i18n("Search...")); searchLine->setClearButtonEnabled(true); connect(searchLine, &QLineEdit::textChanged, this, &NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter); vbox->addWidget(searchLine); mFolderView = new QTreeView(this); mFolderView->setEditTriggers(QAbstractItemView::NoEditTriggers); mFolderView->setAlternatingRowColors(true); vbox->addWidget(mFolderView); mFolderView->setModel(mCollectionFilter); QHBoxLayout *hbox = new QHBoxLayout; vbox->addLayout(hbox); QPushButton *button = new QPushButton(i18n("&Select All"), this); connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotSelectAllCollections); hbox->addWidget(button); button = new QPushButton(i18n("&Unselect All"), this); connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections); hbox->addWidget(button); hbox->addStretch(1); } NewMailNotifierSelectCollectionWidget::~NewMailNotifierSelectCollectionWidget() { } void NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched() { mCollectionFilter->sort(0, Qt::AscendingOrder); mFolderView->expandAll(); } void NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter(const QString &filter) { mCollectionFilter->setFilterWildcard(filter); mFolderView->expandAll(); } void NewMailNotifierSelectCollectionWidget::slotSelectAllCollections() { forceStatus(QModelIndex(), true); } void NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections() { forceStatus(QModelIndex(), false); } void NewMailNotifierSelectCollectionWidget::forceStatus(const QModelIndex &parent, bool status) { const int nbCol = mNewMailNotifierProxyModel->rowCount(parent); for (int i = 0; i < nbCol; ++i) { const QModelIndex child = mNewMailNotifierProxyModel->index(i, 0, parent); mNewMailNotifierProxyModel->setData(child, status ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); forceStatus(child, status); } } void NewMailNotifierSelectCollectionWidget::updateCollectionsRecursive() { QHashIterator i(mNewMailNotifierProxyModel->notificationCollection()); while (i.hasNext()) { i.next(); Akonadi::Collection collection = i.key(); Akonadi::NewMailNotifierAttribute *attr = collection.attribute(); Akonadi::CollectionModifyJob *modifyJob = nullptr; const bool selected = i.value(); if (selected && attr && attr->ignoreNewMail()) { collection.removeAttribute(); modifyJob = new Akonadi::CollectionModifyJob(collection); modifyJob->setProperty("AttributeAdded", true); } else if (!selected && (!attr || !attr->ignoreNewMail())) { attr = collection.attribute(Akonadi::Collection::AddIfMissing); attr->setIgnoreNewMail(true); modifyJob = new Akonadi::CollectionModifyJob(collection); modifyJob->setProperty("AttributeAdded", false); } if (modifyJob) { connect(modifyJob, &Akonadi::CollectionModifyJob::finished, this, &NewMailNotifierSelectCollectionWidget::slotModifyJobDone); } } } void NewMailNotifierSelectCollectionWidget::slotModifyJobDone(KJob *job) { Akonadi::CollectionModifyJob *modifyJob = qobject_cast(job); if (modifyJob && job->error()) { if (job->property("AttributeAdded").toBool()) { qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to append NewMailNotifierAttribute to collection" << modifyJob->collection().id() << ":" << job->errorString(); } else { qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to remove NewMailNotifierAttribute from collection" << modifyJob->collection().id() << ":" << job->errorString(); } } } diff --git a/kioslave/akonadi/akonadislave.cpp b/kioslave/akonadi/akonadislave.cpp index f6b86706e..f9508660f 100644 --- a/kioslave/akonadi/akonadislave.cpp +++ b/kioslave/akonadi/akonadislave.cpp @@ -1,241 +1,241 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "akonadislave.h" #include #include #include #include #include #include #include #include "akonadislave_debug.h" #include #include #include #include #include extern "C" { int Q_DECL_EXPORT kdemain(int argc, char **argv); } int kdemain(int argc, char **argv) { QApplication app(argc, argv); KAboutData aboutData(QStringLiteral("kio_akonadi"), QString(), QStringLiteral("0")); QCommandLineParser parser; KAboutData::setApplicationData(aboutData); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+protocol"), i18n("Protocol name"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+pool"), i18n("Socket name"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+app"), i18n("Socket name"))); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); AkonadiSlave slave(parser.positionalArguments().at(1).toLocal8Bit(), parser.positionalArguments().at(2).toLocal8Bit()); slave.dispatchLoop(); return 0; } using namespace Akonadi; AkonadiSlave::AkonadiSlave(const QByteArray &pool_socket, const QByteArray &app_socket) : KIO::SlaveBase("akonadi", pool_socket, app_socket) { qCDebug(AKONADISLAVE_LOG) << "kio_akonadi starting up"; } AkonadiSlave::~AkonadiSlave() { qCDebug(AKONADISLAVE_LOG) << "kio_akonadi shutting down"; } void AkonadiSlave::get(const QUrl &url) { const Item item = Item::fromUrl(url); ItemFetchJob *job = new ItemFetchJob(item); job->fetchScope().fetchFullPayload(); if (!job->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } if (job->items().count() != 1) { error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item.")); } else { const Item item = job->items().at(0); QByteArray tmp = item.payloadData(); data(tmp); data(QByteArray()); finished(); } finished(); } void AkonadiSlave::stat(const QUrl &url) { qCDebug(AKONADISLAVE_LOG) << url; // Stats for a collection if (Collection::fromUrl(url).isValid()) { Collection collection = Collection::fromUrl(url); if (collection != Collection::root()) { // Check that the collection exists. CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base); if (!job->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } if (job->collections().count() != 1) { error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item.")); return; } collection = job->collections().at(0); } statEntry(entryForCollection(collection)); finished(); } // Stats for an item else if (Item::fromUrl(url).isValid()) { ItemFetchJob *job = new ItemFetchJob(Item::fromUrl(url)); if (!job->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } if (job->items().count() != 1) { error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item.")); return; } const Item item = job->items().at(0); statEntry(entryForItem(item)); finished(); } } void AkonadiSlave::del(const QUrl &url, bool isFile) { qCDebug(AKONADISLAVE_LOG) << url; if (!isFile) { // It's a directory Collection collection = Collection::fromUrl(url); CollectionDeleteJob *job = new CollectionDeleteJob(collection); if (!job->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } finished(); } else { // It's a file ItemDeleteJob *job = new ItemDeleteJob(Item::fromUrl(url)); if (!job->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } finished(); } } void AkonadiSlave::listDir(const QUrl &url) { qCDebug(AKONADISLAVE_LOG) << url; if (!Collection::fromUrl(url).isValid()) { error(KIO::ERR_DOES_NOT_EXIST, i18n("No such collection.")); return; } // Fetching collections Collection collection = Collection::fromUrl(url); if (!collection.isValid()) { error(KIO::ERR_DOES_NOT_EXIST, i18n("No such collection.")); return; } CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::FirstLevel); if (!job->exec()) { error(KIO::ERR_CANNOT_ENTER_DIRECTORY, job->errorString()); return; } const Collection::List collections = job->collections(); for (const Collection &col : collections) { listEntry(entryForCollection(col)); } // Fetching items if (collection != Collection::root()) { ItemFetchJob *fjob = new ItemFetchJob(collection); if (!fjob->exec()) { error(KIO::ERR_INTERNAL, job->errorString()); return; } const Item::List items = fjob->items(); totalSize(collections.count() + items.count()); for (const Item &item : items) { listEntry(entryForItem(item)); } } finished(); } KIO::UDSEntry AkonadiSlave::entryForItem(const Akonadi::Item &item) { KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, QString::number(item.id())); entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, item.mimeType()); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); entry.insert(KIO::UDSEntry::UDS_URL, item.url().url()); entry.insert(KIO::UDSEntry::UDS_SIZE, item.size()); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, item.modificationTime().toSecsSinceEpoch()); return entry; } KIO::UDSEntry AkonadiSlave::entryForCollection(const Akonadi::Collection &collection) { KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, collection.name()); entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, Collection::mimeType()); entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.insert(KIO::UDSEntry::UDS_URL, collection.url().url()); entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); - if (EntityDisplayAttribute *attr = collection.attribute()) { + if (const EntityDisplayAttribute *attr = collection.attribute()) { if (!attr->iconName().isEmpty()) { entry.insert(KIO::UDSEntry::UDS_ICON_NAME, attr->iconName()); } if (!attr->displayName().isEmpty()) { entry.insert(KIO::UDSEntry::UDS_DISPLAY_NAME, attr->displayName()); } } return entry; } diff --git a/resources/dav/resource/davgroupwareresource.cpp b/resources/dav/resource/davgroupwareresource.cpp index 99f6b1e7c..cafeca3ce 100644 --- a/resources/dav/resource/davgroupwareresource.cpp +++ b/resources/dav/resource/davgroupwareresource.cpp @@ -1,1364 +1,1364 @@ /* Copyright (c) 2009 Grégory Oestreicher This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "davgroupwareresource.h" #include "akonadietagcache.h" #include "configdialog.h" #include "ctagattribute.h" #include "davfreebusyhandler.h" #include "davprotocolattribute.h" #include "utils.h" #include "settings.h" #include "settingsadaptor.h" #include "setupwizard.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "davresource_debug.h" #include #include using namespace Akonadi; typedef QSharedPointer IncidencePtr; DavGroupwareResource::DavGroupwareResource(const QString &id) : ResourceBase(id) , FreeBusyProviderBase() , mSyncErrorNotified(false) { AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); setNeedsNetwork(true); mDavCollectionRoot.setParentCollection(Collection::root()); mDavCollectionRoot.setName(identifier()); mDavCollectionRoot.setRemoteId(identifier()); mDavCollectionRoot.setContentMimeTypes(QStringList() << Collection::mimeType()); mDavCollectionRoot.setRights(Collection::CanCreateCollection | Collection::CanDeleteCollection | Collection::CanChangeCollection); EntityDisplayAttribute *attribute = mDavCollectionRoot.attribute(Collection::AddIfMissing); attribute->setIconName(QStringLiteral("folder-remote")); int refreshInterval = Settings::self()->refreshInterval(); if (refreshInterval == 0) { refreshInterval = -1; } Akonadi::CachePolicy cachePolicy; cachePolicy.setInheritFromParent(false); cachePolicy.setSyncOnDemand(false); cachePolicy.setCacheTimeout(-1); cachePolicy.setIntervalCheckTime(refreshInterval); cachePolicy.setLocalParts(QStringList() << QStringLiteral("ALL")); mDavCollectionRoot.setCachePolicy(cachePolicy); changeRecorder()->fetchCollection(true); changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All); Settings::self()->setWinId(winIdForDialogs()); Settings::self()->setResourceIdentifier(identifier()); mFreeBusyHandler = new DavFreeBusyHandler(this); connect(mFreeBusyHandler, &DavFreeBusyHandler::handlesFreeBusy, this, &DavGroupwareResource::onHandlesFreeBusy); connect(mFreeBusyHandler, &DavFreeBusyHandler::freeBusyRetrieved, this, &DavGroupwareResource::onFreeBusyRetrieved); connect(this, &DavGroupwareResource::reloadConfiguration, this, &DavGroupwareResource::onReloadConfig); scheduleCustomTask(this, "initialRetrieveCollections", QVariant(), ResourceBase::Prepend); scheduleCustomTask(this, "createInitialCache", QVariant(), ResourceBase::Prepend); } DavGroupwareResource::~DavGroupwareResource() { delete mFreeBusyHandler; } void DavGroupwareResource::collectionRemoved(const Akonadi::Collection &collection) { qCDebug(DAVRESOURCE_LOG) << "Removing collection " << collection.remoteId(); if (!configurationIsValid()) { return; } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId()); KDAV::DavCollectionDeleteJob *job = new KDAV::DavCollectionDeleteJob(davUrl); job->setProperty("collection", QVariant::fromValue(collection)); connect(job, &KDAV::DavCollectionDeleteJob::result, this, &DavGroupwareResource::onCollectionRemovedFinished); job->start(); } void DavGroupwareResource::cleanup() { Settings::self()->cleanup(); ResourceBase::cleanup(); } QDateTime DavGroupwareResource::lastCacheUpdate() const { return QDateTime::currentDateTime(); } void DavGroupwareResource::canHandleFreeBusy(const QString &email) const { if (!isOnline()) { handlesFreeBusy(email, false); } else { mFreeBusyHandler->canHandleFreeBusy(email); } } void DavGroupwareResource::onHandlesFreeBusy(const QString &email, bool handles) { handlesFreeBusy(email, handles); } void DavGroupwareResource::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end) { if (!isOnline()) { freeBusyRetrieved(email, QString(), false, i18n("Unable to retrieve free-busy info while offline")); } else { mFreeBusyHandler->retrieveFreeBusy(email, start, end); } } void DavGroupwareResource::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText) { freeBusyRetrieved(email, freeBusy, success, errorText); } void DavGroupwareResource::configure(WId windowId) { Settings::self()->setWinId(windowId); // On the initial configuration we start the setup wizard if (Settings::self()->configuredDavUrls().isEmpty()) { SetupWizard wizard; if (windowId) { KWindowSystem::setMainWindow(&wizard, windowId); } const int result = wizard.exec(); if (result == QDialog::Accepted) { const SetupWizard::Url::List urls = wizard.urls(); for (const SetupWizard::Url &url : urls) { Settings::UrlConfiguration *urlConfig = new Settings::UrlConfiguration(); urlConfig->mUrl = url.url; urlConfig->mProtocol = url.protocol; urlConfig->mUser = url.userName; urlConfig->mPassword = wizard.field(QStringLiteral("credentialsPassword")).toString(); Settings::self()->newUrlConfiguration(urlConfig); } if (!urls.isEmpty()) { Settings::self()->setDisplayName(wizard.displayName()); } QString defaultUser = wizard.field(QStringLiteral("credentialsUserName")).toString(); if (!defaultUser.isEmpty()) { Settings::self()->setDefaultUsername(defaultUser); Settings::self()->setDefaultPassword(wizard.field(QStringLiteral("credentialsPassword")).toString()); } } } // continue with the normal config dialog ConfigDialog dialog; if (windowId) { KWindowSystem::setMainWindow(&dialog, windowId); } if (!Settings::self()->defaultUsername().isEmpty()) { dialog.setPassword(Settings::self()->defaultPassword()); } const int result = dialog.exec(); if (result == QDialog::Accepted) { Settings::self()->setSettingsVersion(3); Settings::self()->save(); synchronize(); Q_EMIT configurationDialogAccepted(); } else { Q_EMIT configurationDialogRejected(); } } KJob *DavGroupwareResource::createRetrieveCollectionsJob() { qCDebug(DAVRESOURCE_LOG) << "Retrieving collections list"; mSyncErrorNotified = false; if (!configurationIsValid()) { return nullptr; } Q_EMIT status(Running, i18n("Fetching collections")); KDAV::DavCollectionsMultiFetchJob *job = new KDAV::DavCollectionsMultiFetchJob(Settings::self()->configuredDavUrls()); connect(job, &KDAV::DavCollectionsMultiFetchJob::result, this, &DavGroupwareResource::onRetrieveCollectionsFinished); connect(job, &KDAV::DavCollectionsMultiFetchJob::collectionDiscovered, this, &DavGroupwareResource::onCollectionDiscovered); return job; } void DavGroupwareResource::initialRetrieveCollections() { auto job = createRetrieveCollectionsJob(); if (!job) { return; } job->setProperty("initialCacheSync", QVariant::fromValue(true)); job->start(); } void DavGroupwareResource::retrieveCollections() { auto job = createRetrieveCollectionsJob(); if (!job) { return; } job->setProperty("initialCacheSync", QVariant::fromValue(false)); job->start(); } void DavGroupwareResource::retrieveItems(const Akonadi::Collection &collection) { if (!collection.isValid()) { itemsRetrievalDone(); return; } qCDebug(DAVRESOURCE_LOG) << "Retrieving items for collection " << collection.remoteId(); if (!configurationIsValid()) { return; } // As the resource root collection contains mime types for items we must // work around the fact that Akonadi will rightfully try to retrieve items // from it. So just return an empty list if (collection.remoteId() == identifier()) { itemsRetrievalDone(); return; } if (!mEtagCaches.contains(collection.remoteId())) { qCDebug(DAVRESOURCE_LOG) << "Asked to retrieve items for a collection we don't have in the cache"; itemsRetrievalDone(); return; } // Only continue if the collection has changed or if // it's the first time we see it - CTagAttribute *CTagAttr = collection.attribute(); + const CTagAttribute *CTagAttr = collection.attribute(); if (CTagAttr && mCTagCache.contains(collection.remoteId()) && mCTagCache.value(collection.remoteId()) == CTagAttr->CTag()) { qCDebug(DAVRESOURCE_LOG) << "CTag for collection" << collection.remoteId() << "didn't change: " << CTagAttr->CTag(); itemsRetrievalDone(); return; } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId()); if (!davUrl.url().isValid()) { qCCritical(DAVRESOURCE_LOG) << "Can't find a configured URL, collection.remoteId() is " << collection.remoteId(); cancelTask(i18n("Asked to retrieve items for an unknown collection: %1", collection.remoteId())); //Q_ASSERT_X( false, "DavGroupwareResource::retrieveItems", "Url is invalid" ); return; } KDAV::DavItemsListJob *job = new KDAV::DavItemsListJob(davUrl, mEtagCaches.value(collection.remoteId())); if (Settings::self()->limitSyncRange()) { QDateTime start = Settings::self()->getSyncRangeStart(); qCDebug(DAVRESOURCE_LOG) << "Start time for list job:" << start; if (start.isValid()) { job->setTimeRange(start.toString(QStringLiteral("yyyyMMddTHHMMssZ")), QString()); } } job->setProperty("collection", QVariant::fromValue(collection)); job->setContentMimeTypes(collection.contentMimeTypes()); connect(job, &KDAV::DavItemsListJob::result, this, &DavGroupwareResource::onRetrieveItemsFinished); job->start(); } bool DavGroupwareResource::retrieveItem(const Akonadi::Item &item, const QSet &) { qCDebug(DAVRESOURCE_LOG) << "Retrieving single item. Remote id = " << item.remoteId(); if (!configurationIsValid()) { return false; } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId()); if (!davUrl.url().isValid()) { qCDebug(DAVRESOURCE_LOG) << "Failed to get a valid DavUrl. Parent collection remote ID is" << item.parentCollection().remoteId(); cancelTask(); return false; } KDAV::DavItem davItem; davItem.setContentType(QStringLiteral("text/calendar")); davItem.setEtag(item.remoteRevision()); KDAV::DavItemFetchJob *job = new KDAV::DavItemFetchJob(davItem); job->setProperty("item", QVariant::fromValue(item)); connect(job, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onRetrieveItemFinished); job->start(); return true; } void DavGroupwareResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { qCDebug(DAVRESOURCE_LOG) << "Received notification for added item. Local id = " << item.id() << ". Remote id = " << item.remoteId() << ". Collection remote id = " << collection.remoteId(); if (!configurationIsValid()) { return; } if (collection.remoteId().isEmpty()) { qCCritical(DAVRESOURCE_LOG) << "Invalid remote id for collection " << collection.id() << " = " << collection.remoteId(); cancelTask(i18n("Invalid collection for item %1.", item.id())); return; } KDAV::DavItem davItem = Utils::createDavItem(item, collection); if (davItem.data().isEmpty()) { qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload"; cancelTask(); return; } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), davItem.url().toDisplayString()); qCDebug(DAVRESOURCE_LOG) << "Item " << item.id() << " will be put to " << davItem.url().toDisplayString(); davItem.setUrl(davUrl); KDAV::DavItemCreateJob *job = new KDAV::DavItemCreateJob(davItem); job->setProperty("collection", QVariant::fromValue(collection)); job->setProperty("item", QVariant::fromValue(item)); connect(job, &KDAV::DavItemCreateJob::result, this, &DavGroupwareResource::onItemAddedFinished); job->start(); } void DavGroupwareResource::itemChanged(const Akonadi::Item &item, const QSet &) { qCDebug(DAVRESOURCE_LOG) << "Received notification for changed item. Local id = " << item.id() << ". Remote id = " << item.remoteId(); if (!configurationIsValid()) { return; } const Akonadi::Collection collection = item.parentCollection(); if (!mEtagCaches.contains(collection.remoteId())) { qCDebug(DAVRESOURCE_LOG) << "Changed item is in a collection we don't have in the cache"; // TODO: display an error cancelTask(); return; } QString ridBase = item.remoteId(); if (ridBase.contains(QLatin1Char('#'))) { ridBase.truncate(ridBase.indexOf(QLatin1Char('#'))); } auto cache = mEtagCaches.value(collection.remoteId()); Akonadi::Item::List extraItems; const QStringList lstUrls = cache->urls(); for (const QString &rid : lstUrls) { if (rid.startsWith(ridBase) && rid != item.remoteId()) { Akonadi::Item extraItem; extraItem.setRemoteId(rid); extraItems << extraItem; } } if (extraItems.isEmpty()) { doItemChange(item); } else { Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(extraItems); job->setCollection(item.parentCollection()); job->fetchScope().fetchFullPayload(); job->setProperty("item", QVariant::fromValue(item)); connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemChangePrepared); } } void DavGroupwareResource::onItemChangePrepared(KJob *job) { Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); Akonadi::Item item = job->property("item").value(); doItemChange(item, fetchJob->items()); } void DavGroupwareResource::doItemChange(const Akonadi::Item &item, const Akonadi::Item::List &dependentItems) { KDAV::DavItem davItem = Utils::createDavItem(item, item.parentCollection(), dependentItems); if (davItem.data().isEmpty()) { qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload"; cancelTask(); return; } QString url = item.remoteId(); if (url.contains(QLatin1Char('#'))) { url.truncate(url.indexOf(QLatin1Char('#'))); } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), url); // We have to re-set the URL as it's not necessarily valid after createDavItem() davItem.setUrl(davUrl); davItem.setEtag(item.remoteRevision()); KDAV::DavItemModifyJob *modJob = new KDAV::DavItemModifyJob(davItem); modJob->setProperty("collection", QVariant::fromValue(item.parentCollection())); modJob->setProperty("item", QVariant::fromValue(item)); modJob->setProperty("dependentItems", QVariant::fromValue(dependentItems)); connect(modJob, &KDAV::DavItemModifyJob::result, this, &DavGroupwareResource::onItemChangedFinished); modJob->start(); } void DavGroupwareResource::itemRemoved(const Akonadi::Item &item) { qCDebug(DAVRESOURCE_LOG) << "Received notification for removed item. Remote id = " << item.remoteId(); if (!configurationIsValid()) { return; } const Akonadi::Collection collection = item.parentCollection(); if (!mEtagCaches.contains(collection.remoteId())) { qCDebug(DAVRESOURCE_LOG) << "Removed item is in a collection we don't have in the cache"; // TODO: display an error cancelTask(); return; } QString ridBase = item.remoteId(); if (ridBase.contains(QLatin1Char('#'))) { // A bit tricky: we must remove an incidence contained in a resource // containing multiple ones. ridBase.truncate(ridBase.indexOf(QLatin1Char('#'))); auto cache = mEtagCaches.value(collection.remoteId()); Akonadi::Item::List extraItems; const QStringList lstUrl = cache->urls(); for (const QString &rid : lstUrl) { if (rid.startsWith(ridBase) && rid != item.remoteId()) { Akonadi::Item extraItem; extraItem.setRemoteId(rid); extraItems << extraItem; } } if (extraItems.isEmpty()) { // Urrrr? // Well, just delete the item. doItemRemoval(item); } else { Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(extraItems); job->setCollection(item.parentCollection()); job->fetchScope().fetchFullPayload(); job->setProperty("item", QVariant::fromValue(item)); connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemRemovalPrepared); } } else { // easy as pie: just remove everything at the URL. doItemRemoval(item); } } void DavGroupwareResource::onItemRemovalPrepared(KJob *job) { Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); Akonadi::Item item = job->property("item").value(); const Akonadi::Item::List keptItems = fetchJob->items(); if (keptItems.isEmpty()) { // Urrrr? Not again! doItemRemoval(item); } else { Akonadi::Item mainItem; Akonadi::Item::List extraItems; QString ridBase = item.remoteId(); ridBase.truncate(ridBase.indexOf(QLatin1Char('#'))); for (const Akonadi::Item &kept : keptItems) { if (kept.remoteId() == ridBase && extraItems.isEmpty()) { mainItem = kept; } else { extraItems << kept; } } if (!mainItem.hasPayload()) { mainItem = extraItems.takeFirst(); } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), ridBase); KDAV::DavItem davItem = Utils::createDavItem(mainItem, mainItem.parentCollection(), extraItems); davItem.setUrl(davUrl); davItem.setEtag(item.remoteRevision()); KDAV::DavItemModifyJob *modJob = new KDAV::DavItemModifyJob(davItem); modJob->setProperty("collection", QVariant::fromValue(mainItem.parentCollection())); modJob->setProperty("item", QVariant::fromValue(mainItem)); modJob->setProperty("dependentItems", QVariant::fromValue(extraItems)); modJob->setProperty("isRemoval", QVariant::fromValue(true)); modJob->setProperty("removedItem", QVariant::fromValue(item)); connect(modJob, &KDAV::DavItemModifyJob::result, this, &DavGroupwareResource::onItemChangedFinished); modJob->start(); } } void DavGroupwareResource::doItemRemoval(const Akonadi::Item &item) { const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId()); KDAV::DavItem davItem; davItem.setUrl(davUrl); davItem.setEtag(item.remoteRevision()); KDAV::DavItemDeleteJob *job = new KDAV::DavItemDeleteJob(davItem); job->setProperty("item", QVariant::fromValue(item)); job->setProperty("collection", QVariant::fromValue(item.parentCollection())); connect(job, &KDAV::DavItemDeleteJob::result, this, &DavGroupwareResource::onItemRemovedFinished); job->start(); } void DavGroupwareResource::doSetOnline(bool online) { qCDebug(DAVRESOURCE_LOG) << "Resource changed online status to" << online; if (online) { synchronize(); } ResourceBase::doSetOnline(online); } void DavGroupwareResource::createInitialCache() { // Get all the items fetched by this resource Akonadi::RecursiveItemFetchJob *job = new Akonadi::RecursiveItemFetchJob(mDavCollectionRoot, QStringList()); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); connect(job, &Akonadi::RecursiveItemFetchJob::result, this, &DavGroupwareResource::onCreateInitialCacheReady); job->start(); } void DavGroupwareResource::onCreateInitialCacheReady(KJob *job) { Akonadi::RecursiveItemFetchJob *fetchJob = qobject_cast(job); const Akonadi::Item::List itemsLst = fetchJob->items(); for (const Akonadi::Item &item : itemsLst) { const QString rid = item.remoteId(); if (rid.isEmpty()) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without remote ID. " << item.id(); continue; } const Akonadi::Collection collection = item.parentCollection(); if (collection.remoteId().isEmpty()) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item in a collection without remote ID. " << item.remoteId(); continue; } const QString etag = item.remoteRevision(); if (etag.isEmpty()) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without ETag. " << item.remoteId(); continue; } if (!mEtagCaches.contains(collection.remoteId())) { auto cache = std::shared_ptr(new AkonadiEtagCache(collection)); mEtagCaches.insert(collection.remoteId(), cache); } mEtagCaches[collection.remoteId()]->setEtag(rid, etag); } taskDone(); } void DavGroupwareResource::onReloadConfig() { Settings::self()->reloadConfig(); synchronize(); } void DavGroupwareResource::onCollectionRemovedFinished(KJob *job) { if (job->error()) { cancelTask(i18n("Unable to remove collection: %1", job->errorText())); return; } Akonadi::Collection collection = job->property("collection").value(); if (mEtagCaches.contains(collection.remoteId())) { mEtagCaches[collection.remoteId()]->deleteLater(); mEtagCaches.remove(collection.remoteId()); } changeProcessed(); } void DavGroupwareResource::onRetrieveCollectionsFinished(KJob *job) { const KDAV::DavCollectionsMultiFetchJob *fetchJob = qobject_cast< KDAV::DavCollectionsMultiFetchJob *>(job); if (job->error()) { qCWarning(DAVRESOURCE_LOG) << "Unable to fetch collections" << job->error() << job->errorText(); cancelTask(i18n("Unable to retrieve collections: %1", job->errorText())); mSyncErrorNotified = true; return; } bool initialCacheSync = job->property("initialCacheSync").toBool(); Akonadi::Collection::List collections; collections << mDavCollectionRoot; QSet seenCollectionsUrls; const KDAV::DavCollection::List davCollections = fetchJob->collections(); for (const KDAV::DavCollection &davCollection : davCollections) { if (seenCollectionsUrls.contains(davCollection.url().toDisplayString())) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Duplicate collection reported. " << davCollection.url().toDisplayString(); continue; } else { seenCollectionsUrls.insert(davCollection.url().toDisplayString()); } Akonadi::Collection collection; collection.setParentCollection(mDavCollectionRoot); collection.setRemoteId(davCollection.url().toDisplayString()); collection.setName(collection.remoteId()); if (davCollection.color().isValid()) { CollectionColorAttribute *colorAttr = collection.attribute(Akonadi::Collection::AddIfMissing); colorAttr->setColor(davCollection.color()); } if (!davCollection.displayName().isEmpty()) { EntityDisplayAttribute *attr = collection.attribute(Collection::AddIfMissing); attr->setDisplayName(davCollection.displayName()); } QStringList mimeTypes; mimeTypes << Collection::mimeType(); const KDAV::DavCollection::ContentTypes contentTypes = davCollection.contentTypes(); if (contentTypes & KDAV::DavCollection::Calendar) { mimeTypes << QStringLiteral("text/calendar"); } if (contentTypes & KDAV::DavCollection::Events) { mimeTypes << KCalCore::Event::eventMimeType(); } if (contentTypes & KDAV::DavCollection::Todos) { mimeTypes << KCalCore::Todo::todoMimeType(); } if (contentTypes & KDAV::DavCollection::Contacts) { mimeTypes << KContacts::Addressee::mimeType(); } if (contentTypes & KDAV::DavCollection::FreeBusy) { mimeTypes << KCalCore::FreeBusy::freeBusyMimeType(); } if (contentTypes & KDAV::DavCollection::Journal) { mimeTypes << KCalCore::Journal::journalMimeType(); } collection.setContentMimeTypes(mimeTypes); setCollectionIcon(collection /*by-ref*/); DavProtocolAttribute *protoAttr = collection.attribute(Collection::AddIfMissing); protoAttr->setDavProtocol(davCollection.url().protocol()); /* * We unfortunately have to update the CTag now in the cache * as this information will not be available when retrieveItems() * is called. We leave it untouched in the collection attribute * and will only update it there after successful sync. */ if (!davCollection.CTag().isEmpty()) { mCTagCache.insert(davCollection.url().toDisplayString(), davCollection.CTag()); } KDAV::Privileges privileges = davCollection.privileges(); Akonadi::Collection::Rights rights; if (privileges & KDAV::All || privileges & KDAV::Write) { rights |= Akonadi::Collection::AllRights; } if (privileges & KDAV::WriteContent) { rights |= Akonadi::Collection::CanChangeItem; } if (privileges & KDAV::Bind) { rights |= Akonadi::Collection::CanCreateItem; } if (privileges & KDAV::Unbind) { rights |= Akonadi::Collection::CanDeleteItem; } if (privileges == KDAV::Read) { rights |= Akonadi::Collection::ReadOnly; } collection.setRights(rights); collections << collection; if (!mEtagCaches.contains(collection.remoteId())) { auto cache = std::shared_ptr(new AkonadiEtagCache(collection)); mEtagCaches.insert(collection.remoteId(), cache); } } foreach (const QString &rid, mEtagCaches.keys()) { if (!seenCollectionsUrls.contains(rid)) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Collection disappeared. " << rid; mEtagCaches[rid]->deleteLater(); mEtagCaches.remove(rid); } } if (!initialCacheSync) { collectionsRetrieved(collections); } else { taskDone(); } } void DavGroupwareResource::onRetrieveItemsFinished(KJob *job) { if (job->error()) { if (mSyncErrorNotified) { cancelTask(); } else { cancelTask(i18n("Unable to retrieve items: %1", job->errorText())); mSyncErrorNotified = true; } return; } Collection collection = job->property("collection").value(); const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId()); const bool protocolSupportsMultiget = KDAV::DavManager::self()->davProtocol(davUrl.protocol())->useMultiget(); const KDAV::DavItemsListJob *listJob = qobject_cast< KDAV::DavItemsListJob *>(job); auto cache = mEtagCaches.value(collection.remoteId()); if (!cache) { qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!"; cancelTask(); return; } Akonadi::Item::List changedItems; QSet seenRids; QStringList changedRids; changedItems.reserve(listJob->changedItems().count()); foreach (const KDAV::DavItem &davItem, listJob->changedItems()) { seenRids.insert(davItem.url().toDisplayString()); Akonadi::Item item; item.setParentCollection(collection); item.setRemoteId(davItem.url().toDisplayString()); item.setMimeType(davItem.contentType()); item.setRemoteRevision(davItem.etag()); cache->markAsChanged(item.remoteId()); changedRids << item.remoteId(); changedItems << item; // Only clear the payload (and therefor trigger a refetch from the backend) if we // do not use multiget, because in this case we fetch the complete payload // some lines below already. if (!protocolSupportsMultiget) { qCDebug(DAVRESOURCE_LOG) << "Outdated item " << item.remoteId() << " (etag = " << davItem.etag() << ")"; item.clearPayload(); } } foreach (const QString &rmd, listJob->deletedItems()) { // We don't want to delete dependent items if the main item was seen if (rmd.contains(QLatin1Char('#'))) { const QString base = rmd.left(rmd.indexOf(QLatin1Char('#'))); if (seenRids.contains(base)) { continue; } } qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveItemsFinished: Item disappeared. " << rmd; Akonadi::Item item; item.setParentCollection(collection); item.setRemoteId(rmd); cache->removeEtag(rmd); // Use a job to delete items as itemsRetrievedIncremental seem to choke // when many items are given with just their RID. Akonadi::ItemDeleteJob *deleteJob = new Akonadi::ItemDeleteJob(item); deleteJob->start(); } // If the protocol supports multiget then deviate from the expected behavior // and fetch all items with payload now instead of waiting for Akonadi to // request it item by item in retrieveItem(). // This allows the resource to use the multiget query and let it be nice // to the remote server : only one request for n items instead of n requests. if (protocolSupportsMultiget && !changedRids.isEmpty()) { KDAV::DavItemsFetchJob *fetchJob = new KDAV::DavItemsFetchJob(davUrl, changedRids); connect(fetchJob, &KDAV::DavItemsFetchJob::result, this, &DavGroupwareResource::onMultigetFinished); fetchJob->setProperty("collection", QVariant::fromValue(collection)); fetchJob->setProperty("items", QVariant::fromValue(changedItems)); fetchJob->start(); // delay the call of itemsRetrieved() to onMultigetFinished() } else { // Update the collection CTag attribute now as sync is done. if (mCTagCache.contains(collection.remoteId())) { CTagAttribute *CTagAttr = collection.attribute(Collection::AddIfMissing); qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId()); CTagAttr->setCTag(mCTagCache.value(collection.remoteId())); Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob(collection); job->start(); } itemsRetrievedIncremental(changedItems, Akonadi::Item::List()); } } void DavGroupwareResource::onMultigetFinished(KJob *job) { if (job->error()) { if (mSyncErrorNotified) { cancelTask(); } else { cancelTask(i18n("Unable to retrieve items: %1", job->errorText())); mSyncErrorNotified = true; } return; } Akonadi::Collection collection = job->property("collection").value(); auto cache = mEtagCaches.value(collection.remoteId()); if (!cache) { qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!"; cancelTask(); return; } const Akonadi::Item::List origItems = job->property("items").value(); const KDAV::DavItemsFetchJob *davJob = qobject_cast< KDAV::DavItemsFetchJob *>(job); Akonadi::Item::List items; for (Akonadi::Item item : qAsConst(origItems)) { //krazy:exclude=foreach non-const is intended here const KDAV::DavItem davItem = davJob->item(item.remoteId()); // No data was retrieved for this item, maybe because it is not out of date if (davItem.data().isEmpty()) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Empty item returned. " << item.remoteId(); if (!cache->isOutOfDate(item.remoteId())) { qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Item is not changed, including it. " << item.remoteId(); items << item; } continue; } Akonadi::Item::List extraItems; if (!Utils::parseDavData(davItem, item, extraItems)) { qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Failed to parse item data. " << item.remoteId(); continue; } // update etag item.setRemoteRevision(davItem.etag()); cache->setEtag(item.remoteId(), davItem.etag()); items << item; for (const Akonadi::Item &extraItem : qAsConst(extraItems)) { cache->setEtag(extraItem.remoteId(), davItem.etag()); items << extraItem; } } // Update the collection CTag attribute now as sync is done. if (mCTagCache.contains(collection.remoteId())) { CTagAttribute *CTagAttr = collection.attribute(Collection::AddIfMissing); qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId()); CTagAttr->setCTag(mCTagCache.value(collection.remoteId())); Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob(collection); job->start(); } itemsRetrievedIncremental(items, Akonadi::Item::List()); } void DavGroupwareResource::onRetrieveItemFinished(KJob *job) { onItemFetched(job, ItemUpdateAdd); } void DavGroupwareResource::onItemRefreshed(KJob *job) { ItemFetchUpdateType update = ItemUpdateChange; if (job->property("isRemoval").isValid() && job->property("isRemoval").toBool()) { update = ItemUpdateNone; } onItemFetched(job, update); } void DavGroupwareResource::onItemFetched(KJob *job, ItemFetchUpdateType updateType) { if (job->error()) { if (mSyncErrorNotified) { cancelTask(); } else { cancelTask(i18n("Unable to retrieve item: %1", job->errorText())); mSyncErrorNotified = true; } return; } const KDAV::DavItemFetchJob *fetchJob = qobject_cast< KDAV::DavItemFetchJob *>(job); const KDAV::DavItem davItem = fetchJob->item(); Akonadi::Item item = fetchJob->property("item").value(); Akonadi::Collection collection = fetchJob->property("collection").value(); Akonadi::Item::List extraItems; if (!Utils::parseDavData(davItem, item, extraItems)) { qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onItemFetched: Failed to parse item data. " << item.remoteId(); return; } // update etag item.setRemoteRevision(davItem.etag()); auto etag = mEtagCaches[collection.remoteId()]; etag->setEtag(item.remoteId(), davItem.etag()); if (!extraItems.isEmpty()) { for (int i = 0, total = extraItems.size(); i < total; ++i) { etag->setEtag(extraItems.at(i).remoteId(), davItem.etag()); } Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(extraItems); j->setIgnorePayload(true); } if (updateType == ItemUpdateChange) { changeCommitted(item); } else if (updateType == ItemUpdateAdd) { itemRetrieved(item); } } void DavGroupwareResource::onItemAddedFinished(KJob *job) { const KDAV::DavItemCreateJob *createJob = qobject_cast< KDAV::DavItemCreateJob *>(job); KDAV::DavItem davItem = createJob->item(); Akonadi::Item item = createJob->property("item").value(); item.setRemoteId(davItem.url().toDisplayString()); if (createJob->error()) { qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << createJob->error() << createJob->errorString(); if (createJob->canRetryLater()) { retryAfterFailure(createJob->errorString()); } else { cancelTask(i18n("Unable to add item: %1", createJob->errorString())); } return; } Akonadi::Collection collection = createJob->property("collection").value(); if (davItem.etag().isEmpty()) { const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), item.remoteId()); davItem.setUrl(davUrl); KDAV::DavItemFetchJob *fetchJob = new KDAV::DavItemFetchJob(davItem); fetchJob->setProperty("item", QVariant::fromValue(item)); fetchJob->setProperty("collection", QVariant::fromValue(collection)); connect(fetchJob, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed); fetchJob->start(); } else { item.setRemoteRevision(davItem.etag()); mEtagCaches[collection.remoteId()]->setEtag(davItem.url().toDisplayString(), davItem.etag()); changeCommitted(item); } } void DavGroupwareResource::onItemChangedFinished(KJob *job) { const KDAV::DavItemModifyJob *modifyJob = qobject_cast< KDAV::DavItemModifyJob *>(job); KDAV::DavItem davItem = modifyJob->item(); Akonadi::Collection collection = modifyJob->property("collection").value(); Akonadi::Item item = modifyJob->property("item").value(); Akonadi::Item::List dependentItems = modifyJob->property("dependentItems").value(); bool isRemoval = modifyJob->property("isRemoval").isValid() && modifyJob->property("isRemoval").toBool(); auto cache = mEtagCaches.value(collection.remoteId()); if (!cache) { qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!"; cancelTask(); return; } if (modifyJob->error()) { qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << modifyJob->error() << modifyJob->errorString(); if (modifyJob->hasConflict()) { handleConflict(item, dependentItems, modifyJob->freshItem(), isRemoval, modifyJob->freshResponseCode()); } else if (modifyJob->canRetryLater()) { retryAfterFailure(modifyJob->errorString()); } else { cancelTask(i18n("Unable to change item: %1", modifyJob->errorString())); } return; } if (isRemoval) { Akonadi::Item removedItem = job->property("removedItem").value(); if (removedItem.isValid()) { cache->removeEtag(removedItem.remoteId()); changeProcessed(); } } if (davItem.etag().isEmpty()) { const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId()); davItem.setUrl(davUrl); KDAV::DavItemFetchJob *fetchJob = new KDAV::DavItemFetchJob(davItem); fetchJob->setProperty("item", QVariant::fromValue(item)); fetchJob->setProperty("collection", QVariant::fromValue(collection)); fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems)); fetchJob->setProperty("isRemoval", QVariant::fromValue(isRemoval)); connect(fetchJob, &KDAV::DavItemsFetchJob::result, this, &DavGroupwareResource::onItemRefreshed); fetchJob->start(); } else { if (!isRemoval) { item.setRemoteRevision(davItem.etag()); cache->setEtag(davItem.url().toDisplayString(), davItem.etag()); changeCommitted(item); } if (!dependentItems.isEmpty()) { for (int i = 0, total = dependentItems.size(); i < total; ++i) { dependentItems[i].setRemoteRevision(davItem.etag()); cache->setEtag(dependentItems.at(i).remoteId(), davItem.etag()); } Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(dependentItems); j->setIgnorePayload(true); } } } void DavGroupwareResource::onDeletedItemRecreated(KJob *job) { const KDAV::DavItemCreateJob *createJob = qobject_cast< KDAV::DavItemCreateJob *>(job); KDAV::DavItem davItem = createJob->item(); Akonadi::Item item = createJob->property("item").value(); Akonadi::Collection collection = item.parentCollection(); Akonadi::Item::List dependentItems = createJob->property("dependentItems").value(); if (davItem.etag().isEmpty()) { const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId()); davItem.setUrl(davUrl); KDAV::DavItemFetchJob *fetchJob = new KDAV::DavItemFetchJob(davItem); fetchJob->setProperty("item", QVariant::fromValue(item)); fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems)); connect(fetchJob, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed); fetchJob->start(); } else { item.setRemoteRevision(davItem.etag()); auto etag = mEtagCaches[collection.remoteId()]; etag->setEtag(davItem.url().toDisplayString(), davItem.etag()); changeCommitted(item); if (!dependentItems.isEmpty()) { for (int i = 0, total = dependentItems.size(); i < total; ++i) { dependentItems[i].setRemoteRevision(davItem.etag()); etag->setEtag(dependentItems.at(i).remoteId(), davItem.etag()); } Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(dependentItems); j->setIgnorePayload(true); } } } void DavGroupwareResource::onItemRemovedFinished(KJob *job) { if (job->error()) { const KDAV::DavItemDeleteJob *deleteJob = qobject_cast< KDAV::DavItemDeleteJob *>(job); if (deleteJob->hasConflict()) { // Use a shortcut here as we don't show a conflict dialog to the user. handleConflict(Akonadi::Item(), Akonadi::Item::List(), deleteJob->freshItem(), true, 0); } else if (deleteJob->canRetryLater()) { retryAfterFailure(job->errorString()); } else { cancelTask(i18n("Unable to remove item: %1", job->errorString())); } } else { Akonadi::Item item = job->property("item").value(); Akonadi::Collection collection = job->property("collection").value(); mEtagCaches[collection.remoteId()]->removeEtag(item.remoteId()); changeProcessed(); } } void DavGroupwareResource::onCollectionDiscovered(int protocol, const QString &collection, const QString &config) { Settings::self()->addCollectionUrlMapping(KDAV::Protocol(protocol), collection, config); } void DavGroupwareResource::handleConflict(const Item &lI, const Item::List &localDependentItems, const KDAV::DavItem &rI, bool isLocalRemoval, int responseCode) { Akonadi::Item localItem(lI); Akonadi::Item remoteItem, tmpRemoteItem; // The tmp* vars are here to store the result of the parseDavData() call Akonadi::Item::List remoteDependentItems, tmpRemoteDependentItems; // as we have no idea which item triggered the conflict. qCDebug(DAVRESOURCE_LOG) << "Fresh response code is" << responseCode; bool isRemoteRemoval = (responseCode == 404 || responseCode == 410); if (!isRemoteRemoval) { if (!Utils::parseDavData(rI, tmpRemoteItem, tmpRemoteDependentItems)) { // TODO: set a more correct error message here cancelTask(i18n("Unable to change item: %1", QStringLiteral("conflict resolution failed"))); return; // TODO: we can end up here if the remote item was deleted } // Now try to find the item that really triggered the conflict const Akonadi::Item::List allRemoteItems = Akonadi::Item::List() << tmpRemoteItem << tmpRemoteDependentItems; for (const Akonadi::Item &tmpItem : allRemoteItems) { if (tmpItem.payloadData() != localItem.payloadData()) { if (remoteItem.isValid()) { // Oops, we can only manage one changed item at this stage, sorry... // TODO: make this translatable cancelTask(i18n("Unable to change item: %1", QStringLiteral("more than one item was changed in the backend"))); return; } remoteItem = tmpItem; } else { remoteDependentItems << tmpItem; } } } if (isLocalRemoval) { // TODO: implement with the configurable strategy /* * Here by default we don't delete an event that was modified in the backend, and * instead we just abort the current task. * Also, trigger an immediate sync to refresh the item. */ qCDebug(DAVRESOURCE_LOG) << "Local removal conflict"; // TODO: make this translatable cancelTask(i18n("Unable to remove item: %1", QStringLiteral("it was changed in the backend in the meantime"))); synchronize(); } else if (isRemoteRemoval) { // TODO: implement with the configurable strategy /* * Here also it is a bit tricky to clear the item in the local cache as the resource * will not get notified if the user chooses to delete the item and abandon the local * modification. For the time being let's just re-upload the changed item. */ qCDebug(DAVRESOURCE_LOG) << "Remote removal conflict"; Akonadi::Collection collection = localItem.parentCollection(); KDAV::DavItem davItem = Utils::createDavItem(localItem, collection, localDependentItems); QString urlStr = localItem.remoteId(); if (urlStr.contains(QLatin1Char('#'))) { urlStr.truncate(urlStr.indexOf(QLatin1Char('#'))); } const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), urlStr); davItem.setUrl(davUrl); KDAV::DavItemCreateJob *job = new KDAV::DavItemCreateJob(davItem); job->setProperty("item", QVariant::fromValue(localItem)); job->setProperty("dependentItems", QVariant::fromValue(localDependentItems)); connect(job, &KJob::result, this, &DavGroupwareResource::onDeletedItemRecreated); job->start(); } else { const QString remoteEtag = rI.etag(); Akonadi::Collection collection = localItem.parentCollection(); localItem.setRemoteRevision(remoteEtag); changeCommitted(localItem); // Update the ETag cache in all cases as the new ETag will have to be used // later for any update or deletion mEtagCaches[collection.remoteId()]->setEtag(rI.url().toDisplayString(), remoteEtag); // The first step is to fire a first modify job that will replace the item currently // in the local cache with the one that was found in the backend. Akonadi::Item updatedItem(localItem); updatedItem.setPayloadFromData(remoteItem.payloadData()); updatedItem.setRemoteRevision(remoteEtag); Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(updatedItem); j->setIgnorePayload(false); j->start(); // So now we have in the cache what's in the backend but the user is not aware // that behind the scenes something terrible is happening. Well, nearly... // To notify him of this, and due to the way the conflict handler works, we have // to re-attempt a modification to revert the modify job that was just fired. // So yes, we are effectively re-submitting the client-provided content, but // with a revision that will trigger the conflict dialog. // The only problem is that the user will see that we update the item before // the conflict dialog has time to display (if it's not behind the application // window). localItem.setRevision(0); j = new Akonadi::ItemModifyJob(localItem); j->setIgnorePayload(false); connect(j, &KJob::result, this, &DavGroupwareResource::onConflictModifyJobFinished); j->start(); // Hopefully for the dependent items everything will be fine. Right? // Not so sure in fact. if (!remoteDependentItems.isEmpty()) { auto etag = mEtagCaches[collection.remoteId()]; for (int i = 0; i < remoteDependentItems.size(); ++i) { remoteDependentItems[i].setRemoteRevision(remoteEtag); etag->setEtag(remoteDependentItems.at(i).remoteId(), remoteEtag); } Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(remoteDependentItems); j->setIgnorePayload(true); } } } void DavGroupwareResource::onConflictModifyJobFinished(KJob *job) { Akonadi::ItemModifyJob *j = qobject_cast(job); if (j->error()) { qCCritical(DAVRESOURCE_LOG) << "Conflict update failed: " << job->errorText(); // TODO: what do we do now? We just committed an item that's in a weird state... } } bool DavGroupwareResource::configurationIsValid() { if (Settings::self()->configuredDavUrls().empty()) { Q_EMIT status(NotConfigured, i18n("The resource is not configured yet")); cancelTask(i18n("The resource is not configured yet")); return false; } int newICT = Settings::self()->refreshInterval(); if (newICT == 0) { newICT = -1; } if (newICT != mDavCollectionRoot.cachePolicy().intervalCheckTime()) { Akonadi::CachePolicy cachePolicy = mDavCollectionRoot.cachePolicy(); cachePolicy.setIntervalCheckTime(newICT); mDavCollectionRoot.setCachePolicy(cachePolicy); } if (!Settings::self()->displayName().isEmpty()) { EntityDisplayAttribute *attribute = mDavCollectionRoot.attribute(Collection::AddIfMissing); attribute->setDisplayName(Settings::self()->displayName()); setName(Settings::self()->displayName()); } return true; } void DavGroupwareResource::retryAfterFailure(const QString &errorMessage) { Q_EMIT status(Broken, errorMessage); deferTask(); setTemporaryOffline(Settings::self()->refreshInterval() <= 0 ? 300 : Settings::self()->refreshInterval() * 60); } /*static*/ void DavGroupwareResource::setCollectionIcon(Akonadi::Collection &collection) { const QStringList mimeTypes = collection.contentMimeTypes(); if (mimeTypes.count() == 1) { QHash mapping; mapping.insert(KCalCore::Event::eventMimeType(), QStringLiteral("view-calendar")); mapping.insert(KCalCore::Todo::todoMimeType(), QStringLiteral("view-calendar-tasks")); mapping.insert(KCalCore::Journal::journalMimeType(), QStringLiteral("view-pim-journal")); mapping.insert(KContacts::Addressee::mimeType(), QStringLiteral("view-pim-contacts")); const QString mimetypeFirst = mimeTypes.first(); if (!mimetypeFirst.isEmpty()) { EntityDisplayAttribute *attribute = collection.attribute(Collection::AddIfMissing); attribute->setIconName(mimetypeFirst); } } } AKONADI_RESOURCE_MAIN(DavGroupwareResource) diff --git a/resources/imap/resourcetask.cpp b/resources/imap/resourcetask.cpp index 0486417f4..59f744d18 100644 --- a/resources/imap/resourcetask.cpp +++ b/resources/imap/resourcetask.cpp @@ -1,569 +1,569 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcetask.h" #include #include #include "imapresource_debug.h" #include "imapresource_trace.h" #include "collectionflagsattribute.h" #include #include "imapflags.h" #include "sessionpool.h" #include "resourcestateinterface.h" ResourceTask::ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent) : QObject(parent) , m_pool(nullptr) , m_sessionRequestId(0) , m_session(nullptr) , m_actionIfNoSession(action) , m_resource(resource) , mCancelled(false) { } ResourceTask::~ResourceTask() { if (m_pool) { if (m_sessionRequestId) { m_pool->cancelSessionRequest(m_sessionRequestId); } if (m_session) { m_pool->releaseSession(m_session); } } } void ResourceTask::start(SessionPool *pool) { qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); m_pool = pool; connect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested); m_sessionRequestId = m_pool->requestSession(); if (m_sessionRequestId <= 0) { m_sessionRequestId = 0; abortTask(QString()); // In this case we were likely disconnected, try to get the resource online m_resource->scheduleConnectionAttempt(); } } void ResourceTask::abortTask(const QString &errorString) { if (!mCancelled) { mCancelled = true; switch (m_actionIfNoSession) { case CancelIfNoSession: qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request."; m_resource->cancelTask(errorString.isEmpty() ? i18n("Unable to connect to the IMAP server.") : errorString); break; case DeferIfNoSession: qCDebug(IMAPRESOURCE_LOG) << "Defering this request."; m_resource->deferTask(); break; } } deleteLater(); } void ResourceTask::onSessionRequested(qint64 requestId, KIMAP::Session *session, int errorCode, const QString &errorString) { if (requestId != m_sessionRequestId) { // Not for us, ignore return; } disconnect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested); m_sessionRequestId = 0; if (errorCode != SessionPool::NoError) { abortTask(errorString); return; } m_session = session; connect(m_pool, &SessionPool::connectionLost, this, &ResourceTask::onConnectionLost); connect(m_pool, &SessionPool::disconnectDone, this, &ResourceTask::onPoolDisconnect); qCDebug(IMAPRESOURCE_TRACE) << "starting: " << metaObject()->className(); doStart(m_session); } void ResourceTask::onConnectionLost(KIMAP::Session *session) { if (session == m_session) { // Our session becomes invalid, so get rid of // the pointer, we don't need to release it once the // task is done m_session = nullptr; qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("Connection lost")); } } void ResourceTask::onPoolDisconnect() { // All the sessions in the pool we used changed, // so get rid of the pointer, we don't need to // release our session anymore m_pool = nullptr; qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("Connection lost")); } QString ResourceTask::userName() const { return m_resource->userName(); } QString ResourceTask::resourceName() const { return m_resource->resourceName(); } QStringList ResourceTask::serverCapabilities() const { return m_resource->serverCapabilities(); } QList ResourceTask::serverNamespaces() const { return m_resource->serverNamespaces(); } bool ResourceTask::isAutomaticExpungeEnabled() const { return m_resource->isAutomaticExpungeEnabled(); } bool ResourceTask::isSubscriptionEnabled() const { return m_resource->isSubscriptionEnabled(); } bool ResourceTask::isDisconnectedModeEnabled() const { return m_resource->isDisconnectedModeEnabled(); } int ResourceTask::intervalCheckTime() const { return m_resource->intervalCheckTime(); } static Akonadi::Collection detachCollection(const Akonadi::Collection &collection) { //HACK: Attributes are accessed via a const function, and the implicitly shared private pointer thus doesn't detach. //We force a detach to avoid surprises. (RetrieveItemsTask used to write back the collection changes, even though the task was canceled) //Once this is fixed this function can go away. Akonadi::Collection col = collection; col.setId(col.id()); return col; } Akonadi::Collection ResourceTask::collection() const { return detachCollection(m_resource->collection()); } Akonadi::Item ResourceTask::item() const { return m_resource->item(); } Akonadi::Item::List ResourceTask::items() const { return m_resource->items(); } Akonadi::Collection ResourceTask::parentCollection() const { return detachCollection(m_resource->parentCollection()); } Akonadi::Collection ResourceTask::sourceCollection() const { return detachCollection(m_resource->sourceCollection()); } Akonadi::Collection ResourceTask::targetCollection() const { return detachCollection(m_resource->targetCollection()); } QSet ResourceTask::parts() const { return m_resource->parts(); } QSet< QByteArray > ResourceTask::addedFlags() const { return m_resource->addedFlags(); } QSet< QByteArray > ResourceTask::removedFlags() const { return m_resource->removedFlags(); } QString ResourceTask::rootRemoteId() const { return m_resource->rootRemoteId(); } QString ResourceTask::mailBoxForCollection(const Akonadi::Collection &collection) const { return m_resource->mailBoxForCollection(collection); } void ResourceTask::setIdleCollection(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->setIdleCollection(collection); } } void ResourceTask::applyCollectionChanges(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->applyCollectionChanges(collection); } } void ResourceTask::itemRetrieved(const Akonadi::Item &item) { if (!mCancelled) { m_resource->itemRetrieved(item); emitPercent(100); } deleteLater(); } void ResourceTask::itemsRetrieved(const Akonadi::Item::List &items) { if (!mCancelled) { m_resource->itemsRetrieved(items); } } void ResourceTask::itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed) { if (!mCancelled) { m_resource->itemsRetrievedIncremental(changed, removed); } } void ResourceTask::itemsRetrievalDone() { if (!mCancelled) { m_resource->itemsRetrievalDone(); } deleteLater(); } void ResourceTask::setTotalItems(int totalItems) { if (!mCancelled) { m_resource->setTotalItems(totalItems); } } void ResourceTask::changeCommitted(const Akonadi::Item &item) { if (!mCancelled) { m_resource->itemChangeCommitted(item); } deleteLater(); } void ResourceTask::changesCommitted(const Akonadi::Item::List &items) { if (!mCancelled) { m_resource->itemsChangesCommitted(items); } deleteLater(); } void ResourceTask::searchFinished(const QVector &result, bool isRid) { if (!mCancelled) { m_resource->searchFinished(result, isRid); } deleteLater(); } void ResourceTask::collectionsRetrieved(const Akonadi::Collection::List &collections) { if (!mCancelled) { m_resource->collectionsRetrieved(collections); } deleteLater(); } void ResourceTask::collectionAttributesRetrieved(const Akonadi::Collection &col) { if (!mCancelled) { m_resource->collectionAttributesRetrieved(col); } deleteLater(); } void ResourceTask::changeCommitted(const Akonadi::Collection &collection) { if (!mCancelled) { m_resource->collectionChangeCommitted(collection); } deleteLater(); } void ResourceTask::changeCommitted(const Akonadi::Tag &tag) { if (!mCancelled) { m_resource->tagChangeCommitted(tag); } deleteLater(); } void ResourceTask::changeProcessed() { if (!mCancelled) { m_resource->changeProcessed(); } deleteLater(); } void ResourceTask::cancelTask(const QString &errorString) { qCDebug(IMAPRESOURCE_LOG) << "Cancel task: " << errorString; if (!mCancelled) { mCancelled = true; m_resource->cancelTask(errorString); } deleteLater(); } void ResourceTask::deferTask() { if (!mCancelled) { mCancelled = true; m_resource->deferTask(); } deleteLater(); } void ResourceTask::restartItemRetrieval(Akonadi::Collection::Id col) { if (!mCancelled) { m_resource->restartItemRetrieval(col); } deleteLater(); } void ResourceTask::taskDone() { m_resource->taskDone(); deleteLater(); } void ResourceTask::emitPercent(int percent) { m_resource->emitPercent(percent); } void ResourceTask::emitError(const QString &message) { m_resource->emitError(message); } void ResourceTask::emitWarning(const QString &message) { m_resource->emitWarning(message); } void ResourceTask::synchronizeCollectionTree() { m_resource->synchronizeCollectionTree(); } void ResourceTask::showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName) { m_resource->showInformationDialog(message, title, dontShowAgainName); } QList ResourceTask::fromAkonadiToSupportedImapFlags(const QList &flags, const Akonadi::Collection &collection) { QList imapFlags = fromAkonadiFlags(flags); const Akonadi::CollectionFlagsAttribute *flagAttr = collection.attribute(); // the server does not support arbitrary flags, so filter out those it can't handle if (flagAttr && !flagAttr->flags().isEmpty() && !flagAttr->flags().contains("\\*")) { for (QList< QByteArray >::iterator it = imapFlags.begin(); it != imapFlags.end();) { if (flagAttr->flags().contains(*it)) { ++it; } else { qCDebug(IMAPRESOURCE_LOG) << "Server does not support flag" << *it; it = imapFlags.erase(it); } } } return imapFlags; } QList ResourceTask::fromAkonadiFlags(const QList &flags) { QList newFlags; for (const QByteArray &oldFlag : flags) { if (oldFlag == Akonadi::MessageFlags::Seen) { newFlags.append(ImapFlags::Seen); } else if (oldFlag == Akonadi::MessageFlags::Deleted) { newFlags.append(ImapFlags::Deleted); } else if (oldFlag == Akonadi::MessageFlags::Answered || oldFlag == Akonadi::MessageFlags::Replied) { newFlags.append(ImapFlags::Answered); } else if (oldFlag == Akonadi::MessageFlags::Flagged) { newFlags.append(ImapFlags::Flagged); } else { newFlags.append(oldFlag); } } return newFlags; } QList ResourceTask::toAkonadiFlags(const QList &flags) { QList newFlags; for (const QByteArray &oldFlag : flags) { if (oldFlag == ImapFlags::Seen) { newFlags.append(Akonadi::MessageFlags::Seen); } else if (oldFlag == ImapFlags::Deleted) { newFlags.append(Akonadi::MessageFlags::Deleted); } else if (oldFlag == ImapFlags::Answered) { newFlags.append(Akonadi::MessageFlags::Answered); } else if (oldFlag == ImapFlags::Flagged) { newFlags.append(Akonadi::MessageFlags::Flagged); } else if (oldFlag.isEmpty()) { // filter out empty flags, to avoid isNull/isEmpty confusions higher up continue; } else { newFlags.append(oldFlag); } } return newFlags; } void ResourceTask::kill() { qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className(); abortTask(i18n("killed")); } const QChar ResourceTask::separatorCharacter() const { const QChar separator = m_resource->separatorCharacter(); if (!separator.isNull()) { return separator; } else { //If we request the separator before first folder listing, then try to guess //the separator: //If we create a toplevel folder, assume the separator to be '/'. This is not perfect, but detecting the right //IMAP separator is not straightforward for toplevel folders, and fixes bug 292418 and maybe other, where //subfolders end up with remote id's starting with "i" (the first letter of imap:// ...) QString remoteId; // We don't always have parent collection set (for example for CollectionChangeTask), // in such cases however we can use current collection's remoteId to get the separator const Akonadi::Collection parent = parentCollection(); if (parent.isValid()) { remoteId = parent.remoteId(); } else { remoteId = collection().remoteId(); } return ((remoteId != rootRemoteId()) && !remoteId.isEmpty()) ? remoteId.at(0) : QLatin1Char('/'); } } void ResourceTask::setSeparatorCharacter(QChar separator) { m_resource->setSeparatorCharacter(separator); } bool ResourceTask::serverSupportsAnnotations() const { return serverCapabilities().contains(QLatin1String("METADATA")) || serverCapabilities().contains(QLatin1String("ANNOTATEMORE")); } bool ResourceTask::serverSupportsCondstore() const { // Don't enable CONDSTORE for GMail (X-GM-EXT-1 is a GMail-specific capability) // because it breaks changes synchronization when using labels. return serverCapabilities().contains(QLatin1String("CONDSTORE")) && !serverCapabilities().contains(QLatin1String("X-GM-EXT-1")); } int ResourceTask::batchSize() const { return m_resource->batchSize(); } ResourceStateInterface::Ptr ResourceTask::resourceState() { return m_resource; } KIMAP::Acl::Rights ResourceTask::myRights(const Akonadi::Collection &col) { - Akonadi::ImapAclAttribute *aclAttribute = col.attribute(); + const Akonadi::ImapAclAttribute *aclAttribute = col.attribute(); if (aclAttribute) { //HACK, only return myrights if they are available if (aclAttribute->myRights() != KIMAP::Acl::None) { return aclAttribute->myRights(); } else { //This should be removed after 4.14, and myrights should be always used. return aclAttribute->rights().value(userName().toUtf8()); } } return KIMAP::Acl::None; } void ResourceTask::setItemMergingMode(Akonadi::ItemSync::MergeMode mode) { m_resource->setItemMergingMode(mode); } diff --git a/resources/kalarm/kalarm/CMakeLists.txt b/resources/kalarm/kalarm/CMakeLists.txt index fd8066dfc..772b9e3ce 100644 --- a/resources/kalarm/kalarm/CMakeLists.txt +++ b/resources/kalarm/kalarm/CMakeLists.txt @@ -1,81 +1,81 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../shared ${CMAKE_CURRENT_SOURCE_DIR}/../../ical/shared ) set(kalarmresource_common_SRCS) kconfig_add_kcfg_files(kalarmresource_common_SRCS settings.kcfgc) ############################ Resource ################################## add_definitions(-DSETTINGS_NAMESPACE=Akonadi_KAlarm_Resource) set(kalarmresource_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../../ical/shared/icalresourcebase.cpp kalarmresource.cpp ../shared/kalarmresourcecommon.cpp ../shared/alarmtyperadiowidget.cpp ${kalarmresource_common_SRCS} ) install(FILES kalarmresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents") kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmresource.kcfg org.kde.Akonadi.KAlarm.Settings) qt5_add_dbus_adaptor(kalarmresource_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml settings.h Akonadi_KAlarm_Resource::Settings icalsettingsadaptor ICalSettingsAdaptor) ecm_qt_declare_logging_category(kalarmresource_SRCS HEADER kalarmresource_debug.h IDENTIFIER KALARMRESOURCE_LOG CATEGORY_NAME org.kde.pim.kalarmresource) add_custom_target(kalarm_resource_xml ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml) # ui_alarmtyperadiowidget.h is used by both akonadi_kalarm_resource and kalarmconfig ki18n_wrap_ui(kalarmresource_shared_SRCS ../shared/alarmtyperadiowidget.ui) add_library(kalarmresource_shared_object OBJECT ${kalarmresource_shared_SRCS}) -# The asan build on FreeBSD doesn't like the empty moc object +# The asan build with Clang < 7 fails because an empty moc object is created set_target_properties(kalarmresource_shared_object PROPERTIES AUTOMOC OFF) add_executable(akonadi_kalarm_resource $ ${kalarmresource_SRCS} ) if( APPLE ) set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template) set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.KAlarm") set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi KAlarm Resource") endif () target_link_libraries(akonadi_kalarm_resource KF5::AlarmCalendar KF5::AkonadiCore KF5::CalendarCore KF5::KIOCore KF5::AkonadiAgentBase KF5::DBusAddons KF5::I18n akonadi-singlefileresource ) install(TARGETS akonadi_kalarm_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ############################## Config plugin ################################# set(kalarmconfig_SRCS $ kalarmconfig.cpp ../shared/alarmtyperadiowidget.cpp ${kalarmresource_common_SRCS} ) kcoreaddons_add_plugin(kalarmconfig SOURCES ${kalarmconfig_SRCS} JSON "kalarmconfig.json" INSTALL_NAMESPACE "akonadi/config" ) target_link_libraries(kalarmconfig KF5::AkonadiCore KF5::AlarmCalendar akonadi-singlefileresource ) diff --git a/resources/mbox/mboxresource.cpp b/resources/mbox/mboxresource.cpp index 0ce3ceaef..d93d1085b 100644 --- a/resources/mbox/mboxresource.cpp +++ b/resources/mbox/mboxresource.cpp @@ -1,383 +1,383 @@ /* Copyright (c) 2009 Bertjan Broeksem This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mboxresource.h" #include "mboxresource_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "compactpage.h" #include "deleteditemsattribute.h" #include "lockmethodpage.h" #include "settingsadaptor.h" using namespace Akonadi; static Collection::Id collectionId(const QString &remoteItemId) { // [CollectionId]::[RemoteCollectionId]::[Offset] const QStringList lst = remoteItemId.split(QStringLiteral("::")); Q_ASSERT(lst.size() == 3); return lst.first().toLongLong(); } static QString mboxFile(const QString &remoteItemId) { // [CollectionId]::[RemoteCollectionId]::[Offset] const QStringList lst = remoteItemId.split(QStringLiteral("::")); Q_ASSERT(lst.size() == 3); return lst.at(1); } static quint64 itemOffset(const QString &remoteItemId) { // [CollectionId]::[RemoteCollectionId]::[Offset] const QStringList lst = remoteItemId.split(QStringLiteral("::")); Q_ASSERT(lst.size() == 3); return lst.last().toULongLong(); } MboxResource::MboxResource(const QString &id) : SingleFileResource(id) , mMBox(nullptr) { new SettingsAdaptor(mSettings); KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"), mSettings, QDBusConnection::ExportAdaptors); QStringList mimeTypes; mimeTypes << QStringLiteral("message/rfc822"); setSupportedMimetypes(mimeTypes, QStringLiteral("message-rfc822")); // Register the list of deleted items as an attribute of the collection. AttributeFactory::registerAttribute(); } MboxResource::~MboxResource() { delete mMBox; } Collection MboxResource::rootCollection() const { // Maildir only has a single collection so we treat it as an inbox auto col = SingleFileResource::rootCollection(); col.attribute(Akonadi::Collection::AddIfMissing)->setCollectionType("inbox"); return col; } void MboxResource::retrieveItems(const Akonadi::Collection &col) { Q_UNUSED(col); if (!mMBox) { cancelTask(); return; } if (mMBox->fileName().isEmpty()) { Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured.")); return; } reloadFile(); KMBox::MBoxEntry::List entryList; if (col.hasAttribute()) { - DeletedItemsAttribute *attr = col.attribute(); + const DeletedItemsAttribute *attr = col.attribute(); entryList = mMBox->entries(attr->deletedItemEntries()); } else { // No deleted items (yet) entryList = mMBox->entries(); } mMBox->lock(); // Lock the file so that it doesn't get locked for every // readEntryHeaders() call. Item::List items; QString colId = QString::number(col.id()); QString colRid = col.remoteId(); double count = 1; const int entryListSize(entryList.size()); items.reserve(entryListSize); for (const KMBox::MBoxEntry &entry : qAsConst(entryList)) { // TODO: Use cache policy to see what actually has to been set as payload. // Currently most views need a minimal amount of information so the // Items get Envelopes as payload. KMime::Message *mail = new KMime::Message(); mail->setHead(KMime::CRLFtoLF(mMBox->readMessageHeaders(entry))); mail->parse(); Item item; item.setRemoteId(colId + QLatin1String("::") + colRid + QLatin1String("::") + QString::number(entry.messageOffset())); item.setMimeType(QStringLiteral("message/rfc822")); item.setSize(entry.messageSize()); item.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, item); Q_EMIT percent(count++ / entryListSize); items << item; } mMBox->unlock(); // Now we have the items, unlock itemsRetrieved(items); } bool MboxResource::retrieveItems(const Akonadi::Item::List &items, const QSet &parts) { Q_UNUSED(parts); if (!mMBox) { Q_EMIT error(i18n("MBox not loaded.")); return false; } if (mMBox->fileName().isEmpty()) { Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured.")); return false; } Akonadi::Item::List rv; rv.reserve(items.count()); for (const auto &item : items) { const QString rid = item.remoteId(); const quint64 offset = itemOffset(rid); KMime::Message *mail = mMBox->readMessage(KMBox::MBoxEntry(offset)); if (!mail) { Q_EMIT error(i18n("Failed to read message with uid '%1'.", rid)); return false; } Item i(item); i.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, i); rv.push_back(i); } itemsRetrieved(rv); return true; } void MboxResource::aboutToQuit() { if (!mSettings->readOnly()) { writeFile(); } mSettings->save(); } void MboxResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (!mMBox) { cancelTask(i18n("MBox not loaded.")); return; } if (mMBox->fileName().isEmpty()) { Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured.")); return; } // we can only deal with mail if (!item.hasPayload()) { cancelTask(i18n("Only email messages can be added to the MBox resource.")); return; } const KMBox::MBoxEntry entry = mMBox->appendMessage(item.payload()); if (!entry.isValid()) { cancelTask(i18n("Mail message not added to the MBox.")); return; } scheduleWrite(); const QString rid = QString::number(collection.id()) + QLatin1String("::") + collection.remoteId() + QLatin1String("::") + QString::number(entry.messageOffset()); Item i(item); i.setRemoteId(rid); changeCommitted(i); } void MboxResource::itemChanged(const Akonadi::Item &item, const QSet &parts) { if (parts.contains("PLD:RFC822")) { qCDebug(MBOXRESOURCE_LOG) << itemOffset(item.remoteId()); // Only complete messages can be stored in a MBox file. Because all messages // are stored in one single file we do an ItemDelete and an ItemCreate to // prevent that whole file must been rewritten. CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection(collectionId(item.remoteId())), CollectionFetchJob::Base); connect(fetchJob, &CollectionFetchJob::result, this, &MboxResource::onCollectionFetch); mCurrentItemDeletions.insert(fetchJob, item); fetchJob->start(); return; } changeProcessed(); } void MboxResource::itemRemoved(const Akonadi::Item &item) { CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection(collectionId(item.remoteId())), CollectionFetchJob::Base); if (!fetchJob->exec()) { cancelTask(i18n("Could not fetch the collection: %1", fetchJob->errorString())); return; } Q_ASSERT(fetchJob->collections().size() == 1); Collection mboxCollection = fetchJob->collections().at(0); DeletedItemsAttribute *attr = mboxCollection.attribute(Akonadi::Collection::AddIfMissing); if (mSettings->compactFrequency() == Settings::per_x_messages && mSettings->messageCount() == static_cast(attr->offsetCount() + 1)) { qCDebug(MBOXRESOURCE_LOG) << "Compacting mbox file"; mMBox->purge(attr->deletedItemEntries() << KMBox::MBoxEntry(itemOffset(item.remoteId()))); scheduleWrite(); mboxCollection.removeAttribute(); } else { attr->addDeletedItemOffset(itemOffset(item.remoteId())); } CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection); if (!modifyJob->exec()) { cancelTask(modifyJob->errorString()); return; } changeProcessed(); } void MboxResource::handleHashChange() { Q_EMIT warning(i18n("The MBox file was changed by another program. " "A copy of the new file was made and pending changes " "are appended to that copy. To prevent this from happening " "use locking and make sure that all programs accessing the mbox " "use the same locking method.")); } bool MboxResource::readFromFile(const QString &fileName) { delete mMBox; mMBox = new KMBox::MBox(); switch (mSettings->lockfileMethod()) { case Settings::procmail: mMBox->setLockType(KMBox::MBox::ProcmailLockfile); mMBox->setLockFile(mSettings->lockfile()); break; case Settings::mutt_dotlock: mMBox->setLockType(KMBox::MBox::MuttDotlock); break; case Settings::mutt_dotlock_privileged: mMBox->setLockType(KMBox::MBox::MuttDotlockPrivileged); break; } return mMBox->load(QUrl::fromLocalFile(fileName).toLocalFile()); } bool MboxResource::writeToFile(const QString &fileName) { if (!mMBox->save(fileName)) { Q_EMIT error(i18n("Failed to save mbox file to %1", fileName)); return false; } // HACK: When writeToFile is called with another file than with which the mbox // was loaded we assume that a backup is made as result of the fileChanged slot // in SingleFileResourceBase. The problem is that SingleFileResource assumes that // the implementing resources can save/retrieve the data from before the file // change we have a problem at this point in the mbox resource. Therefore we // copy the original file and append pending changes to it but also add an extra // '\n' to make sure that the hashes differ and the user gets notified. Normally // if this happens the user should make use of locking in all applications that // use the mbox file. if (fileName != mMBox->fileName()) { QFile file(fileName); file.open(QIODevice::WriteOnly); file.seek(file.size()); file.write("\n"); } return true; } /// Private slots void MboxResource::onCollectionFetch(KJob *job) { Q_ASSERT(mCurrentItemDeletions.contains(job)); const Item item = mCurrentItemDeletions.take(job); if (job->error()) { cancelTask(job->errorString()); return; } CollectionFetchJob *fetchJob = dynamic_cast(job); Q_ASSERT(fetchJob); Q_ASSERT(fetchJob->collections().size() == 1); Collection mboxCollection = fetchJob->collections().at(0); DeletedItemsAttribute *attr = mboxCollection.attribute(Akonadi::Collection::AddIfMissing); attr->addDeletedItemOffset(itemOffset(item.remoteId())); CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection); mCurrentItemDeletions.insert(modifyJob, item); connect(modifyJob, &CollectionModifyJob::result, this, &MboxResource::onCollectionModify); modifyJob->start(); } void MboxResource::onCollectionModify(KJob *job) { Q_ASSERT(mCurrentItemDeletions.contains(job)); const Item item = mCurrentItemDeletions.take(job); if (job->error()) { // Failed to store the offset of a deleted item in the DeletedItemsAttribute // of the collection. In this case we shouldn't try to store the modified // item. cancelTask(i18n("Failed to update the changed item because the old item " "could not be deleted Reason: %1", job->errorString())); return; } Collection c(collectionId(item.remoteId())); c.setRemoteId(mboxFile(item.remoteId())); itemAdded(item, c); } AKONADI_RESOURCE_MAIN(MboxResource) diff --git a/resources/shared/singlefileresource/singlefileresource.h b/resources/shared/singlefileresource/singlefileresource.h index 3658bedb0..5c8ca438f 100644 --- a/resources/shared/singlefileresource/singlefileresource.h +++ b/resources/shared/singlefileresource/singlefileresource.h @@ -1,334 +1,334 @@ /* Copyright (c) 2008 Bertjan Broeksema Copyright (c) 2008 Volker Krause Copyright (c) 2010 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_SINGLEFILERESOURCE_H #define AKONADI_SINGLEFILERESOURCE_H #include "akonadi-singlefileresource_export.h" #include "singlefileresourcebase.h" #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QEventLoopLocker *) namespace Akonadi { /** * Base class for single file based resources. */ template class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileResource : public SingleFileResourceBase { public: SingleFileResource(const QString &id) : SingleFileResourceBase(id) , mSettings(new Settings(config())) { // The resource needs network when the path refers to a non local file. setNeedsNetwork(!QUrl::fromUserInput(mSettings->path()).isLocalFile()); } ~SingleFileResource() { delete mSettings; } /** * Read changes from the backend file. */ void readFile(bool taskContext = false) override { if (KDirWatch::self()->contains(mCurrentUrl.toLocalFile())) { KDirWatch::self()->removeFile(mCurrentUrl.toLocalFile()); } if (mSettings->path().isEmpty()) { const QString message = i18n("No file selected."); qWarning() << message; emit status(NotConfigured, i18n("The resource not configured yet")); if (taskContext) { cancelTask(); } return; } mCurrentUrl = QUrl::fromUserInput(mSettings->path()); // the string contains the scheme if remote, doesn't if local path if (mCurrentHash.isEmpty()) { // First call to readFile() lets see if there is a hash stored in a // cache file. If both are the same than there is no need to load the // file and synchronize the resource. mCurrentHash = loadHash(); } if (mCurrentUrl.isLocalFile()) { if (mSettings->displayName().isEmpty() && (name().isEmpty() || name() == identifier()) && !mCurrentUrl.isEmpty()) { setName(mCurrentUrl.fileName()); } // check if the file does not exist yet, if so, create it if (!QFile::exists(mCurrentUrl.toLocalFile())) { QFile f(mCurrentUrl.toLocalFile()); // first create try to create the directory the file should be located in QDir dir = QFileInfo(f).dir(); if (!dir.exists()) { dir.mkpath(dir.path()); } if (f.open(QIODevice::WriteOnly) && f.resize(0)) { emit status(Idle, i18nc("@info:status", "Ready")); } else { const QString message = i18n("Could not create file '%1'.", mCurrentUrl.toDisplayString()); qWarning() << message; emit status(Broken, message); mCurrentUrl.clear(); if (taskContext) { cancelTask(); } return; } } // Cache, because readLocalFile will clear mCurrentUrl on failure. const QString localFileName = mCurrentUrl.toLocalFile(); if (!readLocalFile(localFileName)) { const QString message = i18n("Could not read file '%1'", localFileName); qWarning() << message; emit status(Broken, message); if (taskContext) { cancelTask(); } return; } if (mSettings->monitorFile()) { KDirWatch::self()->addFile(localFileName); } emit status(Idle, i18nc("@info:status", "Ready")); } else { // !mCurrentUrl.isLocalFile() if (mDownloadJob) { const QString message = i18n("Another download is still in progress."); qWarning() << message; emit error(message); if (taskContext) { cancelTask(); } return; } if (mUploadJob) { const QString message = i18n("Another file upload is still in progress."); qWarning() << message; emit error(message); if (taskContext) { cancelTask(); } return; } auto ref = new QEventLoopLocker(); // NOTE: Test what happens with remotefile -> save, close before save is finished. mDownloadJob = KIO::file_copy(mCurrentUrl, QUrl::fromLocalFile(cacheFile()), -1, KIO::Overwrite |KIO::DefaultFlags | KIO::HideProgressInfo); mDownloadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref)); connect(mDownloadJob, &KJob::result, this, &SingleFileResource::slotDownloadJobResult); connect(mDownloadJob, SIGNAL(percent(KJob*,ulong)), SLOT(handleProgress(KJob*,ulong))); emit status(Running, i18n("Downloading remote file.")); } const QString display = mSettings->displayName(); if (!display.isEmpty()) { setName(display); } } void writeFile(const QVariant &task_context) override { writeFile(task_context.canConvert() && task_context.toBool()); } /** * Write changes to the backend file. */ void writeFile(bool taskContext = false) override { if (mSettings->readOnly()) { const QString message = i18n("Trying to write to a read-only file: '%1'.", mSettings->path()); qWarning() << message; emit error(message); if (taskContext) { cancelTask(); } return; } // We don't use the Settings::self()->path() here as that might have changed // and in that case it would probably cause data lose. if (mCurrentUrl.isEmpty()) { const QString message = i18n("No file specified."); qWarning() << message; emit status(Broken, message); if (taskContext) { cancelTask(); } return; } if (mCurrentUrl.isLocalFile()) { KDirWatch::self()->stopScan(); const bool writeResult = writeToFile(mCurrentUrl.toLocalFile()); // Update the hash so we can detect at fileChanged() if the file actually // did change. mCurrentHash = calculateHash(mCurrentUrl.toLocalFile()); saveHash(mCurrentHash); KDirWatch::self()->startScan(); if (!writeResult) { qWarning() << "Error writing to file..."; if (taskContext) { cancelTask(); } return; } emit status(Idle, i18nc("@info:status", "Ready")); } else { // Check if there is a download or an upload in progress. if (mDownloadJob) { const QString message = i18n("A download is still in progress."); qWarning() << message; emit error(message); if (taskContext) { cancelTask(); } return; } if (mUploadJob) { const QString message = i18n("Another file upload is still in progress."); qWarning() << message; emit error(message); if (taskContext) { cancelTask(); } return; } // Write te items to the locally cached file. if (!writeToFile(cacheFile())) { qWarning() << "Error writing to file"; if (taskContext) { cancelTask(); } return; } // Update the hash so we can detect at fileChanged() if the file actually // did change. mCurrentHash = calculateHash(cacheFile()); saveHash(mCurrentHash); auto ref = new QEventLoopLocker(); // Start a job to upload the locally cached file to the remote location. mUploadJob = KIO::file_copy(QUrl::fromLocalFile(cacheFile()), mCurrentUrl, -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo); mUploadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref)); connect(mUploadJob, &KJob::result, this, &SingleFileResource::slotUploadJobResult); connect(mUploadJob, SIGNAL(percent(KJob*,ulong)), SLOT(handleProgress(KJob*,ulong))); emit status(Running, i18n("Uploading cached file to remote location.")); } if (taskContext) { taskDone(); } } void collectionChanged(const Collection &collection) override { QString newName; if (collection.hasAttribute()) { - EntityDisplayAttribute *attr = collection.attribute(); + const EntityDisplayAttribute *attr = collection.attribute(); newName = attr->displayName(); } const QString oldName = mSettings->displayName(); if (newName != oldName) { mSettings->setDisplayName(newName); mSettings->save(); } SingleFileResourceBase::collectionChanged(collection); } Collection rootCollection() const override { Collection c; c.setParentCollection(Collection::root()); c.setRemoteId(mSettings->path()); const QString display = mSettings->displayName(); c.setName(display.isEmpty() ? identifier() : display); c.setContentMimeTypes(mSupportedMimetypes); if (readOnly()) { c.setRights(Collection::CanChangeCollection); } else { Collection::Rights rights; rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanChangeCollection; c.setRights(rights); } EntityDisplayAttribute *attr = c.attribute(Collection::AddIfMissing); attr->setDisplayName(name()); attr->setIconName(mCollectionIcon); return c; } protected: void retrieveCollections() override { Collection::List list; list << rootCollection(); collectionsRetrieved(list); } bool readOnly() const override { return mSettings->readOnly(); } protected: Settings *mSettings = nullptr; }; } #endif diff --git a/resources/shared/singlefileresource/singlefileresourcebase.cpp b/resources/shared/singlefileresource/singlefileresourcebase.cpp index 164f65970..61565310e 100644 --- a/resources/shared/singlefileresource/singlefileresourcebase.cpp +++ b/resources/shared/singlefileresource/singlefileresourcebase.cpp @@ -1,310 +1,310 @@ /* Copyright (c) 2008 Bertjan Broeksema Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "singlefileresourcebase.h" #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QEventLoopLocker *) using namespace Akonadi; SingleFileResourceBase::SingleFileResourceBase(const QString &id) : ResourceBase(id) , mDownloadJob(nullptr) , mUploadJob(nullptr) { connect(this, &SingleFileResourceBase::reloadConfiguration, this, [this]() { applyConfigurationChanges(); reloadFile(); synchronizeCollectionTree(); }); QTimer::singleShot(0, this, [this]() { readFile(); }); changeRecorder()->itemFetchScope().fetchFullPayload(); changeRecorder()->fetchCollection(true); connect(changeRecorder(), &ChangeRecorder::changesAdded, this, &SingleFileResourceBase::scheduleWrite); connect(KDirWatch::self(), &KDirWatch::dirty, this, &SingleFileResourceBase::fileChanged); connect(KDirWatch::self(), &KDirWatch::created, this, &SingleFileResourceBase::fileChanged); } void SingleFileResourceBase::applyConfigurationChanges() { } KSharedConfig::Ptr SingleFileResourceBase::runtimeConfig() const { return KSharedConfig::openConfig(name() + QLatin1String("rc"), KConfig::SimpleConfig, QStandardPaths::CacheLocation); } bool SingleFileResourceBase::readLocalFile(const QString &fileName) { const QByteArray newHash = calculateHash(fileName); if (mCurrentHash != newHash) { if (!mCurrentHash.isEmpty()) { // There was a hash stored in the config file or a chached one from // a previous read and it is different from the hash we just read. handleHashChange(); } if (!readFromFile(fileName)) { mCurrentHash.clear(); mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file return false; } if (mCurrentHash.isEmpty()) { // This is the very first time we read the file so make sure to store // the hash as writeFile() might not be called at all (e.g in case of // read only resources). saveHash(newHash); } // Only synchronize when the contents of the file have changed wrt to // the last time this file was read. Before we synchronize first // clearCache is called to make sure that the cached items get the // actual values as present in the file. invalidateCache(rootCollection()); synchronize(); } else { // The hash didn't change, notify implementing resources about the // actual file name that should be used when reading the file is // necessary. setLocalFileName(fileName); } mCurrentHash = newHash; return true; } void SingleFileResourceBase::setLocalFileName(const QString &fileName) { // Default implementation. if (!readFromFile(fileName)) { mCurrentHash.clear(); mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file return; } } QString SingleFileResourceBase::cacheFile() const { const QString currentDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(currentDir); return currentDir + QLatin1String("/") + identifier(); } QByteArray SingleFileResourceBase::calculateHash(const QString &fileName) const { QFile file(fileName); if (!file.exists()) { return QByteArray(); } if (!file.open(QIODevice::ReadOnly)) { return QByteArray(); } QCryptographicHash hash(QCryptographicHash::Md5); qint64 blockSize = 512 * 1024; // Read blocks of 512K while (!file.atEnd()) { hash.addData(file.read(blockSize)); } file.close(); return hash.result(); } void SingleFileResourceBase::handleHashChange() { // Default implementation does nothing. qDebug() << "The hash has changed."; } QByteArray SingleFileResourceBase::loadHash() const { KConfigGroup generalGroup(runtimeConfig(), "General"); return QByteArray::fromHex(generalGroup.readEntry("hash", QByteArray())); } void SingleFileResourceBase::saveHash(const QByteArray &hash) const { KSharedConfig::Ptr config = runtimeConfig(); KConfigGroup generalGroup(config, "General"); generalGroup.writeEntry("hash", hash.toHex()); config->sync(); } void SingleFileResourceBase::setSupportedMimetypes(const QStringList &mimeTypes, const QString &icon) { mSupportedMimetypes = mimeTypes; mCollectionIcon = icon; } void SingleFileResourceBase::collectionChanged(const Akonadi::Collection &collection) { const QString newName = collection.displayName(); if (collection.hasAttribute()) { - EntityDisplayAttribute *attr = collection.attribute(); + const EntityDisplayAttribute *attr = collection.attribute(); if (!attr->iconName().isEmpty()) { mCollectionIcon = attr->iconName(); } } if (newName != name()) { setName(newName); } changeCommitted(collection); } void SingleFileResourceBase::reloadFile() { // Update the network setting. setNeedsNetwork(!mCurrentUrl.isEmpty() && !mCurrentUrl.isLocalFile()); // if we have something loaded already, make sure we write that back in case // the settings changed if (!mCurrentUrl.isEmpty() && !readOnly()) { writeFile(); } readFile(); // name or rights could have changed synchronizeCollectionTree(); } void SingleFileResourceBase::handleProgress(KJob *, unsigned long pct) { Q_EMIT percent(pct); } void SingleFileResourceBase::fileChanged(const QString &fileName) { if (fileName != mCurrentUrl.toLocalFile()) { return; } const QByteArray newHash = calculateHash(fileName); // There is only a need to synchronize when the file was changed by another // process. At this point we're sure that it is the file that the resource // was configured for because of the check at the beginning of this function. if (newHash == mCurrentHash) { return; } if (!mCurrentUrl.isEmpty()) { QString lostFoundFileName; const QUrl prevUrl = mCurrentUrl; int i = 0; do { lostFoundFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + identifier() + QDir::separator() + prevUrl.fileName() + QLatin1Char('-') + QString::number(++i); } while (QFileInfo::exists(lostFoundFileName)); // create the directory if it doesn't exist yet QDir dir = QFileInfo(lostFoundFileName).dir(); if (!dir.exists()) { dir.mkpath(dir.path()); } mCurrentUrl = QUrl::fromLocalFile(lostFoundFileName); writeFile(); mCurrentUrl = prevUrl; const QString message = i18n("The file '%1' was changed on disk. " "As a precaution, a backup of its previous contents has been created at '%2'.", prevUrl.toDisplayString(), QUrl::fromLocalFile(lostFoundFileName).toDisplayString()); Q_EMIT warning(message); } readFile(); // Notify resources, so that information bound to the file like indexes etc. // can be updated. handleHashChange(); invalidateCache(rootCollection()); synchronize(); } void SingleFileResourceBase::scheduleWrite() { scheduleCustomTask(this, "writeFile", QVariant(true), ResourceBase::AfterChangeReplay); } void SingleFileResourceBase::slotDownloadJobResult(KJob *job) { if (job->error() && job->error() != KIO::ERR_DOES_NOT_EXIST) { const QString message = i18n("Could not load file '%1'.", mCurrentUrl.toDisplayString()); qWarning() << message; Q_EMIT status(Broken, message); } else { readLocalFile(QUrl::fromLocalFile(cacheFile()).toLocalFile()); } mDownloadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) { delete ref; } Q_EMIT status(Idle, i18nc("@info:status", "Ready")); } void SingleFileResourceBase::slotUploadJobResult(KJob *job) { if (job->error()) { const QString message = i18n("Could not save file '%1'.", mCurrentUrl.toDisplayString()); qWarning() << message; Q_EMIT status(Broken, message); } mUploadJob = nullptr; auto ref = job->property("QEventLoopLocker").value(); if (ref) { delete ref; } Q_EMIT status(Idle, i18nc("@info:status", "Ready")); }