File indexing completed on 2024-06-23 05:07:00

0001 /*
0002     SPDX-FileCopyrightText: 2006 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectionmodifyhandler.h"
0008 
0009 #include "akonadi.h"
0010 #include "akonadiserver_debug.h"
0011 #include "cachecleaner.h"
0012 #include "connection.h"
0013 #include "handlerhelper.h"
0014 #include "intervalcheck.h"
0015 #include "search/searchmanager.h"
0016 #include "shared/akranges.h"
0017 #include "storage/collectionqueryhelper.h"
0018 #include "storage/datastore.h"
0019 #include "storage/itemretriever.h"
0020 #include "storage/selectquerybuilder.h"
0021 #include "storage/transaction.h"
0022 
0023 using namespace Akonadi;
0024 using namespace Akonadi::Server;
0025 using namespace AkRanges;
0026 
0027 CollectionModifyHandler::CollectionModifyHandler(AkonadiServer &akonadi)
0028     : Handler(akonadi)
0029 {
0030 }
0031 
0032 bool CollectionModifyHandler::parseStream()
0033 {
0034     const auto &cmd = Protocol::cmdCast<Protocol::ModifyCollectionCommand>(m_command);
0035 
0036     Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context());
0037     if (!collection.isValid()) {
0038         return failureResponse("No such collection");
0039     }
0040 
0041     CacheCleanerInhibitor inhibitor(akonadi(), false);
0042 
0043     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) {
0044         const Collection newParent = Collection::retrieveById(cmd.parentId());
0045         if (newParent.isValid() && collection.parentId() != newParent.id() && collection.resourceId() != newParent.resourceId()) {
0046             inhibitor.inhibit();
0047             ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context());
0048             retriever.setCollection(collection, true);
0049             retriever.setRetrieveFullPayload(true);
0050             if (!retriever.exec()) {
0051                 throw HandlerException(retriever.lastError());
0052             }
0053         }
0054     }
0055 
0056     DataStore *db = connection()->storageBackend();
0057     Transaction transaction(db, QStringLiteral("MODIFY"));
0058     QList<QByteArray> changes;
0059 
0060     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) {
0061         QStringList mts = cmd.mimeTypes();
0062         const MimeType::List currentMts = collection.mimeTypes();
0063         bool equal = true;
0064         for (const MimeType &currentMt : currentMts) {
0065             const int removeMts = mts.removeAll(currentMt.name());
0066             if (removeMts > 0) {
0067                 continue;
0068             }
0069             equal = false;
0070             if (!collection.removeMimeType(currentMt)) {
0071                 return failureResponse("Unable to remove collection mimetype");
0072             }
0073         }
0074         if (!db->appendMimeTypeForCollection(collection.id(), mts)) {
0075             return failureResponse("Unable to add collection mimetypes");
0076         }
0077         if (!equal || !mts.isEmpty()) {
0078             changes.append(AKONADI_PARAM_MIMETYPE);
0079         }
0080     }
0081 
0082     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::CachePolicy) {
0083         bool changed = false;
0084         const Protocol::CachePolicy newCp = cmd.cachePolicy();
0085         if (collection.cachePolicyCacheTimeout() != newCp.cacheTimeout()) {
0086             collection.setCachePolicyCacheTimeout(newCp.cacheTimeout());
0087             changed = true;
0088         }
0089         if (collection.cachePolicyCheckInterval() != newCp.checkInterval()) {
0090             collection.setCachePolicyCheckInterval(newCp.checkInterval());
0091             changed = true;
0092         }
0093         if (collection.cachePolicyInherit() != newCp.inherit()) {
0094             collection.setCachePolicyInherit(newCp.inherit());
0095             changed = true;
0096         }
0097 
0098         QStringList parts = newCp.localParts();
0099         std::sort(parts.begin(), parts.end());
0100         const QString localParts = parts.join(QLatin1Char(' '));
0101         if (collection.cachePolicyLocalParts() != localParts) {
0102             collection.setCachePolicyLocalParts(localParts);
0103             changed = true;
0104         }
0105         if (collection.cachePolicySyncOnDemand() != newCp.syncOnDemand()) {
0106             collection.setCachePolicySyncOnDemand(newCp.syncOnDemand());
0107             changed = true;
0108         }
0109 
0110         if (changed) {
0111             changes.append(AKONADI_PARAM_CACHEPOLICY);
0112         }
0113     }
0114 
0115     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Name) {
0116         if (cmd.name() != collection.name()) {
0117             if (!CollectionQueryHelper::hasAllowedName(collection, cmd.name(), collection.parentId())) {
0118                 return failureResponse("Collection with the same name exists already");
0119             }
0120             collection.setName(cmd.name());
0121             changes.append(AKONADI_PARAM_NAME);
0122         }
0123     }
0124 
0125     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) {
0126         if (collection.parentId() != cmd.parentId()) {
0127             if (!db->moveCollection(collection, Collection::retrieveById(cmd.parentId()))) {
0128                 return failureResponse("Unable to reparent collection");
0129             }
0130             changes.append(AKONADI_PARAM_PARENT);
0131         }
0132     }
0133 
0134     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteID) {
0135         if (cmd.remoteId() != collection.remoteId() && !cmd.remoteId().isEmpty()) {
0136             if (!connection()->isOwnerResource(collection)) {
0137                 qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the collection remoteID from" << collection.remoteId() << "to" << cmd.remoteId();
0138                 return failureResponse("Only resources can modify remote identifiers");
0139             }
0140             collection.setRemoteId(cmd.remoteId());
0141             changes.append(AKONADI_PARAM_REMOTEID);
0142         }
0143     }
0144 
0145     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteRevision) {
0146         if (cmd.remoteRevision() != collection.remoteRevision()) {
0147             collection.setRemoteRevision(cmd.remoteRevision());
0148             changes.append(AKONADI_PARAM_REMOTEREVISION);
0149         }
0150     }
0151 
0152     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::PersistentSearch) {
0153         bool changed = false;
0154         if (cmd.persistentSearchQuery() != collection.queryString()) {
0155             collection.setQueryString(cmd.persistentSearchQuery());
0156             changed = true;
0157         }
0158 
0159         QList<QByteArray> queryAttributes = collection.queryAttributes().toUtf8().split(' ');
0160         if (cmd.persistentSearchRemote() != queryAttributes.contains(AKONADI_PARAM_REMOTE)) {
0161             if (cmd.persistentSearchRemote()) {
0162                 queryAttributes.append(AKONADI_PARAM_REMOTE);
0163             } else {
0164                 queryAttributes.removeOne(AKONADI_PARAM_REMOTE);
0165             }
0166             changed = true;
0167         }
0168         if (cmd.persistentSearchRecursive() != queryAttributes.contains(AKONADI_PARAM_RECURSIVE)) {
0169             if (cmd.persistentSearchRecursive()) {
0170                 queryAttributes.append(AKONADI_PARAM_RECURSIVE);
0171             } else {
0172                 queryAttributes.removeOne(AKONADI_PARAM_RECURSIVE);
0173             }
0174             changed = true;
0175         }
0176         if (changed) {
0177             collection.setQueryAttributes(QString::fromLatin1(queryAttributes.join(' ')));
0178         }
0179 
0180         QList<qint64> inCols = cmd.persistentSearchCollections();
0181         std::sort(inCols.begin(), inCols.end());
0182         const auto cols = inCols | Views::transform([](const auto col) {
0183                               return QString::number(col);
0184                           })
0185             | Actions::toQList;
0186         const QString colStr = cols.join(QLatin1Char(' '));
0187         if (colStr != collection.queryCollections()) {
0188             collection.setQueryCollections(colStr);
0189             changed = true;
0190         }
0191 
0192         if (changed || cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) {
0193             changes.append(AKONADI_PARAM_PERSISTENTSEARCH);
0194         }
0195     }
0196 
0197     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ListPreferences) {
0198         if (cmd.enabled() != collection.enabled()) {
0199             collection.setEnabled(cmd.enabled());
0200             changes.append(AKONADI_PARAM_ENABLED);
0201         }
0202         if (cmd.syncPref() != static_cast<Tristate>(collection.syncPref())) {
0203             collection.setSyncPref(static_cast<Collection::Tristate>(cmd.syncPref()));
0204             changes.append(AKONADI_PARAM_SYNC);
0205         }
0206         if (cmd.displayPref() != static_cast<Tristate>(collection.displayPref())) {
0207             collection.setDisplayPref(static_cast<Collection::Tristate>(cmd.displayPref()));
0208             changes.append(AKONADI_PARAM_DISPLAY);
0209         }
0210         if (cmd.indexPref() != static_cast<Tristate>(collection.indexPref())) {
0211             collection.setIndexPref(static_cast<Collection::Tristate>(cmd.indexPref()));
0212             changes.append(AKONADI_PARAM_INDEX);
0213         }
0214     }
0215 
0216     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemovedAttributes) {
0217         const auto attrs = cmd.removedAttributes();
0218         for (const QByteArray &attr : attrs) {
0219             if (db->removeCollectionAttribute(collection, attr)) {
0220                 changes.append(attr);
0221             }
0222         }
0223     }
0224 
0225     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Attributes) {
0226         const QMap<QByteArray, QByteArray> attrs = cmd.attributes();
0227         for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
0228             SelectQueryBuilder<CollectionAttribute> qb;
0229             qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, collection.id());
0230             qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, iter.key());
0231             if (!qb.exec()) {
0232                 return failureResponse("Unable to retrieve collection attribute");
0233             }
0234 
0235             const CollectionAttribute::List attrsList = qb.result();
0236             if (attrsList.isEmpty()) {
0237                 CollectionAttribute newAttr;
0238                 newAttr.setCollectionId(collection.id());
0239                 newAttr.setType(iter.key());
0240                 newAttr.setValue(iter.value());
0241                 if (!newAttr.insert()) {
0242                     return failureResponse("Unable to add collection attribute");
0243                 }
0244                 changes.append(iter.key());
0245             } else if (attrsList.size() == 1) {
0246                 CollectionAttribute currAttr = attrsList.first();
0247                 if (currAttr.value() == iter.value()) {
0248                     continue;
0249                 }
0250                 currAttr.setValue(iter.value());
0251                 if (!currAttr.update()) {
0252                     return failureResponse("Unable to update collection attribute");
0253                 }
0254                 changes.append(iter.key());
0255             } else {
0256                 return failureResponse("WTF: more than one attribute with the same name");
0257             }
0258         }
0259     }
0260 
0261     if (!changes.isEmpty()) {
0262         if (collection.hasPendingChanges() && !collection.update()) {
0263             return failureResponse("Unable to update collection");
0264         }
0265         db->notificationCollector()->collectionChanged(collection, changes);
0266         // For backwards compatibility. Must be after the changed notification (otherwise the compression removes it).
0267         if (changes.contains(AKONADI_PARAM_ENABLED)) {
0268             if (collection.enabled()) {
0269                 db->notificationCollector()->collectionSubscribed(collection);
0270             } else {
0271                 db->notificationCollector()->collectionUnsubscribed(collection);
0272             }
0273         }
0274         if (!transaction.commit()) {
0275             return failureResponse("Unable to commit transaction");
0276         }
0277 
0278         // Only request Search update AFTER committing the transaction to avoid
0279         // transaction deadlock with SQLite
0280         if (changes.contains(AKONADI_PARAM_PERSISTENTSEARCH)) {
0281             akonadi().searchManager().updateSearch(collection);
0282         }
0283     }
0284 
0285     return successResponse<Protocol::ModifyCollectionResponse>();
0286 }