1 /***************************************************************************
2  *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
3  *                                                                         *
4  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
5  ***************************************************************************/
6 
7 #include "itemmodifyhandler.h"
8 
9 #include "connection.h"
10 #include "handlerhelper.h"
11 #include "storage/datastore.h"
12 #include "storage/dbconfig.h"
13 #include "storage/itemqueryhelper.h"
14 #include "storage/itemretriever.h"
15 #include "storage/parthelper.h"
16 #include "storage/partstreamer.h"
17 #include "storage/parttypehelper.h"
18 #include "storage/selectquerybuilder.h"
19 #include "storage/transaction.h"
20 #include <private/externalpartstorage_p.h>
21 #include <shared/akranges.h>
22 
23 #include "akonadiserver_debug.h"
24 
25 #include <algorithm>
26 #include <functional>
27 
28 using namespace Akonadi;
29 using namespace Akonadi::Server;
30 
payloadChanged(const QSet<QByteArray> & changes)31 static bool payloadChanged(const QSet<QByteArray> &changes)
32 {
33     return changes | AkRanges::Actions::any([](const auto &change) {
34                return change.startsWith(AKONADI_PARAM_PLD);
35            });
36 }
37 
ItemModifyHandler(AkonadiServer & akonadi)38 ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi)
39     : Handler(akonadi)
40 {
41 }
42 
replaceFlags(const PimItem::List & items,const QSet<QByteArray> & flags,bool & flagsChanged)43 bool ItemModifyHandler::replaceFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
44 {
45     Flag::List flagList = HandlerHelper::resolveFlags(flags);
46     DataStore *store = connection()->storageBackend();
47 
48     // TODO: why doesn't this have the "Make sure we don't overwrite some local-only flags" code that itemcreatehandler has?
49     if (!store->setItemsFlags(items, nullptr, flagList, &flagsChanged)) {
50         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceFlags: Unable to replace flags";
51         return false;
52     }
53 
54     return true;
55 }
56 
addFlags(const PimItem::List & items,const QSet<QByteArray> & flags,bool & flagsChanged)57 bool ItemModifyHandler::addFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
58 {
59     const Flag::List flagList = HandlerHelper::resolveFlags(flags);
60     DataStore *store = connection()->storageBackend();
61 
62     if (!store->appendItemsFlags(items, flagList, &flagsChanged)) {
63         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addFlags: Unable to add new item flags";
64         return false;
65     }
66     return true;
67 }
68 
deleteFlags(const PimItem::List & items,const QSet<QByteArray> & flags,bool & flagsChanged)69 bool ItemModifyHandler::deleteFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
70 {
71     DataStore *store = connection()->storageBackend();
72 
73     QVector<Flag> flagList;
74     flagList.reserve(flags.size());
75     for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) {
76         Flag flag = Flag::retrieveByName(QString::fromUtf8(*iter));
77         if (!flag.isValid()) {
78             continue;
79         }
80 
81         flagList.append(flag);
82     }
83 
84     if (!store->removeItemsFlags(items, flagList, &flagsChanged)) {
85         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteFlags: Unable to remove item flags";
86         return false;
87     }
88     return true;
89 }
90 
replaceTags(const PimItem::List & item,const Scope & tags,bool & tagsChanged)91 bool ItemModifyHandler::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged)
92 {
93     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
94     if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) {
95         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceTags: unable to replace tags";
96         return false;
97     }
98     return true;
99 }
100 
addTags(const PimItem::List & items,const Scope & tags,bool & tagsChanged)101 bool ItemModifyHandler::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
102 {
103     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
104     if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) {
105         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addTags: Unable to add new item tags";
106         return false;
107     }
108     return true;
109 }
110 
deleteTags(const PimItem::List & items,const Scope & tags,bool & tagsChanged)111 bool ItemModifyHandler::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
112 {
113     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
114     if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) {
115         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteTags: Unable to remove item tags";
116         return false;
117     }
118     return true;
119 }
120 
parseStream()121 bool ItemModifyHandler::parseStream()
122 {
123     const auto &cmd = Protocol::cmdCast<Protocol::ModifyItemsCommand>(m_command);
124 
125     // parseCommand();
126 
127     DataStore *store = connection()->storageBackend();
128     Transaction transaction(store, QStringLiteral("STORE"));
129     ExternalPartStorageTransaction storageTrx;
130     // Set the same modification time for each item.
131     QDateTime modificationtime = QDateTime::currentDateTimeUtc();
132     if (DbType::type(store->database()) != DbType::Sqlite) {
133         // Remove milliseconds from the modificationtime. PSQL and MySQL don't
134         // support milliseconds in DATETIME column, so FETCHed Items will report
135         // time without milliseconds, while this command would return answer
136         // with milliseconds
137         modificationtime = modificationtime.addMSecs(-modificationtime.time().msec());
138     }
139 
140     // retrieve selected items
141     SelectQueryBuilder<PimItem> qb;
142     qb.setForUpdate();
143     ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb);
144     if (!qb.exec()) {
145         return failureResponse("Unable to retrieve items");
146     }
147     PimItem::List pimItems = qb.result();
148     if (pimItems.isEmpty()) {
149         return failureResponse("No items found");
150     }
151 
152     for (int i = 0; i < pimItems.size(); ++i) {
153         if (cmd.oldRevision() > -1) {
154             // check for conflicts if a resources tries to overwrite an item with dirty payload
155             const PimItem &pimItem = pimItems.at(i);
156             if (connection()->isOwnerResource(pimItem)) {
157                 if (pimItem.dirty()) {
158                     const QString error =
159                         QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE.");
160                     return failureResponse(
161                         error.arg(pimItem.collection().resource().name()).arg(pimItem.id()).arg(pimItem.remoteId()).arg(pimItem.collectionId()));
162                 }
163             }
164 
165             // check and update revisions
166             if (pimItem.rev() != cmd.oldRevision()) {
167                 const QString error = QStringLiteral(
168                     "[LLCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with revision %5; the item was modified elsewhere and has "
169                     "revision %6, aborting STORE.");
170                 return failureResponse(error.arg(pimItem.collection().resource().name())
171                                            .arg(pimItem.id())
172                                            .arg(pimItem.remoteId())
173                                            .arg(pimItem.collectionId())
174                                            .arg(cmd.oldRevision())
175                                            .arg(pimItems.at(i).rev()));
176             }
177         }
178     }
179 
180     PimItem &item = pimItems.first();
181 
182     QSet<QByteArray> changes;
183     qint64 partSizes = 0;
184     qint64 size = 0;
185 
186     bool flagsChanged = false;
187     bool tagsChanged = false;
188 
189     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) {
190         if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) {
191             return failureResponse("Unable to add item flags");
192         }
193     }
194 
195     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) {
196         if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) {
197             return failureResponse("Unable to remove item flags");
198         }
199     }
200 
201     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) {
202         if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) {
203             return failureResponse("Unable to reset flags");
204         }
205     }
206 
207     if (flagsChanged) {
208         changes << AKONADI_PARAM_FLAGS;
209     }
210 
211     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) {
212         if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) {
213             return failureResponse("Unable to add item tags");
214         }
215     }
216 
217     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) {
218         if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) {
219             return failureResponse("Unable to remove item tags");
220         }
221     }
222 
223     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) {
224         if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) {
225             return failureResponse("Unable to reset item tags");
226         }
227     }
228 
229     if (tagsChanged) {
230         changes << AKONADI_PARAM_TAGS;
231     }
232 
233     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) {
234         if (item.remoteId() != cmd.remoteId() && !cmd.remoteId().isEmpty()) {
235             if (!connection()->isOwnerResource(item)) {
236                 qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the remoteID for item" << item.id() << "from" << item.remoteId() << "to"
237                                              << cmd.remoteId();
238                 return failureResponse("Only resources can modify remote identifiers");
239             }
240             item.setRemoteId(cmd.remoteId());
241             changes << AKONADI_PARAM_REMOTEID;
242         }
243     }
244 
245     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) {
246         if (item.gid() != cmd.gid()) {
247             item.setGid(cmd.gid());
248         }
249         changes << AKONADI_PARAM_GID;
250     }
251 
252     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) {
253         if (item.remoteRevision() != cmd.remoteRevision()) {
254             if (!connection()->isOwnerResource(item)) {
255                 return failureResponse("Only resources can modify remote revisions");
256             }
257             item.setRemoteRevision(cmd.remoteRevision());
258             changes << AKONADI_PARAM_REMOTEREVISION;
259         }
260     }
261 
262     if (item.isValid() && !cmd.dirty()) {
263         item.setDirty(false);
264     }
265 
266     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) {
267         size = cmd.itemSize();
268         changes << AKONADI_PARAM_SIZE;
269     }
270 
271     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) {
272         const auto removedParts = cmd.removedParts();
273         if (!removedParts.isEmpty()) {
274             if (!store->removeItemParts(item, removedParts)) {
275                 return failureResponse("Unable to remove item parts");
276             }
277             for (const QByteArray &part : removedParts) {
278                 changes.insert(part);
279             }
280         }
281     }
282 
283     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) {
284         PartStreamer streamer(connection(), item);
285         const auto partNames = cmd.parts();
286         for (const QByteArray &partName : partNames) {
287             qint64 partSize = 0;
288             try {
289                 streamer.stream(true, partName, partSize);
290             } catch (const PartStreamerException &e) {
291                 return failureResponse(e.what());
292             }
293 
294             changes.insert(partName);
295             partSizes += partSize;
296         }
297     }
298 
299     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) {
300         PartStreamer streamer(connection(), item);
301         const Protocol::Attributes attrs = cmd.attributes();
302         for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
303             bool changed = false;
304             try {
305                 streamer.streamAttribute(true, iter.key(), iter.value(), &changed);
306             } catch (const PartStreamerException &e) {
307                 return failureResponse(e.what());
308             }
309 
310             if (changed) {
311                 changes.insert(iter.key());
312             }
313         }
314     }
315 
316     QDateTime datetime;
317     if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) {
318         // update item size
319         if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) {
320             pimItems.first().setSize(qMax(size, partSizes));
321         }
322 
323         const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID));
324         const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION));
325         const bool onlyRemoteIdAndRevisionChanged =
326             (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID) && changes.contains(AKONADI_PARAM_REMOTEREVISION));
327         const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS));
328         const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID));
329         // If only the remote id and/or the remote revision changed, we don't have to increase the REV,
330         // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible
331         const bool revisionNeedsUpdate =
332             (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged);
333 
334         // run update query and prepare change notifications
335         for (int i = 0; i < pimItems.count(); ++i) {
336             PimItem &item = pimItems[i];
337             if (revisionNeedsUpdate) {
338                 item.setRev(item.rev() + 1);
339             }
340 
341             item.setDatetime(modificationtime);
342             item.setAtime(modificationtime);
343             if (!connection()->isOwnerResource(item) && payloadChanged(changes)) {
344                 item.setDirty(true);
345             }
346             if (!item.update()) {
347                 return failureResponse("Unable to write item changes into the database");
348             }
349 
350             if (cmd.invalidateCache()) {
351                 if (!store->invalidateItemCache(item)) {
352                     return failureResponse("Unable to invalidate item cache in the database");
353                 }
354             }
355 
356             // flags change notification went separately during command parsing
357             // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened
358             if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) {
359                 // Don't send FLAGS notification in itemChanged
360                 changes.remove(AKONADI_PARAM_FLAGS);
361                 store->notificationCollector()->itemChanged(item, changes);
362             }
363 
364             if (!cmd.noResponse()) {
365                 Protocol::ModifyItemsResponse resp;
366                 resp.setId(item.id());
367                 resp.setNewRevision(item.rev());
368                 sendResponse(std::move(resp));
369             }
370         }
371 
372         if (!transaction.commit()) {
373             return failureResponse("Cannot commit transaction.");
374         }
375         // Always commit storage changes (deletion) after DB transaction
376         storageTrx.commit();
377 
378         datetime = modificationtime;
379     } else {
380         datetime = pimItems.first().datetime();
381     }
382 
383     Protocol::ModifyItemsResponse resp;
384     resp.setModificationDateTime(datetime);
385     return successResponse(std::move(resp));
386 }
387