1 /*
2     SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "xmldocument.h"
8 #include "format_p.h"
9 #include "xmlreader.h"
10 
11 #include <KLocalizedString>
12 
13 #include <QFile>
14 #include <qdom.h>
15 
16 #ifdef HAVE_LIBXML2
17 #include <QStandardPaths>
18 #include <libxml/parser.h>
19 #include <libxml/xmlIO.h>
20 #include <libxml/xmlschemas.h>
21 #endif
22 
23 using namespace Akonadi;
24 
25 // helper class for dealing with libxml resource management
26 template<typename T, void FreeFunc(T)> class XmlPtr
27 {
28 public:
XmlPtr(const T & t)29     explicit XmlPtr(const T &t)
30         : p(t)
31     {
32     }
33 
~XmlPtr()34     ~XmlPtr()
35     {
36         FreeFunc(p);
37     }
38 
operator T() const39     operator T() const // NOLINT(google-explicit-constructor)
40     {
41         return p;
42     }
43 
operator bool() const44     explicit operator bool() const
45     {
46         return p != nullptr;
47     }
48 
49 private:
50     Q_DISABLE_COPY(XmlPtr)
51     T p;
52 };
53 
findElementByRidHelper(const QDomElement & elem,const QString & rid,const QString & elemName)54 static QDomElement findElementByRidHelper(const QDomElement &elem, const QString &rid, const QString &elemName)
55 {
56     if (elem.isNull()) {
57         return QDomElement();
58     }
59     if (elem.tagName() == elemName && elem.attribute(Format::Attr::remoteId()) == rid) {
60         return elem;
61     }
62     const QDomNodeList children = elem.childNodes();
63     for (int i = 0; i < children.count(); ++i) {
64         const QDomElement child = children.at(i).toElement();
65         if (child.isNull()) {
66             continue;
67         }
68         const QDomElement rv = findElementByRidHelper(child, rid, elemName);
69         if (!rv.isNull()) {
70             return rv;
71         }
72     }
73     return QDomElement();
74 }
75 
76 namespace Akonadi
77 {
78 class XmlDocumentPrivate
79 {
80 public:
XmlDocumentPrivate()81     XmlDocumentPrivate()
82         : valid(false)
83     {
84         lastError = i18n("No data loaded.");
85     }
86 
findElementByRid(const QString & rid,const QString & elemName) const87     QDomElement findElementByRid(const QString &rid, const QString &elemName) const
88     {
89         return findElementByRidHelper(document.documentElement(), rid, elemName);
90     }
91 
92     QDomDocument document;
93     QString lastError;
94     bool valid;
95 };
96 
97 } // namespace Akonadi
98 
XmlDocument()99 XmlDocument::XmlDocument()
100     : d(new XmlDocumentPrivate)
101 {
102     const QDomElement rootElem = d->document.createElement(Format::Tag::root());
103     d->document.appendChild(rootElem);
104 }
105 
XmlDocument(const QString & fileName)106 XmlDocument::XmlDocument(const QString &fileName)
107     : d(new XmlDocumentPrivate)
108 {
109     loadFile(fileName);
110 }
111 
112 XmlDocument::~XmlDocument() = default;
113 
loadFile(const QString & fileName)114 bool Akonadi::XmlDocument::loadFile(const QString &fileName)
115 {
116     d->valid = false;
117     d->document = QDomDocument();
118 
119     if (fileName.isEmpty()) {
120         d->lastError = i18n("No filename specified");
121         return false;
122     }
123 
124     QFile file(fileName);
125     QByteArray data;
126     if (file.exists()) {
127         if (!file.open(QIODevice::ReadOnly)) {
128             d->lastError = i18n("Unable to open data file '%1'.", fileName);
129             return false;
130         }
131         data = file.readAll();
132     } else {
133         d->lastError = i18n("File %1 does not exist.", fileName);
134         return false;
135     }
136 
137 #ifdef HAVE_LIBXML2
138     // schema validation
139     XmlPtr<xmlDocPtr, xmlFreeDoc> sourceDoc(xmlParseMemory(data.constData(), data.length()));
140     if (!sourceDoc) {
141         d->lastError = i18n("Unable to parse data file '%1'.", fileName);
142         return false;
143     }
144 
145     const QString &schemaFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/akonadi/akonadi-xml.xsd"));
146     XmlPtr<xmlDocPtr, xmlFreeDoc> schemaDoc(xmlReadFile(schemaFileName.toLocal8Bit().constData(), nullptr, XML_PARSE_NONET));
147     if (!schemaDoc) {
148         d->lastError = i18n("Schema definition could not be loaded and parsed.");
149         return false;
150     }
151     XmlPtr<xmlSchemaParserCtxtPtr, xmlSchemaFreeParserCtxt> parserContext(xmlSchemaNewDocParserCtxt(schemaDoc));
152     if (!parserContext) {
153         d->lastError = i18n("Unable to create schema parser context.");
154         return false;
155     }
156     XmlPtr<xmlSchemaPtr, xmlSchemaFree> schema(xmlSchemaParse(parserContext));
157     if (!schema) {
158         d->lastError = i18n("Unable to create schema.");
159         return false;
160     }
161     XmlPtr<xmlSchemaValidCtxtPtr, xmlSchemaFreeValidCtxt> validationContext(xmlSchemaNewValidCtxt(schema));
162     if (!validationContext) {
163         d->lastError = i18n("Unable to create schema validation context.");
164         return false;
165     }
166 
167     if (xmlSchemaValidateDoc(validationContext, sourceDoc) != 0) {
168         d->lastError = i18n("Invalid file format.");
169         return false;
170     }
171 #endif
172 
173     // DOM loading
174     QString errMsg;
175     if (!d->document.setContent(data, true, &errMsg)) {
176         d->lastError = i18n("Unable to parse data file: %1", errMsg);
177         return false;
178     }
179 
180     d->valid = true;
181     d->lastError.clear();
182     return true;
183 }
184 
writeToFile(const QString & fileName) const185 bool XmlDocument::writeToFile(const QString &fileName) const
186 {
187     QFile f(fileName);
188     if (!f.open(QFile::WriteOnly)) {
189         d->lastError = f.errorString();
190         return false;
191     }
192 
193     f.write(d->document.toByteArray(2));
194 
195     d->lastError.clear();
196     return true;
197 }
198 
isValid() const199 bool XmlDocument::isValid() const
200 {
201     return d->valid;
202 }
203 
lastError() const204 QString XmlDocument::lastError() const
205 {
206     return d->lastError;
207 }
208 
document() const209 QDomDocument &XmlDocument::document() const
210 {
211     return d->document;
212 }
213 
collectionElement(const Collection & collection) const214 QDomElement XmlDocument::collectionElement(const Collection &collection) const
215 {
216     if (collection == Collection::root()) {
217         return d->document.documentElement();
218     }
219     if (collection.remoteId().isEmpty()) {
220         return QDomElement();
221     }
222     if (collection.parentCollection().remoteId().isEmpty() && collection.parentCollection() != Collection::root()) {
223         return d->findElementByRid(collection.remoteId(), Format::Tag::collection());
224     }
225     QDomElement parent = collectionElement(collection.parentCollection());
226     if (parent.isNull()) {
227         return QDomElement();
228     }
229     const QDomNodeList children = parent.childNodes();
230     for (int i = 0; i < children.count(); ++i) {
231         const QDomElement child = children.at(i).toElement();
232         if (child.isNull()) {
233             continue;
234         }
235         if (child.tagName() == Format::Tag::collection() && child.attribute(Format::Attr::remoteId()) == collection.remoteId()) {
236             return child;
237         }
238     }
239     return QDomElement();
240 }
241 
itemElementByRemoteId(const QString & rid) const242 QDomElement XmlDocument::itemElementByRemoteId(const QString &rid) const
243 {
244     return d->findElementByRid(rid, Format::Tag::item());
245 }
246 
collectionElementByRemoteId(const QString & rid) const247 QDomElement XmlDocument::collectionElementByRemoteId(const QString &rid) const
248 {
249     return d->findElementByRid(rid, Format::Tag::collection());
250 }
251 
collectionByRemoteId(const QString & rid) const252 Collection XmlDocument::collectionByRemoteId(const QString &rid) const
253 {
254     const QDomElement elem = d->findElementByRid(rid, Format::Tag::collection());
255     return XmlReader::elementToCollection(elem);
256 }
257 
itemByRemoteId(const QString & rid,bool includePayload) const258 Item XmlDocument::itemByRemoteId(const QString &rid, bool includePayload) const
259 {
260     return XmlReader::elementToItem(itemElementByRemoteId(rid), includePayload);
261 }
262 
collections() const263 Collection::List XmlDocument::collections() const
264 {
265     return XmlReader::readCollections(d->document.documentElement());
266 }
267 
tags() const268 Tag::List XmlDocument::tags() const
269 {
270     return XmlReader::readTags(d->document.documentElement());
271 }
272 
childCollections(const Collection & parentCollection) const273 Collection::List XmlDocument::childCollections(const Collection &parentCollection) const
274 {
275     QDomElement parentElem = collectionElement(parentCollection);
276 
277     if (parentElem.isNull()) {
278         d->lastError = QStringLiteral("Parent node not found.");
279         return Collection::List();
280     }
281 
282     Collection::List rv;
283     const QDomNodeList children = parentElem.childNodes();
284     for (int i = 0; i < children.count(); ++i) {
285         const QDomElement childElem = children.at(i).toElement();
286         if (childElem.isNull() || childElem.tagName() != Format::Tag::collection()) {
287             continue;
288         }
289         Collection c = XmlReader::elementToCollection(childElem);
290         c.setParentCollection(parentCollection);
291         rv.append(c);
292     }
293 
294     return rv;
295 }
296 
items(const Akonadi::Collection & collection,bool includePayload) const297 Item::List XmlDocument::items(const Akonadi::Collection &collection, bool includePayload) const
298 {
299     const QDomElement colElem = collectionElement(collection);
300     if (colElem.isNull()) {
301         d->lastError = i18n("Unable to find collection %1", collection.name());
302         return Item::List();
303     } else {
304         d->lastError.clear();
305     }
306 
307     Item::List items;
308     const QDomNodeList children = colElem.childNodes();
309     for (int i = 0; i < children.count(); ++i) {
310         const QDomElement itemElem = children.at(i).toElement();
311         if (itemElem.isNull() || itemElem.tagName() != Format::Tag::item()) {
312             continue;
313         }
314         items += XmlReader::elementToItem(itemElem, includePayload);
315     }
316 
317     return items;
318 }
319