File indexing completed on 2024-04-21 15:05:54

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
0004     SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
0005     SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kxmlguiversionhandler_p.h"
0011 
0012 #include "kxmlguiclient.h"
0013 #include "kxmlguifactory.h"
0014 
0015 #include <QDomDocument>
0016 #include <QDomElement>
0017 #include <QFile>
0018 #include <QMap>
0019 #include <QStandardPaths>
0020 
0021 struct DocStruct {
0022     QString file;
0023     QString data;
0024 };
0025 
0026 static QList<QDomElement> extractToolBars(const QDomDocument &doc)
0027 {
0028     QList<QDomElement> toolbars;
0029     QDomElement parent = doc.documentElement();
0030     for (QDomElement e = parent.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
0031         if (e.tagName().compare(QStringLiteral("ToolBar"), Qt::CaseInsensitive) == 0) {
0032             toolbars.append(e);
0033         }
0034     }
0035     return toolbars;
0036 }
0037 
0038 static QStringList toolBarNames(const QList<QDomElement> &toolBars)
0039 {
0040     QStringList names;
0041     names.reserve(toolBars.count());
0042     for (const QDomElement &e : toolBars) {
0043         names.append(e.attribute(QStringLiteral("name")));
0044     }
0045     return names;
0046 }
0047 
0048 static void removeToolBars(QDomDocument &doc, const QStringList &toolBarNames)
0049 {
0050     QDomElement parent = doc.documentElement();
0051     const QList<QDomElement> toolBars = extractToolBars(doc);
0052     for (const QDomElement &e : toolBars) {
0053         if (toolBarNames.contains(e.attribute(QStringLiteral("name")))) {
0054             parent.removeChild(e);
0055         }
0056     }
0057 }
0058 
0059 static void insertToolBars(QDomDocument &doc, const QList<QDomElement> &toolBars)
0060 {
0061     QDomElement parent = doc.documentElement();
0062     QDomElement menuBar = parent.namedItem(QStringLiteral("MenuBar")).toElement();
0063     QDomElement insertAfter = menuBar;
0064     if (menuBar.isNull()) {
0065         insertAfter = parent.firstChildElement(); // if null, insertAfter will do an append
0066     }
0067     for (const QDomElement &e : toolBars) {
0068         QDomNode result = parent.insertAfter(e, insertAfter);
0069         Q_ASSERT(!result.isNull());
0070     }
0071 }
0072 
0073 //
0074 
0075 typedef QMap<QString, QMap<QString, QString>> ActionPropertiesMap;
0076 
0077 static ActionPropertiesMap extractActionProperties(const QDomDocument &doc)
0078 {
0079     ActionPropertiesMap properties;
0080 
0081     QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
0082 
0083     if (actionPropElement.isNull()) {
0084         return properties;
0085     }
0086 
0087     QDomNode n = actionPropElement.firstChild();
0088     while (!n.isNull()) {
0089         QDomElement e = n.toElement();
0090         n = n.nextSibling(); // Advance now so that we can safely delete e
0091         if (e.isNull()) {
0092             continue;
0093         }
0094 
0095         if (e.tagName().compare(QStringLiteral("action"), Qt::CaseInsensitive) != 0) {
0096             continue;
0097         }
0098 
0099         const QString actionName = e.attribute(QStringLiteral("name"));
0100         if (actionName.isEmpty()) {
0101             continue;
0102         }
0103 
0104         QMap<QString, QMap<QString, QString>>::Iterator propIt = properties.find(actionName);
0105         if (propIt == properties.end()) {
0106             propIt = properties.insert(actionName, QMap<QString, QString>());
0107         }
0108 
0109         const QDomNamedNodeMap attributes = e.attributes();
0110         const int attributeslength = attributes.length();
0111 
0112         for (int i = 0; i < attributeslength; ++i) {
0113             const QDomAttr attr = attributes.item(i).toAttr();
0114 
0115             if (attr.isNull()) {
0116                 continue;
0117             }
0118 
0119             const QString name = attr.name();
0120 
0121             if (name == QLatin1String("name") || name.isEmpty()) {
0122                 continue;
0123             }
0124 
0125             (*propIt)[name] = attr.value();
0126         }
0127     }
0128 
0129     return properties;
0130 }
0131 
0132 static void storeActionProperties(QDomDocument &doc, const ActionPropertiesMap &properties)
0133 {
0134     QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties")).toElement();
0135 
0136     if (actionPropElement.isNull()) {
0137         actionPropElement = doc.createElement(QStringLiteral("ActionProperties"));
0138         doc.documentElement().appendChild(actionPropElement);
0139     }
0140 
0141     // Remove only those ActionProperties entries from the document, that are present
0142     // in the properties argument. In real life this means that local ActionProperties
0143     // takes precedence over global ones, if they exists (think local override of shortcuts).
0144     QDomNode actionNode = actionPropElement.firstChild();
0145     while (!actionNode.isNull()) {
0146         if (properties.contains(actionNode.toElement().attribute(QStringLiteral("name")))) {
0147             QDomNode nextNode = actionNode.nextSibling();
0148             actionPropElement.removeChild(actionNode);
0149             actionNode = nextNode;
0150         } else {
0151             actionNode = actionNode.nextSibling();
0152         }
0153     }
0154 
0155     ActionPropertiesMap::ConstIterator it = properties.begin();
0156     const ActionPropertiesMap::ConstIterator end = properties.end();
0157     for (; it != end; ++it) {
0158         QDomElement action = doc.createElement(QStringLiteral("Action"));
0159         action.setAttribute(QStringLiteral("name"), it.key());
0160         actionPropElement.appendChild(action);
0161 
0162         const QMap<QString, QString> attributes = (*it);
0163         QMap<QString, QString>::ConstIterator attrIt = attributes.begin();
0164         const QMap<QString, QString>::ConstIterator attrEnd = attributes.end();
0165         for (; attrIt != attrEnd; ++attrIt) {
0166             action.setAttribute(attrIt.key(), attrIt.value());
0167         }
0168     }
0169 }
0170 
0171 KXmlGuiVersionHandler::KXmlGuiVersionHandler(const QStringList &files)
0172 {
0173     Q_ASSERT(!files.isEmpty());
0174 
0175     if (files.count() == 1) {
0176         // No need to parse version numbers if there's only one file anyway
0177         m_file = files.first();
0178         m_doc = KXMLGUIFactory::readConfigFile(m_file);
0179         return;
0180     }
0181 
0182     std::vector<DocStruct> allDocuments;
0183     allDocuments.reserve(files.size());
0184 
0185     for (const QString &file : files) {
0186         allDocuments.push_back({file, KXMLGUIFactory::readConfigFile(file)});
0187     }
0188 
0189     auto best = allDocuments.end();
0190     uint bestVersion = 0;
0191 
0192     auto docIt = allDocuments.begin();
0193     const auto docEnd = allDocuments.end();
0194     for (; docIt != docEnd; ++docIt) {
0195         const QString versionStr = KXMLGUIClient::findVersionNumber((*docIt).data);
0196         if (versionStr.isEmpty()) {
0197             // qCDebug(DEBUG_KXMLGUI) << "found no version in" << (*docIt).file;
0198             continue;
0199         }
0200 
0201         bool ok = false;
0202         uint version = versionStr.toUInt(&ok);
0203         if (!ok) {
0204             continue;
0205         }
0206         // qCDebug(DEBUG_KXMLGUI) << "found version" << version << "for" << (*docIt).file;
0207 
0208         if (version > bestVersion) {
0209             best = docIt;
0210             // qCDebug(DEBUG_KXMLGUI) << "best version is now " << version;
0211             bestVersion = version;
0212         }
0213     }
0214 
0215     if (best != docEnd) {
0216         if (best != allDocuments.begin()) {
0217             auto local = allDocuments.begin();
0218 
0219             if ((*local).file.startsWith(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))) {
0220                 // load the local document and extract the action properties
0221                 QDomDocument localDocument;
0222                 localDocument.setContent((*local).data);
0223 
0224                 const ActionPropertiesMap properties = extractActionProperties(localDocument);
0225                 const QList<QDomElement> toolbars = extractToolBars(localDocument);
0226 
0227                 // in case the document has a ActionProperties section
0228                 // we must not delete it but copy over the global doc
0229                 // to the local and insert the ActionProperties section
0230 
0231                 // TODO: kedittoolbar should mark toolbars as modified so that
0232                 // we don't keep old toolbars just because the user defined a shortcut
0233 
0234                 if (!properties.isEmpty() || !toolbars.isEmpty()) {
0235                     // now load the global one with the higher version number
0236                     // into memory
0237                     QDomDocument document;
0238                     document.setContent((*best).data);
0239                     // and store the properties in there
0240                     storeActionProperties(document, properties);
0241                     if (!toolbars.isEmpty()) {
0242                         // remove application toolbars present in the user file
0243                         // (not others, that the app might have added since)
0244                         removeToolBars(document, toolBarNames(toolbars));
0245                         // add user toolbars
0246                         insertToolBars(document, toolbars);
0247                     }
0248 
0249                     (*local).data = document.toString();
0250                     // make sure we pick up the new local doc, when we return later
0251                     best = local;
0252 
0253                     // write out the new version of the local document
0254                     QFile f((*local).file);
0255                     if (f.open(QIODevice::WriteOnly)) {
0256                         const QByteArray utf8data = (*local).data.toUtf8();
0257                         f.write(utf8data.constData(), utf8data.length());
0258                         f.close();
0259                     }
0260                 } else {
0261                     // Move away the outdated local file, to speed things up next time
0262                     const QString f = (*local).file;
0263                     const QString backup = f + QLatin1String(".backup");
0264                     QFile::rename(f, backup);
0265                 }
0266             }
0267         }
0268         m_doc = (*best).data;
0269         m_file = (*best).file;
0270     } else {
0271         // qCDebug(DEBUG_KXMLGUI) << "returning first one...";
0272         const auto &[file, data] = allDocuments.at(0);
0273         m_file = file;
0274         m_doc = data;
0275     }
0276 }