1 /* This file is part of the KDE libraries
2 Copyright (C) 2000 Simon Hausmann <hausmann@kde.org>
3 Copyright (C) 2000 Kurt Granroth <granroth@kde.org>
4 Copyright 2007 David Faure <faure@kde.org>
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20 */
21
22 #include "kxmlguiversionhandler_p.h"
23
24 #include "kxmlguifactory.h"
25
26 #include <QFile>
27 #include <QDomDocument>
28 #include <QDomElement>
29 #include <QStandardPaths>
30 #include <QMap>
31
32 struct DocStruct {
33 QString file;
34 QString data;
35 };
36
extractToolBars(const QDomDocument & doc)37 static QList<QDomElement> extractToolBars(const QDomDocument &doc)
38 {
39 QList<QDomElement> toolbars;
40 QDomElement parent = doc.documentElement();
41 for (QDomElement e = parent.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
42 if (e.tagName().compare(QStringLiteral("ToolBar"), Qt::CaseInsensitive) == 0) {
43 toolbars.append(e);
44 }
45 }
46 return toolbars;
47 }
48
removeAllToolBars(QDomDocument & doc)49 static void removeAllToolBars(QDomDocument &doc)
50 {
51 QDomElement parent = doc.documentElement();
52 const QList<QDomElement> toolBars = extractToolBars(doc);
53 Q_FOREACH (const QDomElement &e, toolBars) {
54 parent.removeChild(e);
55 }
56 }
57
insertToolBars(QDomDocument & doc,const QList<QDomElement> & toolBars)58 static void insertToolBars(QDomDocument &doc, const QList<QDomElement> &toolBars)
59 {
60 QDomElement parent = doc.documentElement();
61 QDomElement menuBar = parent.namedItem(QStringLiteral("MenuBar")).toElement();
62 QDomElement insertAfter = menuBar;
63 if (menuBar.isNull()) {
64 insertAfter = parent.firstChildElement(); // if null, insertAfter will do an append
65 }
66 Q_FOREACH (const QDomElement &e, toolBars) {
67 QDomNode result = parent.insertAfter(e, insertAfter);
68 Q_ASSERT(!result.isNull());
69 }
70 }
71
72 //
73
74 typedef QMap<QString, QMap<QString, QString> > ActionPropertiesMap;
75
extractActionProperties(const QDomDocument & doc)76 static ActionPropertiesMap extractActionProperties(const QDomDocument &doc)
77 {
78 ActionPropertiesMap properties;
79
80 QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
81
82 if (actionPropElement.isNull()) {
83 return properties;
84 }
85
86 QDomNode n = actionPropElement.firstChild();
87 while (!n.isNull()) {
88 QDomElement e = n.toElement();
89 n = n.nextSibling(); // Advance now so that we can safely delete e
90 if (e.isNull()) {
91 continue;
92 }
93
94 if (e.tagName().compare(QStringLiteral("action"), Qt::CaseInsensitive) != 0) {
95 continue;
96 }
97
98 const QString actionName = e.attribute(QStringLiteral("name"));
99 if (actionName.isEmpty()) {
100 continue;
101 }
102
103 QMap<QString, QMap<QString, QString> >::Iterator propIt = properties.find(actionName);
104 if (propIt == properties.end()) {
105 propIt = properties.insert(actionName, QMap<QString, QString>());
106 }
107
108 const QDomNamedNodeMap attributes = e.attributes();
109 const uint attributeslength = attributes.length();
110
111 for (uint i = 0; i < attributeslength; ++i) {
112 const QDomAttr attr = attributes.item(i).toAttr();
113
114 if (attr.isNull()) {
115 continue;
116 }
117
118 const QString name = attr.name();
119
120 if (name == QStringLiteral("name") || name.isEmpty()) {
121 continue;
122 }
123
124 (*propIt)[ name ] = attr.value();
125 }
126
127 }
128
129 return properties;
130 }
131
storeActionProperties(QDomDocument & doc,const ActionPropertiesMap & properties)132 static void storeActionProperties(QDomDocument &doc,
133 const ActionPropertiesMap &properties)
134 {
135 QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
136
137 if (actionPropElement.isNull()) {
138 actionPropElement = doc.createElement(QStringLiteral("ActionProperties"));
139 doc.documentElement().appendChild(actionPropElement);
140 }
141
142 //Remove only those ActionProperties entries from the document, that are present
143 //in the properties argument. In real life this means that local ActionProperties
144 //takes precedence over global ones, if they exists (think local override of shortcuts).
145 QDomNode actionNode = actionPropElement.firstChild();
146 while (!actionNode.isNull()) {
147 if (properties.contains(actionNode.toElement().attribute(QStringLiteral("name")))) {
148 QDomNode nextNode = actionNode.nextSibling();
149 actionPropElement.removeChild(actionNode);
150 actionNode = nextNode;
151 } else {
152 actionNode = actionNode.nextSibling();
153 }
154 }
155
156 ActionPropertiesMap::ConstIterator it = properties.begin();
157 const ActionPropertiesMap::ConstIterator end = properties.end();
158 for (; it != end; ++it) {
159 QDomElement action = doc.createElement(QStringLiteral("Action"));
160 action.setAttribute(QStringLiteral("name"), it.key());
161 actionPropElement.appendChild(action);
162
163 const QMap<QString, QString> attributes = (*it);
164 QMap<QString, QString>::ConstIterator attrIt = attributes.begin();
165 const QMap<QString, QString>::ConstIterator attrEnd = attributes.end();
166 for (; attrIt != attrEnd; ++attrIt) {
167 action.setAttribute(attrIt.key(), attrIt.value());
168 }
169 }
170 }
171
findVersionNumber(const QString & xml)172 QString KXmlGuiVersionHandler::findVersionNumber(const QString &xml)
173 {
174 enum { ST_START, ST_AFTER_OPEN, ST_AFTER_GUI,
175 ST_EXPECT_VERSION, ST_VERSION_NUM
176 } state = ST_START;
177 const int length = xml.length();
178 for (int pos = 0; pos < length; pos++) {
179 switch (state) {
180 case ST_START:
181 if (xml[pos] == QLatin1Char('<')) {
182 state = ST_AFTER_OPEN;
183 }
184 break;
185 case ST_AFTER_OPEN: {
186 //Jump to gui..
187 const int guipos = xml.indexOf(QStringLiteral("gui"), pos, Qt::CaseInsensitive);
188 if (guipos == -1) {
189 return QString(); //Reject
190 }
191
192 pos = guipos + 2; //Position at i, so we're moved ahead to the next character by the ++;
193 state = ST_AFTER_GUI;
194 break;
195 }
196 case ST_AFTER_GUI:
197 state = ST_EXPECT_VERSION;
198 break;
199 case ST_EXPECT_VERSION: {
200 const int verpos = xml.indexOf(QStringLiteral("version"), pos, Qt::CaseInsensitive);
201 if (verpos == -1) {
202 return QString(); //Reject
203 }
204 pos = verpos + 7; // strlen("version") is 7
205 while (xml.at(pos).isSpace()) {
206 ++pos;
207 }
208 if (xml.at(pos++) != QLatin1Char('=')) {
209 return QString(); //Reject
210 }
211 while (xml.at(pos).isSpace()) {
212 ++pos;
213 }
214
215 state = ST_VERSION_NUM;
216 break;
217 }
218 case ST_VERSION_NUM: {
219 int endpos;
220 for (endpos = pos; endpos < length; endpos++) {
221 const ushort ch = xml[endpos].unicode();
222 if (ch >= QLatin1Char('0') && ch <= QLatin1Char('9')) {
223 continue; //Number..
224 }
225 if (ch == QLatin1Char('"')) { //End of parameter
226 break;
227 } else { //This shouldn't be here..
228 endpos = length;
229 }
230 }
231
232 if (endpos != pos && endpos < length) {
233 const QString matchCandidate = xml.mid(pos, endpos - pos); //Don't include " ".
234 return matchCandidate;
235 }
236
237 state = ST_EXPECT_VERSION; //Try to match a well-formed version..
238 break;
239 } //case..
240 } //switch
241 } //for
242
243 return QString();
244 }
245
KXmlGuiVersionHandler(const QStringList & files)246 KXmlGuiVersionHandler::KXmlGuiVersionHandler(const QStringList &files)
247 {
248 Q_ASSERT(!files.isEmpty());
249
250 if (files.count() == 1) {
251 // No need to parse version numbers if there's only one file anyway
252 m_file = files.first();
253 m_doc = KXMLGUIFactory::readConfigFile(m_file);
254 return;
255 }
256
257 QList<DocStruct> allDocuments;
258
259 Q_FOREACH (const QString &file, files) {
260 DocStruct d;
261 d.file = file;
262 d.data = KXMLGUIFactory::readConfigFile(file);
263 allDocuments.append(d);
264 }
265
266 QList<DocStruct>::iterator best = allDocuments.end();
267 uint bestVersion = 0;
268
269 QList<DocStruct>::iterator docIt = allDocuments.begin();
270 const QList<DocStruct>::iterator docEnd = allDocuments.end();
271 for (; docIt != docEnd; ++docIt) {
272 const QString versionStr = findVersionNumber((*docIt).data);
273 if (versionStr.isEmpty()) {
274 //qDebug(260) << "found no version in" << (*docIt).file;
275 continue;
276 }
277
278 bool ok = false;
279 uint version = versionStr.toUInt(&ok);
280 if (!ok) {
281 continue;
282 }
283 //qDebug(260) << "found version" << version << "for" << (*docIt).file;
284
285 if (version > bestVersion) {
286 best = docIt;
287 //qDebug(260) << "best version is now " << version;
288 bestVersion = version;
289 }
290 }
291
292 if (best != docEnd) {
293 if (best != allDocuments.begin()) {
294 QList<DocStruct>::iterator local = allDocuments.begin();
295
296 if ((*local).file.startsWith(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))) {
297 // load the local document and extract the action properties
298 QDomDocument localDocument;
299 localDocument.setContent((*local).data);
300
301 const ActionPropertiesMap properties = extractActionProperties(localDocument);
302 const QList<QDomElement> toolbars = extractToolBars(localDocument);
303
304 // in case the document has a ActionProperties section
305 // we must not delete it but copy over the global doc
306 // to the local and insert the ActionProperties section
307
308 // TODO: kedittoolbar should mark toolbars as modified so that
309 // we don't keep old toolbars just because the user defined a shortcut
310
311 if (!properties.isEmpty() || !toolbars.isEmpty()) {
312 // now load the global one with the higher version number
313 // into memory
314 QDomDocument document;
315 document.setContent((*best).data);
316 // and store the properties in there
317 storeActionProperties(document, properties);
318 if (!toolbars.isEmpty()) {
319 // remove application toolbars
320 removeAllToolBars(document);
321 // add user toolbars
322 insertToolBars(document, toolbars);
323 }
324
325 (*local).data = document.toString();
326 // make sure we pick up the new local doc, when we return later
327 best = local;
328
329 // write out the new version of the local document
330 QFile f((*local).file);
331 if (f.open(QIODevice::WriteOnly)) {
332 const QByteArray utf8data = (*local).data.toUtf8();
333 f.write(utf8data.constData(), utf8data.length());
334 f.close();
335 }
336 } else {
337 // Move away the outdated local file, to speed things up next time
338 const QString f = (*local).file;
339 const QString backup = f + QStringLiteral(".backup");
340 QFile::rename(f, backup);
341 }
342 }
343 }
344 m_doc = (*best).data;
345 m_file = (*best).file;
346 } else {
347 //qDebug(260) << "returning first one...";
348 m_doc = allDocuments.first().data;
349 m_file = allDocuments.first().file;
350 }
351 }
352