Warning, file /plasma/plasma-workspace/kcms/autostart/autostartmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2020 Méven Car <meven.car@enioka.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "autostartmodel.h" 0008 #include "kcm_autostart_debug.h" 0009 0010 #include <KConfigGroup> 0011 #include <KDesktopFile> 0012 #include <KShell> 0013 #include <QDebug> 0014 #include <QQuickItem> 0015 #include <QQuickRenderControl> 0016 #include <QStandardPaths> 0017 #include <QWindow> 0018 0019 #include <QDirIterator> 0020 #include <QFileInfo> 0021 #include <QMimeDatabase> 0022 #include <QRegularExpression> 0023 0024 #include <set> 0025 0026 #include <KFileItem> 0027 #include <KFileUtils> 0028 #include <KIO/CopyJob> 0029 #include <KIO/DeleteJob> 0030 #include <KLocalizedString> 0031 #include <KOpenWithDialog> 0032 #include <KPropertiesDialog> 0033 #include <autostartscriptdesktopfile.h> 0034 0035 // FDO user autostart directories are 0036 // .config/autostart which has .desktop files executed by klaunch or systemd, some of which might be scripts 0037 0038 // Then we have Plasma-specific locations which run scripts 0039 // .config/autostart-scripts which has scripts executed by plasma_session (now migrated to .desktop files) 0040 // .config/plasma-workspace/shutdown which has scripts executed by plasma-shutdown 0041 // .config/plasma-workspace/env which has scripts executed by startplasma 0042 0043 // in the case of pre-startup they have to end in .sh 0044 // everywhere else it doesn't matter 0045 0046 // the comment above describes how autostart *currently* works, it is not definitive documentation on how autostart *should* work 0047 0048 // share/autostart shouldn't be an option as this should be reserved for global autostart entries 0049 0050 std::optional<AutostartEntry> AutostartModel::loadDesktopEntry(const QString &fileName) 0051 { 0052 KDesktopFile config(fileName); 0053 const KConfigGroup grp = config.desktopGroup(); 0054 const auto name = config.readName(); 0055 const bool hidden = grp.readEntry("Hidden", false); 0056 0057 if (hidden) { 0058 return {}; 0059 } 0060 0061 const QStringList notShowList = grp.readXdgListEntry("NotShowIn"); 0062 const QStringList onlyShowList = grp.readXdgListEntry("OnlyShowIn"); 0063 const bool enabled = !(notShowList.contains(QLatin1String("KDE")) || (!onlyShowList.isEmpty() && !onlyShowList.contains(QLatin1String("KDE")))); 0064 0065 if (!enabled) { 0066 return {}; 0067 } 0068 0069 const auto lstEntry = grp.readXdgListEntry("OnlyShowIn"); 0070 const bool onlyInPlasma = lstEntry.contains(QLatin1String("KDE")); 0071 const QString iconName = !config.readIcon().isEmpty() ? config.readIcon() : QStringLiteral("dialog-scripts"); 0072 const auto kind = AutostartScriptDesktopFile::isAutostartScript(config) ? XdgScripts : XdgAutoStart; // .config/autostart load desktop at startup 0073 const QString tryCommand = grp.readEntry("TryExec"); 0074 0075 // Try to filter out entries that point to nonexistant programs 0076 // If TryExec is either found in $PATH or is an absolute file path that exists 0077 // This doesn't detect uninstalled Flatpaks for example though 0078 if (!tryCommand.isEmpty() && QStandardPaths::findExecutable(tryCommand).isEmpty() && !QFile::exists(tryCommand)) { 0079 return {}; 0080 } 0081 0082 if (kind == XdgScripts) { 0083 const QString targetScriptPath = grp.readEntry("Exec"); 0084 const QString targetFileName = QUrl::fromLocalFile(targetScriptPath).fileName(); 0085 const QString targetScriptDir = QFileInfo(targetScriptPath).absoluteDir().path(); 0086 0087 return AutostartEntry{targetFileName, targetScriptDir, kind, enabled, fileName, onlyInPlasma, iconName}; 0088 } 0089 0090 return AutostartEntry{name, name, kind, enabled, fileName, onlyInPlasma, iconName}; 0091 } 0092 0093 AutostartModel::AutostartModel(QObject *parent) 0094 : QAbstractListModel(parent) 0095 , m_xdgConfigPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)) 0096 , m_xdgAutoStartPath(m_xdgConfigPath.filePath(QStringLiteral("autostart"))) 0097 { 0098 } 0099 0100 void AutostartModel::load() 0101 { 0102 beginResetModel(); 0103 0104 m_entries.clear(); 0105 0106 // Creates if doesn't already exist 0107 m_xdgAutoStartPath.mkpath(QStringLiteral(".")); 0108 0109 // Needed to add all script entries after application entries 0110 QVector<AutostartEntry> scriptEntries; 0111 const auto filesInfo = m_xdgAutoStartPath.entryInfoList(QDir::Files); 0112 for (const QFileInfo &fi : filesInfo) { 0113 if (!KDesktopFile::isDesktopFile(fi.fileName())) { 0114 continue; 0115 } 0116 0117 const std::optional<AutostartEntry> entry = loadDesktopEntry(fi.absoluteFilePath()); 0118 0119 if (!entry) { 0120 continue; 0121 } 0122 0123 if (entry->source == XdgScripts) { 0124 scriptEntries.push_back(entry.value()); 0125 } else { 0126 m_entries.push_back(entry.value()); 0127 } 0128 } 0129 0130 m_entries.append(scriptEntries); 0131 0132 loadScriptsFromDir(QStringLiteral("plasma-workspace/env/"), AutostartModel::AutostartEntrySource::PlasmaEnvScripts); 0133 0134 loadScriptsFromDir(QStringLiteral("plasma-workspace/shutdown/"), AutostartModel::AutostartEntrySource::PlasmaShutdown); 0135 0136 endResetModel(); 0137 } 0138 0139 void AutostartModel::loadScriptsFromDir(const QString &subDir, AutostartModel::AutostartEntrySource kind) 0140 { 0141 QDir dir(m_xdgConfigPath.filePath(subDir)); 0142 // Creates if doesn't already exist 0143 dir.mkpath(QStringLiteral(".")); 0144 0145 const auto autostartDirFilesInfo = dir.entryInfoList(QDir::Files); 0146 for (const QFileInfo &fi : autostartDirFilesInfo) { 0147 QString targetFileDir = fi.absoluteDir().path(); 0148 QString targetFilePath = fi.absoluteFilePath(); 0149 QString fileName = QUrl::fromLocalFile(targetFilePath).fileName(); 0150 const bool isSymlink = fi.isSymLink(); 0151 if (isSymlink) { 0152 targetFilePath = fi.symLinkTarget(); 0153 QFileInfo symLinkTarget(targetFilePath); 0154 targetFileDir = symLinkTarget.absoluteDir().path(); 0155 fileName = symLinkTarget.fileName(); 0156 } 0157 0158 m_entries.push_back({fileName, targetFileDir, kind, true, fi.absoluteFilePath(), false, QStringLiteral("dialog-scripts")}); 0159 } 0160 } 0161 0162 int AutostartModel::rowCount(const QModelIndex &parent) const 0163 { 0164 if (parent.isValid()) { 0165 return 0; 0166 } 0167 0168 return m_entries.count(); 0169 } 0170 0171 bool AutostartModel::reloadEntry(const QModelIndex &index, const QString &fileName) 0172 { 0173 if (!checkIndex(index)) { 0174 return false; 0175 } 0176 0177 const std::optional<AutostartEntry> newEntry = loadDesktopEntry(fileName); 0178 0179 if (!newEntry) { 0180 return false; 0181 } 0182 0183 m_entries.replace(index.row(), newEntry.value()); 0184 Q_EMIT dataChanged(index, index); 0185 return true; 0186 } 0187 0188 QVariant AutostartModel::data(const QModelIndex &index, int role) const 0189 { 0190 if (!checkIndex(index)) { 0191 return QVariant(); 0192 } 0193 0194 const auto &entry = m_entries.at(index.row()); 0195 0196 switch (role) { 0197 case Name: 0198 return entry.name; 0199 case Enabled: 0200 return entry.enabled; 0201 case Source: 0202 return entry.source; 0203 case FileName: 0204 return entry.fileName; 0205 case OnlyInPlasma: 0206 return entry.onlyInPlasma; 0207 case IconName: 0208 return entry.iconName; 0209 case TargetFileDirPath: 0210 return entry.targetFileDirPath; 0211 } 0212 0213 return QVariant(); 0214 } 0215 0216 void AutostartModel::addApplication(const KService::Ptr &service) 0217 { 0218 QString desktopPath; 0219 // It is important to ensure that we make an exact copy of an existing 0220 // desktop file (if selected) to enable users to override global autostarts. 0221 // Also see 0222 // https://bugs.launchpad.net/ubuntu/+source/kde-workspace/+bug/923360 0223 if (service->desktopEntryName().isEmpty() || service->entryPath().isEmpty()) { 0224 // create a new desktop file in s_desktopPath 0225 desktopPath = m_xdgAutoStartPath.filePath(service->name() + QStringLiteral(".desktop")); 0226 0227 if (QFileInfo::exists(desktopPath)) { 0228 QUrl baseUrl = QUrl::fromLocalFile(m_xdgAutoStartPath.path()); 0229 QString newName = suggestName(baseUrl, service->name() + QStringLiteral(".desktop")); 0230 desktopPath = m_xdgAutoStartPath.filePath(newName); 0231 } 0232 0233 KDesktopFile desktopFile(desktopPath); 0234 KConfigGroup kcg = desktopFile.desktopGroup(); 0235 kcg.writeEntry("Name", service->name()); 0236 kcg.writeEntry("Exec", service->exec()); 0237 kcg.writeEntry("Icon", service->icon()); 0238 kcg.writeEntry("Path", ""); 0239 kcg.writeEntry("Terminal", service->terminal() ? "True" : "False"); 0240 kcg.writeEntry("Type", "Application"); 0241 desktopFile.sync(); 0242 0243 } else { 0244 desktopPath = m_xdgAutoStartPath.filePath(service->storageId()); 0245 0246 KDesktopFile desktopFile(service->entryPath()); 0247 0248 if (QFileInfo::exists(desktopPath)) { 0249 QUrl baseUrl = QUrl::fromLocalFile(m_xdgAutoStartPath.path()); 0250 QString newName = suggestName(baseUrl, service->storageId()); 0251 desktopPath = m_xdgAutoStartPath.filePath(newName); 0252 } 0253 0254 // copy original desktop file to new path 0255 auto newDesktopFile = desktopFile.copyTo(desktopPath); 0256 newDesktopFile->sync(); 0257 } 0258 0259 const QString iconName = !service->icon().isEmpty() ? service->icon() : QStringLiteral("dialog-scripts"); 0260 0261 const auto entry = AutostartEntry{service->name(), 0262 service->name(), 0263 AutostartModel::AutostartEntrySource::XdgAutoStart, // .config/autostart load desktop at startup 0264 true, 0265 desktopPath, 0266 false, 0267 iconName}; 0268 0269 int lastApplication = -1; 0270 for (const AutostartEntry &e : qAsConst(m_entries)) { 0271 if (e.source == AutostartModel::AutostartEntrySource::XdgScripts) { 0272 break; 0273 } 0274 ++lastApplication; 0275 } 0276 0277 // push before the script items 0278 const int index = lastApplication + 1; 0279 0280 beginInsertRows(QModelIndex(), index, index); 0281 0282 m_entries.insert(index, entry); 0283 0284 endInsertRows(); 0285 } 0286 0287 void AutostartModel::showApplicationDialog(QQuickItem *context) 0288 { 0289 KOpenWithDialog *owdlg = new KOpenWithDialog(); 0290 owdlg->setAttribute(Qt::WA_DeleteOnClose); 0291 0292 if (context && context->window()) { 0293 if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { 0294 owdlg->winId(); // so it creates windowHandle 0295 owdlg->windowHandle()->setTransientParent(actualWindow); 0296 owdlg->setModal(true); 0297 } 0298 } 0299 0300 connect(owdlg, &QDialog::finished, this, [this, owdlg](int result) { 0301 if (result != QDialog::Accepted) { 0302 return; 0303 } 0304 0305 const KService::Ptr service = owdlg->service(); 0306 0307 Q_ASSERT(service); 0308 if (!service) { 0309 return; // Don't crash if KOpenWith wasn't able to create service. 0310 } 0311 0312 addApplication(service); 0313 }); 0314 owdlg->open(); 0315 } 0316 0317 void AutostartModel::addScript(const QUrl &url, AutostartModel::AutostartEntrySource kind) 0318 { 0319 const QFileInfo file(url.toLocalFile()); 0320 0321 if (!file.isAbsolute()) { 0322 Q_EMIT error(i18n("\"%1\" is not an absolute url.", url.toLocalFile())); 0323 return; 0324 } else if (!file.exists()) { 0325 Q_EMIT error(i18n("\"%1\" does not exist.", url.toLocalFile())); 0326 return; 0327 } else if (!file.isFile()) { 0328 Q_EMIT error(i18n("\"%1\" is not a file.", url.toLocalFile())); 0329 return; 0330 } else if (!file.isReadable()) { 0331 Q_EMIT error(i18n("\"%1\" is not readable.", url.toLocalFile())); 0332 return; 0333 } 0334 0335 QFile scriptFile(url.toLocalFile()); 0336 0337 if (!(scriptFile.permissions() & QFile::ExeUser)) { 0338 Q_EMIT nonExecutableScript(url.toLocalFile(), kind); 0339 } 0340 0341 QString fileName = url.fileName(); 0342 0343 if (kind == AutostartModel::AutostartEntrySource::XdgScripts) { 0344 int lastLoginScript = -1; 0345 for (const AutostartEntry &e : qAsConst(m_entries)) { 0346 if (e.source == AutostartModel::AutostartEntrySource::PlasmaShutdown) { 0347 break; 0348 } 0349 ++lastLoginScript; 0350 } 0351 0352 // path of the desktop file that is about to be created 0353 const QString newFilePath = m_xdgAutoStartPath.absoluteFilePath(fileName + QStringLiteral(".desktop")); 0354 0355 if (QFileInfo::exists(newFilePath)) { 0356 const QUrl baseUrl = QUrl::fromLocalFile(m_xdgAutoStartPath.path()); 0357 fileName = suggestName(baseUrl, fileName + QStringLiteral(".desktop")); 0358 0359 // remove the .desktop part from String 0360 fileName.chop(8); 0361 } 0362 AutostartScriptDesktopFile desktopFile(fileName, KShell::quoteArg(file.filePath())); 0363 insertScriptEntry(lastLoginScript + 1, file.fileName(), file.absoluteDir().path(), desktopFile.fileName(), kind); 0364 0365 } else if (kind == AutostartModel::AutostartEntrySource::PlasmaShutdown) { 0366 const QUrl destinationScript = QUrl::fromLocalFile(QDir(m_xdgConfigPath.filePath(QStringLiteral("plasma-workspace/shutdown/"))).filePath(fileName)); 0367 KIO::CopyJob *job = KIO::link(url, destinationScript, KIO::HideProgressInfo); 0368 job->setAutoRename(true); 0369 job->setProperty("finalUrl", destinationScript); 0370 0371 connect(job, &KIO::CopyJob::renamed, this, [](KIO::Job *job, const QUrl &from, const QUrl &to) { 0372 Q_UNUSED(from) 0373 // in case the destination filename had to be renamed 0374 job->setProperty("finalUrl", to); 0375 }); 0376 0377 connect(job, &KJob::finished, this, [this, url, kind](KJob *theJob) { 0378 if (theJob->error()) { 0379 qCWarning(KCM_AUTOSTART_DEBUG) << "Could not add script entry" << theJob->errorString(); 0380 return; 0381 } 0382 const QUrl dest = theJob->property("finalUrl").toUrl(); 0383 const QFileInfo destFile(dest.path()); 0384 const QString symLinkFileName = QUrl::fromLocalFile(destFile.symLinkTarget()).fileName(); 0385 const QFileInfo symLinkTarget{destFile.symLinkTarget()}; 0386 const QString symLinkTargetDir = symLinkTarget.absoluteDir().path(); 0387 insertScriptEntry(m_entries.size(), symLinkFileName, symLinkTargetDir, dest.path(), kind); 0388 }); 0389 0390 job->start(); 0391 } else { 0392 Q_ASSERT(0); 0393 } 0394 } 0395 0396 void AutostartModel::insertScriptEntry(int index, const QString &name, const QString &targetFileDirPath, const QString &path, AutostartEntrySource kind) 0397 { 0398 beginInsertRows(QModelIndex(), index, index); 0399 0400 AutostartEntry entry = AutostartEntry{name, targetFileDirPath, kind, true, path, false, QStringLiteral("dialog-scripts")}; 0401 0402 m_entries.insert(index, entry); 0403 0404 endInsertRows(); 0405 } 0406 0407 void AutostartModel::removeEntry(int row) 0408 { 0409 const auto entry = m_entries.at(row); 0410 0411 KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(entry.fileName), KIO::HideProgressInfo); 0412 0413 connect(job, &KJob::finished, this, [this, row, entry](KJob *theJob) { 0414 if (theJob->error()) { 0415 qCWarning(KCM_AUTOSTART_DEBUG) << "Could not remove entry" << theJob->errorString(); 0416 return; 0417 } 0418 0419 beginRemoveRows(QModelIndex(), row, row); 0420 m_entries.remove(row); 0421 0422 endRemoveRows(); 0423 }); 0424 0425 job->start(); 0426 } 0427 0428 QHash<int, QByteArray> AutostartModel::roleNames() const 0429 { 0430 QHash<int, QByteArray> roleNames = QAbstractListModel::roleNames(); 0431 0432 roleNames.insert(Name, QByteArrayLiteral("name")); 0433 roleNames.insert(Enabled, QByteArrayLiteral("enabled")); 0434 roleNames.insert(Source, QByteArrayLiteral("source")); 0435 roleNames.insert(FileName, QByteArrayLiteral("fileName")); 0436 roleNames.insert(OnlyInPlasma, QByteArrayLiteral("onlyInPlasma")); 0437 roleNames.insert(IconName, QByteArrayLiteral("iconName")); 0438 roleNames.insert(TargetFileDirPath, QByteArrayLiteral("targetFileDirPath")); 0439 0440 return roleNames; 0441 } 0442 0443 void AutostartModel::editApplication(int row, QQuickItem *context) 0444 { 0445 const QModelIndex idx = index(row, 0); 0446 0447 const QString fileName = data(idx, AutostartModel::Roles::FileName).toString(); 0448 KFileItem kfi(QUrl::fromLocalFile(fileName)); 0449 kfi.setDelayedMimeTypes(true); 0450 0451 KPropertiesDialog *dlg = new KPropertiesDialog(kfi, nullptr); 0452 dlg->setAttribute(Qt::WA_DeleteOnClose); 0453 0454 if (context && context->window()) { 0455 if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { 0456 dlg->winId(); // so it creates windowHandle 0457 dlg->windowHandle()->setTransientParent(actualWindow); 0458 dlg->setModal(true); 0459 } 0460 } 0461 0462 connect(dlg, &QDialog::finished, this, [this, idx, dlg](int result) { 0463 if (result == QDialog::Accepted) { 0464 reloadEntry(idx, dlg->item().localPath()); 0465 } 0466 }); 0467 dlg->open(); 0468 } 0469 0470 void AutostartModel::makeFileExecutable(const QString &fileName) 0471 { 0472 QFile file(fileName); 0473 0474 file.setPermissions(file.permissions() | QFile::ExeUser); 0475 } 0476 0477 // Use slightly modified code copied from frameworks KFileUtils because desktop filenames cannot contain '(' or ' '. 0478 QString AutostartModel::makeSuggestedName(const QString &oldName) 0479 { 0480 QString basename; 0481 0482 // Extract the original file extension from the filename 0483 QMimeDatabase db; 0484 QString nameSuffix = db.suffixForFileName(oldName); 0485 0486 if (oldName.lastIndexOf(QLatin1Char('.')) == 0) { 0487 basename = QStringLiteral("."); 0488 nameSuffix = oldName; 0489 } else if (nameSuffix.isEmpty()) { 0490 const int lastDot = oldName.lastIndexOf(QLatin1Char('.')); 0491 if (lastDot == -1) { 0492 basename = oldName; 0493 } else { 0494 basename = oldName.left(lastDot); 0495 nameSuffix = oldName.mid(lastDot); 0496 } 0497 } else { 0498 nameSuffix.prepend(QLatin1Char('.')); 0499 basename = oldName.left(oldName.length() - nameSuffix.length()); 0500 } 0501 0502 // check if (number) exists at the end of the oldName and increment that number 0503 const QRegularExpression re(QStringLiteral("_(\\d+)_")); 0504 QRegularExpressionMatch rmatch; 0505 oldName.lastIndexOf(re, -1, &rmatch); 0506 if (rmatch.hasMatch()) { 0507 const int currentNum = rmatch.captured(1).toInt(); 0508 const QString number = QString::number(currentNum + 1); 0509 basename.replace(rmatch.capturedStart(1), rmatch.capturedLength(1), number); 0510 } else { 0511 // number does not exist, so just append " _1_" to filename 0512 basename += QLatin1String("_1_"); 0513 } 0514 0515 return basename + nameSuffix; 0516 } 0517 0518 QString AutostartModel::suggestName(const QUrl &baseURL, const QString &oldName) 0519 { 0520 QString suggestedName = makeSuggestedName(oldName); 0521 0522 if (baseURL.isLocalFile()) { 0523 const QString basePath = baseURL.toLocalFile() + QLatin1Char('/'); 0524 while (QFileInfo::exists(basePath + suggestedName)) { 0525 suggestedName = makeSuggestedName(suggestedName); 0526 } 0527 } 0528 0529 return suggestedName; 0530 }