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