1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
4 SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
5 SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kxmlguiversionhandler_p.h"
11
12#include "kxmlguiclient.h"
13#include "kxmlguifactory.h"
14
15#include <QDomDocument>
16#include <QDomElement>
17#include <QFile>
18#include <QMap>
19#include <QStandardPaths>
20
21struct DocStruct {
22 QString file;
23 QString data;
24};
25
26static QList<QDomElement> extractToolBars(const QDomDocument &doc)
27{
28 QList<QDomElement> toolbars;
29 QDomElement parent = doc.documentElement();
30 for (QDomElement e = parent.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
31 if (e.tagName().compare(QStringLiteral("ToolBar"), cs: Qt::CaseInsensitive) == 0) {
32 toolbars.append(t: e);
33 }
34 }
35 return toolbars;
36}
37
38static QStringList toolBarNames(const QList<QDomElement> &toolBars)
39{
40 QStringList names;
41 names.reserve(asize: toolBars.count());
42 for (const QDomElement &e : toolBars) {
43 names.append(t: e.attribute(QStringLiteral("name")));
44 }
45 return names;
46}
47
48static void removeToolBars(QDomDocument &doc, const QStringList &toolBarNames)
49{
50 QDomElement parent = doc.documentElement();
51 const QList<QDomElement> toolBars = extractToolBars(doc);
52 for (const QDomElement &e : toolBars) {
53 if (toolBarNames.contains(str: e.attribute(QStringLiteral("name")))) {
54 parent.removeChild(oldChild: e);
55 }
56 }
57}
58
59static void insertToolBars(QDomDocument &doc, const QList<QDomElement> &toolBars)
60{
61 QDomElement parent = doc.documentElement();
62 QDomElement menuBar = parent.namedItem(QStringLiteral("MenuBar")).toElement();
63 QDomElement insertAfter = menuBar;
64 if (menuBar.isNull()) {
65 insertAfter = parent.firstChildElement(); // if null, insertAfter will do an append
66 }
67 for (const QDomElement &e : toolBars) {
68 QDomNode result = parent.insertAfter(newChild: e, refChild: insertAfter);
69 Q_ASSERT(!result.isNull());
70 }
71}
72
73//
74
75typedef QMap<QString, QMap<QString, QString>> ActionPropertiesMap;
76
77static ActionPropertiesMap extractActionProperties(const QDomDocument &doc)
78{
79 ActionPropertiesMap properties;
80
81 QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
82
83 if (actionPropElement.isNull()) {
84 return properties;
85 }
86
87 QDomNode n = actionPropElement.firstChild();
88 while (!n.isNull()) {
89 QDomElement e = n.toElement();
90 n = n.nextSibling(); // Advance now so that we can safely delete e
91 if (e.isNull()) {
92 continue;
93 }
94
95 if (e.tagName().compare(QStringLiteral("action"), cs: Qt::CaseInsensitive) != 0) {
96 continue;
97 }
98
99 const QString actionName = e.attribute(QStringLiteral("name"));
100 if (actionName.isEmpty()) {
101 continue;
102 }
103
104 QMap<QString, QMap<QString, QString>>::Iterator propIt = properties.find(key: actionName);
105 if (propIt == properties.end()) {
106 propIt = properties.insert(key: actionName, value: QMap<QString, QString>());
107 }
108
109 const QDomNamedNodeMap attributes = e.attributes();
110 const int attributeslength = attributes.length();
111
112 for (int i = 0; i < attributeslength; ++i) {
113 const QDomAttr attr = attributes.item(index: i).toAttr();
114
115 if (attr.isNull()) {
116 continue;
117 }
118
119 const QString name = attr.name();
120
121 if (name == QLatin1String("name") || name.isEmpty()) {
122 continue;
123 }
124
125 (*propIt)[name] = attr.value();
126 }
127 }
128
129 return properties;
130}
131
132static void storeActionProperties(QDomDocument &doc, const ActionPropertiesMap &properties)
133{
134 QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
135
136 if (actionPropElement.isNull()) {
137 actionPropElement = doc.createElement(QStringLiteral("ActionProperties"));
138 doc.documentElement().appendChild(newChild: actionPropElement);
139 }
140
141 // Remove only those ActionProperties entries from the document, that are present
142 // in the properties argument. In real life this means that local ActionProperties
143 // takes precedence over global ones, if they exists (think local override of shortcuts).
144 QDomNode actionNode = actionPropElement.firstChild();
145 while (!actionNode.isNull()) {
146 if (properties.contains(key: actionNode.toElement().attribute(QStringLiteral("name")))) {
147 QDomNode nextNode = actionNode.nextSibling();
148 actionPropElement.removeChild(oldChild: actionNode);
149 actionNode = nextNode;
150 } else {
151 actionNode = actionNode.nextSibling();
152 }
153 }
154
155 ActionPropertiesMap::ConstIterator it = properties.begin();
156 const ActionPropertiesMap::ConstIterator end = properties.end();
157 for (; it != end; ++it) {
158 QDomElement action = doc.createElement(QStringLiteral("Action"));
159 action.setAttribute(QStringLiteral("name"), value: it.key());
160 actionPropElement.appendChild(newChild: action);
161
162 const QMap<QString, QString> attributes = (*it);
163 QMap<QString, QString>::ConstIterator attrIt = attributes.begin();
164 const QMap<QString, QString>::ConstIterator attrEnd = attributes.end();
165 for (; attrIt != attrEnd; ++attrIt) {
166 action.setAttribute(name: attrIt.key(), value: attrIt.value());
167 }
168 }
169}
170
171KXmlGuiVersionHandler::KXmlGuiVersionHandler(const QStringList &files)
172{
173 Q_ASSERT(!files.isEmpty());
174
175 if (files.count() == 1) {
176 // No need to parse version numbers if there's only one file anyway
177 m_file = files.first();
178 m_doc = KXMLGUIFactory::readConfigFile(filename: m_file);
179 return;
180 }
181
182 std::vector<DocStruct> allDocuments;
183 allDocuments.reserve(n: files.size());
184
185 for (const QString &file : files) {
186 allDocuments.push_back(x: {.file: file, .data: KXMLGUIFactory::readConfigFile(filename: file)});
187 }
188
189 auto best = allDocuments.end();
190 uint bestVersion = 0;
191
192 auto docIt = allDocuments.begin();
193 const auto docEnd = allDocuments.end();
194 for (; docIt != docEnd; ++docIt) {
195 const QString versionStr = KXMLGUIClient::findVersionNumber(xml: (*docIt).data);
196 if (versionStr.isEmpty()) {
197 // qCDebug(DEBUG_KXMLGUI) << "found no version in" << (*docIt).file;
198 continue;
199 }
200
201 bool ok = false;
202 uint version = versionStr.toUInt(ok: &ok);
203 if (!ok) {
204 continue;
205 }
206 // qCDebug(DEBUG_KXMLGUI) << "found version" << version << "for" << (*docIt).file;
207
208 if (version > bestVersion) {
209 best = docIt;
210 // qCDebug(DEBUG_KXMLGUI) << "best version is now " << version;
211 bestVersion = version;
212 }
213 }
214
215 if (best != docEnd) {
216 if (best != allDocuments.begin()) {
217 auto local = allDocuments.begin();
218
219 if ((*local).file.startsWith(s: QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation))) {
220 // load the local document and extract the action properties
221 QDomDocument localDocument;
222 localDocument.setContent(data: (*local).data);
223
224 const ActionPropertiesMap properties = extractActionProperties(doc: localDocument);
225 const QList<QDomElement> toolbars = extractToolBars(doc: localDocument);
226
227 // in case the document has a ActionProperties section
228 // we must not delete it but copy over the global doc
229 // to the local and insert the ActionProperties section
230
231 // TODO: kedittoolbar should mark toolbars as modified so that
232 // we don't keep old toolbars just because the user defined a shortcut
233
234 if (!properties.isEmpty() || !toolbars.isEmpty()) {
235 // now load the global one with the higher version number
236 // into memory
237 QDomDocument document;
238 document.setContent(data: (*best).data);
239 // and store the properties in there
240 storeActionProperties(doc&: document, properties);
241 if (!toolbars.isEmpty()) {
242 // remove application toolbars present in the user file
243 // (not others, that the app might have added since)
244 removeToolBars(doc&: document, toolBarNames: toolBarNames(toolBars: toolbars));
245 // add user toolbars
246 insertToolBars(doc&: document, toolBars: toolbars);
247 }
248
249 (*local).data = document.toString();
250 // make sure we pick up the new local doc, when we return later
251 best = local;
252
253 // write out the new version of the local document
254 QFile f((*local).file);
255 if (f.open(flags: QIODevice::WriteOnly)) {
256 const QByteArray utf8data = (*local).data.toUtf8();
257 f.write(data: utf8data.constData(), len: utf8data.length());
258 f.close();
259 }
260 } else {
261 // Move away the outdated local file, to speed things up next time
262 const QString f = (*local).file;
263 const QString backup = f + QLatin1String(".backup");
264 QFile::rename(oldName: f, newName: backup);
265 }
266 }
267 }
268 m_doc = (*best).data;
269 m_file = (*best).file;
270 } else {
271 // qCDebug(DEBUG_KXMLGUI) << "returning first one...";
272 const auto &[file, data] = allDocuments.at(n: 0);
273 m_file = file;
274 m_doc = data;
275 }
276}
277

source code of kxmlgui/src/kxmlguiversionhandler.cpp