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