1 /*
2     SPDX-FileCopyrightText: 2006 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "collectionmodifyhandler.h"
8 
9 #include "akonadi.h"
10 #include "akonadiserver_debug.h"
11 #include "cachecleaner.h"
12 #include "connection.h"
13 #include "handlerhelper.h"
14 #include "intervalcheck.h"
15 #include "search/searchmanager.h"
16 #include "shared/akranges.h"
17 #include "storage/collectionqueryhelper.h"
18 #include "storage/datastore.h"
19 #include "storage/itemretriever.h"
20 #include "storage/selectquerybuilder.h"
21 #include "storage/transaction.h"
22 
23 using namespace Akonadi;
24 using namespace Akonadi::Server;
25 using namespace AkRanges;
26 
CollectionModifyHandler(AkonadiServer & akonadi)27 CollectionModifyHandler::CollectionModifyHandler(AkonadiServer &akonadi)
28     : Handler(akonadi)
29 {
30 }
31 
parseStream()32 bool CollectionModifyHandler::parseStream()
33 {
34     const auto &cmd = Protocol::cmdCast<Protocol::ModifyCollectionCommand>(m_command);
35 
36     Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context());
37     if (!collection.isValid()) {
38         return failureResponse("No such collection");
39     }
40 
41     CacheCleanerInhibitor inhibitor(akonadi(), false);
42 
43     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) {
44         const Collection newParent = Collection::retrieveById(cmd.parentId());
45         if (newParent.isValid() && collection.parentId() != newParent.id() && collection.resourceId() != newParent.resourceId()) {
46             inhibitor.inhibit();
47             ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context());
48             retriever.setCollection(collection, true);
49             retriever.setRetrieveFullPayload(true);
50             if (!retriever.exec()) {
51                 throw HandlerException(retriever.lastError());
52             }
53         }
54     }
55 
56     DataStore *db = connection()->storageBackend();
57     Transaction transaction(db, QStringLiteral("MODIFY"));
58     QList<QByteArray> changes;
59 
60     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) {
61         QStringList mts = cmd.mimeTypes();
62         const MimeType::List currentMts = collection.mimeTypes();
63         bool equal = true;
64         for (const MimeType &currentMt : currentMts) {
65             const int removeMts = mts.removeAll(currentMt.name());
66             if (removeMts > 0) {
67                 continue;
68             }
69             equal = false;
70             if (!collection.removeMimeType(currentMt)) {
71                 return failureResponse("Unable to remove collection mimetype");
72             }
73         }
74         if (!db->appendMimeTypeForCollection(collection.id(), mts)) {
75             return failureResponse("Unable to add collection mimetypes");
76         }
77         if (!equal || !mts.isEmpty()) {
78             changes.append(AKONADI_PARAM_MIMETYPE);
79         }
80     }
81 
82     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::CachePolicy) {
83         bool changed = false;
84         const Protocol::CachePolicy newCp = cmd.cachePolicy();
85         if (collection.cachePolicyCacheTimeout() != newCp.cacheTimeout()) {
86             collection.setCachePolicyCacheTimeout(newCp.cacheTimeout());
87             changed = true;
88         }
89         if (collection.cachePolicyCheckInterval() != newCp.checkInterval()) {
90             collection.setCachePolicyCheckInterval(newCp.checkInterval());
91             changed = true;
92         }
93         if (collection.cachePolicyInherit() != newCp.inherit()) {
94             collection.setCachePolicyInherit(newCp.inherit());
95             changed = true;
96         }
97 
98         QStringList parts = newCp.localParts();
99         std::sort(parts.begin(), parts.end());
100         const QString localParts = parts.join(QLatin1Char(' '));
101         if (collection.cachePolicyLocalParts() != localParts) {
102             collection.setCachePolicyLocalParts(localParts);
103             changed = true;
104         }
105         if (collection.cachePolicySyncOnDemand() != newCp.syncOnDemand()) {
106             collection.setCachePolicySyncOnDemand(newCp.syncOnDemand());
107             changed = true;
108         }
109 
110         if (changed) {
111             changes.append(AKONADI_PARAM_CACHEPOLICY);
112         }
113     }
114 
115     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Name) {
116         if (cmd.name() != collection.name()) {
117             if (!CollectionQueryHelper::hasAllowedName(collection, cmd.name(), collection.parentId())) {
118                 return failureResponse("Collection with the same name exists already");
119             }
120             collection.setName(cmd.name());
121             changes.append(AKONADI_PARAM_NAME);
122         }
123     }
124 
125     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) {
126         if (collection.parentId() != cmd.parentId()) {
127             if (!db->moveCollection(collection, Collection::retrieveById(cmd.parentId()))) {
128                 return failureResponse("Unable to reparent collection");
129             }
130             changes.append(AKONADI_PARAM_PARENT);
131         }
132     }
133 
134     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteID) {
135         if (cmd.remoteId() != collection.remoteId() && !cmd.remoteId().isEmpty()) {
136             if (!connection()->isOwnerResource(collection)) {
137                 qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the collection remoteID from" << collection.remoteId() << "to" << cmd.remoteId();
138                 return failureResponse("Only resources can modify remote identifiers");
139             }
140             collection.setRemoteId(cmd.remoteId());
141             changes.append(AKONADI_PARAM_REMOTEID);
142         }
143     }
144 
145     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteRevision) {
146         if (cmd.remoteRevision() != collection.remoteRevision()) {
147             collection.setRemoteRevision(cmd.remoteRevision());
148             changes.append(AKONADI_PARAM_REMOTEREVISION);
149         }
150     }
151 
152     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::PersistentSearch) {
153         bool changed = false;
154         if (cmd.persistentSearchQuery() != collection.queryString()) {
155             collection.setQueryString(cmd.persistentSearchQuery());
156             changed = true;
157         }
158 
159         QList<QByteArray> queryAttributes = collection.queryAttributes().toUtf8().split(' ');
160         if (cmd.persistentSearchRemote() != queryAttributes.contains(AKONADI_PARAM_REMOTE)) {
161             if (cmd.persistentSearchRemote()) {
162                 queryAttributes.append(AKONADI_PARAM_REMOTE);
163             } else {
164                 queryAttributes.removeOne(AKONADI_PARAM_REMOTE);
165             }
166             changed = true;
167         }
168         if (cmd.persistentSearchRecursive() != queryAttributes.contains(AKONADI_PARAM_RECURSIVE)) {
169             if (cmd.persistentSearchRecursive()) {
170                 queryAttributes.append(AKONADI_PARAM_RECURSIVE);
171             } else {
172                 queryAttributes.removeOne(AKONADI_PARAM_RECURSIVE);
173             }
174             changed = true;
175         }
176         if (changed) {
177             collection.setQueryAttributes(QString::fromLatin1(queryAttributes.join(' ')));
178         }
179 
180         QVector<qint64> inCols = cmd.persistentSearchCollections();
181         std::sort(inCols.begin(), inCols.end());
182         const auto cols = inCols | Views::transform([](const auto col) {
183                               return QString::number(col);
184                           })
185             | Actions::toQList;
186         const QString colStr = cols.join(QLatin1Char(' '));
187         if (colStr != collection.queryCollections()) {
188             collection.setQueryCollections(colStr);
189             changed = true;
190         }
191 
192         if (changed || cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) {
193             changes.append(AKONADI_PARAM_PERSISTENTSEARCH);
194         }
195     }
196 
197     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ListPreferences) {
198         if (cmd.enabled() != collection.enabled()) {
199             collection.setEnabled(cmd.enabled());
200             changes.append(AKONADI_PARAM_ENABLED);
201         }
202         if (cmd.syncPref() != static_cast<Tristate>(collection.syncPref())) {
203             collection.setSyncPref(static_cast<Collection::Tristate>(cmd.syncPref()));
204             changes.append(AKONADI_PARAM_SYNC);
205         }
206         if (cmd.displayPref() != static_cast<Tristate>(collection.displayPref())) {
207             collection.setDisplayPref(static_cast<Collection::Tristate>(cmd.displayPref()));
208             changes.append(AKONADI_PARAM_DISPLAY);
209         }
210         if (cmd.indexPref() != static_cast<Tristate>(collection.indexPref())) {
211             collection.setIndexPref(static_cast<Collection::Tristate>(cmd.indexPref()));
212             changes.append(AKONADI_PARAM_INDEX);
213         }
214     }
215 
216     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemovedAttributes) {
217         const auto attrs = cmd.removedAttributes();
218         for (const QByteArray &attr : attrs) {
219             if (db->removeCollectionAttribute(collection, attr)) {
220                 changes.append(attr);
221             }
222         }
223     }
224 
225     if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Attributes) {
226         const QMap<QByteArray, QByteArray> attrs = cmd.attributes();
227         for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
228             SelectQueryBuilder<CollectionAttribute> qb;
229             qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, collection.id());
230             qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, iter.key());
231             if (!qb.exec()) {
232                 return failureResponse("Unable to retrieve collection attribute");
233             }
234 
235             const CollectionAttribute::List attrsList = qb.result();
236             if (attrsList.isEmpty()) {
237                 CollectionAttribute newAttr;
238                 newAttr.setCollectionId(collection.id());
239                 newAttr.setType(iter.key());
240                 newAttr.setValue(iter.value());
241                 if (!newAttr.insert()) {
242                     return failureResponse("Unable to add collection attribute");
243                 }
244                 changes.append(iter.key());
245             } else if (attrsList.size() == 1) {
246                 CollectionAttribute currAttr = attrsList.first();
247                 if (currAttr.value() == iter.value()) {
248                     continue;
249                 }
250                 currAttr.setValue(iter.value());
251                 if (!currAttr.update()) {
252                     return failureResponse("Unable to update collection attribute");
253                 }
254                 changes.append(iter.key());
255             } else {
256                 return failureResponse("WTF: more than one attribute with the same name");
257             }
258         }
259     }
260 
261     if (!changes.isEmpty()) {
262         if (collection.hasPendingChanges() && !collection.update()) {
263             return failureResponse("Unable to update collection");
264         }
265         db->notificationCollector()->collectionChanged(collection, changes);
266         // For backwards compatibility. Must be after the changed notification (otherwise the compression removes it).
267         if (changes.contains(AKONADI_PARAM_ENABLED)) {
268             if (collection.enabled()) {
269                 db->notificationCollector()->collectionSubscribed(collection);
270             } else {
271                 db->notificationCollector()->collectionUnsubscribed(collection);
272             }
273         }
274         if (!transaction.commit()) {
275             return failureResponse("Unable to commit transaction");
276         }
277 
278         // Only request Search update AFTER committing the transaction to avoid
279         // transaction deadlock with SQLite
280         if (changes.contains(AKONADI_PARAM_PERSISTENTSEARCH)) {
281             akonadi().searchManager().updateSearch(collection);
282         }
283     }
284 
285     return successResponse<Protocol::ModifyCollectionResponse>();
286 }
287