File indexing completed on 2024-05-26 05:14:35

0001 /***************************************************************************
0002  *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
0003  *                                                                         *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
0005  ***************************************************************************/
0006 
0007 #include "handlerhelper.h"
0008 #include "akonadi.h"
0009 #include "commandcontext.h"
0010 #include "connection.h"
0011 #include "handler.h"
0012 #include "storage/collectionqueryhelper.h"
0013 #include "storage/collectionstatistics.h"
0014 #include "storage/countquerybuilder.h"
0015 #include "storage/datastore.h"
0016 #include "storage/queryhelper.h"
0017 #include "storage/selectquerybuilder.h"
0018 #include "utils.h"
0019 
0020 #include "private/imapset_p.h"
0021 #include "private/protocol_p.h"
0022 #include "private/scope_p.h"
0023 
0024 using namespace Akonadi;
0025 using namespace Akonadi::Server;
0026 
0027 Collection HandlerHelper::collectionFromIdOrName(const QByteArray &id)
0028 {
0029     // id is a number
0030     bool ok = false;
0031     qint64 collectionId = id.toLongLong(&ok);
0032     if (ok) {
0033         return Collection::retrieveById(collectionId);
0034     }
0035 
0036     // id is a path
0037     QString path = QString::fromUtf8(id); // ### should be UTF-7 for real IMAP compatibility
0038 
0039     const QStringList pathParts = path.split(QLatin1Char('/'), Qt::SkipEmptyParts);
0040 
0041     Collection col;
0042     for (const QString &part : pathParts) {
0043         SelectQueryBuilder<Collection> qb;
0044         qb.addValueCondition(Collection::nameColumn(), Query::Equals, part);
0045         if (col.isValid()) {
0046             qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, col.id());
0047         } else {
0048             qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant());
0049         }
0050         if (!qb.exec()) {
0051             return Collection();
0052         }
0053         Collection::List list = qb.result();
0054         if (list.count() != 1) {
0055             return Collection();
0056         }
0057         col = list.first();
0058     }
0059     return col;
0060 }
0061 
0062 QString HandlerHelper::pathForCollection(const Collection &col)
0063 {
0064     QStringList parts;
0065     Collection current = col;
0066     while (current.isValid()) {
0067         parts.prepend(current.name());
0068         current = current.parent();
0069     }
0070     return parts.join(QLatin1Char('/'));
0071 }
0072 
0073 Protocol::CachePolicy HandlerHelper::cachePolicyResponse(const Collection &col)
0074 {
0075     Protocol::CachePolicy cachePolicy;
0076     cachePolicy.setInherit(col.cachePolicyInherit());
0077     cachePolicy.setCacheTimeout(col.cachePolicyCacheTimeout());
0078     cachePolicy.setCheckInterval(col.cachePolicyCheckInterval());
0079     if (!col.cachePolicyLocalParts().isEmpty()) {
0080         cachePolicy.setLocalParts(col.cachePolicyLocalParts().split(QLatin1Char(' ')));
0081     }
0082     cachePolicy.setSyncOnDemand(col.cachePolicySyncOnDemand());
0083     return cachePolicy;
0084 }
0085 
0086 Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(AkonadiServer &akonadi, const Collection &col)
0087 {
0088     QStringList mimeTypes;
0089     const auto mimeTypesList = col.mimeTypes();
0090     mimeTypes.reserve(mimeTypesList.size());
0091     for (const MimeType &mt : mimeTypesList) {
0092         mimeTypes << mt.name();
0093     }
0094 
0095     return fetchCollectionsResponse(akonadi, col, col.attributes(), false, 0, QStack<Collection>(), QStack<CollectionAttribute::List>(), mimeTypes);
0096 }
0097 
0098 Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(AkonadiServer &akonadi,
0099                                                                            const Collection &col,
0100                                                                            const CollectionAttribute::List &attrs,
0101                                                                            bool includeStatistics,
0102                                                                            int ancestorDepth,
0103                                                                            const QStack<Collection> &ancestors,
0104                                                                            const QStack<CollectionAttribute::List> &ancestorAttributes,
0105                                                                            const QStringList &mimeTypes)
0106 {
0107     Protocol::FetchCollectionsResponse response;
0108     response.setId(col.id());
0109     response.setParentId(col.parentId());
0110     response.setName(col.name());
0111     response.setMimeTypes(mimeTypes);
0112     response.setRemoteId(col.remoteId());
0113     response.setRemoteRevision(col.remoteRevision());
0114     response.setResource(col.resource().name());
0115     response.setIsVirtual(col.isVirtual());
0116 
0117     if (includeStatistics) {
0118         const auto stats = akonadi.collectionStatistics().statistics(col);
0119         if (stats.count > -1) {
0120             Protocol::FetchCollectionStatsResponse statsResponse;
0121             statsResponse.setCount(stats.count);
0122             statsResponse.setUnseen(stats.count - stats.read);
0123             statsResponse.setSize(stats.size);
0124             response.setStatistics(statsResponse);
0125         }
0126     }
0127 
0128     if (!col.queryString().isEmpty()) {
0129         response.setSearchQuery(col.queryString());
0130         QList<qint64> searchCols;
0131         const QStringList searchColIds = col.queryCollections().split(QLatin1Char(' '));
0132         searchCols.reserve(searchColIds.size());
0133         for (const QString &searchColId : searchColIds) {
0134             searchCols << searchColId.toLongLong();
0135         }
0136         response.setSearchCollections(searchCols);
0137     }
0138 
0139     Protocol::CachePolicy cachePolicy = cachePolicyResponse(col);
0140     response.setCachePolicy(cachePolicy);
0141 
0142     if (ancestorDepth) {
0143         QList<Protocol::Ancestor> ancestorList = HandlerHelper::ancestorsResponse(ancestorDepth, ancestors, ancestorAttributes);
0144         response.setAncestors(ancestorList);
0145     }
0146 
0147     response.setEnabled(col.enabled());
0148     response.setDisplayPref(static_cast<Tristate>(col.displayPref()));
0149     response.setSyncPref(static_cast<Tristate>(col.syncPref()));
0150     response.setIndexPref(static_cast<Tristate>(col.indexPref()));
0151 
0152     QMap<QByteArray, QByteArray> ra;
0153     for (const CollectionAttribute &attr : attrs) {
0154         ra.insert(attr.type(), attr.value());
0155     }
0156     response.setAttributes(ra);
0157 
0158     return response;
0159 }
0160 
0161 QList<Protocol::Ancestor>
0162 HandlerHelper::ancestorsResponse(int ancestorDepth, const QStack<Collection> &_ancestors, const QStack<CollectionAttribute::List> &_ancestorsAttributes)
0163 {
0164     QList<Protocol::Ancestor> rv;
0165     if (ancestorDepth > 0) {
0166         QStack<Collection> ancestors(_ancestors);
0167         QStack<CollectionAttribute::List> ancestorAttributes(_ancestorsAttributes);
0168         for (int i = 0; i < ancestorDepth; ++i) {
0169             if (ancestors.isEmpty()) {
0170                 Protocol::Ancestor ancestor;
0171                 ancestor.setId(0);
0172                 rv << ancestor;
0173                 break;
0174             }
0175             const Collection c = ancestors.pop();
0176             Protocol::Ancestor a;
0177             a.setId(c.id());
0178             a.setRemoteId(c.remoteId());
0179             a.setName(c.name());
0180             if (!ancestorAttributes.isEmpty()) {
0181                 QMap<QByteArray, QByteArray> attrs;
0182                 const auto ancestorAttrs = ancestorAttributes.pop();
0183                 for (const CollectionAttribute &attr : ancestorAttrs) {
0184                     attrs.insert(attr.type(), attr.value());
0185                 }
0186                 a.setAttributes(attrs);
0187             }
0188 
0189             rv << a;
0190         }
0191     }
0192 
0193     return rv;
0194 }
0195 
0196 Protocol::FetchTagsResponse HandlerHelper::fetchTagsResponse(const Tag &tag, const Protocol::TagFetchScope &tagFetchScope, Connection *connection)
0197 {
0198     Protocol::FetchTagsResponse response;
0199     response.setId(tag.id());
0200     if (tagFetchScope.fetchIdOnly()) {
0201         return response;
0202     }
0203 
0204     response.setType(tag.tagType().name().toUtf8());
0205     // FIXME FIXME FIXME Terrible hack to workaround limitations of the generated entities code:
0206     // The invalid parent is represented in code by -1 but in the DB it is stored as NULL, which
0207     // gets converted to 0 by our entities code.
0208     if (tag.parentId() == 0) {
0209         response.setParentId(-1);
0210     } else {
0211         response.setParentId(tag.parentId());
0212     }
0213     response.setGid(tag.gid().toUtf8());
0214     if (tagFetchScope.fetchRemoteID() && connection) {
0215         // Fail silently if retrieving tag RID is not allowed in current context
0216         if (connection->context().resource().isValid()) {
0217             QueryBuilder qb(TagRemoteIdResourceRelation::tableName());
0218             qb.addColumn(TagRemoteIdResourceRelation::remoteIdColumn());
0219             qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, connection->context().resource().id());
0220             qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tag.id());
0221             if (!qb.exec()) {
0222                 throw HandlerException("Unable to query Tag Remote ID");
0223             }
0224             QSqlQuery query = qb.query();
0225             // RID may not be available
0226             if (query.next()) {
0227                 response.setRemoteId(Utils::variantToByteArray(query.value(0)));
0228             }
0229             query.finish();
0230         }
0231     }
0232 
0233     if (tagFetchScope.fetchAllAttributes() || !tagFetchScope.attributes().isEmpty()) {
0234         QueryBuilder qb(TagAttribute::tableName());
0235         qb.addColumns({TagAttribute::typeFullColumnName(), TagAttribute::valueFullColumnName()});
0236         Query::Condition cond(Query::And);
0237         cond.addValueCondition(TagAttribute::tagIdFullColumnName(), Query::Equals, tag.id());
0238         if (!tagFetchScope.fetchAllAttributes() && !tagFetchScope.attributes().isEmpty()) {
0239             QVariantList types;
0240             const auto scope = tagFetchScope.attributes();
0241             std::transform(scope.cbegin(), scope.cend(), std::back_inserter(types), [](const QByteArray &ba) {
0242                 return QVariant(ba);
0243             });
0244             cond.addValueCondition(TagAttribute::typeFullColumnName(), Query::In, types);
0245         }
0246         qb.addCondition(cond);
0247         if (!qb.exec()) {
0248             throw HandlerException("Unable to query Tag Attributes");
0249         }
0250         QSqlQuery query = qb.query();
0251         Protocol::Attributes attributes;
0252         while (query.next()) {
0253             attributes.insert(Utils::variantToByteArray(query.value(0)), Utils::variantToByteArray(query.value(1)));
0254         }
0255         query.finish();
0256         response.setAttributes(attributes);
0257     }
0258 
0259     return response;
0260 }
0261 
0262 Protocol::FetchRelationsResponse HandlerHelper::fetchRelationsResponse(const Relation &relation)
0263 {
0264     Protocol::FetchRelationsResponse resp;
0265     resp.setLeft(relation.leftId());
0266     resp.setLeftMimeType(relation.left().mimeType().name().toUtf8());
0267     resp.setRight(relation.rightId());
0268     resp.setRightMimeType(relation.right().mimeType().name().toUtf8());
0269     resp.setType(relation.relationType().name().toUtf8());
0270     return resp;
0271 }
0272 
0273 Flag::List HandlerHelper::resolveFlags(const QSet<QByteArray> &flagNames)
0274 {
0275     Flag::List flagList;
0276     flagList.reserve(flagNames.size());
0277     for (const QByteArray &flagName : flagNames) {
0278         const Flag flag = Flag::retrieveByNameOrCreate(QString::fromUtf8(flagName));
0279         if (!flag.isValid()) {
0280             throw HandlerException("Unable to create flag");
0281         }
0282         flagList.append(flag);
0283     }
0284     return flagList;
0285 }
0286 
0287 Tag::List HandlerHelper::resolveTagsByUID(const ImapSet &tags)
0288 {
0289     if (tags.isEmpty()) {
0290         return Tag::List();
0291     }
0292     SelectQueryBuilder<Tag> qb;
0293     QueryHelper::setToQuery(tags, Tag::idFullColumnName(), qb);
0294     if (!qb.exec()) {
0295         throw HandlerException("Unable to resolve tags");
0296     }
0297     const Tag::List result = qb.result();
0298     if (result.isEmpty()) {
0299         throw HandlerException("No tags found");
0300     }
0301     return result;
0302 }
0303 
0304 Tag::List HandlerHelper::resolveTagsByGID(const QStringList &tagsGIDs)
0305 {
0306     Tag::List tagList;
0307     if (tagsGIDs.isEmpty()) {
0308         return tagList;
0309     }
0310 
0311     for (const QString &tagGID : tagsGIDs) {
0312         Tag::List tags = Tag::retrieveFiltered(Tag::gidColumn(), tagGID);
0313         Tag tag;
0314         if (tags.isEmpty()) {
0315             tag.setGid(tagGID);
0316             tag.setParentId(0);
0317 
0318             const TagType type = TagType::retrieveByNameOrCreate(QStringLiteral("PLAIN"));
0319             if (!type.isValid()) {
0320                 throw HandlerException("Unable to create tag type");
0321             }
0322             tag.setTagType(type);
0323             if (!tag.insert()) {
0324                 throw HandlerException("Unable to create tag");
0325             }
0326         } else if (tags.count() == 1) {
0327             tag = tags[0];
0328         } else {
0329             // Should not happen
0330             throw HandlerException("Tag GID is not unique");
0331         }
0332 
0333         tagList.append(tag);
0334     }
0335 
0336     return tagList;
0337 }
0338 
0339 Tag::List HandlerHelper::resolveTagsByRID(const QStringList &tagsRIDs, const CommandContext &context)
0340 {
0341     Tag::List tags;
0342     if (tagsRIDs.isEmpty()) {
0343         return tags;
0344     }
0345 
0346     if (!context.resource().isValid()) {
0347         throw HandlerException("Tags can be resolved by their RID only in resource context");
0348     }
0349 
0350     tags.reserve(tagsRIDs.size());
0351     for (const QString &tagRID : tagsRIDs) {
0352         SelectQueryBuilder<Tag> qb;
0353         Query::Condition cond;
0354         cond.addColumnCondition(Tag::idFullColumnName(), Query::Equals, TagRemoteIdResourceRelation::tagIdFullColumnName());
0355         cond.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context.resource().id());
0356         qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), cond);
0357         qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::Equals, tagRID);
0358         if (!qb.exec()) {
0359             throw HandlerException("Unable to resolve tags");
0360         }
0361 
0362         Tag tag;
0363         Tag::List results = qb.result();
0364         if (results.isEmpty()) {
0365             // If the tag does not exist, we create a new one with GID matching RID
0366             Tag::List tags = resolveTagsByGID(QStringList() << tagRID);
0367             if (tags.count() != 1) {
0368                 throw HandlerException("Unable to resolve tag");
0369             }
0370             tag = tags[0];
0371             TagRemoteIdResourceRelation rel;
0372             rel.setRemoteId(tagRID);
0373             rel.setTagId(tag.id());
0374             rel.setResourceId(context.resource().id());
0375             if (!rel.insert()) {
0376                 throw HandlerException("Unable to create tag");
0377             }
0378         } else if (results.count() == 1) {
0379             tag = results[0];
0380         } else {
0381             throw HandlerException("Tag RID is not unique within this resource context");
0382         }
0383 
0384         tags.append(tag);
0385     }
0386 
0387     return tags;
0388 }
0389 
0390 Collection HandlerHelper::collectionFromScope(const Scope &scope, const CommandContext &context)
0391 {
0392     if (scope.scope() == Scope::Invalid || scope.scope() == Scope::Gid) {
0393         throw HandlerException("Invalid collection scope");
0394     }
0395 
0396     SelectQueryBuilder<Collection> qb;
0397     CollectionQueryHelper::scopeToQuery(scope, context, qb);
0398     if (!qb.exec()) {
0399         throw HandlerException("Failed to execute SQL query");
0400     }
0401 
0402     const Collection::List c = qb.result();
0403     if (c.isEmpty()) {
0404         return Collection();
0405     } else if (c.count() == 1) {
0406         return c.at(0);
0407     } else {
0408         throw HandlerException("Query returned more than one result");
0409     }
0410 }
0411 
0412 Tag::List HandlerHelper::tagsFromScope(const Scope &scope, const CommandContext &context)
0413 {
0414     if (scope.scope() == Scope::Invalid || scope.scope() == Scope::HierarchicalRid) {
0415         throw HandlerException("Invalid tag scope");
0416     }
0417 
0418     if (scope.scope() == Scope::Uid) {
0419         return resolveTagsByUID(scope.uidSet());
0420     } else if (scope.scope() == Scope::Gid) {
0421         return resolveTagsByGID(scope.gidSet());
0422     } else if (scope.scope() == Scope::Rid) {
0423         return resolveTagsByRID(scope.ridSet(), context);
0424     }
0425 
0426     Q_ASSERT(false);
0427     return Tag::List();
0428 }