File indexing completed on 2025-01-05 04:47:12

0001 /*
0002     SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "xmldocument.h"
0008 #include "format_p.h"
0009 #include "xmlreader.h"
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <QFile>
0014 #include <qdom.h>
0015 
0016 #ifdef HAVE_LIBXML2
0017 #include <QStandardPaths>
0018 #include <libxml/parser.h>
0019 #include <libxml/xmlIO.h>
0020 #include <libxml/xmlschemas.h>
0021 #endif
0022 
0023 using namespace Akonadi;
0024 
0025 // helper class for dealing with libxml resource management
0026 template<typename T, void FreeFunc(T)>
0027 class XmlPtr
0028 {
0029 public:
0030     explicit XmlPtr(const T &t)
0031         : p(t)
0032     {
0033     }
0034 
0035     ~XmlPtr()
0036     {
0037         FreeFunc(p);
0038     }
0039 
0040     operator T() const // NOLINT(google-explicit-constructor)
0041     {
0042         return p;
0043     }
0044 
0045     explicit operator bool() const
0046     {
0047         return p != nullptr;
0048     }
0049 
0050 private:
0051     Q_DISABLE_COPY(XmlPtr)
0052     T p;
0053 };
0054 
0055 static QDomElement findElementByRidHelper(const QDomElement &elem, const QString &rid, const QString &elemName)
0056 {
0057     if (elem.isNull()) {
0058         return QDomElement();
0059     }
0060     if (elem.tagName() == elemName && elem.attribute(Format::Attr::remoteId()) == rid) {
0061         return elem;
0062     }
0063     const QDomNodeList children = elem.childNodes();
0064     for (int i = 0; i < children.count(); ++i) {
0065         const QDomElement child = children.at(i).toElement();
0066         if (child.isNull()) {
0067             continue;
0068         }
0069         const QDomElement rv = findElementByRidHelper(child, rid, elemName);
0070         if (!rv.isNull()) {
0071             return rv;
0072         }
0073     }
0074     return QDomElement();
0075 }
0076 
0077 namespace Akonadi
0078 {
0079 class XmlDocumentPrivate
0080 {
0081 public:
0082     XmlDocumentPrivate()
0083         : lastError(i18n("No data loaded."))
0084         , valid(false)
0085     {
0086     }
0087 
0088     QDomElement findElementByRid(const QString &rid, const QString &elemName) const
0089     {
0090         return findElementByRidHelper(document.documentElement(), rid, elemName);
0091     }
0092 
0093     QDomDocument document;
0094     QString lastError;
0095     bool valid;
0096 };
0097 
0098 } // namespace Akonadi
0099 
0100 XmlDocument::XmlDocument()
0101     : d(new XmlDocumentPrivate)
0102 {
0103     const QDomElement rootElem = d->document.createElement(Format::Tag::root());
0104     d->document.appendChild(rootElem);
0105 }
0106 
0107 XmlDocument::XmlDocument(const QString &fileName, const QString &xsdFile)
0108     : d(new XmlDocumentPrivate)
0109 {
0110     loadFile(fileName, xsdFile);
0111 }
0112 
0113 XmlDocument::~XmlDocument() = default;
0114 
0115 bool Akonadi::XmlDocument::loadFile(const QString &fileName, const QString &xsdFile)
0116 {
0117     d->valid = false;
0118     d->document = QDomDocument();
0119 
0120     if (fileName.isEmpty()) {
0121         d->lastError = i18n("No filename specified");
0122         return false;
0123     }
0124 
0125     QFile file(fileName);
0126     QByteArray data;
0127     if (file.exists()) {
0128         if (!file.open(QIODevice::ReadOnly)) {
0129             d->lastError = i18n("Unable to open data file '%1'.", fileName);
0130             return false;
0131         }
0132         data = file.readAll();
0133     } else {
0134         d->lastError = i18n("File %1 does not exist.", fileName);
0135         return false;
0136     }
0137 
0138 #ifdef HAVE_LIBXML2
0139     // schema validation
0140     XmlPtr<xmlDocPtr, xmlFreeDoc> sourceDoc(xmlParseMemory(data.constData(), data.length()));
0141     if (!sourceDoc) {
0142         d->lastError = i18n("Unable to parse data file '%1'.", fileName);
0143         return false;
0144     }
0145 
0146     const QString &schemaFileName =
0147         xsdFile.isEmpty() ? QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf6/akonadi/akonadi-xml.xsd")) : xsdFile;
0148     const XmlPtr<xmlDocPtr, xmlFreeDoc> schemaDoc(xmlReadFile(schemaFileName.toLocal8Bit().constData(), nullptr, XML_PARSE_NONET));
0149     if (!schemaDoc) {
0150         d->lastError = i18n("Schema definition could not be loaded and parsed.");
0151         return false;
0152     }
0153     XmlPtr<xmlSchemaParserCtxtPtr, xmlSchemaFreeParserCtxt> parserContext(xmlSchemaNewDocParserCtxt(schemaDoc));
0154     if (!parserContext) {
0155         d->lastError = i18n("Unable to create schema parser context.");
0156         return false;
0157     }
0158     const XmlPtr<xmlSchemaPtr, xmlSchemaFree> schema(xmlSchemaParse(parserContext));
0159     if (!schema) {
0160         d->lastError = i18n("Unable to create schema.");
0161         return false;
0162     }
0163     const XmlPtr<xmlSchemaValidCtxtPtr, xmlSchemaFreeValidCtxt> validationContext(xmlSchemaNewValidCtxt(schema));
0164     if (!validationContext) {
0165         d->lastError = i18n("Unable to create schema validation context.");
0166         return false;
0167     }
0168 
0169     if (xmlSchemaValidateDoc(validationContext, sourceDoc) != 0) {
0170         d->lastError = i18n("Invalid file format.");
0171         return false;
0172     }
0173 #endif
0174 
0175     // DOM loading
0176     QString errMsg;
0177     if (!d->document.setContent(data, true, &errMsg)) {
0178         d->lastError = i18n("Unable to parse data file: %1", errMsg);
0179         return false;
0180     }
0181 
0182     d->valid = true;
0183     d->lastError.clear();
0184     return true;
0185 }
0186 
0187 bool XmlDocument::writeToFile(const QString &fileName) const
0188 {
0189     QFile f(fileName);
0190     if (!f.open(QFile::WriteOnly)) {
0191         d->lastError = f.errorString();
0192         return false;
0193     }
0194 
0195     f.write(d->document.toByteArray(2));
0196 
0197     d->lastError.clear();
0198     return true;
0199 }
0200 
0201 bool XmlDocument::isValid() const
0202 {
0203     return d->valid;
0204 }
0205 
0206 QString XmlDocument::lastError() const
0207 {
0208     return d->lastError;
0209 }
0210 
0211 QDomDocument &XmlDocument::document() const
0212 {
0213     return d->document;
0214 }
0215 
0216 QDomElement XmlDocument::collectionElement(const Collection &collection) const
0217 {
0218     if (collection == Collection::root()) {
0219         return d->document.documentElement();
0220     }
0221     if (collection.remoteId().isEmpty()) {
0222         return QDomElement();
0223     }
0224     if (collection.parentCollection().remoteId().isEmpty() && collection.parentCollection() != Collection::root()) {
0225         return d->findElementByRid(collection.remoteId(), Format::Tag::collection());
0226     }
0227     QDomElement parent = collectionElement(collection.parentCollection());
0228     if (parent.isNull()) {
0229         return QDomElement();
0230     }
0231     const QDomNodeList children = parent.childNodes();
0232     for (int i = 0; i < children.count(); ++i) {
0233         const QDomElement child = children.at(i).toElement();
0234         if (child.isNull()) {
0235             continue;
0236         }
0237         if (child.tagName() == Format::Tag::collection() && child.attribute(Format::Attr::remoteId()) == collection.remoteId()) {
0238             return child;
0239         }
0240     }
0241     return QDomElement();
0242 }
0243 
0244 QDomElement XmlDocument::itemElementByRemoteId(const QString &rid) const
0245 {
0246     return d->findElementByRid(rid, Format::Tag::item());
0247 }
0248 
0249 QDomElement XmlDocument::collectionElementByRemoteId(const QString &rid) const
0250 {
0251     return d->findElementByRid(rid, Format::Tag::collection());
0252 }
0253 
0254 Collection XmlDocument::collectionByRemoteId(const QString &rid) const
0255 {
0256     const QDomElement elem = d->findElementByRid(rid, Format::Tag::collection());
0257     return XmlReader::elementToCollection(elem);
0258 }
0259 
0260 Item XmlDocument::itemByRemoteId(const QString &rid, bool includePayload) const
0261 {
0262     return XmlReader::elementToItem(itemElementByRemoteId(rid), includePayload);
0263 }
0264 
0265 Collection::List XmlDocument::collections() const
0266 {
0267     return XmlReader::readCollections(d->document.documentElement());
0268 }
0269 
0270 Tag::List XmlDocument::tags() const
0271 {
0272     return XmlReader::readTags(d->document.documentElement());
0273 }
0274 
0275 Collection::List XmlDocument::childCollections(const Collection &parentCollection) const
0276 {
0277     QDomElement parentElem = collectionElement(parentCollection);
0278 
0279     if (parentElem.isNull()) {
0280         d->lastError = QStringLiteral("Parent node not found.");
0281         return Collection::List();
0282     }
0283 
0284     Collection::List rv;
0285     const QDomNodeList children = parentElem.childNodes();
0286     for (int i = 0; i < children.count(); ++i) {
0287         const QDomElement childElem = children.at(i).toElement();
0288         if (childElem.isNull() || childElem.tagName() != Format::Tag::collection()) {
0289             continue;
0290         }
0291         Collection c = XmlReader::elementToCollection(childElem);
0292         c.setParentCollection(parentCollection);
0293         rv.append(c);
0294     }
0295 
0296     return rv;
0297 }
0298 
0299 Item::List XmlDocument::items(const Akonadi::Collection &collection, bool includePayload) const
0300 {
0301     const QDomElement colElem = collectionElement(collection);
0302     if (colElem.isNull()) {
0303         d->lastError = i18n("Unable to find collection %1", collection.name());
0304         return Item::List();
0305     } else {
0306         d->lastError.clear();
0307     }
0308 
0309     Item::List items;
0310     const QDomNodeList children = colElem.childNodes();
0311     for (int i = 0; i < children.count(); ++i) {
0312         const QDomElement itemElem = children.at(i).toElement();
0313         if (itemElem.isNull() || itemElem.tagName() != Format::Tag::item()) {
0314             continue;
0315         }
0316         items += XmlReader::elementToItem(itemElem, includePayload);
0317     }
0318 
0319     return items;
0320 }