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 ¤tMt : 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