File indexing completed on 2024-06-02 05:20:41

0001 /*
0002     SPDX-FileCopyrightText: 2009 Tobias Koenig <tokoe@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "contactsresource.h"
0008 
0009 #include "contactsresourcesettingsadaptor.h"
0010 #include "settings.h"
0011 
0012 #include <QDir>
0013 #include <QFile>
0014 
0015 #include "contacts_resources_debug.h"
0016 #include <Akonadi/ChangeRecorder>
0017 #include <Akonadi/CollectionFetchScope>
0018 #include <Akonadi/EntityDisplayAttribute>
0019 #include <Akonadi/ItemFetchScope>
0020 #include <QDBusConnection>
0021 
0022 #include <KLocalizedString>
0023 
0024 using namespace Akonadi;
0025 
0026 ContactsResource::ContactsResource(const QString &id)
0027     : ResourceBase(id)
0028 {
0029     // setup the resource
0030     ContactsResourceSettings::instance(KSharedConfig::openConfig());
0031     new ContactsResourceSettingsAdaptor(ContactsResourceSettings::self());
0032     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), ContactsResourceSettings::self(), QDBusConnection::ExportAdaptors);
0033 
0034     changeRecorder()->fetchCollection(true);
0035     changeRecorder()->itemFetchScope().fetchFullPayload(true);
0036     changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
0037     changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
0038 
0039     setHierarchicalRemoteIdentifiersEnabled(true);
0040 
0041     mSupportedMimeTypes << KContacts::Addressee::mimeType() << KContacts::ContactGroup::mimeType() << Collection::mimeType();
0042 
0043     if (name().startsWith(QLatin1StringView("akonadi_contacts_resource"))) {
0044         setName(i18n("Personal Contacts"));
0045     }
0046 
0047     // Make sure we have a valid directory (XDG dirs want this very much).
0048     initializeDirectory(ContactsResourceSettings::self()->path());
0049 
0050     if (ContactsResourceSettings::self()->isConfigured()) {
0051         synchronize();
0052     }
0053     connect(this, &ContactsResource::reloadConfiguration, this, &ContactsResource::slotReloadConfig);
0054 }
0055 
0056 ContactsResource::~ContactsResource()
0057 {
0058     delete ContactsResourceSettings::self();
0059 }
0060 
0061 void ContactsResource::aboutToQuit()
0062 {
0063 }
0064 
0065 void ContactsResource::slotReloadConfig()
0066 {
0067     ContactsResourceSettings::self()->setIsConfigured(true);
0068     ContactsResourceSettings::self()->save();
0069 
0070     clearCache();
0071     initializeDirectory(baseDirectoryPath());
0072 
0073     synchronize();
0074 }
0075 
0076 Collection::List ContactsResource::createCollectionsForDirectory(const QDir &parentDirectory, const Collection &parentCollection) const
0077 {
0078     Collection::List collections;
0079 
0080     QDir dir(parentDirectory);
0081     dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable);
0082     const QFileInfoList entries = dir.entryInfoList();
0083     collections.reserve(entries.count() * 2);
0084 
0085     for (const QFileInfo &entry : entries) {
0086         const QDir subdir(entry.absoluteFilePath());
0087 
0088         Collection collection;
0089         collection.setParentCollection(parentCollection);
0090         collection.setRemoteId(entry.fileName());
0091         collection.setName(entry.fileName());
0092         collection.setContentMimeTypes(mSupportedMimeTypes);
0093         collection.setRights(supportedRights(false));
0094 
0095         collections << collection;
0096         collections << createCollectionsForDirectory(subdir, collection);
0097     }
0098 
0099     return collections;
0100 }
0101 
0102 void ContactsResource::retrieveCollections()
0103 {
0104     // create the resource collection
0105     Collection resourceCollection;
0106     resourceCollection.setParentCollection(Collection::root());
0107     resourceCollection.setRemoteId(baseDirectoryPath());
0108     resourceCollection.setName(name());
0109     resourceCollection.setContentMimeTypes(mSupportedMimeTypes);
0110     resourceCollection.setRights(supportedRights(true));
0111 
0112     const QDir baseDir(baseDirectoryPath());
0113 
0114     Collection::List collections = createCollectionsForDirectory(baseDir, resourceCollection);
0115     collections.append(resourceCollection);
0116 
0117     collectionsRetrieved(collections);
0118 }
0119 
0120 void ContactsResource::retrieveItems(const Akonadi::Collection &collection)
0121 {
0122     QDir directory(directoryForCollection(collection));
0123     if (!directory.exists()) {
0124         cancelTask(i18n("Directory '%1' does not exists", collection.remoteId()));
0125         return;
0126     }
0127 
0128     directory.setFilter(QDir::Files | QDir::Readable);
0129 
0130     Item::List items;
0131 
0132     const QFileInfoList entries = directory.entryInfoList();
0133 
0134     for (const QFileInfo &entry : entries) {
0135         const QString entryFileName = entry.fileName();
0136         if (entryFileName == QLatin1StringView("WARNING_README.txt")) {
0137             continue;
0138         }
0139 
0140         Item item;
0141         item.setRemoteId(entryFileName);
0142 
0143         if (entryFileName.endsWith(QLatin1StringView(".vcf"))) {
0144             item.setMimeType(KContacts::Addressee::mimeType());
0145         } else if (entryFileName.endsWith(QLatin1StringView(".ctg"))) {
0146             item.setMimeType(KContacts::ContactGroup::mimeType());
0147         } else {
0148             cancelTask(i18n("Found file of unknown format: '%1'", entry.absoluteFilePath()));
0149             return;
0150         }
0151 
0152         items.append(item);
0153     }
0154 
0155     itemsRetrieved(items);
0156 }
0157 
0158 bool ContactsResource::retrieveItems(const Akonadi::Item::List &items, const QSet<QByteArray> &parts)
0159 {
0160     Q_UNUSED(parts);
0161 
0162     Akonadi::Item::List resultItems;
0163     resultItems.reserve(items.size());
0164 
0165     for (const Akonadi::Item &item : items) {
0166         Item newItem(item);
0167         if (!doRetrieveItem(newItem)) {
0168             return false;
0169         }
0170         resultItems.append(newItem);
0171     }
0172 
0173     itemsRetrieved(resultItems);
0174 
0175     return true;
0176 }
0177 
0178 bool ContactsResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
0179 {
0180     Item newItem(item);
0181 
0182     if (!doRetrieveItem(newItem)) {
0183         return false;
0184     }
0185 
0186     itemRetrieved(newItem);
0187 
0188     return true;
0189 }
0190 
0191 bool ContactsResource::doRetrieveItem(Akonadi::Item &item)
0192 {
0193     const QString filePath = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId();
0194 
0195     QFile file(filePath);
0196     if (!file.open(QIODevice::ReadOnly)) {
0197         Q_EMIT error(i18n("Unable to open file '%1'", filePath));
0198         return false;
0199     }
0200 
0201     if (filePath.endsWith(QLatin1StringView(".vcf"))) {
0202         KContacts::VCardConverter converter;
0203 
0204         const QByteArray content = file.readAll();
0205         const KContacts::Addressee contact = converter.parseVCard(content);
0206         if (contact.isEmpty()) {
0207             Q_EMIT error(i18n("Found invalid contact in file '%1'", filePath));
0208             return false;
0209         }
0210 
0211         item.setPayload<KContacts::Addressee>(contact);
0212     } else if (filePath.endsWith(QLatin1StringView(".ctg"))) {
0213         KContacts::ContactGroup group;
0214         QString errorMessage;
0215 
0216         if (!KContacts::ContactGroupTool::convertFromXml(&file, group, &errorMessage)) {
0217             Q_EMIT error(i18n("Found invalid contact group in file '%1': %2", filePath, errorMessage));
0218             return false;
0219         }
0220 
0221         item.setPayload<KContacts::ContactGroup>(group);
0222     } else {
0223         Q_EMIT error(i18n("Found file of unknown format: '%1'", filePath));
0224         return false;
0225     }
0226 
0227     return true;
0228 }
0229 
0230 void ContactsResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
0231 {
0232     if (ContactsResourceSettings::self()->readOnly()) {
0233         cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
0234         return;
0235     }
0236 
0237     const QString directoryPath = directoryForCollection(collection);
0238 
0239     Item newItem(item);
0240 
0241     if (item.hasPayload<KContacts::Addressee>()) {
0242         const auto contact = item.payload<KContacts::Addressee>();
0243 
0244         const QString fileName = directoryPath + QLatin1Char('/') + contact.uid() + QStringLiteral(".vcf");
0245 
0246         KContacts::VCardConverter converter;
0247         const QByteArray content = converter.createVCard(contact);
0248 
0249         QFile file(fileName);
0250         if (!file.open(QIODevice::WriteOnly)) {
0251             cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
0252             return;
0253         }
0254 
0255         file.write(content);
0256         file.close();
0257 
0258         newItem.setRemoteId(contact.uid() + QStringLiteral(".vcf"));
0259     } else if (item.hasPayload<KContacts::ContactGroup>()) {
0260         const auto group = item.payload<KContacts::ContactGroup>();
0261 
0262         const QString fileName = directoryPath + QLatin1Char('/') + group.id() + QStringLiteral(".ctg");
0263 
0264         QFile file(fileName);
0265         if (!file.open(QIODevice::WriteOnly)) {
0266             cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
0267             return;
0268         }
0269 
0270         KContacts::ContactGroupTool::convertToXml(group, &file);
0271 
0272         file.close();
0273 
0274         newItem.setRemoteId(group.id() + QLatin1StringView(".ctg"));
0275     } else {
0276         qCWarning(CONTACTSRESOURCES_LOG) << "got item without (usable) payload, ignoring it";
0277     }
0278 
0279     changeCommitted(newItem);
0280 }
0281 
0282 void ContactsResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
0283 {
0284     if (ContactsResourceSettings::self()->readOnly()) {
0285         cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId()));
0286         return;
0287     }
0288 
0289     Item newItem(item);
0290 
0291     const QString fileName = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId();
0292 
0293     if (item.hasPayload<KContacts::Addressee>()) {
0294         const auto contact = item.payload<KContacts::Addressee>();
0295 
0296         KContacts::VCardConverter converter;
0297         const QByteArray content = converter.createVCard(contact);
0298 
0299         QFile file(fileName);
0300         if (!file.open(QIODevice::WriteOnly)) {
0301             cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
0302             return;
0303         }
0304         file.write(content);
0305         file.close();
0306 
0307         newItem.setRemoteId(item.remoteId());
0308     } else if (item.hasPayload<KContacts::ContactGroup>()) {
0309         const auto group = item.payload<KContacts::ContactGroup>();
0310 
0311         QFile file(fileName);
0312         if (!file.open(QIODevice::WriteOnly)) {
0313             cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
0314             return;
0315         }
0316 
0317         KContacts::ContactGroupTool::convertToXml(group, &file);
0318 
0319         file.close();
0320 
0321         newItem.setRemoteId(item.remoteId());
0322     } else {
0323         cancelTask(i18n("Received item with unknown payload %1", item.mimeType()));
0324         return;
0325     }
0326 
0327     changeCommitted(newItem);
0328 }
0329 
0330 void ContactsResource::itemRemoved(const Akonadi::Item &item)
0331 {
0332     if (ContactsResourceSettings::self()->readOnly()) {
0333         cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId()));
0334         return;
0335     }
0336 
0337     // If the parent collection has no valid remote id, the parent
0338     // collection will be removed in a second, so stop here and remove
0339     // all items in collectionRemoved().
0340     if (item.parentCollection().remoteId().isEmpty()) {
0341         changeProcessed();
0342         return;
0343     }
0344 
0345     const QString fileName = directoryForCollection(item.parentCollection()) + QLatin1Char('/') + item.remoteId();
0346 
0347     if (!QFile::remove(fileName)) {
0348         cancelTask(i18n("Unable to remove file '%1'", fileName));
0349         return;
0350     }
0351 
0352     changeProcessed();
0353 }
0354 
0355 void ContactsResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
0356 {
0357     if (ContactsResourceSettings::self()->readOnly()) {
0358         cancelTask(i18n("Trying to write to a read-only directory: '%1'", parent.remoteId()));
0359         return;
0360     }
0361 
0362     const QString dirName = directoryForCollection(parent) + QLatin1Char('/') + collection.name();
0363 
0364     if (!QDir::root().mkpath(dirName)) {
0365         cancelTask(i18n("Unable to create folder '%1'.", dirName));
0366         return;
0367     }
0368 
0369     initializeDirectory(dirName);
0370 
0371     Collection newCollection(collection);
0372     newCollection.setRemoteId(collection.name());
0373     changeCommitted(newCollection);
0374 }
0375 
0376 void ContactsResource::collectionChanged(const Akonadi::Collection &collection)
0377 {
0378     if (ContactsResourceSettings::self()->readOnly()) {
0379         cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
0380         return;
0381     }
0382 
0383     if (collection.parentCollection() == Collection::root()) {
0384         if (collection.name() != name()) {
0385             setName(collection.name());
0386         }
0387         changeProcessed();
0388         return;
0389     }
0390 
0391     if (collection.remoteId() == collection.name()) {
0392         changeProcessed();
0393         return;
0394     }
0395 
0396     const QString dirName = directoryForCollection(collection);
0397 
0398     QFileInfo oldDirectory(dirName);
0399     if (!QDir::root().rename(dirName, oldDirectory.absolutePath() + QLatin1Char('/') + collection.name())) {
0400         cancelTask(i18n("Unable to rename folder '%1'.", collection.name()));
0401         return;
0402     }
0403 
0404     Collection newCollection(collection);
0405     newCollection.setRemoteId(collection.name());
0406     changeCommitted(newCollection);
0407 }
0408 
0409 void ContactsResource::collectionRemoved(const Akonadi::Collection &collection)
0410 {
0411     if (ContactsResourceSettings::self()->readOnly()) {
0412         cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
0413         return;
0414     }
0415 
0416     const QString collectionDir = directoryForCollection(collection);
0417     if (collectionDir.isEmpty()) {
0418         cancelTask(i18n("Unknown folder to delete."));
0419         return;
0420     }
0421     if (!QDir(collectionDir).removeRecursively()) {
0422         cancelTask(i18n("Unable to delete folder '%1'.", collection.name()));
0423         return;
0424     }
0425 
0426     changeProcessed();
0427 }
0428 
0429 void ContactsResource::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination)
0430 {
0431     const QString sourceFileName = directoryForCollection(collectionSource) + QLatin1Char('/') + item.remoteId();
0432     const QString targetFileName = directoryForCollection(collectionDestination) + QLatin1Char('/') + item.remoteId();
0433 
0434     if (QFile::rename(sourceFileName, targetFileName)) {
0435         changeProcessed();
0436     } else {
0437         cancelTask(i18n("Unable to move file '%1' to '%2', '%2' already exists.", sourceFileName, targetFileName));
0438     }
0439 }
0440 
0441 void ContactsResource::collectionMoved(const Akonadi::Collection &collection,
0442                                        const Akonadi::Collection &collectionSource,
0443                                        const Akonadi::Collection &collectionDestination)
0444 {
0445     const QString sourceDirectoryName = directoryForCollection(collectionSource) + QLatin1Char('/') + collection.remoteId();
0446     const QString targetDirectoryName = directoryForCollection(collectionDestination) + QLatin1Char('/') + collection.remoteId();
0447 
0448     if (QFile::rename(sourceDirectoryName, targetDirectoryName)) {
0449         changeProcessed();
0450     } else {
0451         cancelTask(i18n("Unable to move directory '%1' to '%2', '%2' already exists.", sourceDirectoryName, targetDirectoryName));
0452     }
0453 }
0454 
0455 QString ContactsResource::baseDirectoryPath() const
0456 {
0457     return ContactsResourceSettings::self()->path();
0458 }
0459 
0460 void ContactsResource::initializeDirectory(const QString &path) const
0461 {
0462     QDir dir(path);
0463 
0464     // if folder does not exists, create it
0465     QDir::root().mkpath(dir.absolutePath());
0466 
0467     // check whether warning file is in place...
0468     QFile file(dir.absolutePath() + QStringLiteral("/WARNING_README.txt"));
0469     if (!file.exists()) {
0470         // ... if not, create it
0471         file.open(QIODevice::WriteOnly);
0472         file.write(
0473             "Important Warning!!!\n\n"
0474             "Don't create or copy vCards inside this folder manually, they are managed by the Akonadi framework!\n");
0475         file.close();
0476     }
0477 }
0478 
0479 Collection::Rights ContactsResource::supportedRights(bool isResourceCollection) const
0480 {
0481     Collection::Rights rights = Collection::ReadOnly;
0482 
0483     if (!ContactsResourceSettings::self()->readOnly()) {
0484         rights |= Collection::CanChangeItem;
0485         rights |= Collection::CanCreateItem;
0486         rights |= Collection::CanDeleteItem;
0487         rights |= Collection::CanCreateCollection;
0488         rights |= Collection::CanChangeCollection;
0489 
0490         if (!isResourceCollection) {
0491             rights |= Collection::CanDeleteCollection;
0492         }
0493     }
0494 
0495     return rights;
0496 }
0497 
0498 QString ContactsResource::directoryForCollection(const Collection &collection) const
0499 {
0500     if (collection.remoteId().isEmpty()) {
0501         qCWarning(CONTACTSRESOURCES_LOG) << "Got incomplete ancestor chain:" << collection;
0502         return {};
0503     }
0504 
0505     if (collection.parentCollection() == Collection::root()) {
0506         if (collection.remoteId() != baseDirectoryPath()) {
0507             qCWarning(CONTACTSRESOURCES_LOG) << "RID mismatch, is " << collection.remoteId() << " expected " << baseDirectoryPath();
0508         }
0509         return collection.remoteId();
0510     }
0511 
0512     const QString parentDirectory = directoryForCollection(collection.parentCollection());
0513     if (parentDirectory.isNull()) { // invalid, != isEmpty() here!
0514         return {};
0515     }
0516 
0517     QString directory = parentDirectory;
0518     if (!directory.endsWith(QLatin1Char('/'))) {
0519         directory += QLatin1Char('/') + collection.remoteId();
0520     } else {
0521         directory += collection.remoteId();
0522     }
0523 
0524     return directory;
0525 }
0526 
0527 AKONADI_RESOURCE_MAIN(ContactsResource)
0528 
0529 #include "moc_contactsresource.cpp"