File indexing completed on 2024-10-06 13:19:12
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 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0099 stream.setCodec("UTF-8"); 0100 #endif 0101 0102 stream << m_doc.toString(); 0103 0104 file.close(); 0105 0106 if (file.error() != QFile::NoError) { 0107 qCWarning(KMENUEDIT_LOG) << "Could not close " << m_fileName; 0108 m_error = i18n("Could not write to %1", m_fileName); 0109 return false; 0110 } 0111 0112 m_bDirty = false; 0113 0114 return true; 0115 } 0116 0117 QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create) 0118 { 0119 QString menuNodeName; 0120 QString subMenuName; 0121 int i = menuName.indexOf(QLatin1Char('/')); 0122 if (i >= 0) { 0123 menuNodeName = menuName.left(i); 0124 subMenuName = menuName.mid(i + 1); 0125 } else { 0126 menuNodeName = menuName; 0127 } 0128 if (i == 0) { 0129 return findMenu(elem, subMenuName, create); 0130 } 0131 0132 if (menuNodeName.isEmpty()) { 0133 return elem; 0134 } 0135 0136 QDomNode n = elem.firstChild(); 0137 while (!n.isNull()) { 0138 QDomElement e = n.toElement(); // try to convert the node to an element. 0139 if (e.tagName() == MF_MENU) { 0140 QString name; 0141 0142 QDomNode n2 = e.firstChild(); 0143 while (!n2.isNull()) { 0144 QDomElement e2 = n2.toElement(); 0145 if (!e2.isNull() && e2.tagName() == MF_NAME) { 0146 name = e2.text(); 0147 break; 0148 } 0149 n2 = n2.nextSibling(); 0150 } 0151 0152 if (name == menuNodeName) { 0153 if (subMenuName.isEmpty()) { 0154 return e; 0155 } else { 0156 return findMenu(e, subMenuName, create); 0157 } 0158 } 0159 } 0160 n = n.nextSibling(); 0161 } 0162 0163 if (!create) { 0164 return QDomElement(); 0165 } 0166 0167 // Create new node. 0168 QDomElement newElem = m_doc.createElement(MF_MENU); 0169 QDomElement newNameElem = m_doc.createElement(MF_NAME); 0170 newNameElem.appendChild(m_doc.createTextNode(menuNodeName)); 0171 newElem.appendChild(newNameElem); 0172 elem.appendChild(newElem); 0173 0174 if (subMenuName.isEmpty()) { 0175 return newElem; 0176 } else { 0177 return findMenu(newElem, subMenuName, create); 0178 } 0179 } 0180 0181 static QString relativeToDesktopDirsLocation(const QString &file) 0182 { 0183 const QString canonical = QFileInfo(file).canonicalFilePath(); 0184 const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); 0185 for (const QString &dir : dirs) { 0186 const QString base = dir + QStringLiteral("/desktop-directories"); 0187 if (canonical.startsWith(base)) { 0188 return canonical.mid(base.length() + 1); 0189 } 0190 } 0191 return QString(); 0192 } 0193 0194 static QString entryToDirId(const QString &path) 0195 { 0196 // See also KDesktopFile::locateLocal 0197 QString local; 0198 if (QFileInfo(path).isAbsolute()) { 0199 // XDG Desktop menu items come with absolute paths, we need to 0200 // extract their relative path and then build a local path. 0201 local = relativeToDesktopDirsLocation(path); 0202 } 0203 0204 if (local.isEmpty() || local.startsWith(QLatin1Char('/'))) { 0205 // What now? Use filename only and hope for the best. 0206 local = path.mid(path.lastIndexOf(QLatin1Char('/')) + 1); 0207 } 0208 return local; 0209 } 0210 0211 static void purgeIncludesExcludes(const QDomElement &elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode) 0212 { 0213 // Remove any previous includes/excludes of appId 0214 QDomNode n = elem.firstChild(); 0215 while (!n.isNull()) { 0216 QDomElement e = n.toElement(); // try to convert the node to an element. 0217 bool bIncludeNode = (e.tagName() == MenuFile::MF_INCLUDE); 0218 bool bExcludeNode = (e.tagName() == MenuFile::MF_EXCLUDE); 0219 if (bIncludeNode) { 0220 includeNode = e; 0221 } 0222 if (bExcludeNode) { 0223 excludeNode = e; 0224 } 0225 if (bIncludeNode || bExcludeNode) { 0226 QDomNode n2 = e.firstChild(); 0227 while (!n2.isNull()) { 0228 QDomNode next = n2.nextSibling(); 0229 QDomElement e2 = n2.toElement(); 0230 if (!e2.isNull() && e2.tagName() == MenuFile::MF_FILENAME) { 0231 if (e2.text() == appId) { 0232 e.removeChild(e2); 0233 break; 0234 } 0235 } 0236 n2 = next; 0237 } 0238 } 0239 n = n.nextSibling(); 0240 } 0241 } 0242 0243 static void purgeDeleted(QDomElement elem) 0244 { 0245 // Remove any previous includes/excludes of appId 0246 QDomNode n = elem.firstChild(); 0247 while (!n.isNull()) { 0248 QDomNode next = n.nextSibling(); 0249 QDomElement e = n.toElement(); // try to convert the node to an element. 0250 if ((e.tagName() == MenuFile::MF_DELETED) || (e.tagName() == MenuFile::MF_NOTDELETED)) { 0251 elem.removeChild(e); 0252 } 0253 n = next; 0254 } 0255 } 0256 0257 static void purgeLayout(QDomElement elem) 0258 { 0259 // Remove any previous includes/excludes of appId 0260 QDomNode n = elem.firstChild(); 0261 while (!n.isNull()) { 0262 QDomNode next = n.nextSibling(); 0263 QDomElement e = n.toElement(); // try to convert the node to an element. 0264 if (e.tagName() == MenuFile::MF_LAYOUT) { 0265 elem.removeChild(e); 0266 } 0267 n = next; 0268 } 0269 } 0270 0271 void MenuFile::addEntry(const QString &menuName, const QString &menuId) 0272 { 0273 m_bDirty = true; 0274 0275 m_removedEntries.removeAll(menuId); 0276 0277 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0278 0279 QDomElement excludeNode; 0280 QDomElement includeNode; 0281 0282 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); 0283 0284 if (includeNode.isNull()) { 0285 includeNode = m_doc.createElement(MF_INCLUDE); 0286 elem.appendChild(includeNode); 0287 } 0288 0289 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0290 fileNode.appendChild(m_doc.createTextNode(menuId)); 0291 includeNode.appendChild(fileNode); 0292 } 0293 0294 void MenuFile::setLayout(const QString &menuName, const QStringList &layout) 0295 { 0296 m_bDirty = true; 0297 0298 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0299 0300 purgeLayout(elem); 0301 0302 QDomElement layoutNode = m_doc.createElement(MF_LAYOUT); 0303 elem.appendChild(layoutNode); 0304 0305 for (QStringList::ConstIterator it = layout.constBegin(); it != layout.constEnd(); ++it) { 0306 QString li = *it; 0307 if (li == QLatin1String(":S")) { 0308 layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR)); 0309 } else if (li == QLatin1String(":M")) { 0310 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0311 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("menus")); 0312 layoutNode.appendChild(mergeNode); 0313 } else if (li == QLatin1String(":F")) { 0314 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0315 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("files")); 0316 layoutNode.appendChild(mergeNode); 0317 } else if (li == QLatin1String(":A")) { 0318 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0319 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("all")); 0320 layoutNode.appendChild(mergeNode); 0321 } else if (li.endsWith(QLatin1Char('/'))) { 0322 li.chop(1); 0323 QDomElement menuNode = m_doc.createElement(MF_MENUNAME); 0324 menuNode.appendChild(m_doc.createTextNode(li)); 0325 layoutNode.appendChild(menuNode); 0326 } else { 0327 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0328 fileNode.appendChild(m_doc.createTextNode(li)); 0329 layoutNode.appendChild(fileNode); 0330 } 0331 } 0332 } 0333 0334 void MenuFile::removeEntry(const QString &menuName, const QString &menuId) 0335 { 0336 m_bDirty = true; 0337 m_removedEntries.append(menuId); 0338 0339 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0340 0341 QDomElement excludeNode; 0342 QDomElement includeNode; 0343 0344 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); 0345 0346 if (excludeNode.isNull()) { 0347 excludeNode = m_doc.createElement(MF_EXCLUDE); 0348 elem.appendChild(excludeNode); 0349 } 0350 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0351 fileNode.appendChild(m_doc.createTextNode(menuId)); 0352 excludeNode.appendChild(fileNode); 0353 } 0354 0355 void MenuFile::addMenu(const QString &menuName, const QString &menuFile) 0356 { 0357 m_bDirty = true; 0358 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0359 0360 QDomElement dirElem = m_doc.createElement(MF_DIRECTORY); 0361 dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile))); 0362 elem.appendChild(dirElem); 0363 } 0364 0365 void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu) 0366 { 0367 m_bDirty = true; 0368 0369 // Undelete the new menu 0370 QDomElement elem = findMenu(m_doc.documentElement(), newMenu, true); 0371 purgeDeleted(elem); 0372 elem.appendChild(m_doc.createElement(MF_NOTDELETED)); 0373 0374 // TODO: GET RID OF COMMON PART, IT BREAKS STUFF 0375 // Find common part 0376 QStringList oldMenuParts = oldMenu.split(QLatin1Char('/')); 0377 QStringList newMenuParts = newMenu.split(QLatin1Char('/')); 0378 QString commonMenuName; 0379 int max = qMin(oldMenuParts.count(), newMenuParts.count()); 0380 int i = 0; 0381 for (; i < max; i++) { 0382 if (oldMenuParts[i] != newMenuParts[i]) { 0383 break; 0384 } 0385 commonMenuName += QLatin1Char('/') + oldMenuParts[i]; 0386 } 0387 QString oldMenuName; 0388 for (int j = i; j < oldMenuParts.count() - 1; j++) { 0389 if (i != j) { 0390 oldMenuName += QLatin1Char('/'); 0391 } 0392 oldMenuName += oldMenuParts[j]; 0393 } 0394 QString newMenuName; 0395 for (int j = i; j < newMenuParts.count() - 1; j++) { 0396 if (i != j) { 0397 newMenuName += QLatin1Char('/'); 0398 } 0399 newMenuName += newMenuParts[j]; 0400 } 0401 0402 if (oldMenuName == newMenuName) { 0403 return; // Can happen 0404 } 0405 elem = findMenu(m_doc.documentElement(), commonMenuName, true); 0406 0407 // Add instructions for moving 0408 QDomElement moveNode = m_doc.createElement(MF_MOVE); 0409 QDomElement node = m_doc.createElement(MF_OLD); 0410 node.appendChild(m_doc.createTextNode(oldMenuName)); 0411 moveNode.appendChild(node); 0412 node = m_doc.createElement(MF_NEW); 0413 node.appendChild(m_doc.createTextNode(newMenuName)); 0414 moveNode.appendChild(node); 0415 elem.appendChild(moveNode); 0416 } 0417 0418 void MenuFile::removeMenu(const QString &menuName) 0419 { 0420 m_bDirty = true; 0421 0422 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0423 0424 purgeDeleted(elem); 0425 elem.appendChild(m_doc.createElement(MF_DELETED)); 0426 } 0427 0428 /** 0429 * Returns a unique menu-name for a new menu under @p menuName 0430 * inspired by @p newMenu 0431 */ 0432 QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList &excludeList) 0433 { 0434 QDomElement elem = findMenu(m_doc.documentElement(), menuName, false); 0435 0436 QString result = newMenu; 0437 if (result.endsWith(QLatin1Char('/'))) { 0438 result.chop(1); 0439 } 0440 0441 static const QRegularExpression re(QStringLiteral("(.*)(?=-\\d+)")); 0442 const QRegularExpressionMatch match = re.match(result); 0443 result = match.hasMatch() ? match.captured(1) : result; 0444 0445 int trunc = result.length(); // Position of trailing '/' 0446 0447 result.append(QLatin1Char('/')); 0448 0449 for (int n = 1; ++n;) { 0450 if (findMenu(elem, result, false).isNull() && !excludeList.contains(result)) { 0451 return result; 0452 } 0453 0454 result.truncate(trunc); 0455 result.append(QStringLiteral("-%1/").arg(n)); 0456 } 0457 return QString(); // Never reached 0458 } 0459 0460 void MenuFile::performAction(const ActionAtom *atom) 0461 { 0462 switch (atom->action) { 0463 case ADD_ENTRY: 0464 addEntry(atom->arg1, atom->arg2); 0465 return; 0466 case REMOVE_ENTRY: 0467 removeEntry(atom->arg1, atom->arg2); 0468 return; 0469 case ADD_MENU: 0470 addMenu(atom->arg1, atom->arg2); 0471 return; 0472 case REMOVE_MENU: 0473 removeMenu(atom->arg1); 0474 return; 0475 case MOVE_MENU: 0476 moveMenu(atom->arg1, atom->arg2); 0477 return; 0478 } 0479 } 0480 0481 MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2) 0482 { 0483 ActionAtom *atom = new ActionAtom; 0484 atom->action = action; 0485 atom->arg1 = arg1; 0486 atom->arg2 = arg2; 0487 m_actionList.append(atom); 0488 return atom; 0489 } 0490 0491 void MenuFile::popAction(ActionAtom *atom) 0492 { 0493 if (m_actionList.last() != atom) { 0494 qWarning("MenuFile::popAction Error, action not last in list."); 0495 return; 0496 } 0497 m_actionList.removeLast(); 0498 delete atom; 0499 } 0500 0501 bool MenuFile::performAllActions() 0502 { 0503 Q_FOREACH (ActionAtom *atom, m_actionList) { 0504 performAction(atom); 0505 delete atom; 0506 } 0507 m_actionList.clear(); 0508 0509 // Entries that have been removed from the menu are added to .hidden 0510 // so that they don't re-appear in Lost & Found 0511 QStringList removed = m_removedEntries; 0512 m_removedEntries.clear(); 0513 for (QStringList::ConstIterator it = removed.constBegin(); it != removed.constEnd(); ++it) { 0514 addEntry(QStringLiteral("/.hidden/"), *it); 0515 } 0516 0517 m_removedEntries.clear(); 0518 0519 if (!m_bDirty) { 0520 return true; 0521 } 0522 0523 return save(); 0524 } 0525 0526 bool MenuFile::dirty() const 0527 { 0528 return (m_actionList.count() != 0) || m_bDirty; 0529 } 0530 0531 void MenuFile::restoreMenuSystem(const QString &filename) 0532 { 0533 m_error.clear(); 0534 0535 m_fileName = filename; 0536 m_doc.clear(); 0537 m_bDirty = false; 0538 Q_FOREACH (ActionAtom *atom, m_actionList) { 0539 delete atom; 0540 } 0541 m_actionList.clear(); 0542 0543 m_removedEntries.clear(); 0544 create(); 0545 }