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"