Akonadi

itemmodifyhandler.cpp
1 /***************************************************************************
2  * Copyright (C) 2006 by Tobias Koenig <[email protected]> *
3  * *
4  * This program is free software; you can redistribute it and/or modify *
5  * it under the terms of the GNU Library General Public License as *
6  * published by the Free Software Foundation; either version 2 of the *
7  * License, or (at your option) any later version. *
8  * *
9  * This program is distributed in the hope that it will be useful, *
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12  * GNU General Public License for more details. *
13  * *
14  * You should have received a copy of the GNU Library General Public *
15  * License along with this program; if not, write to the *
16  * Free Software Foundation, Inc., *
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
18  ***************************************************************************/
19 
20 #include "itemmodifyhandler.h"
21 
22 #include "connection.h"
23 #include "handlerhelper.h"
24 #include "storage/datastore.h"
25 #include "storage/transaction.h"
26 #include "storage/itemqueryhelper.h"
27 #include "storage/selectquerybuilder.h"
28 #include "storage/parthelper.h"
29 #include "storage/dbconfig.h"
30 #include "storage/itemretriever.h"
31 #include "storage/parttypehelper.h"
32 #include "storage/partstreamer.h"
33 #include <shared/akranges.h>
34 #include <private/externalpartstorage_p.h>
35 
36 
37 #include "akonadiserver_debug.h"
38 
39 #include <algorithm>
40 #include <functional>
41 
42 using namespace Akonadi;
43 using namespace Akonadi::Server;
44 
45 static bool payloadChanged(const QSet<QByteArray> &changes)
46 {
47  return changes | AkRanges::Actions::any([](const auto &change) { return change.startsWith(AKONADI_PARAM_PLD); });
48 }
49 
50 ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi)
51  : Handler(akonadi)
52 {}
53 
54 bool ItemModifyHandler::replaceFlags(const PimItem::List &item, const QSet<QByteArray> &flags, bool &flagsChanged)
55 {
56  Flag::List flagList = HandlerHelper::resolveFlags(flags);
57  DataStore *store = connection()->storageBackend();
58 
59  if (!store->setItemsFlags(item, flagList, &flagsChanged)) {
60  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceFlags: Unable to replace flags";
61  return false;
62  }
63 
64  return true;
65 }
66 
67 bool ItemModifyHandler::addFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
68 {
69  const Flag::List flagList = HandlerHelper::resolveFlags(flags);
70  DataStore *store = connection()->storageBackend();
71 
72  if (!store->appendItemsFlags(items, flagList, &flagsChanged)) {
73  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addFlags: Unable to add new item flags";
74  return false;
75  }
76  return true;
77 }
78 
79 bool ItemModifyHandler::deleteFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
80 {
81  DataStore *store = connection()->storageBackend();
82 
83  QVector<Flag> flagList;
84  flagList.reserve(flags.size());
85  for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) {
87  if (!flag.isValid()) {
88  continue;
89  }
90 
91  flagList.append(flag);
92  }
93 
94  if (!store->removeItemsFlags(items, flagList, &flagsChanged)) {
95  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteFlags: Unable to remove item flags";
96  return false;
97  }
98  return true;
99 }
100 
101 bool ItemModifyHandler::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged)
102 {
103  const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
104  if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) {
105  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceTags: unable to replace tags";
106  return false;
107  }
108  return true;
109 }
110 
111 bool ItemModifyHandler::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
112 {
113  const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
114  if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) {
115  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addTags: Unable to add new item tags";
116  return false;
117  }
118  return true;
119 }
120 
121 bool ItemModifyHandler::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
122 {
123  const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
124  if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) {
125  qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteTags: Unable to remove item tags";
126  return false;
127  }
128  return true;
129 }
130 
132 {
133  const auto &cmd = Protocol::cmdCast<Protocol::ModifyItemsCommand>(m_command);
134 
135  //parseCommand();
136 
137  DataStore *store = connection()->storageBackend();
138  Transaction transaction(store, QStringLiteral("STORE"));
139  ExternalPartStorageTransaction storageTrx;
140  // Set the same modification time for each item.
141  QDateTime modificationtime = QDateTime::currentDateTimeUtc();
142  if (DbType::type(store->database()) != DbType::Sqlite) {
143  // Remove milliseconds from the modificationtime. PSQL and MySQL don't
144  // support milliseconds in DATETIME column, so FETCHed Items will report
145  // time without milliseconds, while this command would return answer
146  // with milliseconds
147  modificationtime = modificationtime.addMSecs(-modificationtime.time().msec());
148  }
149 
150  // retrieve selected items
152  qb.setForUpdate();
153  ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb);
154  if (!qb.exec()) {
155  return failureResponse("Unable to retrieve items");
156  }
157  PimItem::List pimItems = qb.result();
158  if (pimItems.isEmpty()) {
159  return failureResponse("No items found");
160  }
161 
162  for (int i = 0; i < pimItems.size(); ++i) {
163  if (cmd.oldRevision() > -1) {
164  // check for conflicts if a resources tries to overwrite an item with dirty payload
165  const PimItem &pimItem = pimItems.at(i);
166  if (connection()->isOwnerResource(pimItem)) {
167  if (pimItem.dirty()) {
168  const QString error = QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE.");
169  return failureResponse(
170  error.arg(pimItem.collection().resource().name())
171  .arg(pimItem.id())
172  .arg(pimItem.remoteId()).arg(pimItem.collectionId()));
173  }
174  }
175 
176  // check and update revisions
177  if (pimItem.rev() != (int) cmd.oldRevision()) {
178  const QString error = QStringLiteral("[LLCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with revision %5; the item was modified elsewhere and has revision %6, aborting STORE.");
179  return failureResponse(error.arg(pimItem.collection().resource().name())
180  .arg(pimItem.id())
181  .arg(pimItem.remoteId()).arg(pimItem.collectionId())
182  .arg(cmd.oldRevision()).arg(pimItems.at(i).rev()));
183  }
184  }
185  }
186 
187  PimItem &item = pimItems.first();
188 
189  QSet<QByteArray> changes;
190  qint64 partSizes = 0;
191  qint64 size = 0;
192 
193  bool flagsChanged = false;
194  bool tagsChanged = false;
195 
196  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) {
197  if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) {
198  return failureResponse("Unable to add item flags");
199  }
200  }
201 
202  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) {
203  if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) {
204  return failureResponse("Unable to remove item flags");
205  }
206  }
207 
208  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) {
209  if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) {
210  return failureResponse("Unable to reset flags");
211  }
212  }
213 
214  if (flagsChanged) {
215  changes << AKONADI_PARAM_FLAGS;
216  }
217 
218  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) {
219  if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) {
220  return failureResponse("Unable to add item tags");
221  }
222  }
223 
224  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) {
225  if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) {
226  return failureResponse("Unable to remove item tags");
227  }
228  }
229 
230  if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) {
231  if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) {
232  return failureResponse("Unable to reset item tags");
233  }
234  }
235 
236  if (tagsChanged) {
237  changes << AKONADI_PARAM_TAGS;
238  }
239 
240  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) {
241  if (item.remoteId() != cmd.remoteId() && !cmd.remoteId().isEmpty()) {
242  if (!connection()->isOwnerResource(item)) {
243  qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the remoteID for item" << item.id() << "from" << item.remoteId() << "to" << cmd.remoteId();
244  return failureResponse("Only resources can modify remote identifiers");
245  }
246  item.setRemoteId(cmd.remoteId());
247  changes << AKONADI_PARAM_REMOTEID;
248  }
249  }
250 
251  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) {
252  if (item.gid() != cmd.gid()) {
253  item.setGid(cmd.gid());
254  }
255  changes << AKONADI_PARAM_GID;
256  }
257 
258  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) {
259  if (item.remoteRevision() != cmd.remoteRevision()) {
260  if (!connection()->isOwnerResource(item)) {
261  return failureResponse("Only resources can modify remote revisions");
262  }
263  item.setRemoteRevision(cmd.remoteRevision());
264  changes << AKONADI_PARAM_REMOTEREVISION;
265  }
266  }
267 
268  if (item.isValid() && !cmd.dirty()) {
269  item.setDirty(false);
270  }
271 
272  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) {
273  size = cmd.itemSize();
274  changes << AKONADI_PARAM_SIZE;
275  }
276 
277  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) {
278  if (!cmd.removedParts().isEmpty()) {
279  if (!store->removeItemParts(item, cmd.removedParts())) {
280  return failureResponse("Unable to remove item parts");
281  }
282  Q_FOREACH (const QByteArray &part, cmd.removedParts()) {
283  changes.insert(part);
284  }
285  }
286  }
287 
288  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) {
289  PartStreamer streamer(connection(), item);
290  Q_FOREACH (const QByteArray &partName, cmd.parts()) {
291  qint64 partSize = 0;
292  try {
293  streamer.stream(true, partName, partSize);
294  } catch (const PartStreamerException &e) {
295  return failureResponse(e.what());
296  }
297 
298  changes.insert(partName);
299  partSizes += partSize;
300  }
301  }
302 
303  if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) {
304  PartStreamer streamer(connection(), item);
305  const Protocol::Attributes attrs = cmd.attributes();
306  for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
307  bool changed = false;
308  try {
309  streamer.streamAttribute(true, iter.key(), iter.value(), &changed);
310  } catch (const PartStreamerException &e) {
311  return failureResponse(e.what());
312  }
313 
314  if (changed) {
315  changes.insert(iter.key());
316  }
317  }
318  }
319 
320  QDateTime datetime;
321  if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) {
322 
323  // update item size
324  if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) {
325  pimItems.first().setSize(qMax(size, partSizes));
326  }
327 
328  const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID));
329  const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION));
330  const bool onlyRemoteIdAndRevisionChanged = (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID)
331  && changes.contains(AKONADI_PARAM_REMOTEREVISION));
332  const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS));
333  const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID));
334  // If only the remote id and/or the remote revision changed, we don't have to increase the REV,
335  // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible
336  const bool revisionNeedsUpdate = (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged);
337 
338  // run update query and prepare change notifications
339  for (int i = 0; i < pimItems.count(); ++i) {
340 
341  PimItem &item = pimItems[i];
342  if (revisionNeedsUpdate) {
343  item.setRev(item.rev() + 1);
344  }
345 
346  item.setDatetime(modificationtime);
347  item.setAtime(modificationtime);
348  if (!connection()->isOwnerResource(item) && payloadChanged(changes)) {
349  item.setDirty(true);
350  }
351  if (!item.update()) {
352  return failureResponse("Unable to write item changes into the database");
353  }
354 
355  if (cmd.invalidateCache()) {
356  if (!store->invalidateItemCache(item)) {
357  return failureResponse("Unable to invalidate item cache in the database");
358  }
359  }
360 
361  // flags change notification went separately during command parsing
362  // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened
363  if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) {
364  // Don't send FLAGS notification in itemChanged
365  changes.remove(AKONADI_PARAM_FLAGS);
366  store->notificationCollector()->itemChanged(item, changes);
367  }
368 
369  if (!cmd.noResponse()) {
370  Protocol::ModifyItemsResponse resp;
371  resp.setId(item.id());
372  resp.setNewRevision(item.rev());
373  sendResponse(std::move(resp));
374  }
375  }
376 
377  if (!transaction.commit()) {
378  return failureResponse("Cannot commit transaction.");
379  }
380  // Always commit storage changes (deletion) after DB transaction
381  storageTrx.commit();
382 
383  datetime = modificationtime;
384  } else {
385  datetime = pimItems.first().datetime();
386  }
387 
388  Protocol::ModifyItemsResponse resp;
389  resp.setModificationDateTime(datetime);
390  return successResponse(std::move(resp));
391 }
void setDatetime(const QDateTime &datetime)
Sets the value of the datetime column of this record.
Definition: entities.cpp:4067
QSet::const_iterator cbegin() const const
void setForUpdate(bool forUpdate=true)
Indicate to the database to acquire an exclusive lock on the rows already during SELECT statement...
void append(const T &value)
int size() const const
static Flag::List resolveFlags(const QSet< QByteArray > &flagNames)
Converts a bytearray list of flag names into flag records.
Resource resource() const
Retrieve the Resource record referred to by the resourceId column of this record. ...
The handler interfaces describes an entity capable of handling an AkonadiIMAP command.
Definition: handler.h:48
int rev() const
Returns the value of the rev column of this record.
Definition: entities.cpp:3983
bool isOwnerResource(const PimItem &item) const
Returns true if this connection belongs to the owning resource of item.
QTime time() const const
bool parseStream() override
Parse and handle the IMAP message using the streaming parser.
T & first()
QSet::iterator insert(const T &value)
QSet::const_iterator cend() const const
static Flag retrieveByName(const QString &name)
Returns the record with name name.
Definition: entities.cpp:5328
Representation of a record in the Flag table.
Definition: entities.h:1595
QString fromUtf8(const char *str, int size)
Helper class for DataStore transaction handling.
Definition: transaction.h:37
Collection collection() const
Retrieve the Collection record referred to by the collectionId column of this record.
Definition: entities.cpp:4555
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Definition: dbtype.cpp:24
bool isEmpty() const const
bool dirty() const
Returns the value of the dirty column of this record.
Definition: entities.cpp:4087
QVector< T > result()
Returns the result of this SELECT query.
QString gid() const
Returns the value of the gid column of this record.
Definition: entities.cpp:4022
QString remoteRevision() const
Returns the value of the remoteRevision column of this record.
Definition: entities.cpp:4009
bool commit()
Commits the transaction.
Definition: transaction.cpp:42
void setRemoteId(const QString &remoteId)
Sets the value of the remoteId column of this record.
Definition: entities.cpp:4002
int msec() const const
void reserve(int size)
void setGid(const QString &gid)
Sets the value of the gid column of this record.
Definition: entities.cpp:4028
bool contains(const T &value) const const
void setRemoteRevision(const QString &remoteRevision)
Sets the value of the remoteRevision column of this record.
Definition: entities.cpp:4015
const QList< QKeySequence > & end()
const T & at(int i) const const
Helper class for creating and executing database SELECT queries.
bool remove(const T &value)
Definition: item.h:44
bool isEmpty() const const
Helper integration between Akonadi and Qt.
qint64 collectionId() const
Returns the value of the collectionId column of this record.
Definition: entities.cpp:4035
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int count(const T &value) const const
void setDirty(bool dirty)
Sets the value of the dirty column of this record.
Definition: entities.cpp:4093
bool isEmpty() const const
void setAtime(const QDateTime &atime)
Sets the value of the atime column of this record.
Definition: entities.cpp:4080
bool update()
Stores all changes made to this record into the database.
Definition: entities.cpp:4897
QString remoteId() const
Returns the value of the remoteId column of this record.
Definition: entities.cpp:3996
This class handles all the database access.
Definition: datastore.h:102
void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb)
Add conditions to qb for the given item operation scope scope.
int size() const const
QString name() const
Returns the value of the name column of this record.
Definition: entities.cpp:545
QDateTime currentDateTimeUtc()
void setRev(int rev)
Sets the value of the rev column of this record.
Definition: entities.cpp:3989
Representation of a record in the PimItem table.
Definition: entities.h:1194
bool exec()
Executes the query, returns true on success.
QDateTime addMSecs(qint64 msecs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon May 25 2020 22:46:10 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.