Akonadi

itemmodifyhandler.cpp
1 /***************************************************************************
2  * SPDX-FileCopyrightText: 2006 Tobias Koenig <[email protected]> *
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 
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 
38 ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi)
39  : Handler(akonadi)
40 {
41 }
42 
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 
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 
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 
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 
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 
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 
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 }
NotificationCollector * notificationCollector()
Returns the notification collector of this DataStore object.
Definition: datastore.cpp:206
This class handles all the database access.
Definition: datastore.h:94
Helper class for DataStore transaction handling.
Definition: transaction.h:22
void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb)
Add conditions to qb for the given item operation scope scope.
QDateTime addMSecs(qint64 msecs) const const
bool remove(const T &value)
QString fromUtf8(const char *str, int size)
void setForUpdate(bool forUpdate=true)
Indicate to the database to acquire an exclusive lock on the rows already during SELECT statement.
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Definition: dbtype.cpp:11
QSqlDatabase database()
Returns the QSqlDatabase object.
Definition: datastore.cpp:135
QTime time() const const
void append(const T &value)
QDateTime currentDateTimeUtc()
int size() const const
QVector< T > result()
Returns the result of this SELECT query.
int msec() const const
void reserve(int size)
bool contains(const T &value) const const
bool exec()
Executes the query, returns true on success.
QSet::const_iterator cend() const const
Helper class for creating and executing database SELECT queries.
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition: handler.h:39
QSet::const_iterator cbegin() const const
static Flag::List resolveFlags(const QSet< QByteArray > &flagNames)
Converts a bytearray list of flag names into flag records.
QSet::iterator insert(const T &value)
bool parseStream() override
Parse and handle the IMAP message using the streaming parser.
bool isOwnerResource(const PimItem &item) const
Returns true if this connection belongs to the owning resource of item.
bool commit()
Commits the transaction.
Definition: transaction.cpp:29
void itemChanged(const PimItem &item, const QSet< QByteArray > &changedParts, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about a changed item.
bool isEmpty() const const
Helper integration between Akonadi and Qt.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Jul 2 2022 06:41:48 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.