File indexing completed on 2024-04-21 05:27:18

0001 /*
0002  *   SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only
0005  *
0006  */
0007 
0008 #include "menufile.h"
0009 
0010 #include <QDir>
0011 #include <QFile>
0012 #include <QFileInfo>
0013 #include <QRegularExpression>
0014 #include <QStandardPaths>
0015 #include <QTextStream>
0016 
0017 #include "kmenuedit_debug.h"
0018 #include <KLocalizedString>
0019 
0020 const QString MenuFile::MF_MENU = QStringLiteral("Menu");
0021 const QString MenuFile::MF_PUBLIC_ID = QStringLiteral("-//freedesktop//DTD Menu 1.0//EN");
0022 const QString MenuFile::MF_SYSTEM_ID = QStringLiteral("http://www.freedesktop.org/standards/menu-spec/1.0/menu.dtd");
0023 const QString MenuFile::MF_NAME = QStringLiteral("Name");
0024 const QString MenuFile::MF_INCLUDE = QStringLiteral("Include");
0025 const QString MenuFile::MF_EXCLUDE = QStringLiteral("Exclude");
0026 const QString MenuFile::MF_FILENAME = QStringLiteral("Filename");
0027 const QString MenuFile::MF_DELETED = QStringLiteral("Deleted");
0028 const QString MenuFile::MF_NOTDELETED = QStringLiteral("NotDeleted");
0029 const QString MenuFile::MF_MOVE = QStringLiteral("Move");
0030 const QString MenuFile::MF_OLD = QStringLiteral("Old");
0031 const QString MenuFile::MF_NEW = QStringLiteral("New");
0032 const QString MenuFile::MF_DIRECTORY = QStringLiteral("Directory");
0033 const QString MenuFile::MF_LAYOUT = QStringLiteral("Layout");
0034 const QString MenuFile::MF_MENUNAME = QStringLiteral("Menuname");
0035 const QString MenuFile::MF_SEPARATOR = QStringLiteral("Separator");
0036 const QString MenuFile::MF_MERGE = QStringLiteral("Merge");
0037 
0038 MenuFile::MenuFile(const QString &file)
0039     : m_fileName(file)
0040     , m_bDirty(false)
0041 {
0042     load();
0043 }
0044 
0045 MenuFile::~MenuFile()
0046 {
0047 }
0048 
0049 bool MenuFile::load()
0050 {
0051     if (m_fileName.isEmpty()) {
0052         return false;
0053     }
0054 
0055     QFile file(m_fileName);
0056     if (!file.open(QIODevice::ReadOnly)) {
0057         if (file.exists()) {
0058             qCWarning(KMENUEDIT_LOG) << "Could not read " << m_fileName;
0059         }
0060         create();
0061         return false;
0062     }
0063 
0064     QString errorMsg;
0065     int errorRow;
0066     int errorCol;
0067     if (!m_doc.setContent(&file, &errorMsg, &errorRow, &errorCol)) {
0068         qCWarning(KMENUEDIT_LOG) << "Parse error in " << m_fileName << ", line " << errorRow << ", col " << errorCol << ": " << errorMsg;
0069         file.close();
0070         create();
0071         return false;
0072     }
0073     file.close();
0074 
0075     return true;
0076 }
0077 
0078 void MenuFile::create()
0079 {
0080     QDomImplementation impl;
0081     QDomDocumentType docType = impl.createDocumentType(MF_MENU, MF_PUBLIC_ID, MF_SYSTEM_ID);
0082     m_doc = impl.createDocument(QString(), MF_MENU, docType);
0083 }
0084 
0085 bool MenuFile::save()
0086 {
0087     QFile file(m_fileName);
0088     // create directory if it doesn't exist
0089     QFileInfo info(file);
0090     info.dir().mkpath(QStringLiteral("."));
0091 
0092     if (!file.open(QIODevice::WriteOnly)) {
0093         qCWarning(KMENUEDIT_LOG) << "Could not write " << m_fileName;
0094         m_error = i18n("Could not write to %1", m_fileName);
0095         return false;
0096     }
0097     QTextStream stream(&file);
0098 
0099     stream << m_doc.toString();
0100 
0101     file.close();
0102 
0103     if (file.error() != QFile::NoError) {
0104         qCWarning(KMENUEDIT_LOG) << "Could not close " << m_fileName;
0105         m_error = i18n("Could not write to %1", m_fileName);
0106         return false;
0107     }
0108 
0109     m_bDirty = false;
0110 
0111     return true;
0112 }
0113 
0114 QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create)
0115 {
0116     QString menuNodeName;
0117     QString subMenuName;
0118     int i = menuName.indexOf(QLatin1Char('/'));
0119     if (i >= 0) {
0120         menuNodeName = menuName.left(i);
0121         subMenuName = menuName.mid(i + 1);
0122     } else {
0123         menuNodeName = menuName;
0124     }
0125     if (i == 0) {
0126         return findMenu(elem, subMenuName, create);
0127     }
0128 
0129     if (menuNodeName.isEmpty()) {
0130         return elem;
0131     }
0132 
0133     QDomNode n = elem.firstChild();
0134     while (!n.isNull()) {
0135         QDomElement e = n.toElement(); // try to convert the node to an element.
0136         if (e.tagName() == MF_MENU) {
0137             QString name;
0138 
0139             QDomNode n2 = e.firstChild();
0140             while (!n2.isNull()) {
0141                 QDomElement e2 = n2.toElement();
0142                 if (!e2.isNull() && e2.tagName() == MF_NAME) {
0143                     name = e2.text();
0144                     break;
0145                 }
0146                 n2 = n2.nextSibling();
0147             }
0148 
0149             if (name == menuNodeName) {
0150                 if (subMenuName.isEmpty()) {
0151                     return e;
0152                 } else {
0153                     return findMenu(e, subMenuName, create);
0154                 }
0155             }
0156         }
0157         n = n.nextSibling();
0158     }
0159 
0160     if (!create) {
0161         return QDomElement();
0162     }
0163 
0164     // Create new node.
0165     QDomElement newElem = m_doc.createElement(MF_MENU);
0166     QDomElement newNameElem = m_doc.createElement(MF_NAME);
0167     newNameElem.appendChild(m_doc.createTextNode(menuNodeName));
0168     newElem.appendChild(newNameElem);
0169     elem.appendChild(newElem);
0170 
0171     if (subMenuName.isEmpty()) {
0172         return newElem;
0173     } else {
0174         return findMenu(newElem, subMenuName, create);
0175     }
0176 }
0177 
0178 static QString relativeToDesktopDirsLocation(const QString &file)
0179 {
0180     const QString canonical = QFileInfo(file).canonicalFilePath();
0181     const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0182     for (const QString &dir : dirs) {
0183         const QString base = dir + QStringLiteral("/desktop-directories");
0184         if (canonical.startsWith(base)) {
0185             return canonical.mid(base.length() + 1);
0186         }
0187     }
0188     return QString();
0189 }
0190 
0191 static QString entryToDirId(const QString &path)
0192 {
0193     // See also KDesktopFile::locateLocal
0194     QString local;
0195     if (QFileInfo(path).isAbsolute()) {
0196         // XDG Desktop menu items come with absolute paths, we need to
0197         // extract their relative path and then build a local path.
0198         local = relativeToDesktopDirsLocation(path);
0199     }
0200 
0201     if (local.isEmpty() || local.startsWith(QLatin1Char('/'))) {
0202         // What now? Use filename only and hope for the best.
0203         local = path.mid(path.lastIndexOf(QLatin1Char('/')) + 1);
0204     }
0205     return local;
0206 }
0207 
0208 static void purgeIncludesExcludes(const QDomElement &elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode)
0209 {
0210     // Remove any previous includes/excludes of appId
0211     QDomNode n = elem.firstChild();
0212     while (!n.isNull()) {
0213         QDomElement e = n.toElement(); // try to convert the node to an element.
0214         bool bIncludeNode = (e.tagName() == MenuFile::MF_INCLUDE);
0215         bool bExcludeNode = (e.tagName() == MenuFile::MF_EXCLUDE);
0216         if (bIncludeNode) {
0217             includeNode = e;
0218         }
0219         if (bExcludeNode) {
0220             excludeNode = e;
0221         }
0222         if (bIncludeNode || bExcludeNode) {
0223             QDomNode n2 = e.firstChild();
0224             while (!n2.isNull()) {
0225                 QDomNode next = n2.nextSibling();
0226                 QDomElement e2 = n2.toElement();
0227                 if (!e2.isNull() && e2.tagName() == MenuFile::MF_FILENAME) {
0228                     if (e2.text() == appId) {
0229                         e.removeChild(e2);
0230                         break;
0231                     }
0232                 }
0233                 n2 = next;
0234             }
0235         }
0236         n = n.nextSibling();
0237     }
0238 }
0239 
0240 static void purgeDeleted(QDomElement elem)
0241 {
0242     // Remove any previous includes/excludes of appId
0243     QDomNode n = elem.firstChild();
0244     while (!n.isNull()) {
0245         QDomNode next = n.nextSibling();
0246         QDomElement e = n.toElement(); // try to convert the node to an element.
0247         if ((e.tagName() == MenuFile::MF_DELETED) || (e.tagName() == MenuFile::MF_NOTDELETED)) {
0248             elem.removeChild(e);
0249         }
0250         n = next;
0251     }
0252 }
0253 
0254 static void purgeLayout(QDomElement elem)
0255 {
0256     // Remove any previous includes/excludes of appId
0257     QDomNode n = elem.firstChild();
0258     while (!n.isNull()) {
0259         QDomNode next = n.nextSibling();
0260         QDomElement e = n.toElement(); // try to convert the node to an element.
0261         if (e.tagName() == MenuFile::MF_LAYOUT) {
0262             elem.removeChild(e);
0263         }
0264         n = next;
0265     }
0266 }
0267 
0268 void MenuFile::addEntry(const QString &menuName, const QString &menuId)
0269 {
0270     m_bDirty = true;
0271 
0272     m_removedEntries.removeAll(menuId);
0273 
0274     QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
0275 
0276     QDomElement excludeNode;
0277     QDomElement includeNode;
0278 
0279     purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
0280 
0281     if (includeNode.isNull()) {
0282         includeNode = m_doc.createElement(MF_INCLUDE);
0283         elem.appendChild(includeNode);
0284     }
0285 
0286     QDomElement fileNode = m_doc.createElement(MF_FILENAME);
0287     fileNode.appendChild(m_doc.createTextNode(menuId));
0288     includeNode.appendChild(fileNode);
0289 }
0290 
0291 void MenuFile::setLayout(const QString &menuName, const QStringList &layout)
0292 {
0293     m_bDirty = true;
0294 
0295     QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
0296 
0297     purgeLayout(elem);
0298 
0299     QDomElement layoutNode = m_doc.createElement(MF_LAYOUT);
0300     elem.appendChild(layoutNode);
0301 
0302     for (QStringList::ConstIterator it = layout.constBegin(); it != layout.constEnd(); ++it) {
0303         QString li = *it;
0304         if (li == QLatin1String(":S")) {
0305             layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR));
0306         } else if (li == QLatin1String(":M")) {
0307             QDomElement mergeNode = m_doc.createElement(MF_MERGE);
0308             mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("menus"));
0309             layoutNode.appendChild(mergeNode);
0310         } else if (li == QLatin1String(":F")) {
0311             QDomElement mergeNode = m_doc.createElement(MF_MERGE);
0312             mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("files"));
0313             layoutNode.appendChild(mergeNode);
0314         } else if (li == QLatin1String(":A")) {
0315             QDomElement mergeNode = m_doc.createElement(MF_MERGE);
0316             mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("all"));
0317             layoutNode.appendChild(mergeNode);
0318         } else if (li.endsWith(QLatin1Char('/'))) {
0319             li.chop(1);
0320             QDomElement menuNode = m_doc.createElement(MF_MENUNAME);
0321             menuNode.appendChild(m_doc.createTextNode(li));
0322             layoutNode.appendChild(menuNode);
0323         } else {
0324             QDomElement fileNode = m_doc.createElement(MF_FILENAME);
0325             fileNode.appendChild(m_doc.createTextNode(li));
0326             layoutNode.appendChild(fileNode);
0327         }
0328     }
0329 }
0330 
0331 void MenuFile::removeEntry(const QString &menuName, const QString &menuId)
0332 {
0333     m_bDirty = true;
0334     m_removedEntries.append(menuId);
0335 
0336     QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
0337 
0338     QDomElement excludeNode;
0339     QDomElement includeNode;
0340 
0341     purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
0342 
0343     if (excludeNode.isNull()) {
0344         excludeNode = m_doc.createElement(MF_EXCLUDE);
0345         elem.appendChild(excludeNode);
0346     }
0347     QDomElement fileNode = m_doc.createElement(MF_FILENAME);
0348     fileNode.appendChild(m_doc.createTextNode(menuId));
0349     excludeNode.appendChild(fileNode);
0350 }
0351 
0352 void MenuFile::addMenu(const QString &menuName, const QString &menuFile)
0353 {
0354     m_bDirty = true;
0355     QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
0356 
0357     QDomElement dirElem = m_doc.createElement(MF_DIRECTORY);
0358     dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile)));
0359     elem.appendChild(dirElem);
0360 }
0361 
0362 void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu)
0363 {
0364     m_bDirty = true;
0365 
0366     // Undelete the new menu
0367     QDomElement elem = findMenu(m_doc.documentElement(), newMenu, true);
0368     purgeDeleted(elem);
0369     elem.appendChild(m_doc.createElement(MF_NOTDELETED));
0370 
0371     // TODO: GET RID OF COMMON PART, IT BREAKS STUFF
0372     // Find common part
0373     QStringList oldMenuParts = oldMenu.split(QLatin1Char('/'));
0374     QStringList newMenuParts = newMenu.split(QLatin1Char('/'));
0375     QString commonMenuName;
0376     int max = qMin(oldMenuParts.count(), newMenuParts.count());
0377     int i = 0;
0378     for (; i < max; i++) {
0379         if (oldMenuParts[i] != newMenuParts[i]) {
0380             break;
0381         }
0382         commonMenuName += QLatin1Char('/') + oldMenuParts[i];
0383     }
0384     QString oldMenuName;
0385     for (int j = i; j < oldMenuParts.count() - 1; j++) {
0386         if (i != j) {
0387             oldMenuName += QLatin1Char('/');
0388         }
0389         oldMenuName += oldMenuParts[j];
0390     }
0391     QString newMenuName;
0392     for (int j = i; j < newMenuParts.count() - 1; j++) {
0393         if (i != j) {
0394             newMenuName += QLatin1Char('/');
0395         }
0396         newMenuName += newMenuParts[j];
0397     }
0398 
0399     if (oldMenuName == newMenuName) {
0400         return; // Can happen
0401     }
0402     elem = findMenu(m_doc.documentElement(), commonMenuName, true);
0403 
0404     // Add instructions for moving
0405     QDomElement moveNode = m_doc.createElement(MF_MOVE);
0406     QDomElement node = m_doc.createElement(MF_OLD);
0407     node.appendChild(m_doc.createTextNode(oldMenuName));
0408     moveNode.appendChild(node);
0409     node = m_doc.createElement(MF_NEW);
0410     node.appendChild(m_doc.createTextNode(newMenuName));
0411     moveNode.appendChild(node);
0412     elem.appendChild(moveNode);
0413 }
0414 
0415 void MenuFile::removeMenu(const QString &menuName)
0416 {
0417     m_bDirty = true;
0418 
0419     QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
0420 
0421     purgeDeleted(elem);
0422     elem.appendChild(m_doc.createElement(MF_DELETED));
0423 }
0424 
0425 /**
0426  * Returns a unique menu-name for a new menu under @p menuName
0427  * inspired by @p newMenu
0428  */
0429 QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList &excludeList)
0430 {
0431     QDomElement elem = findMenu(m_doc.documentElement(), menuName, false);
0432 
0433     QString result = newMenu;
0434     if (result.endsWith(QLatin1Char('/'))) {
0435         result.chop(1);
0436     }
0437 
0438     static const QRegularExpression re(QStringLiteral("(.*)(?=-\\d+)"));
0439     const QRegularExpressionMatch match = re.match(result);
0440     result = match.hasMatch() ? match.captured(1) : result;
0441 
0442     int trunc = result.length(); // Position of trailing '/'
0443 
0444     result.append(QLatin1Char('/'));
0445 
0446     for (int n = 1; ++n;) {
0447         if (findMenu(elem, result, false).isNull() && !excludeList.contains(result)) {
0448             return result;
0449         }
0450 
0451         result.truncate(trunc);
0452         result.append(QStringLiteral("-%1/").arg(n));
0453     }
0454     return QString(); // Never reached
0455 }
0456 
0457 void MenuFile::performAction(const ActionAtom *atom)
0458 {
0459     switch (atom->action) {
0460     case ADD_ENTRY:
0461         addEntry(atom->arg1, atom->arg2);
0462         return;
0463     case REMOVE_ENTRY:
0464         removeEntry(atom->arg1, atom->arg2);
0465         return;
0466     case ADD_MENU:
0467         addMenu(atom->arg1, atom->arg2);
0468         return;
0469     case REMOVE_MENU:
0470         removeMenu(atom->arg1);
0471         return;
0472     case MOVE_MENU:
0473         moveMenu(atom->arg1, atom->arg2);
0474         return;
0475     }
0476 }
0477 
0478 MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2)
0479 {
0480     ActionAtom *atom = new ActionAtom;
0481     atom->action = action;
0482     atom->arg1 = arg1;
0483     atom->arg2 = arg2;
0484     m_actionList.append(atom);
0485     return atom;
0486 }
0487 
0488 void MenuFile::popAction(ActionAtom *atom)
0489 {
0490     if (m_actionList.last() != atom) {
0491         qWarning("MenuFile::popAction Error, action not last in list.");
0492         return;
0493     }
0494     m_actionList.removeLast();
0495     delete atom;
0496 }
0497 
0498 bool MenuFile::performAllActions()
0499 {
0500     Q_FOREACH (ActionAtom *atom, m_actionList) {
0501         performAction(atom);
0502         delete atom;
0503     }
0504     m_actionList.clear();
0505 
0506     // Entries that have been removed from the menu are added to .hidden
0507     // so that they don't re-appear in Lost & Found
0508     QStringList removed = m_removedEntries;
0509     m_removedEntries.clear();
0510     for (QStringList::ConstIterator it = removed.constBegin(); it != removed.constEnd(); ++it) {
0511         addEntry(QStringLiteral("/.hidden/"), *it);
0512     }
0513 
0514     m_removedEntries.clear();
0515 
0516     if (!m_bDirty) {
0517         return true;
0518     }
0519 
0520     return save();
0521 }
0522 
0523 bool MenuFile::dirty() const
0524 {
0525     return (m_actionList.count() != 0) || m_bDirty;
0526 }
0527 
0528 void MenuFile::restoreMenuSystem(const QString &filename)
0529 {
0530     m_error.clear();
0531 
0532     m_fileName = filename;
0533     m_doc.clear();
0534     m_bDirty = false;
0535     Q_FOREACH (ActionAtom *atom, m_actionList) {
0536         delete atom;
0537     }
0538     m_actionList.clear();
0539 
0540     m_removedEntries.clear();
0541     create();
0542 }