File indexing completed on 2024-06-23 05:07:02

0001 /***************************************************************************
0002  *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
0003  *                                                                         *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
0005  ***************************************************************************/
0006 
0007 #include "itemmodifyhandler.h"
0008 
0009 #include "connection.h"
0010 #include "handlerhelper.h"
0011 #include "private/externalpartstorage_p.h"
0012 #include "shared/akranges.h"
0013 #include "storage/datastore.h"
0014 #include "storage/dbconfig.h"
0015 #include "storage/itemqueryhelper.h"
0016 #include "storage/itemretriever.h"
0017 #include "storage/parthelper.h"
0018 #include "storage/partstreamer.h"
0019 #include "storage/parttypehelper.h"
0020 #include "storage/selectquerybuilder.h"
0021 #include "storage/transaction.h"
0022 
0023 #include "akonadiserver_debug.h"
0024 
0025 #include <algorithm>
0026 #include <functional>
0027 
0028 using namespace Akonadi;
0029 using namespace Akonadi::Server;
0030 
0031 static bool payloadChanged(const QSet<QByteArray> &changes)
0032 {
0033     return changes | AkRanges::Actions::any([](const auto &change) {
0034                return change.startsWith(AKONADI_PARAM_PLD);
0035            });
0036 }
0037 
0038 ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi)
0039     : Handler(akonadi)
0040 {
0041 }
0042 
0043 bool ItemModifyHandler::replaceFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
0044 {
0045     Flag::List flagList = HandlerHelper::resolveFlags(flags);
0046     DataStore *store = connection()->storageBackend();
0047 
0048     // TODO: why doesn't this have the "Make sure we don't overwrite some local-only flags" code that itemcreatehandler has?
0049     if (!store->setItemsFlags(items, nullptr, flagList, &flagsChanged)) {
0050         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceFlags: Unable to replace flags";
0051         return false;
0052     }
0053 
0054     return true;
0055 }
0056 
0057 bool ItemModifyHandler::addFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
0058 {
0059     const Flag::List flagList = HandlerHelper::resolveFlags(flags);
0060     DataStore *store = connection()->storageBackend();
0061 
0062     if (!store->appendItemsFlags(items, flagList, &flagsChanged)) {
0063         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addFlags: Unable to add new item flags";
0064         return false;
0065     }
0066     return true;
0067 }
0068 
0069 bool ItemModifyHandler::deleteFlags(const PimItem::List &items, const QSet<QByteArray> &flags, bool &flagsChanged)
0070 {
0071     DataStore *store = connection()->storageBackend();
0072 
0073     QList<Flag> flagList;
0074     flagList.reserve(flags.size());
0075     for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) {
0076         Flag flag = Flag::retrieveByName(QString::fromUtf8(*iter));
0077         if (!flag.isValid()) {
0078             continue;
0079         }
0080 
0081         flagList.append(flag);
0082     }
0083 
0084     if (!store->removeItemsFlags(items, flagList, &flagsChanged)) {
0085         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteFlags: Unable to remove item flags";
0086         return false;
0087     }
0088     return true;
0089 }
0090 
0091 bool ItemModifyHandler::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged)
0092 {
0093     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
0094     if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) {
0095         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceTags: unable to replace tags";
0096         return false;
0097     }
0098     return true;
0099 }
0100 
0101 bool ItemModifyHandler::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
0102 {
0103     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
0104     if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) {
0105         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addTags: Unable to add new item tags";
0106         return false;
0107     }
0108     return true;
0109 }
0110 
0111 bool ItemModifyHandler::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged)
0112 {
0113     const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context());
0114     if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) {
0115         qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteTags: Unable to remove item tags";
0116         return false;
0117     }
0118     return true;
0119 }
0120 
0121 bool ItemModifyHandler::parseStream()
0122 {
0123     const auto &cmd = Protocol::cmdCast<Protocol::ModifyItemsCommand>(m_command);
0124 
0125     // parseCommand();
0126 
0127     DataStore *store = connection()->storageBackend();
0128     Transaction transaction(store, QStringLiteral("STORE"));
0129     ExternalPartStorageTransaction storageTrx;
0130     // Set the same modification time for each item.
0131     QDateTime modificationtime = QDateTime::currentDateTimeUtc();
0132     if (DbType::type(store->database()) != DbType::Sqlite) {
0133         // Remove milliseconds from the modificationtime. PSQL and MySQL don't
0134         // support milliseconds in DATETIME column, so FETCHed Items will report
0135         // time without milliseconds, while this command would return answer
0136         // with milliseconds
0137         modificationtime = modificationtime.addMSecs(-modificationtime.time().msec());
0138     }
0139 
0140     // retrieve selected items
0141     SelectQueryBuilder<PimItem> qb;
0142     qb.setForUpdate();
0143     ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb);
0144     if (!qb.exec()) {
0145         return failureResponse("Unable to retrieve items");
0146     }
0147     PimItem::List pimItems = qb.result();
0148     if (pimItems.isEmpty()) {
0149         return failureResponse("No items found");
0150     }
0151 
0152     for (int i = 0; i < pimItems.size(); ++i) {
0153         if (cmd.oldRevision() > -1) {
0154             // check for conflicts if a resources tries to overwrite an item with dirty payload
0155             const PimItem &pimItem = pimItems.at(i);
0156             if (connection()->isOwnerResource(pimItem)) {
0157                 if (pimItem.dirty()) {
0158                     const QString error =
0159                         QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE.");
0160                     return failureResponse(
0161                         error.arg(pimItem.collection().resource().name()).arg(pimItem.id()).arg(pimItem.remoteId()).arg(pimItem.collectionId()));
0162                 }
0163             }
0164 
0165             // check and update revisions
0166             if (pimItem.rev() != cmd.oldRevision()) {
0167                 const QString error = QStringLiteral(
0168                     "[LLCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with revision %5; the item was modified elsewhere and has "
0169                     "revision %6, aborting STORE.");
0170                 return failureResponse(error.arg(pimItem.collection().resource().name())
0171                                            .arg(pimItem.id())
0172                                            .arg(pimItem.remoteId())
0173                                            .arg(pimItem.collectionId())
0174                                            .arg(cmd.oldRevision())
0175                                            .arg(pimItems.at(i).rev()));
0176             }
0177         }
0178     }
0179 
0180     PimItem &item = pimItems.first();
0181 
0182     QSet<QByteArray> changes;
0183     qint64 partSizes = 0;
0184     qint64 size = 0;
0185 
0186     bool flagsChanged = false;
0187     bool tagsChanged = false;
0188 
0189     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) {
0190         if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) {
0191             return failureResponse("Unable to add item flags");
0192         }
0193     }
0194 
0195     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) {
0196         if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) {
0197             return failureResponse("Unable to remove item flags");
0198         }
0199     }
0200 
0201     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) {
0202         if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) {
0203             return failureResponse("Unable to reset flags");
0204         }
0205     }
0206 
0207     if (flagsChanged) {
0208         changes << AKONADI_PARAM_FLAGS;
0209     }
0210 
0211     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) {
0212         if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) {
0213             return failureResponse("Unable to add item tags");
0214         }
0215     }
0216 
0217     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) {
0218         if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) {
0219             return failureResponse("Unable to remove item tags");
0220         }
0221     }
0222 
0223     if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) {
0224         if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) {
0225             return failureResponse("Unable to reset item tags");
0226         }
0227     }
0228 
0229     if (tagsChanged) {
0230         changes << AKONADI_PARAM_TAGS;
0231     }
0232 
0233     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) {
0234         if (item.remoteId() != cmd.remoteId() && !cmd.remoteId().isEmpty()) {
0235             if (!connection()->isOwnerResource(item)) {
0236                 qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the remoteID for item" << item.id() << "from" << item.remoteId() << "to"
0237                                              << cmd.remoteId();
0238                 return failureResponse("Only resources can modify remote identifiers");
0239             }
0240             item.setRemoteId(cmd.remoteId());
0241             changes << AKONADI_PARAM_REMOTEID;
0242         }
0243     }
0244 
0245     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) {
0246         if (item.gid() != cmd.gid()) {
0247             item.setGid(cmd.gid());
0248         }
0249         changes << AKONADI_PARAM_GID;
0250     }
0251 
0252     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) {
0253         if (item.remoteRevision() != cmd.remoteRevision()) {
0254             if (!connection()->isOwnerResource(item)) {
0255                 return failureResponse("Only resources can modify remote revisions");
0256             }
0257             item.setRemoteRevision(cmd.remoteRevision());
0258             changes << AKONADI_PARAM_REMOTEREVISION;
0259         }
0260     }
0261 
0262     if (item.isValid() && !cmd.dirty()) {
0263         item.setDirty(false);
0264     }
0265 
0266     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) {
0267         size = cmd.itemSize();
0268         changes << AKONADI_PARAM_SIZE;
0269     }
0270 
0271     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) {
0272         const auto removedParts = cmd.removedParts();
0273         if (!removedParts.isEmpty()) {
0274             if (!store->removeItemParts(item, removedParts)) {
0275                 return failureResponse("Unable to remove item parts");
0276             }
0277             for (const QByteArray &part : removedParts) {
0278                 changes.insert(part);
0279             }
0280         }
0281     }
0282 
0283     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) {
0284         PartStreamer streamer(connection(), item);
0285         const auto partNames = cmd.parts();
0286         for (const QByteArray &partName : partNames) {
0287             qint64 partSize = 0;
0288             try {
0289                 streamer.stream(true, partName, partSize);
0290             } catch (const PartStreamerException &e) {
0291                 return failureResponse(e.what());
0292             }
0293 
0294             changes.insert(partName);
0295             partSizes += partSize;
0296         }
0297     }
0298 
0299     if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) {
0300         PartStreamer streamer(connection(), item);
0301         const Protocol::Attributes attrs = cmd.attributes();
0302         for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) {
0303             bool changed = false;
0304             try {
0305                 streamer.streamAttribute(true, iter.key(), iter.value(), &changed);
0306             } catch (const PartStreamerException &e) {
0307                 return failureResponse(e.what());
0308             }
0309 
0310             if (changed) {
0311                 changes.insert(iter.key());
0312             }
0313         }
0314     }
0315 
0316     QDateTime datetime;
0317     if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) {
0318         // update item size
0319         if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) {
0320             pimItems.first().setSize(qMax(size, partSizes));
0321         }
0322 
0323         const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID));
0324         const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION));
0325         const bool onlyRemoteIdAndRevisionChanged =
0326             (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID) && changes.contains(AKONADI_PARAM_REMOTEREVISION));
0327         const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS));
0328         const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID));
0329         // If only the remote id and/or the remote revision changed, we don't have to increase the REV,
0330         // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible
0331         const bool revisionNeedsUpdate =
0332             (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged);
0333 
0334         // run update query and prepare change notifications
0335         for (int i = 0; i < pimItems.count(); ++i) {
0336             PimItem &item = pimItems[i];
0337             if (revisionNeedsUpdate) {
0338                 item.setRev(item.rev() + 1);
0339             }
0340 
0341             item.setDatetime(modificationtime);
0342             item.setAtime(modificationtime);
0343             if (!connection()->isOwnerResource(item) && payloadChanged(changes)) {
0344                 item.setDirty(true);
0345             }
0346             if (!item.update()) {
0347                 return failureResponse("Unable to write item changes into the database");
0348             }
0349 
0350             if (cmd.invalidateCache()) {
0351                 if (!store->invalidateItemCache(item)) {
0352                     return failureResponse("Unable to invalidate item cache in the database");
0353                 }
0354             }
0355 
0356             // flags change notification went separately during command parsing
0357             // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened
0358             if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) {
0359                 // Don't send FLAGS notification in itemChanged
0360                 changes.remove(AKONADI_PARAM_FLAGS);
0361                 store->notificationCollector()->itemChanged(item, changes);
0362             }
0363 
0364             if (!cmd.noResponse()) {
0365                 Protocol::ModifyItemsResponse resp;
0366                 resp.setId(item.id());
0367                 resp.setNewRevision(item.rev());
0368                 sendResponse(std::move(resp));
0369             }
0370         }
0371 
0372         if (!transaction.commit()) {
0373             return failureResponse("Cannot commit transaction.");
0374         }
0375         // Always commit storage changes (deletion) after DB transaction
0376         storageTrx.commit();
0377 
0378         datetime = modificationtime;
0379     } else {
0380         datetime = pimItems.first().datetime();
0381     }
0382 
0383     Protocol::ModifyItemsResponse resp;
0384     resp.setModificationDateTime(datetime);
0385     return successResponse(std::move(resp));
0386 }