Akonadi

itemmodifyhandler.cpp
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 "private/externalpartstorage_p.h"
12#include "shared/akranges.h"
13#include "storage/datastore.h"
14#include "storage/dbconfig.h"
15#include "storage/itemqueryhelper.h"
16#include "storage/itemretriever.h"
17#include "storage/parthelper.h"
18#include "storage/partstreamer.h"
19#include "storage/parttypehelper.h"
20#include "storage/selectquerybuilder.h"
21#include "storage/transaction.h"
22
23#include "akonadiserver_debug.h"
24
25#include <algorithm>
26#include <functional>
27
28using namespace Akonadi;
29using namespace Akonadi::Server;
30
31static 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
38ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi)
39 : Handler(akonadi)
40{
41}
42
43bool 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
57bool 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
69bool ItemModifyHandler::deleteFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
70{
71 DataStore *store = connection()->storageBackend();
72
73 QList<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
91bool 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
101bool 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
111bool 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
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
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}
bool isOwnerResource(const PimItem &item) const
Returns true if this connection belongs to the owning resource of item.
This class handles all the database access.
Definition datastore.h:95
NotificationCollector * notificationCollector()
Returns the notification collector of this DataStore object.
QSqlDatabase database()
Returns the QSqlDatabase object.
static Flag::List resolveFlags(const QSet< QByteArray > &flagNames)
Converts a bytearray list of flag names into flag records.
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition handler.h:32
bool parseStream() override
Parse and handle the IMAP message using the streaming parser.
void itemChanged(const PimItem &item, const QSet< QByteArray > &changedParts, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about a changed item.
bool exec()
Executes the query, returns true on success.
void setForUpdate(bool forUpdate=true)
Indicate to the database to acquire an exclusive lock on the rows already during SELECT statement.
Helper class for creating and executing database SELECT queries.
QList< T > result()
Returns the result of this SELECT query.
Helper class for DataStore transaction handling.
Definition transaction.h:23
bool commit()
Commits the transaction.
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Definition dbtype.cpp:11
void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb)
Add conditions to qb for the given item operation scope scope.
Helper integration between Akonadi and Qt.
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QDateTime addMSecs(qint64 msecs) const const
QDateTime currentDateTimeUtc()
QTime time() const const
void append(QList< T > &&value)
void reserve(qsizetype size)
const_iterator cbegin() const const
const_iterator cend() const const
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
bool remove(const T &value)
qsizetype size() const const
QString fromUtf8(QByteArrayView str)
int msec() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jun 7 2024 11:56:45 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.