File indexing completed on 2024-05-19 05:38:00

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