File indexing completed on 2024-04-28 15:27:26

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
0004     SPDX-FileCopyrightText: 1999, 2000 Preston Brown <pbrown@kde.org>
0005     SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
0006     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0007     SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
0008     SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
0009     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-or-later
0012 */
0013 
0014 /*
0015  * kpropertiesdialog.cpp
0016  * View/Edit Properties of files, locally or remotely
0017  *
0018  * some FilePermissionsPropsPlugin-changes by
0019  *  Henner Zeller <zeller@think.de>
0020  * some layout management by
0021  *  Bertrand Leconte <B.Leconte@mail.dotcom.fr>
0022  * the rest of the layout management, bug fixes, adaptation to libkio,
0023  * template feature by
0024  *  David Faure <faure@kde.org>
0025  * More layout, cleanups, and fixes by
0026  *  Preston Brown <pbrown@kde.org>
0027  * Plugin capability, cleanups and port to KDialog by
0028  *  Simon Hausmann <hausmann@kde.org>
0029  * KDesktopPropsPlugin by
0030  *  Waldo Bastian <bastian@kde.org>
0031  */
0032 
0033 #include "kpropertiesdialog.h"
0034 #include "../utils_p.h"
0035 #include "kio_widgets_debug.h"
0036 #include "kpropertiesdialog_p.h"
0037 
0038 #include <config-kiowidgets.h>
0039 
0040 #include <gpudetection_p.h>
0041 #include <kacl.h>
0042 #include <kbuildsycocaprogressdialog.h>
0043 #include <kdirnotify.h>
0044 #include <kfileitemlistproperties.h>
0045 #include <kio/chmodjob.h>
0046 #include <kio/copyjob.h>
0047 #include <kio/desktopexecparser.h>
0048 #include <kio/directorysizejob.h>
0049 #include <kio/global.h>
0050 #include <kio/jobuidelegate.h>
0051 #include <kio/renamedialog.h>
0052 #include <kio/statjob.h>
0053 #include <kioglobal_p.h>
0054 #include <kmountpoint.h>
0055 #include <kprotocolinfo.h>
0056 #include <kprotocolmanager.h>
0057 #include <kurlrequester.h>
0058 
0059 #include <KApplicationTrader>
0060 #include <KAuthorized>
0061 #include <KCapacityBar>
0062 #include <KColorScheme>
0063 #include <KCompletion>
0064 #include <KConfigGroup>
0065 #include <KDesktopFile>
0066 #include <KDialogJobUiDelegate>
0067 #include <KIO/ApplicationLauncherJob>
0068 #include <KIO/FileSystemFreeSpaceJob>
0069 #include <KIO/OpenFileManagerWindowJob>
0070 #include <KIconButton>
0071 #include <KJobUiDelegate>
0072 #include <KJobWidgets>
0073 #include <KLazyLocalizedString>
0074 #include <KLineEdit>
0075 #include <KLocalizedString>
0076 #include <KMessageBox>
0077 #include <KMessageWidget>
0078 #include <KMimeTypeChooser>
0079 #include <KMimeTypeEditor>
0080 #include <KMimeTypeTrader>
0081 #include <KPluginFactory>
0082 #include <KPluginMetaData>
0083 #include <KSeparator>
0084 #include <KService>
0085 #include <KSharedConfig>
0086 #include <KShell>
0087 #include <KSqueezedTextLabel>
0088 #include <KSycoca>
0089 #include <KWindowConfig>
0090 
0091 #include <qplatformdefs.h>
0092 
0093 #include <QCheckBox>
0094 #include <QClipboard>
0095 #include <QComboBox>
0096 #include <QDBusConnection>
0097 #include <QDBusInterface>
0098 #include <QDBusReply>
0099 #include <QDebug>
0100 #include <QDialogButtonBox>
0101 #include <QDir>
0102 #include <QFile>
0103 #include <QFileDialog>
0104 #include <QFileInfo>
0105 #include <QFileSystemWatcher>
0106 #include <QFutureWatcher>
0107 #include <QLabel>
0108 #include <QLayout>
0109 #include <QLocale>
0110 #include <QMimeDatabase>
0111 #include <QProgressBar>
0112 #include <QPushButton>
0113 #include <QRegularExpression>
0114 #include <QStandardPaths>
0115 #include <QStyle>
0116 #include <QUrl>
0117 #include <QVector>
0118 #include <QtConcurrentRun>
0119 
0120 #include <cerrno>
0121 extern "C" {
0122 #if HAVE_SYS_XATTR_H
0123 #include <sys/xattr.h>
0124 #endif
0125 #if HAVE_SYS_EXTATTR_H
0126 #include <sys/extattr.h>
0127 #endif
0128 #if HAVE_SYS_MOUNT_H
0129 #include <sys/mount.h>
0130 #endif
0131 }
0132 
0133 #include "ui_checksumswidget.h"
0134 #include "ui_kfilepropspluginwidget.h"
0135 #include "ui_kpropertiesdesktopadvbase.h"
0136 #include "ui_kpropertiesdesktopbase.h"
0137 
0138 #include <algorithm>
0139 #include <cerrno>
0140 #include <functional>
0141 #include <vector>
0142 
0143 #if HAVE_POSIX_ACL
0144 #include "kacleditwidget.h"
0145 #endif
0146 
0147 #ifdef Q_OS_WIN
0148 #include <process.h>
0149 #include <qt_windows.h>
0150 #include <shellapi.h>
0151 #ifdef __GNUC__
0152 #warning TODO: port completely to win32
0153 #endif
0154 #endif
0155 
0156 using namespace KDEPrivate;
0157 
0158 static QString nameFromFileName(QString nameStr)
0159 {
0160     if (nameStr.endsWith(QLatin1String(".desktop"))) {
0161         nameStr.chop(8);
0162     }
0163     if (nameStr.endsWith(QLatin1String(".kdelnk"))) {
0164         nameStr.chop(7);
0165     }
0166     // Make it human-readable (%2F => '/', ...)
0167     nameStr = KIO::decodeFileName(nameStr);
0168     return nameStr;
0169 }
0170 
0171 constexpr mode_t KFilePermissionsPropsPlugin::fperm[3][4] = {{S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID},
0172                                                              {S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID},
0173                                                              {S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX}};
0174 
0175 static QString couldNotSaveMsg(const QString &path)
0176 {
0177     return xi18nc("@info", "Could not save properties due to insufficient write access to:<nl/><filename>%1</filename>.", path);
0178 }
0179 
0180 class KPropertiesDialogPrivate
0181 {
0182 public:
0183     explicit KPropertiesDialogPrivate(KPropertiesDialog *qq)
0184         : q(qq)
0185     {
0186     }
0187     ~KPropertiesDialogPrivate()
0188     {
0189         // qDeleteAll deletes the pages in order, this prevents crashes when closing the dialog
0190         qDeleteAll(m_pages);
0191     }
0192 
0193     /**
0194      * Common initialization for all constructors
0195      */
0196     void init();
0197     /**
0198      * Inserts all pages in the dialog.
0199      */
0200     void insertPages();
0201 
0202     KPropertiesDialog *const q;
0203     bool m_aborted = false;
0204     KPageWidgetItem *fileSharePageItem = nullptr;
0205     KFilePropsPlugin *m_filePropsPlugin = nullptr;
0206     KFilePermissionsPropsPlugin *m_permissionsPropsPlugin = nullptr;
0207     KDesktopPropsPlugin *m_desktopPropsPlugin = nullptr;
0208     KUrlPropsPlugin *m_urlPropsPlugin = nullptr;
0209 
0210     /**
0211      * The URL of the props dialog (when shown for only one file)
0212      */
0213     QUrl m_singleUrl;
0214     /**
0215      * List of items this props dialog is shown for
0216      */
0217     KFileItemList m_items;
0218     /**
0219      * For templates
0220      */
0221     QString m_defaultName;
0222     QUrl m_currentDir;
0223 
0224     /**
0225      * List of all plugins inserted ( first one first )
0226      */
0227     std::vector<KPropertiesDialogPlugin *> m_pages;
0228 };
0229 
0230 KPropertiesDialog::KPropertiesDialog(const KFileItem &item, QWidget *parent)
0231     : KPageDialog(parent)
0232     , d(new KPropertiesDialogPrivate(this))
0233 {
0234     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(item.name())));
0235 
0236     Q_ASSERT(!item.isNull());
0237     d->m_items.append(item);
0238 
0239     d->m_singleUrl = item.url();
0240     Q_ASSERT(!d->m_singleUrl.isEmpty());
0241 
0242     d->init();
0243 }
0244 
0245 KPropertiesDialog::KPropertiesDialog(const QString &title, QWidget *parent)
0246     : KPageDialog(parent)
0247     , d(new KPropertiesDialogPrivate(this))
0248 {
0249     setWindowTitle(i18n("Properties for %1", title));
0250 
0251     d->init();
0252 }
0253 
0254 KPropertiesDialog::KPropertiesDialog(const KFileItemList &_items, QWidget *parent)
0255     : KPageDialog(parent)
0256     , d(new KPropertiesDialogPrivate(this))
0257 {
0258     if (_items.count() > 1) {
0259         setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", _items.count()));
0260     } else {
0261         setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_items.first().name())));
0262     }
0263 
0264     Q_ASSERT(!_items.isEmpty());
0265     d->m_singleUrl = _items.first().url();
0266     Q_ASSERT(!d->m_singleUrl.isEmpty());
0267 
0268     d->m_items = _items;
0269 
0270     d->init();
0271 }
0272 
0273 KPropertiesDialog::KPropertiesDialog(const QUrl &_url, QWidget *parent)
0274     : KPageDialog(parent)
0275     , d(new KPropertiesDialogPrivate(this))
0276 {
0277     d->m_singleUrl = _url.adjusted(QUrl::StripTrailingSlash);
0278 
0279     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(d->m_singleUrl.fileName())));
0280 
0281     KIO::StatJob *job = KIO::stat(d->m_singleUrl);
0282     KJobWidgets::setWindow(job, parent);
0283     job->exec();
0284     KIO::UDSEntry entry = job->statResult();
0285 
0286     d->m_items.append(KFileItem(entry, d->m_singleUrl));
0287     d->init();
0288 }
0289 
0290 KPropertiesDialog::KPropertiesDialog(const QList<QUrl> &urls, QWidget *parent)
0291     : KPageDialog(parent)
0292     , d(new KPropertiesDialogPrivate(this))
0293 {
0294     if (urls.count() > 1) {
0295         setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", urls.count()));
0296     } else {
0297         setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(urls.first().fileName())));
0298     }
0299 
0300     Q_ASSERT(!urls.isEmpty());
0301     d->m_singleUrl = urls.first();
0302     Q_ASSERT(!d->m_singleUrl.isEmpty());
0303 
0304     d->m_items.reserve(urls.size());
0305     for (const QUrl &url : urls) {
0306         KIO::StatJob *job = KIO::stat(url);
0307         KJobWidgets::setWindow(job, parent);
0308         job->exec();
0309         KIO::UDSEntry entry = job->statResult();
0310 
0311         d->m_items.append(KFileItem(entry, url));
0312     }
0313 
0314     d->init();
0315 }
0316 
0317 KPropertiesDialog::KPropertiesDialog(const QUrl &_tempUrl, const QUrl &_currentDir, const QString &_defaultName, QWidget *parent)
0318     : KPageDialog(parent)
0319     , d(new KPropertiesDialogPrivate(this))
0320 {
0321     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_tempUrl.fileName())));
0322 
0323     d->m_singleUrl = _tempUrl;
0324     d->m_defaultName = _defaultName;
0325     d->m_currentDir = _currentDir;
0326     Q_ASSERT(!d->m_singleUrl.isEmpty());
0327 
0328     // Create the KFileItem for the _template_ file, in order to read from it.
0329     d->m_items.append(KFileItem(d->m_singleUrl));
0330     d->init();
0331 }
0332 
0333 #ifdef Q_OS_WIN
0334 bool showWin32FilePropertyDialog(const QString &fileName)
0335 {
0336     QString path_ = QDir::toNativeSeparators(QFileInfo(fileName).absoluteFilePath());
0337 
0338 #ifndef _WIN32_WCE
0339     SHELLEXECUTEINFOW execInfo;
0340 #else
0341     SHELLEXECUTEINFO execInfo;
0342 #endif
0343     memset(&execInfo, 0, sizeof(execInfo));
0344     execInfo.cbSize = sizeof(execInfo);
0345 #ifndef _WIN32_WCE
0346     execInfo.fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
0347 #else
0348     execInfo.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
0349 #endif
0350     const QString verb(QLatin1String("properties"));
0351     execInfo.lpVerb = (LPCWSTR)verb.utf16();
0352     execInfo.lpFile = (LPCWSTR)path_.utf16();
0353 #ifndef _WIN32_WCE
0354     return ShellExecuteExW(&execInfo);
0355 #else
0356     return ShellExecuteEx(&execInfo);
0357     // There is no native file property dialog in wince
0358     // return false;
0359 #endif
0360 }
0361 #endif
0362 
0363 bool KPropertiesDialog::showDialog(const KFileItem &item, QWidget *parent, bool modal)
0364 {
0365     // TODO: do we really want to show the win32 property dialog?
0366     // This means we lose metainfo, support for .desktop files, etc. (DF)
0367 #ifdef Q_OS_WIN
0368     QString localPath = item.localPath();
0369     if (!localPath.isEmpty()) {
0370         return showWin32FilePropertyDialog(localPath);
0371     }
0372 #endif
0373     KPropertiesDialog *dlg = new KPropertiesDialog(item, parent);
0374     if (modal) {
0375         dlg->exec();
0376     } else {
0377         dlg->show();
0378     }
0379 
0380     return true;
0381 }
0382 
0383 bool KPropertiesDialog::showDialog(const QUrl &_url, QWidget *parent, bool modal)
0384 {
0385 #ifdef Q_OS_WIN
0386     if (_url.isLocalFile()) {
0387         return showWin32FilePropertyDialog(_url.toLocalFile());
0388     }
0389 #endif
0390     KPropertiesDialog *dlg = new KPropertiesDialog(_url, parent);
0391     if (modal) {
0392         dlg->exec();
0393     } else {
0394         dlg->show();
0395     }
0396 
0397     return true;
0398 }
0399 
0400 bool KPropertiesDialog::showDialog(const KFileItemList &_items, QWidget *parent, bool modal)
0401 {
0402     if (_items.count() == 1) {
0403         const KFileItem &item = _items.first();
0404         if (item.entry().count() == 0 && item.localPath().isEmpty()) // this remote item wasn't listed by a worker
0405                                                                      // Let's stat to get more info on the file
0406         {
0407             return KPropertiesDialog::showDialog(item.url(), parent, modal);
0408         } else {
0409             return KPropertiesDialog::showDialog(_items.first(), parent, modal);
0410         }
0411     }
0412     KPropertiesDialog *dlg = new KPropertiesDialog(_items, parent);
0413     if (modal) {
0414         dlg->exec();
0415     } else {
0416         dlg->show();
0417     }
0418     return true;
0419 }
0420 
0421 bool KPropertiesDialog::showDialog(const QList<QUrl> &urls, QWidget *parent, bool modal)
0422 {
0423     KPropertiesDialog *dlg = new KPropertiesDialog(urls, parent);
0424     if (modal) {
0425         dlg->exec();
0426     } else {
0427         dlg->show();
0428     }
0429     return true;
0430 }
0431 
0432 void KPropertiesDialogPrivate::init()
0433 {
0434     q->setFaceType(KPageDialog::Tabbed);
0435 
0436     insertPages();
0437 
0438     KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog");
0439     KWindowConfig::restoreWindowSize(q->windowHandle(), group);
0440 }
0441 
0442 void KPropertiesDialog::showFileSharingPage()
0443 {
0444     if (d->fileSharePageItem) {
0445         setCurrentPage(d->fileSharePageItem);
0446     }
0447 }
0448 
0449 void KPropertiesDialog::setFileSharingPage(QWidget *page)
0450 {
0451     d->fileSharePageItem = addPage(page, i18nc("@title:tab", "Share"));
0452 }
0453 
0454 void KPropertiesDialog::setFileNameReadOnly(bool ro)
0455 {
0456     if (d->m_filePropsPlugin) {
0457         d->m_filePropsPlugin->setFileNameReadOnly(ro);
0458     }
0459 
0460     if (d->m_urlPropsPlugin) {
0461         d->m_urlPropsPlugin->setFileNameReadOnly(ro);
0462     }
0463 }
0464 
0465 KPropertiesDialog::~KPropertiesDialog()
0466 {
0467     KConfigGroup group(KSharedConfig::openConfig(), "KPropertiesDialog");
0468     KWindowConfig::saveWindowSize(windowHandle(), group, KConfigBase::Persistent);
0469 }
0470 
0471 void KPropertiesDialog::insertPlugin(KPropertiesDialogPlugin *plugin)
0472 {
0473     connect(plugin, &KPropertiesDialogPlugin::changed, plugin, qOverload<>(&KPropertiesDialogPlugin::setDirty));
0474 
0475     d->m_pages.push_back(plugin);
0476 }
0477 
0478 QUrl KPropertiesDialog::url() const
0479 {
0480     return d->m_singleUrl;
0481 }
0482 
0483 KFileItem &KPropertiesDialog::item()
0484 {
0485     return d->m_items.first();
0486 }
0487 
0488 KFileItemList KPropertiesDialog::items() const
0489 {
0490     return d->m_items;
0491 }
0492 
0493 QUrl KPropertiesDialog::currentDir() const
0494 {
0495     return d->m_currentDir;
0496 }
0497 
0498 QString KPropertiesDialog::defaultName() const
0499 {
0500     return d->m_defaultName;
0501 }
0502 
0503 bool KPropertiesDialog::canDisplay(const KFileItemList &_items)
0504 {
0505     // TODO: cache the result of those calls. Currently we parse .desktop files far too many times
0506     /* clang-format off */
0507     return KFilePropsPlugin::supports(_items)
0508         || KFilePermissionsPropsPlugin::supports(_items)
0509         || KDesktopPropsPlugin::supports(_items)
0510         || KUrlPropsPlugin::supports(_items);
0511     /* clang-format on */
0512 }
0513 
0514 void KPropertiesDialog::slotOk()
0515 {
0516     accept();
0517 }
0518 
0519 void KPropertiesDialog::accept()
0520 {
0521     d->m_aborted = false;
0522 
0523     auto acceptAndClose = [this]() {
0524         Q_EMIT applied();
0525         Q_EMIT propertiesClosed();
0526         deleteLater(); // Somewhat like Qt::WA_DeleteOnClose would do.
0527         KPageDialog::accept();
0528     };
0529 
0530     const bool isAnyDirty = std::any_of(d->m_pages.cbegin(), d->m_pages.cend(), [](const KPropertiesDialogPlugin *page) {
0531         return page->isDirty();
0532     });
0533 
0534     if (!isAnyDirty) { // No point going further
0535         acceptAndClose();
0536         return;
0537     }
0538 
0539     // If any page is dirty, then set the main one (KFilePropsPlugin) as
0540     // dirty too. This is what makes it possible to save changes to a global
0541     // desktop file into a local one. In other cases, it doesn't hurt.
0542     if (d->m_filePropsPlugin) {
0543         d->m_filePropsPlugin->setDirty(true);
0544     }
0545 
0546     // Changes are applied in the following order:
0547     // - KFilePropsPlugin changes, this is because in case of renaming an item or saving changes
0548     //   of a template or a .desktop file, the renaming or copying respectively, must be finished
0549     //   first, before applying the rest of the changes
0550     // - KFilePermissionsPropsPlugin changes, e.g. if the item was read-only and was changed to
0551     //   read/write, this must be applied first for other changes to work
0552     // - The rest of the changes from the other plugins/tabs
0553     // - KFilePropsPlugin::postApplyChanges()
0554 
0555     auto applyOtherChanges = [this, acceptAndClose]() {
0556         Q_ASSERT(!d->m_filePropsPlugin->isDirty());
0557         Q_ASSERT(!d->m_permissionsPropsPlugin->isDirty());
0558 
0559         // Apply the changes for the rest of the plugins
0560         for (auto *page : d->m_pages) {
0561             if (d->m_aborted) {
0562                 break;
0563             }
0564 
0565             if (page->isDirty()) {
0566                 // qDebug() << "applying changes for " << page->metaObject()->className();
0567                 page->applyChanges();
0568             }
0569             /* else {
0570                 qDebug() << "skipping page " << page->metaObject()->className();
0571             } */
0572         }
0573 
0574         if (!d->m_aborted && d->m_filePropsPlugin) {
0575             d->m_filePropsPlugin->postApplyChanges();
0576         }
0577 
0578         if (!d->m_aborted) {
0579             acceptAndClose();
0580         } // Else, keep dialog open for user to fix the problem.
0581     };
0582 
0583     auto applyPermissionsChanges = [this, applyOtherChanges]() {
0584         connect(d->m_permissionsPropsPlugin, &KFilePermissionsPropsPlugin::changesApplied, this, [applyOtherChanges]() {
0585             applyOtherChanges();
0586         });
0587 
0588         d->m_permissionsPropsPlugin->applyChanges();
0589     };
0590 
0591     if (d->m_filePropsPlugin && d->m_filePropsPlugin->isDirty()) {
0592         // changesApplied() is _not_ emitted if applying the changes was aborted
0593         connect(d->m_filePropsPlugin, &KFilePropsPlugin::changesApplied, this, [this, applyPermissionsChanges, applyOtherChanges]() {
0594             if (d->m_permissionsPropsPlugin && d->m_permissionsPropsPlugin->isDirty()) {
0595                 applyPermissionsChanges();
0596             } else {
0597                 applyOtherChanges();
0598             }
0599         });
0600 
0601         d->m_filePropsPlugin->applyChanges();
0602     }
0603 }
0604 
0605 void KPropertiesDialog::slotCancel()
0606 {
0607     reject();
0608 }
0609 
0610 void KPropertiesDialog::reject()
0611 {
0612     Q_EMIT canceled();
0613     Q_EMIT propertiesClosed();
0614 
0615     deleteLater();
0616     KPageDialog::reject();
0617 }
0618 
0619 void KPropertiesDialogPrivate::insertPages()
0620 {
0621     if (m_items.isEmpty()) {
0622         return;
0623     }
0624 
0625     if (KFilePropsPlugin::supports(m_items)) {
0626         m_filePropsPlugin = new KFilePropsPlugin(q);
0627         q->insertPlugin(m_filePropsPlugin);
0628     }
0629 
0630     if (KFilePermissionsPropsPlugin::supports(m_items)) {
0631         m_permissionsPropsPlugin = new KFilePermissionsPropsPlugin(q);
0632         q->insertPlugin(m_permissionsPropsPlugin);
0633     }
0634 
0635     if (KChecksumsPlugin::supports(m_items)) {
0636         KPropertiesDialogPlugin *p = new KChecksumsPlugin(q);
0637         q->insertPlugin(p);
0638     }
0639 
0640     if (KDesktopPropsPlugin::supports(m_items)) {
0641         m_desktopPropsPlugin = new KDesktopPropsPlugin(q);
0642         q->insertPlugin(m_desktopPropsPlugin);
0643     }
0644 
0645     if (KUrlPropsPlugin::supports(m_items)) {
0646         m_urlPropsPlugin = new KUrlPropsPlugin(q);
0647         q->insertPlugin(m_urlPropsPlugin);
0648     }
0649 
0650     if (m_items.count() != 1) {
0651         return;
0652     }
0653 
0654     const KFileItem item = m_items.first();
0655     const QString mimetype = item.mimetype();
0656 
0657     if (mimetype.isEmpty()) {
0658         return;
0659     }
0660 
0661     const auto scheme = item.url().scheme();
0662     QString query =
0663         QStringLiteral("(((not exist [X-KDE-Protocol]) and (not exist [X-KDE-Protocols])) or ([X-KDE-Protocol] == '%1') or ('%1' in [X-KDE-Protocols]))")
0664             .arg(scheme);
0665 
0666     // qDebug() << "trader query: " << query;
0667 
0668     QStringList addedPlugins;
0669     const auto filter = [mimetype, scheme](const KPluginMetaData &metaData) {
0670         const auto supportedProtocol = metaData.value(QStringLiteral("X-KDE-Protocol"), QString());
0671         if (!supportedProtocol.isEmpty() && supportedProtocol != scheme) {
0672             return false;
0673         }
0674 
0675         const auto supportedProtocols = metaData.value(QStringLiteral("X-KDE-Protocols"), QStringList());
0676         if (!supportedProtocols.isEmpty()) {
0677             const auto none = std::none_of(supportedProtocols.cbegin(), supportedProtocols.cend(), [scheme](const auto &protocol) {
0678                 return !protocol.isEmpty() && protocol == scheme;
0679             });
0680             if (none) {
0681                 return false;
0682             }
0683         }
0684 
0685         return metaData.mimeTypes().isEmpty() || metaData.supportsMimeType(mimetype);
0686     };
0687     const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/propertiesdialog"), filter);
0688     for (const auto &jsonMetadata : jsonPlugins) {
0689         if (auto plugin = KPluginFactory::instantiatePlugin<KPropertiesDialogPlugin>(jsonMetadata, q).plugin) {
0690             q->insertPlugin(plugin);
0691             addedPlugins.append(jsonMetadata.pluginId());
0692         }
0693     }
0694 
0695 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
0696     QT_WARNING_PUSH
0697     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0698     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0699     const KService::List offers = KMimeTypeTrader::self()->query(mimetype, QStringLiteral("KPropertiesDialog/Plugin"), query);
0700     for (const KService::Ptr &ptr : offers) {
0701         if (addedPlugins.contains(ptr->desktopEntryName())) {
0702             continue;
0703         }
0704         qCWarning(KIO_WIDGETS) << "Plugin" << ptr->desktopEntryName() << "is using the deprecated loading style. Please port it to JSON loading.";
0705         KPropertiesDialogPlugin *plugin = ptr->createInstance<KPropertiesDialogPlugin>(q);
0706         if (!plugin) {
0707             continue;
0708         }
0709         plugin->setObjectName(ptr->name());
0710 
0711         q->insertPlugin(plugin);
0712         addedPlugins.append(ptr->desktopEntryName());
0713     }
0714     QT_WARNING_POP
0715 #endif
0716 }
0717 
0718 void KPropertiesDialog::updateUrl(const QUrl &_newUrl)
0719 {
0720     Q_ASSERT(d->m_items.count() == 1);
0721     // qDebug() << "KPropertiesDialog::updateUrl (pre)" << _newUrl;
0722     QUrl newUrl = _newUrl;
0723     Q_EMIT saveAs(d->m_singleUrl, newUrl);
0724     // qDebug() << "KPropertiesDialog::updateUrl (post)" << newUrl;
0725 
0726     d->m_singleUrl = newUrl;
0727     d->m_items.first().setUrl(newUrl);
0728     Q_ASSERT(!d->m_singleUrl.isEmpty());
0729     // If we have an Desktop page, set it dirty, so that a full file is saved locally
0730     // Same for a URL page (because of the Name= hack)
0731     if (d->m_urlPropsPlugin) {
0732         d->m_urlPropsPlugin->setDirty();
0733     } else if (d->m_desktopPropsPlugin) {
0734         d->m_desktopPropsPlugin->setDirty();
0735     }
0736 }
0737 
0738 void KPropertiesDialog::rename(const QString &_name)
0739 {
0740     Q_ASSERT(d->m_items.count() == 1);
0741     // qDebug() << "KPropertiesDialog::rename " << _name;
0742     QUrl newUrl;
0743     // if we're creating from a template : use currentdir
0744     if (!d->m_currentDir.isEmpty()) {
0745         newUrl = d->m_currentDir;
0746         newUrl.setPath(Utils::concatPaths(newUrl.path(), _name));
0747     } else {
0748         // It's a directory, so strip the trailing slash first
0749         newUrl = d->m_singleUrl.adjusted(QUrl::StripTrailingSlash);
0750         // Now change the filename
0751         newUrl = newUrl.adjusted(QUrl::RemoveFilename); // keep trailing slash
0752         newUrl.setPath(Utils::concatPaths(newUrl.path(), _name));
0753     }
0754     updateUrl(newUrl);
0755 }
0756 
0757 void KPropertiesDialog::abortApplying()
0758 {
0759     d->m_aborted = true;
0760 }
0761 
0762 class KPropertiesDialogPluginPrivate
0763 {
0764 public:
0765     KPropertiesDialogPluginPrivate()
0766     {
0767     }
0768     ~KPropertiesDialogPluginPrivate()
0769     {
0770     }
0771 
0772     bool m_bDirty;
0773     int fontHeight;
0774 };
0775 
0776 KPropertiesDialogPlugin::KPropertiesDialogPlugin(KPropertiesDialog *_props)
0777     : QObject(_props)
0778     , d(new KPropertiesDialogPluginPrivate)
0779 {
0780     properties = _props;
0781     d->fontHeight = 2 * properties->fontMetrics().height();
0782     d->m_bDirty = false;
0783 }
0784 
0785 KPropertiesDialogPlugin::~KPropertiesDialogPlugin() = default;
0786 
0787 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 1)
0788 bool KPropertiesDialogPlugin::isDesktopFile(const KFileItem &_item)
0789 {
0790     return _item.isDesktopFile();
0791 }
0792 #endif
0793 
0794 void KPropertiesDialogPlugin::setDirty(bool b)
0795 {
0796     d->m_bDirty = b;
0797 }
0798 
0799 void KPropertiesDialogPlugin::setDirty()
0800 {
0801     d->m_bDirty = true;
0802 }
0803 
0804 bool KPropertiesDialogPlugin::isDirty() const
0805 {
0806     return d->m_bDirty;
0807 }
0808 
0809 void KPropertiesDialogPlugin::applyChanges()
0810 {
0811     qCWarning(KIO_WIDGETS) << "applyChanges() not implemented in page !";
0812 }
0813 
0814 int KPropertiesDialogPlugin::fontHeight() const
0815 {
0816     return d->fontHeight;
0817 }
0818 
0819 ///////////////////////////////////////////////////////////////////////////////
0820 
0821 class KFilePropsPlugin::KFilePropsPluginPrivate
0822 {
0823 public:
0824     KFilePropsPluginPrivate()
0825         : m_ui(new Ui_KFilePropsPluginWidget())
0826     {
0827         m_ui->setupUi(&m_mainWidget);
0828     }
0829 
0830     ~KFilePropsPluginPrivate()
0831     {
0832         if (dirSizeJob) {
0833             dirSizeJob->kill();
0834         }
0835     }
0836 
0837     void hideMountPointLabels()
0838     {
0839         m_ui->fsLabel_Left->hide();
0840         m_ui->fsLabel->hide();
0841 
0842         m_ui->mountPointLabel_Left->hide();
0843         m_ui->mountPointLabel->hide();
0844 
0845         m_ui->mountSrcLabel_Left->hide();
0846         m_ui->mountSrcLabel->hide();
0847     }
0848 
0849     QWidget m_mainWidget;
0850     Ui_KFilePropsPluginWidget *m_ui = nullptr;
0851     KIO::DirectorySizeJob *dirSizeJob = nullptr;
0852     QTimer *dirSizeUpdateTimer = nullptr;
0853     bool bMultiple;
0854     bool bIconChanged;
0855     bool bKDesktopMode;
0856     bool bDesktopFile;
0857     QString mimeType;
0858     QString oldFileName;
0859 
0860     QString m_sRelativePath;
0861     bool m_bFromTemplate;
0862 
0863     /**
0864      * The initial filename
0865      */
0866     QString oldName;
0867 };
0868 
0869 KFilePropsPlugin::KFilePropsPlugin(KPropertiesDialog *_props)
0870     : KPropertiesDialogPlugin(_props)
0871     , d(new KFilePropsPluginPrivate)
0872 {
0873     const auto itemsList = properties->items();
0874     d->bMultiple = (itemsList.count() > 1);
0875     d->bIconChanged = false;
0876     d->bDesktopFile = KDesktopPropsPlugin::supports(itemsList);
0877     // qDebug() << "KFilePropsPlugin::KFilePropsPlugin bMultiple=" << d->bMultiple;
0878 
0879     // We set this data from the first item, and we'll
0880     // check that the other items match against it, resetting when not.
0881     const KFileItem firstItem = properties->item();
0882     auto [url, isLocal] = firstItem.isMostLocalUrl();
0883     bool isReallyLocal = firstItem.url().isLocalFile();
0884     bool bDesktopFile = firstItem.isDesktopFile();
0885     mode_t mode = firstItem.mode();
0886     bool hasDirs = firstItem.isDir() && !firstItem.isLink();
0887     bool hasRoot = url.path() == QLatin1String("/");
0888     QString iconStr = firstItem.iconName();
0889     QString directory = properties->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
0890     QString protocol = properties->url().scheme();
0891     d->bKDesktopMode = protocol == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
0892     QString mimeComment = firstItem.mimeComment();
0893     d->mimeType = firstItem.mimetype();
0894     KIO::filesize_t totalSize = firstItem.size();
0895     QString magicMimeName;
0896     QString magicMimeComment;
0897     QMimeDatabase db;
0898     if (isLocal) {
0899         QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent);
0900         if (magicMimeType.isValid() && !magicMimeType.isDefault()) {
0901             magicMimeName = magicMimeType.name();
0902             magicMimeComment = magicMimeType.comment();
0903         }
0904     }
0905 #ifdef Q_OS_WIN
0906     if (isReallyLocal) {
0907         directory = QDir::toNativeSeparators(directory.mid(1));
0908     }
0909 #endif
0910 
0911     // Those things only apply to 'single file' mode
0912     QString filename;
0913     bool isTrash = false;
0914     d->m_bFromTemplate = false;
0915 
0916     // And those only to 'multiple' mode
0917     uint iDirCount = hasDirs ? 1 : 0;
0918     uint iFileCount = 1 - iDirCount;
0919 
0920     properties->addPage(&d->m_mainWidget, i18nc("@title:tab File properties", "&General"));
0921 
0922     d->m_ui->symlinkTargetMessageWidget->hide();
0923 
0924     if (!d->bMultiple) {
0925         QString path;
0926         if (!d->m_bFromTemplate) {
0927             isTrash = (properties->url().scheme() == QLatin1String("trash"));
0928             // Extract the full name, but without file: for local files
0929             path = properties->url().toDisplayString(QUrl::PreferLocalFile);
0930         } else {
0931             path = Utils::concatPaths(properties->currentDir().path(), properties->defaultName());
0932             directory = properties->currentDir().toDisplayString(QUrl::PreferLocalFile);
0933         }
0934 
0935         if (d->bDesktopFile) {
0936             determineRelativePath(path);
0937         }
0938 
0939         // Extract the file name only
0940         filename = properties->defaultName();
0941         if (filename.isEmpty()) { // no template
0942             const QFileInfo finfo(firstItem.name()); // this gives support for UDS_NAME, e.g. for kio_trash or kio_system
0943             filename = finfo.fileName(); // Make sure only the file's name is displayed (#160964).
0944         } else {
0945             d->m_bFromTemplate = true;
0946             setDirty(); // to enforce that the copy happens
0947         }
0948         d->oldFileName = filename;
0949 
0950         // Make it human-readable
0951         filename = nameFromFileName(filename);
0952         d->oldName = filename;
0953     } else {
0954         // Multiple items: see what they have in common
0955         for (const auto &item : itemsList) {
0956             if (item == firstItem) {
0957                 continue;
0958             }
0959 
0960             const QUrl url = item.url();
0961             // qDebug() << "KFilePropsPlugin::KFilePropsPlugin " << url.toDisplayString();
0962             // The list of things we check here should match the variables defined
0963             // at the beginning of this method.
0964             if (url.isLocalFile() != isLocal) {
0965                 isLocal = false; // not all local
0966             }
0967             if (bDesktopFile && item.isDesktopFile() != bDesktopFile) {
0968                 bDesktopFile = false; // not all desktop files
0969             }
0970             if (item.mode() != mode) {
0971                 mode = static_cast<mode_t>(0);
0972             }
0973             if (KIO::iconNameForUrl(url) != iconStr) {
0974                 iconStr = QStringLiteral("document-multiple");
0975             }
0976             if (url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() != directory) {
0977                 directory.clear();
0978             }
0979             if (url.scheme() != protocol) {
0980                 protocol.clear();
0981             }
0982             if (!mimeComment.isNull() && item.mimeComment() != mimeComment) {
0983                 mimeComment.clear();
0984             }
0985             if (isLocal && !magicMimeComment.isNull()) {
0986                 QMimeType magicMimeType = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent);
0987                 if (magicMimeType.isValid() && magicMimeType.comment() != magicMimeComment) {
0988                     magicMimeName.clear();
0989                     magicMimeComment.clear();
0990                 }
0991             }
0992 
0993             if (isLocal && url.path() == QLatin1String("/")) {
0994                 hasRoot = true;
0995             }
0996             if (item.isDir() && !item.isLink()) {
0997                 iDirCount++;
0998                 hasDirs = true;
0999             } else {
1000                 iFileCount++;
1001                 totalSize += item.size();
1002             }
1003         }
1004     }
1005 
1006     if (!isReallyLocal && !protocol.isEmpty()) {
1007         directory += QLatin1String(" (") + protocol + QLatin1Char(')');
1008     }
1009 
1010     if (!isTrash //
1011         && (bDesktopFile || Utils::isDirMask(mode)) //
1012         && !d->bMultiple // not implemented for multiple
1013         && enableIconButton()) {
1014         d->m_ui->iconLabel->hide();
1015 
1016         const int bsize = 66 + (2 * d->m_ui->iconButton->style()->pixelMetric(QStyle::PM_ButtonMargin));
1017         d->m_ui->iconButton->setFixedSize(bsize, bsize);
1018         d->m_ui->iconButton->setIconSize(48);
1019         d->m_ui->iconButton->setStrictIconSize(false);
1020         if (bDesktopFile && isLocal) {
1021             const KDesktopFile config(url.toLocalFile());
1022             if (config.hasDeviceType()) {
1023                 d->m_ui->iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Device);
1024             } else {
1025                 d->m_ui->iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Application);
1026             }
1027         } else {
1028             d->m_ui->iconButton->setIconType(KIconLoader::Desktop, KIconLoader::Place);
1029         }
1030 
1031         d->m_ui->iconButton->setIcon(iconStr);
1032         connect(d->m_ui->iconButton, &KIconButton::iconChanged, this, &KFilePropsPlugin::slotIconChanged);
1033     } else {
1034         d->m_ui->iconButton->hide();
1035 
1036         const int bsize = 66 + (2 * d->m_ui->iconLabel->style()->pixelMetric(QStyle::PM_ButtonMargin));
1037         d->m_ui->iconLabel->setFixedSize(bsize, bsize);
1038         d->m_ui->iconLabel->setPixmap(QIcon::fromTheme(iconStr).pixmap(48));
1039     }
1040 
1041     KFileItemListProperties itemList(KFileItemList{firstItem});
1042     if (d->bMultiple || isTrash || hasRoot || !(d->m_bFromTemplate || itemList.supportsMoving())) {
1043         d->m_ui->fileNameLineEdit->hide();
1044         setFileNameReadOnly(true);
1045         if (d->bMultiple) {
1046             d->m_ui->fileNameLabel->setText(KIO::itemsSummaryString(iFileCount + iDirCount, iFileCount, iDirCount, 0, false));
1047         }
1048     } else {
1049         d->m_ui->fileNameLabel->hide();
1050 
1051         d->m_ui->fileNameLineEdit->setText(filename);
1052         connect(d->m_ui->fileNameLineEdit, &QLineEdit::textChanged, this, &KFilePropsPlugin::nameFileChanged);
1053     }
1054 
1055     // Mimetype widgets
1056     if (!mimeComment.isEmpty() && !isTrash) {
1057         d->m_ui->mimeCommentLabel->setText(mimeComment);
1058         d->m_ui->mimeCommentLabel->setToolTip(d->mimeType);
1059 
1060         const int hSpacing = properties->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
1061         d->m_ui->defaultHandlerLayout->setSpacing(hSpacing);
1062 
1063 #ifndef Q_OS_WIN
1064         updateDefaultHandler(d->mimeType);
1065         connect(KSycoca::self(), &KSycoca::databaseChanged, this, [this] {
1066             updateDefaultHandler(d->mimeType);
1067         });
1068 
1069         connect(d->m_ui->configureMimeBtn, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotEditFileType);
1070 #endif
1071 
1072     } else {
1073         d->m_ui->typeLabel->hide();
1074         d->m_ui->mimeCommentLabel->hide();
1075         d->m_ui->configureMimeBtn->hide();
1076 
1077         d->m_ui->defaultHandlerLabel_Left->hide();
1078         d->m_ui->defaultHandlerIcon->hide();
1079         d->m_ui->defaultHandlerLabel->hide();
1080     }
1081 
1082 #ifdef Q_OS_WIN
1083     d->m_ui->defaultHandlerLabel_Left->hide();
1084     d->m_ui->defaultHandlerIcon->hide();
1085     d->m_ui->defaultHandlerLabel->hide();
1086 #endif
1087 
1088     if (!magicMimeComment.isEmpty() && magicMimeComment != mimeComment) {
1089         d->m_ui->magicMimeCommentLabel->setText(magicMimeComment);
1090         d->m_ui->magicMimeCommentLabel->setToolTip(magicMimeName);
1091     } else {
1092         d->m_ui->contentLabel->hide();
1093         d->m_ui->magicMimeCommentLabel->hide();
1094     }
1095 
1096     d->m_ui->configureMimeBtn->setVisible(KAuthorized::authorizeAction(QStringLiteral("editfiletype")) && !d->m_ui->defaultHandlerLabel->isHidden());
1097 
1098     // Location:
1099     if (!directory.isEmpty()) {
1100         d->m_ui->locationLabel->setText(directory);
1101 
1102         // Layout direction for this label is always LTR; but if we are in RTL mode,
1103         // align the text to the right, otherwise the text is on the wrong side of the dialog
1104         if (properties->layoutDirection() == Qt::RightToLeft) {
1105             d->m_ui->locationLabel->setAlignment(Qt::AlignRight);
1106         }
1107     }
1108 
1109     // Size widgets
1110     if (!hasDirs) { // Only files [and symlinks]
1111         d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)").arg(KIO::convertSize(totalSize), QLocale().toString(totalSize)));
1112         d->m_ui->calculateSizeBtn->hide();
1113         d->m_ui->stopCalculateSizeBtn->hide();
1114         d->m_ui->sizeDetailsBtn->hide();
1115     } else { // Directory
1116         connect(d->m_ui->calculateSizeBtn, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeDetermine);
1117         connect(d->m_ui->stopCalculateSizeBtn, &QAbstractButton::clicked, this, &KFilePropsPlugin::slotSizeStop);
1118 
1119         if (auto filelight = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"))) {
1120             d->m_ui->sizeDetailsBtn->setText(i18nc("@action:button", "Explore in %1", filelight->name()));
1121             d->m_ui->sizeDetailsBtn->setIcon(QIcon::fromTheme(filelight->icon()));
1122             connect(d->m_ui->sizeDetailsBtn, &QPushButton::clicked, this, &KFilePropsPlugin::slotSizeDetails);
1123         } else {
1124             d->m_ui->sizeDetailsBtn->hide();
1125         }
1126 
1127         //         sizelay->addStretch(10); // so that the buttons don't grow horizontally
1128 
1129         // auto-launch for local dirs only, and not for '/'
1130         if (isLocal && !hasRoot) {
1131             d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
1132             slotSizeDetermine();
1133         } else {
1134             d->m_ui->stopCalculateSizeBtn->setEnabled(false);
1135         }
1136     }
1137 
1138     // Symlink widgets
1139     if (!d->bMultiple && firstItem.isLink()) {
1140         d->m_ui->symlinkTargetEdit->setText(firstItem.linkDest());
1141         connect(d->m_ui->symlinkTargetEdit, &QLineEdit::textChanged, this, qOverload<>(&KFilePropsPlugin::setDirty));
1142 
1143         connect(d->m_ui->symlinkTargetOpenDir, &QPushButton::clicked, this, [this] {
1144             const QUrl resolvedTargetLocation = properties->item().url().resolved(QUrl(d->m_ui->symlinkTargetEdit->text()));
1145 
1146             KIO::StatJob *statJob = KIO::statDetails(resolvedTargetLocation, KIO::StatJob::SourceSide, KIO::StatNoDetails, KIO::HideProgressInfo);
1147             connect(statJob, &KJob::finished, this, [this, statJob] {
1148                 if (statJob->error()) {
1149                     d->m_ui->symlinkTargetMessageWidget->setText(statJob->errorString());
1150                     d->m_ui->symlinkTargetMessageWidget->animatedShow();
1151                     return;
1152                 }
1153 
1154                 KIO::highlightInFileManager({statJob->url()});
1155                 properties->close();
1156             });
1157         });
1158     } else {
1159         d->m_ui->symlinkTargetLabel->hide();
1160         d->m_ui->symlinkTargetEdit->hide();
1161         d->m_ui->symlinkTargetOpenDir->hide();
1162     }
1163 
1164     // Time widgets
1165     if (!d->bMultiple) {
1166         QLocale locale;
1167         if (const QDateTime dt = firstItem.time(KFileItem::CreationTime); !dt.isNull()) {
1168             d->m_ui->createdTimeLabel->setText(locale.toString(dt, QLocale::LongFormat));
1169         } else {
1170             d->m_ui->createdTimeLabel->hide();
1171             d->m_ui->createdTimeLabel_Left->hide();
1172         }
1173 
1174         if (const QDateTime dt = firstItem.time(KFileItem::ModificationTime); !dt.isNull()) {
1175             d->m_ui->modifiedTimeLabel->setText(locale.toString(dt, QLocale::LongFormat));
1176         } else {
1177             d->m_ui->modifiedTimeLabel->hide();
1178             d->m_ui->modifiedTimeLabel_Left->hide();
1179         }
1180 
1181         if (const QDateTime dt = firstItem.time(KFileItem::AccessTime); !dt.isNull()) {
1182             d->m_ui->accessTimeLabel->setText(locale.toString(dt, QLocale::LongFormat));
1183         } else {
1184             d->m_ui->accessTimeLabel->hide();
1185             d->m_ui->accessTimeLabel_Left->hide();
1186         }
1187     } else {
1188         d->m_ui->createdTimeLabel->hide();
1189         d->m_ui->createdTimeLabel_Left->hide();
1190         d->m_ui->modifiedTimeLabel->hide();
1191         d->m_ui->modifiedTimeLabel_Left->hide();
1192         d->m_ui->accessTimeLabel->hide();
1193         d->m_ui->accessTimeLabel_Left->hide();
1194     }
1195 
1196     // File system and mount point widgets
1197     if (hasDirs) { // only for directories
1198         if (isLocal) {
1199             KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(url.toLocalFile());
1200 
1201             if (mp) {
1202                 d->m_ui->fsLabel->setText(mp->mountType());
1203                 d->m_ui->mountPointLabel->setText(mp->mountPoint());
1204                 d->m_ui->mountSrcLabel->setText(mp->mountedFrom());
1205             } else {
1206                 qCWarning(KIO_WIDGETS) << "Could not find mount point for" << url;
1207                 d->hideMountPointLabels();
1208             }
1209         } else {
1210             d->hideMountPointLabels();
1211         }
1212 
1213         KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url);
1214         connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult);
1215     } else {
1216         d->m_ui->fsSeparator->hide();
1217         d->m_ui->freespaceLabel->hide();
1218         d->m_ui->capacityBar->hide();
1219         d->hideMountPointLabels();
1220     }
1221 
1222     // UDSEntry extra fields
1223     if (const auto extraFields = KProtocolInfo::extraFields(url); !d->bMultiple && !extraFields.isEmpty()) {
1224         int curRow = d->m_ui->gridLayout->rowCount();
1225         KSeparator *sep = new KSeparator(Qt::Horizontal, &d->m_mainWidget);
1226         d->m_ui->gridLayout->addWidget(sep, curRow++, 0, 1, 3);
1227 
1228         QLocale locale;
1229         for (int i = 0; i < extraFields.count(); ++i) {
1230             const auto &field = extraFields.at(i);
1231 
1232             QString text = firstItem.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + i);
1233             if (field.type == KProtocolInfo::ExtraField::Invalid || text.isEmpty()) {
1234                 continue;
1235             }
1236 
1237             if (field.type == KProtocolInfo::ExtraField::DateTime) {
1238                 const QDateTime date = QDateTime::fromString(text, Qt::ISODate);
1239                 if (!date.isValid()) {
1240                     continue;
1241                 }
1242 
1243                 text = locale.toString(date, QLocale::LongFormat);
1244             }
1245 
1246             auto *label = new QLabel(i18n("%1:", field.name), &d->m_mainWidget);
1247             d->m_ui->gridLayout->addWidget(label, curRow, 0, Qt::AlignRight);
1248 
1249             auto *squeezedLabel = new KSqueezedTextLabel(text, &d->m_mainWidget);
1250             if (properties->layoutDirection() == Qt::RightToLeft) {
1251                 squeezedLabel->setAlignment(Qt::AlignRight);
1252             } else {
1253                 squeezedLabel->setLayoutDirection(Qt::LeftToRight);
1254             }
1255 
1256             d->m_ui->gridLayout->addWidget(squeezedLabel, curRow++, 1);
1257             squeezedLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
1258         }
1259     }
1260 }
1261 
1262 bool KFilePropsPlugin::enableIconButton() const
1263 {
1264     const KFileItem item = properties->item();
1265 
1266     // desktop files are special, files in /usr/share/applications can be
1267     // edited by overlaying them in .local/share/applications
1268     // https://bugs.kde.org/show_bug.cgi?id=429613
1269     if (item.isDesktopFile()) {
1270         return true;
1271     }
1272 
1273     // If the current item is a directory, check if it's writable,
1274     // so we can create/update a .directory
1275     // Current item is a file, same thing: check if it is writable
1276     if (item.isWritable()) {
1277         // exclude remote dirs as changing the icon has no effect (bug 205954)
1278         if (item.isLocalFile() || item.url().scheme() == QLatin1String("desktop")) {
1279             return true;
1280         }
1281     }
1282 
1283     return false;
1284 }
1285 
1286 void KFilePropsPlugin::setFileNameReadOnly(bool readOnly)
1287 {
1288     Q_ASSERT(readOnly); // false isn't supported
1289 
1290     if (readOnly) {
1291         Q_ASSERT(!d->m_bFromTemplate);
1292 
1293         d->m_ui->fileNameLineEdit->hide();
1294 
1295         d->m_ui->fileNameLabel->show();
1296         d->m_ui->fileNameLabel->setText(d->oldName); // will get overwritten if d->bMultiple
1297     }
1298 }
1299 
1300 void KFilePropsPlugin::slotEditFileType()
1301 {
1302     QString mime;
1303     if (d->mimeType == QLatin1String("application/octet-stream")) {
1304         const int pos = d->oldFileName.lastIndexOf(QLatin1Char('.'));
1305         if (pos != -1) {
1306             mime = QLatin1Char('*') + QStringView(d->oldFileName).mid(pos);
1307         } else {
1308             mime = QStringLiteral("*");
1309         }
1310     } else {
1311         mime = d->mimeType;
1312     }
1313     KMimeTypeEditor::editMimeType(mime, properties->window());
1314 }
1315 
1316 void KFilePropsPlugin::slotIconChanged()
1317 {
1318     d->bIconChanged = true;
1319     Q_EMIT changed();
1320 }
1321 
1322 void KFilePropsPlugin::nameFileChanged(const QString &text)
1323 {
1324     properties->buttonBox()->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
1325     Q_EMIT changed();
1326 }
1327 
1328 static QString relativeAppsLocation(const QString &file)
1329 {
1330     // Don't resolve symlinks, so that editing /usr/share/applications/foo.desktop that is
1331     // a symlink works
1332     const QString absolute = QFileInfo(file).absoluteFilePath();
1333     const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
1334     for (const QString &base : dirs) {
1335         QDir base_dir = QDir(base);
1336         if (base_dir.exists() && absolute.startsWith(base_dir.canonicalPath())) {
1337             return absolute.mid(base.length() + 1);
1338         }
1339     }
1340     return QString(); // return empty if the file is not in apps
1341 }
1342 
1343 void KFilePropsPlugin::determineRelativePath(const QString &path)
1344 {
1345     // now let's make it relative
1346     d->m_sRelativePath = relativeAppsLocation(path);
1347 }
1348 
1349 void KFilePropsPlugin::slotFreeSpaceResult(KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available)
1350 {
1351     if (!job->error()) {
1352         const quint64 used = size - available;
1353         const int percentUsed = qRound(100.0 * qreal(used) / qreal(size));
1354 
1355         d->m_ui->capacityBar->setText(i18nc("Available space out of total partition size (percent used)",
1356                                             "%1 free of %2 (%3% used)",
1357                                             KIO::convertSize(available),
1358                                             KIO::convertSize(size),
1359                                             percentUsed));
1360 
1361         d->m_ui->capacityBar->setValue(percentUsed);
1362     } else {
1363         d->m_ui->capacityBar->setText(i18nc("@info:status", "Unknown size"));
1364         d->m_ui->capacityBar->setValue(0);
1365     }
1366 }
1367 
1368 void KFilePropsPlugin::slotDirSizeUpdate()
1369 {
1370     KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
1371     KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
1372     KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
1373     d->m_ui->sizeLabel->setText(i18n("Calculating... %1 (%2)\n%3, %4",
1374                                      KIO::convertSize(totalSize),
1375                                      QLocale().toString(totalSize),
1376                                      i18np("1 file", "%1 files", totalFiles),
1377                                      i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
1378 }
1379 
1380 void KFilePropsPlugin::slotDirSizeFinished(KJob *job)
1381 {
1382     if (job->error()) {
1383         d->m_ui->sizeLabel->setText(job->errorString());
1384     } else {
1385         KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
1386         KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
1387         KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
1388         d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)\n%3, %4")
1389                                         .arg(KIO::convertSize(totalSize),
1390                                              QLocale().toString(totalSize),
1391                                              i18np("1 file", "%1 files", totalFiles),
1392                                              i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
1393     }
1394     d->m_ui->stopCalculateSizeBtn->setEnabled(false);
1395     // just in case you change something and try again :)
1396     d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
1397     d->m_ui->calculateSizeBtn->setEnabled(true);
1398     d->dirSizeJob = nullptr;
1399     delete d->dirSizeUpdateTimer;
1400     d->dirSizeUpdateTimer = nullptr;
1401 }
1402 
1403 void KFilePropsPlugin::slotSizeDetermine()
1404 {
1405     d->m_ui->sizeLabel->setText(i18n("Calculating...\n"));
1406     // qDebug() << "properties->item()=" << properties->item() << "URL=" << properties->item().url();
1407 
1408     d->dirSizeJob = KIO::directorySize(properties->items());
1409     d->dirSizeUpdateTimer = new QTimer(this);
1410     connect(d->dirSizeUpdateTimer, &QTimer::timeout, this, &KFilePropsPlugin::slotDirSizeUpdate);
1411     d->dirSizeUpdateTimer->start(500);
1412     connect(d->dirSizeJob, &KJob::result, this, &KFilePropsPlugin::slotDirSizeFinished);
1413     d->m_ui->stopCalculateSizeBtn->setEnabled(true);
1414     d->m_ui->calculateSizeBtn->setEnabled(false);
1415 
1416     // also update the "Free disk space" display
1417     if (!d->m_ui->capacityBar->isHidden()) {
1418         const KFileItem item = properties->item();
1419         KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(item.url());
1420         connect(job, &KIO::FileSystemFreeSpaceJob::result, this, &KFilePropsPlugin::slotFreeSpaceResult);
1421     }
1422 }
1423 
1424 void KFilePropsPlugin::slotSizeStop()
1425 {
1426     if (d->dirSizeJob) {
1427         KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
1428         d->m_ui->sizeLabel->setText(i18n("At least %1\n", KIO::convertSize(totalSize)));
1429         d->dirSizeJob->kill();
1430         d->dirSizeJob = nullptr;
1431     }
1432     if (d->dirSizeUpdateTimer) {
1433         d->dirSizeUpdateTimer->stop();
1434     }
1435 
1436     d->m_ui->stopCalculateSizeBtn->setEnabled(false);
1437     d->m_ui->calculateSizeBtn->setEnabled(true);
1438 }
1439 
1440 void KFilePropsPlugin::slotSizeDetails()
1441 {
1442     // Open the current folder in filelight
1443     KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
1444     if (service) {
1445         auto *job = new KIO::ApplicationLauncherJob(service);
1446         job->setUrls({properties->url()});
1447         job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, properties));
1448         job->start();
1449     }
1450 }
1451 
1452 KFilePropsPlugin::~KFilePropsPlugin() = default;
1453 
1454 bool KFilePropsPlugin::supports(const KFileItemList & /*_items*/)
1455 {
1456     return true;
1457 }
1458 
1459 void KFilePropsPlugin::applyChanges()
1460 {
1461     if (d->dirSizeJob) {
1462         slotSizeStop();
1463     }
1464 
1465     // qDebug() << "KFilePropsPlugin::applyChanges";
1466 
1467     if (!d->m_ui->fileNameLineEdit->isHidden()) {
1468         QString n = d->m_ui->fileNameLineEdit->text();
1469         // Remove trailing spaces (#4345)
1470         while (!n.isEmpty() && n[n.length() - 1].isSpace()) {
1471             n.chop(1);
1472         }
1473         if (n.isEmpty()) {
1474             KMessageBox::error(properties, i18n("The new file name is empty."));
1475             properties->abortApplying();
1476             return;
1477         }
1478 
1479         // Do we need to rename the file ?
1480         // qDebug() << "oldname = " << d->oldName;
1481         // qDebug() << "newname = " << n;
1482         if (d->oldName != n || d->m_bFromTemplate) { // true for any from-template file
1483             KIO::CopyJob *job = nullptr;
1484             QUrl oldurl = properties->url();
1485 
1486             QString newFileName = KIO::encodeFileName(n);
1487             if (d->bDesktopFile && !newFileName.endsWith(QLatin1String(".desktop")) && !newFileName.endsWith(QLatin1String(".kdelnk"))) {
1488                 newFileName += QLatin1String(".desktop");
1489             }
1490 
1491             // Tell properties. Warning, this changes the result of properties->url() !
1492             properties->rename(newFileName);
1493 
1494             // Update also relative path (for apps)
1495             if (!d->m_sRelativePath.isEmpty()) {
1496                 determineRelativePath(properties->url().toLocalFile());
1497             }
1498 
1499             // qDebug() << "New URL = " << properties->url();
1500             // qDebug() << "old = " << oldurl.url();
1501 
1502             // Don't remove the template !!
1503             if (!d->m_bFromTemplate) { // (normal renaming)
1504                 job = KIO::moveAs(oldurl, properties->url());
1505             } else { // Copying a template
1506                 job = KIO::copyAs(oldurl, properties->url());
1507             }
1508             KJobWidgets::setWindow(job, properties);
1509             connect(job, &KJob::result, this, &KFilePropsPlugin::slotCopyFinished);
1510             connect(job, &KIO::CopyJob::renamed, this, &KFilePropsPlugin::slotFileRenamed);
1511             return;
1512         }
1513 
1514         properties->updateUrl(properties->url());
1515         // Update also relative path (for apps)
1516         if (!d->m_sRelativePath.isEmpty()) {
1517             determineRelativePath(properties->url().toLocalFile());
1518         }
1519     }
1520 
1521     // No job, keep going
1522     slotCopyFinished(nullptr);
1523 }
1524 
1525 void KFilePropsPlugin::slotCopyFinished(KJob *job)
1526 {
1527     // qDebug() << "KFilePropsPlugin::slotCopyFinished";
1528     if (job) {
1529         if (job->error()) {
1530             job->uiDelegate()->showErrorMessage();
1531             // Didn't work. Revert the URL to the old one
1532             properties->updateUrl(static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst());
1533             properties->abortApplying(); // Don't apply the changes to the wrong file !
1534             return;
1535         }
1536     }
1537 
1538     Q_ASSERT(!properties->item().isNull());
1539     Q_ASSERT(!properties->item().url().isEmpty());
1540 
1541     // Save the file locally
1542     if (d->bDesktopFile && !d->m_sRelativePath.isEmpty()) {
1543         // qDebug() << "KFilePropsPlugin::slotCopyFinished " << d->m_sRelativePath;
1544         const QString newPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + d->m_sRelativePath;
1545         const QUrl newURL = QUrl::fromLocalFile(newPath);
1546         // qDebug() << "KFilePropsPlugin::slotCopyFinished path=" << newURL;
1547         properties->updateUrl(newURL);
1548     }
1549 
1550     if (d->bKDesktopMode && d->bDesktopFile) {
1551         // Renamed? Update Name field
1552         // Note: The desktop KIO worker does this as well, but not when
1553         //       the file is copied from a template.
1554         if (d->m_bFromTemplate) {
1555             KIO::StatJob *job = KIO::stat(properties->url());
1556             job->exec();
1557             KIO::UDSEntry entry = job->statResult();
1558 
1559             KFileItem item(entry, properties->url());
1560             KDesktopFile config(item.localPath());
1561             KConfigGroup cg = config.desktopGroup();
1562             QString nameStr = nameFromFileName(properties->url().fileName());
1563             cg.writeEntry("Name", nameStr);
1564             cg.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized);
1565         }
1566     }
1567 
1568     if (!d->m_ui->symlinkTargetEdit->isHidden() && !d->bMultiple) {
1569         const KFileItem item = properties->item();
1570         const QString newTarget = d->m_ui->symlinkTargetEdit->text();
1571         if (newTarget != item.linkDest()) {
1572             // qDebug() << "Updating target of symlink to" << newTarget;
1573             KIO::Job *job = KIO::symlink(newTarget, item.url(), KIO::Overwrite);
1574             job->uiDelegate()->setAutoErrorHandlingEnabled(true);
1575             job->exec();
1576         }
1577     }
1578 
1579     // "Link to Application" templates need to be made executable
1580     // Instead of matching against a filename we check if the destination
1581     // is an Application now.
1582     if (d->m_bFromTemplate) {
1583         // destination is not necessarily local, use the src template
1584         KDesktopFile templateResult(static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst().toLocalFile());
1585         if (templateResult.hasApplicationType()) {
1586             // We can either stat the file and add the +x bit or use the larger chmod() job
1587             // with a umask designed to only touch u+x.  This is only one KIO job, so let's
1588             // do that.
1589 
1590             KFileItem appLink(properties->item());
1591             KFileItemList fileItemList;
1592             fileItemList << appLink;
1593 
1594             // first 0100 adds u+x, second 0100 only allows chmod to change u+x
1595             KIO::Job *chmodJob = KIO::chmod(fileItemList, 0100, 0100, QString(), QString(), KIO::HideProgressInfo);
1596             chmodJob->exec();
1597         }
1598     }
1599 
1600     setDirty(false);
1601     Q_EMIT changesApplied();
1602 }
1603 
1604 void KFilePropsPlugin::applyIconChanges()
1605 {
1606     if (d->m_ui->iconButton->isHidden() || !d->bIconChanged) {
1607         return;
1608     }
1609     // handle icon changes - only local files (or pseudo-local) for now
1610     // TODO: Use KTempFile and KIO::file_copy with overwrite = true
1611     QUrl url = properties->url();
1612     KIO::StatJob *job = KIO::mostLocalUrl(url);
1613     KJobWidgets::setWindow(job, properties);
1614     job->exec();
1615     url = job->mostLocalUrl();
1616 
1617     if (url.isLocalFile()) {
1618         QString path;
1619 
1620         if (Utils::isDirMask(properties->item().mode())) {
1621             path = url.toLocalFile() + QLatin1String("/.directory");
1622             // don't call updateUrl because the other tabs (i.e. permissions)
1623             // apply to the directory, not the .directory file.
1624         } else {
1625             path = url.toLocalFile();
1626         }
1627 
1628         // Get the default image
1629         QMimeDatabase db;
1630         const QString str = db.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchExtension).iconName();
1631         // Is it another one than the default ?
1632         QString sIcon;
1633         if (const QString currIcon = d->m_ui->iconButton->icon(); str != currIcon) {
1634             sIcon = currIcon;
1635         }
1636         // (otherwise write empty value)
1637 
1638         // qDebug() << "**" << path << "**";
1639 
1640         // If default icon and no .directory file -> don't create one
1641         if (!sIcon.isEmpty() || QFile::exists(path)) {
1642             KDesktopFile cfg(path);
1643             // qDebug() << "sIcon = " << (sIcon);
1644             // qDebug() << "str = " << (str);
1645             cfg.desktopGroup().writeEntry("Icon", sIcon);
1646             cfg.sync();
1647 
1648             cfg.reparseConfiguration();
1649             if (cfg.desktopGroup().readEntry("Icon") != sIcon) {
1650                 properties->abortApplying();
1651 
1652                 KMessageBox::error(nullptr, couldNotSaveMsg(path));
1653             }
1654         }
1655     }
1656 }
1657 
1658 void KFilePropsPlugin::updateDefaultHandler(const QString &mimeType)
1659 {
1660     const bool isGeneric = d->mimeType == QLatin1String("application/octet-stream");
1661 
1662     const auto service = KApplicationTrader::preferredService(mimeType);
1663     if (!isGeneric && service) {
1664         const int iconSize = properties->style()->pixelMetric(QStyle::PM_SmallIconSize);
1665         d->m_ui->defaultHandlerIcon->setPixmap(QIcon::fromTheme(service->icon()).pixmap(iconSize));
1666         d->m_ui->defaultHandlerIcon->show();
1667         d->m_ui->defaultHandlerLabel->setText(service->name());
1668         d->m_ui->defaultHandlerLabel->setDisabled(false);
1669     } else {
1670         d->m_ui->defaultHandlerIcon->hide();
1671         if (isGeneric) {
1672             d->m_ui->defaultHandlerLabel->setText(i18n("No registered file type"));
1673         } else {
1674             d->m_ui->defaultHandlerLabel->setText(i18n("No associated application"));
1675         }
1676         d->m_ui->defaultHandlerLabel->setDisabled(true);
1677     }
1678 
1679     if (isGeneric) {
1680         d->m_ui->configureMimeBtn->setText(i18nc("@action:button Create new file type", "Create…"));
1681         d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
1682     } else {
1683         d->m_ui->configureMimeBtn->setText(i18nc("@action:button", "Change…"));
1684         d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
1685     }
1686 }
1687 
1688 void KFilePropsPlugin::slotFileRenamed(KIO::Job *, const QUrl &, const QUrl &newUrl)
1689 {
1690     // This is called in case of an existing local file during the copy/move operation,
1691     // if the user chooses Rename.
1692     properties->updateUrl(newUrl);
1693 }
1694 
1695 void KFilePropsPlugin::postApplyChanges()
1696 {
1697     // Save the icon only after applying the permissions changes (#46192)
1698     applyIconChanges();
1699 
1700     const KFileItemList items = properties->items();
1701     const QList<QUrl> lst = items.urlList();
1702     org::kde::KDirNotify::emitFilesChanged(QList<QUrl>(lst));
1703 }
1704 
1705 class KFilePermissionsPropsPlugin::KFilePermissionsPropsPluginPrivate
1706 {
1707 public:
1708     KFilePermissionsPropsPluginPrivate()
1709     {
1710     }
1711     ~KFilePermissionsPropsPluginPrivate()
1712     {
1713     }
1714 
1715     QFrame *m_frame = nullptr;
1716     QCheckBox *cbRecursive = nullptr;
1717     QLabel *explanationLabel = nullptr;
1718     QComboBox *ownerPermCombo = nullptr;
1719     QComboBox *groupPermCombo = nullptr;
1720     QComboBox *othersPermCombo = nullptr;
1721     QCheckBox *extraCheckbox = nullptr;
1722     mode_t partialPermissions;
1723     KFilePermissionsPropsPlugin::PermissionsMode pmode;
1724     bool canChangePermissions;
1725     bool isIrregular;
1726     bool hasExtendedACL;
1727     KACL extendedACL;
1728     KACL defaultACL;
1729     bool fileSystemSupportsACLs;
1730 
1731     QComboBox *grpCombo = nullptr;
1732 
1733     KLineEdit *usrEdit = nullptr;
1734     KLineEdit *grpEdit = nullptr;
1735 
1736     // Old permissions
1737     mode_t permissions;
1738     // Old group
1739     QString strGroup;
1740     // Old owner
1741     QString strOwner;
1742 };
1743 
1744 static constexpr mode_t UniOwner{S_IRUSR | S_IWUSR | S_IXUSR};
1745 static constexpr mode_t UniGroup{S_IRGRP | S_IWGRP | S_IXGRP};
1746 static constexpr mode_t UniOthers{S_IROTH | S_IWOTH | S_IXOTH};
1747 static constexpr mode_t UniRead{S_IRUSR | S_IRGRP | S_IROTH};
1748 static constexpr mode_t UniWrite{S_IWUSR | S_IWGRP | S_IWOTH};
1749 static constexpr mode_t UniExec{S_IXUSR | S_IXGRP | S_IXOTH};
1750 static constexpr mode_t UniSpecial{S_ISUID | S_ISGID | S_ISVTX};
1751 static constexpr mode_t s_invalid_mode_t{static_cast<mode_t>(-1)};
1752 
1753 // synced with PermissionsTarget
1754 constexpr mode_t KFilePermissionsPropsPlugin::permissionsMasks[3] = {UniOwner, UniGroup, UniOthers};
1755 constexpr mode_t KFilePermissionsPropsPlugin::standardPermissions[4] = {0, UniRead, UniRead | UniWrite, s_invalid_mode_t};
1756 
1757 // synced with PermissionsMode and standardPermissions
1758 static constexpr KLazyLocalizedString permissionsTexts[4][4] = {
1759     {kli18n("No Access"), kli18n("Can Only View"), kli18n("Can View & Modify"), {}},
1760     {kli18n("No Access"), kli18n("Can Only View Content"), kli18n("Can View & Modify Content"), {}},
1761     {{}, {}, {}, {}}, // no texts for links
1762     {kli18n("No Access"), kli18n("Can Only View/Read Content"), kli18n("Can View/Read & Modify/Write"), {}}};
1763 
1764 KFilePermissionsPropsPlugin::KFilePermissionsPropsPlugin(KPropertiesDialog *_props)
1765     : KPropertiesDialogPlugin(_props)
1766     , d(new KFilePermissionsPropsPluginPrivate)
1767 {
1768     const auto &[localUrl, isLocal] = properties->item().isMostLocalUrl();
1769     bool isTrash = (properties->url().scheme() == QLatin1String("trash"));
1770     KUser myself(KUser::UseEffectiveUID);
1771     const bool IamRoot = myself.isSuperUser();
1772 
1773     const KFileItem firstItem = properties->item();
1774     bool isLink = firstItem.isLink();
1775     bool isDir = firstItem.isDir(); // all dirs
1776     bool hasDir = firstItem.isDir(); // at least one dir
1777     d->permissions = firstItem.permissions(); // common permissions to all files
1778     d->partialPermissions = d->permissions; // permissions that only some files have (at first we take everything)
1779     d->isIrregular = isIrregular(d->permissions, isDir, isLink);
1780     d->strOwner = firstItem.user();
1781     d->strGroup = firstItem.group();
1782     d->hasExtendedACL = firstItem.ACL().isExtended() || firstItem.defaultACL().isValid();
1783     d->extendedACL = firstItem.ACL();
1784     d->defaultACL = firstItem.defaultACL();
1785     d->fileSystemSupportsACLs = false;
1786 
1787     if (properties->items().count() > 1) {
1788         // Multiple items: see what they have in common
1789         const KFileItemList items = properties->items();
1790         for (const auto &item : items) {
1791             if (item == firstItem) { // No need to check the first one again
1792                 continue;
1793             }
1794 
1795             const bool isItemDir = item.isDir();
1796             const bool isItemLink = item.isLink();
1797 
1798             if (!d->isIrregular) {
1799                 d->isIrregular |= isIrregular(item.permissions(), isItemDir == isDir, isItemLink == isLink);
1800             }
1801 
1802             d->hasExtendedACL = d->hasExtendedACL || item.hasExtendedACL();
1803 
1804             if (isItemLink != isLink) {
1805                 isLink = false;
1806             }
1807 
1808             if (isItemDir != isDir) {
1809                 isDir = false;
1810             }
1811             hasDir |= isItemDir;
1812 
1813             if (item.permissions() != d->permissions) {
1814                 d->permissions &= item.permissions();
1815                 d->partialPermissions |= item.permissions();
1816             }
1817 
1818             if (item.user() != d->strOwner) {
1819                 d->strOwner.clear();
1820             }
1821 
1822             if (item.group() != d->strGroup) {
1823                 d->strGroup.clear();
1824             }
1825         }
1826     }
1827 
1828     if (isLink) {
1829         d->pmode = PermissionsOnlyLinks;
1830     } else if (isDir) {
1831         d->pmode = PermissionsOnlyDirs;
1832     } else if (hasDir) {
1833         d->pmode = PermissionsMixed;
1834     } else {
1835         d->pmode = PermissionsOnlyFiles;
1836     }
1837 
1838     // keep only what's not in the common permissions
1839     d->partialPermissions = d->partialPermissions & ~d->permissions;
1840 
1841     bool isMyFile = false;
1842 
1843     if (isLocal && !d->strOwner.isEmpty()) { // local files, and all owned by the same person
1844         if (myself.isValid()) {
1845             isMyFile = (d->strOwner == myself.loginName());
1846         } else {
1847             qCWarning(KIO_WIDGETS) << "I don't exist ?! geteuid=" << KUserId::currentEffectiveUserId().toString();
1848         }
1849     } else {
1850         // We don't know, for remote files, if they are ours or not.
1851         // So we let the user change permissions, and
1852         // KIO::chmod will tell, if he had no right to do it.
1853         isMyFile = true;
1854     }
1855 
1856     d->canChangePermissions = (isMyFile || IamRoot) && (!isLink);
1857 
1858     // create GUI
1859 
1860     d->m_frame = new QFrame();
1861     properties->addPage(d->m_frame, i18n("&Permissions"));
1862 
1863     QBoxLayout *box = new QVBoxLayout(d->m_frame);
1864     box->setContentsMargins(0, 0, 0, 0);
1865 
1866     QWidget *l;
1867     QLabel *lbl;
1868     QGroupBox *gb;
1869     QGridLayout *gl;
1870     QPushButton *pbAdvancedPerm = nullptr;
1871 
1872     /* Group: Access Permissions */
1873     gb = new QGroupBox(i18n("Access Permissions"), d->m_frame);
1874     box->addWidget(gb);
1875 
1876     gl = new QGridLayout(gb);
1877     gl->setColumnStretch(1, 1);
1878 
1879     l = d->explanationLabel = new QLabel(gb);
1880     if (isLink) {
1881         d->explanationLabel->setText(
1882             i18np("This file is a link and does not have permissions.", "All files are links and do not have permissions.", properties->items().count()));
1883     } else if (!d->canChangePermissions) {
1884         d->explanationLabel->setText(i18n("Only the owner can change permissions."));
1885     }
1886     gl->addWidget(l, 0, 0, 1, 2);
1887 
1888     lbl = new QLabel(i18n("O&wner:"), gb);
1889     gl->addWidget(lbl, 1, 0, Qt::AlignRight);
1890     l = d->ownerPermCombo = new QComboBox(gb);
1891     lbl->setBuddy(l);
1892     gl->addWidget(l, 1, 1);
1893     connect(d->ownerPermCombo, qOverload<int>(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed);
1894     l->setWhatsThis(i18n("Specifies the actions that the owner is allowed to do."));
1895 
1896     lbl = new QLabel(i18n("Gro&up:"), gb);
1897     gl->addWidget(lbl, 2, 0, Qt::AlignRight);
1898     l = d->groupPermCombo = new QComboBox(gb);
1899     lbl->setBuddy(l);
1900     gl->addWidget(l, 2, 1);
1901     connect(d->groupPermCombo, qOverload<int>(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed);
1902     l->setWhatsThis(i18n("Specifies the actions that the members of the group are allowed to do."));
1903 
1904     lbl = new QLabel(i18n("O&thers:"), gb);
1905     gl->addWidget(lbl, 3, 0, Qt::AlignRight);
1906     l = d->othersPermCombo = new QComboBox(gb);
1907     lbl->setBuddy(l);
1908     gl->addWidget(l, 3, 1);
1909     connect(d->othersPermCombo, qOverload<int>(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed);
1910     l->setWhatsThis(
1911         i18n("Specifies the actions that all users, who are neither "
1912              "owner nor in the group, are allowed to do."));
1913 
1914     if (!isLink) {
1915         l = d->extraCheckbox = new QCheckBox(hasDir ? i18n("Only own&er can rename and delete folder content") : i18n("Is &executable"), gb);
1916         connect(d->extraCheckbox, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed);
1917         gl->addWidget(l, 4, 1);
1918         l->setWhatsThis(hasDir ? i18n("Enable this option to allow only the folder's owner to "
1919                                       "delete or rename the contained files and folders. Other "
1920                                       "users can only add new files, which requires the 'Modify "
1921                                       "Content' permission.")
1922                                : i18n("Enable this option to mark the file as executable. This only makes "
1923                                       "sense for programs and scripts. It is required when you want to "
1924                                       "execute them."));
1925 
1926         QLayoutItem *spacer = new QSpacerItem(0, 20, QSizePolicy::Minimum, QSizePolicy::Expanding);
1927         gl->addItem(spacer, 5, 0, 1, 3);
1928 
1929         pbAdvancedPerm = new QPushButton(i18n("A&dvanced Permissions"), gb);
1930         gl->addWidget(pbAdvancedPerm, 6, 0, 1, 2, Qt::AlignRight);
1931         connect(pbAdvancedPerm, &QAbstractButton::clicked, this, &KFilePermissionsPropsPlugin::slotShowAdvancedPermissions);
1932     } else {
1933         d->extraCheckbox = nullptr;
1934     }
1935 
1936     /**** Group: Ownership ****/
1937     gb = new QGroupBox(i18n("Ownership"), d->m_frame);
1938     box->addWidget(gb);
1939 
1940     gl = new QGridLayout(gb);
1941     gl->addItem(new QSpacerItem(0, 10), 0, 0);
1942 
1943     /*** Set Owner ***/
1944     l = new QLabel(i18n("User:"), gb);
1945     gl->addWidget(l, 1, 0, Qt::AlignRight);
1946 
1947     /* GJ: Don't autocomplete more than 1000 users. This is a kind of random
1948      * value. Huge sites having 10.000+ user have a fair chance of using NIS,
1949      * (possibly) making this unacceptably slow.
1950      * OTOH, it is nice to offer this functionality for the standard user.
1951      */
1952     int maxEntries = 1000;
1953 
1954     /* File owner: For root, offer a KLineEdit with autocompletion.
1955      * For a user, who can never chown() a file, offer a QLabel.
1956      */
1957     if (IamRoot && isLocal) {
1958         d->usrEdit = new KLineEdit(gb);
1959         KCompletion *kcom = d->usrEdit->completionObject();
1960         kcom->setOrder(KCompletion::Sorted);
1961         QStringList userNames = KUser::allUserNames(maxEntries);
1962         kcom->setItems(userNames);
1963         d->usrEdit->setCompletionMode((userNames.size() < maxEntries) ? KCompletion::CompletionAuto : KCompletion::CompletionNone);
1964         d->usrEdit->setText(d->strOwner);
1965         gl->addWidget(d->usrEdit, 1, 1);
1966         connect(d->usrEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
1967     } else {
1968         l = new QLabel(d->strOwner, gb);
1969         gl->addWidget(l, 1, 1);
1970     }
1971 
1972     /*** Set Group ***/
1973     QStringList groupList = myself.groupNames();
1974     const bool isMyGroup = groupList.contains(d->strGroup);
1975 
1976     /* add the group the file currently belongs to ..
1977      * .. if it is not there already
1978      */
1979     if (!isMyGroup) {
1980         groupList += d->strGroup;
1981     }
1982 
1983     l = new QLabel(i18n("Group:"), gb);
1984     gl->addWidget(l, 2, 0, Qt::AlignRight);
1985 
1986     /* Set group: if possible to change:
1987      * - Offer a KLineEdit for root, since he can change to any group.
1988      * - Offer a QComboBox for a normal user, since he can change to a fixed
1989      *   (small) set of groups only.
1990      * If not changeable: offer a QLabel.
1991      */
1992     if (IamRoot && isLocal) {
1993         d->grpEdit = new KLineEdit(gb);
1994         KCompletion *kcom = new KCompletion;
1995         kcom->setItems(groupList);
1996         d->grpEdit->setCompletionObject(kcom, true);
1997         d->grpEdit->setAutoDeleteCompletionObject(true);
1998         d->grpEdit->setCompletionMode(KCompletion::CompletionAuto);
1999         d->grpEdit->setText(d->strGroup);
2000         gl->addWidget(d->grpEdit, 2, 1);
2001         connect(d->grpEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
2002     } else if ((groupList.count() > 1) && isMyFile && isLocal) {
2003         d->grpCombo = new QComboBox(gb);
2004         d->grpCombo->setObjectName(QStringLiteral("combogrouplist"));
2005         d->grpCombo->addItems(groupList);
2006         d->grpCombo->setCurrentIndex(groupList.indexOf(d->strGroup));
2007         gl->addWidget(d->grpCombo, 2, 1);
2008         connect(d->grpCombo, qOverload<int>(&QComboBox::activated), this, &KPropertiesDialogPlugin::changed);
2009     } else {
2010         l = new QLabel(d->strGroup, gb);
2011         gl->addWidget(l, 2, 1);
2012     }
2013 
2014     gl->setColumnStretch(2, 10);
2015 
2016     // "Apply recursive" checkbox
2017     if (hasDir && !isLink && !isTrash) {
2018         d->cbRecursive = new QCheckBox(i18n("Apply changes to all subfolders and their contents"), d->m_frame);
2019         connect(d->cbRecursive, &QAbstractButton::clicked, this, &KPropertiesDialogPlugin::changed);
2020         box->addWidget(d->cbRecursive);
2021     }
2022 
2023     updateAccessControls();
2024 
2025     if (isTrash) {
2026         // don't allow to change properties for file into trash
2027         enableAccessControls(false);
2028         if (pbAdvancedPerm) {
2029             pbAdvancedPerm->setEnabled(false);
2030         }
2031     }
2032 
2033     box->addStretch(10);
2034 }
2035 
2036 #if HAVE_POSIX_ACL
2037 static bool fileSystemSupportsACL(const QByteArray &path)
2038 {
2039     bool fileSystemSupportsACLs = false;
2040 #ifdef Q_OS_FREEBSD
2041     // FIXME: unbreak and enable this
2042     // Maybe use pathconf(2) to perform this check?
2043     // struct statfs buf;
2044     // fileSystemSupportsACLs = (statfs(path.data(), &buf) == 0) && (buf.f_flags & MNT_ACLS);
2045     Q_UNUSED(path);
2046 #elif defined Q_OS_MACOS
2047     fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0, 0, XATTR_NOFOLLOW) >= 0 || errno == ENODATA;
2048 #else
2049     fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0) >= 0 || errno == ENODATA;
2050 #endif
2051     return fileSystemSupportsACLs;
2052 }
2053 #endif
2054 
2055 void KFilePermissionsPropsPlugin::slotShowAdvancedPermissions()
2056 {
2057     bool isDir = (d->pmode == PermissionsOnlyDirs) || (d->pmode == PermissionsMixed);
2058     QDialog dlg(properties);
2059     dlg.setModal(true);
2060     dlg.setWindowTitle(i18n("Advanced Permissions"));
2061 
2062     QLabel *l;
2063     QLabel *cl[3];
2064     QGroupBox *gb;
2065     QGridLayout *gl;
2066 
2067     QVBoxLayout *vbox = new QVBoxLayout(&dlg);
2068     // Group: Access Permissions
2069     gb = new QGroupBox(i18n("Access Permissions"), &dlg);
2070     vbox->addWidget(gb);
2071 
2072     gl = new QGridLayout(gb);
2073     gl->addItem(new QSpacerItem(0, 10), 0, 0);
2074 
2075     QVector<QWidget *> theNotSpecials;
2076 
2077     l = new QLabel(i18n("Class"), gb);
2078     gl->addWidget(l, 1, 0);
2079     theNotSpecials.append(l);
2080 
2081     QString readWhatsThis;
2082     QString readLabel;
2083     if (isDir) {
2084         readLabel = i18n("Show\nEntries");
2085         readWhatsThis = i18n("This flag allows viewing the content of the folder.");
2086     } else {
2087         readLabel = i18n("Read");
2088         readWhatsThis = i18n("The Read flag allows viewing the content of the file.");
2089     }
2090 
2091     QString writeWhatsThis;
2092     QString writeLabel;
2093     if (isDir) {
2094         writeLabel = i18n("Write\nEntries");
2095         writeWhatsThis = i18n(
2096             "This flag allows adding, renaming and deleting of files. "
2097             "Note that deleting and renaming can be limited using the Sticky flag.");
2098     } else {
2099         writeLabel = i18n("Write");
2100         writeWhatsThis = i18n("The Write flag allows modifying the content of the file.");
2101     }
2102 
2103     QString execLabel;
2104     QString execWhatsThis;
2105     if (isDir) {
2106         execLabel = i18nc("Enter folder", "Enter");
2107         execWhatsThis = i18n("Enable this flag to allow entering the folder.");
2108     } else {
2109         execLabel = i18n("Exec");
2110         execWhatsThis = i18n("Enable this flag to allow executing the file as a program.");
2111     }
2112     // GJ: Add space between normal and special modes
2113     QSize size = l->sizeHint();
2114     size.setWidth(size.width() + 15);
2115     l->setFixedSize(size);
2116     gl->addWidget(l, 1, 3);
2117 
2118     l = new QLabel(i18n("Special"), gb);
2119     gl->addWidget(l, 1, 4, 1, 1);
2120     QString specialWhatsThis;
2121     if (isDir) {
2122         specialWhatsThis = i18n(
2123             "Special flag. Valid for the whole folder, the exact "
2124             "meaning of the flag can be seen in the right hand column.");
2125     } else {
2126         specialWhatsThis = i18n(
2127             "Special flag. The exact meaning of the flag can be seen "
2128             "in the right hand column.");
2129     }
2130     l->setWhatsThis(specialWhatsThis);
2131 
2132     cl[0] = new QLabel(i18n("User"), gb);
2133     gl->addWidget(cl[0], 2, 0);
2134     theNotSpecials.append(cl[0]);
2135 
2136     cl[1] = new QLabel(i18n("Group"), gb);
2137     gl->addWidget(cl[1], 3, 0);
2138     theNotSpecials.append(cl[1]);
2139 
2140     cl[2] = new QLabel(i18n("Others"), gb);
2141     gl->addWidget(cl[2], 4, 0);
2142     theNotSpecials.append(cl[2]);
2143 
2144     QString setUidWhatsThis;
2145     if (isDir) {
2146         setUidWhatsThis = i18n(
2147             "If this flag is set, the owner of this folder will be "
2148             "the owner of all new files.");
2149     } else {
2150         setUidWhatsThis = i18n(
2151             "If this file is an executable and the flag is set, it will "
2152             "be executed with the permissions of the owner.");
2153     }
2154 
2155     QString setGidWhatsThis;
2156     if (isDir) {
2157         setGidWhatsThis = i18n(
2158             "If this flag is set, the group of this folder will be "
2159             "set for all new files.");
2160     } else {
2161         setGidWhatsThis = i18n(
2162             "If this file is an executable and the flag is set, it will "
2163             "be executed with the permissions of the group.");
2164     }
2165 
2166     QString stickyWhatsThis;
2167     if (isDir) {
2168         stickyWhatsThis = i18n(
2169             "If the Sticky flag is set on a folder, only the owner "
2170             "and root can delete or rename files. Otherwise everybody "
2171             "with write permissions can do this.");
2172     } else {
2173         stickyWhatsThis = i18n(
2174             "The Sticky flag on a file is ignored on Linux, but may "
2175             "be used on some systems");
2176     }
2177     mode_t aPermissions = 0;
2178     mode_t aPartialPermissions = 0;
2179     mode_t dummy1 = 0;
2180     mode_t dummy2 = 0;
2181 
2182     if (!d->isIrregular) {
2183         switch (d->pmode) {
2184         case PermissionsOnlyFiles:
2185             getPermissionMasks(aPartialPermissions, dummy1, aPermissions, dummy2);
2186             break;
2187         case PermissionsOnlyDirs:
2188         case PermissionsMixed:
2189             getPermissionMasks(dummy1, aPartialPermissions, dummy2, aPermissions);
2190             break;
2191         case PermissionsOnlyLinks:
2192             aPermissions = UniRead | UniWrite | UniExec | UniSpecial;
2193             break;
2194         }
2195     } else {
2196         aPermissions = d->permissions;
2197         aPartialPermissions = d->partialPermissions;
2198     }
2199 
2200     // Draw Checkboxes
2201     QCheckBox *cba[3][4];
2202     for (int row = 0; row < 3; ++row) {
2203         for (int col = 0; col < 4; ++col) {
2204             QCheckBox *cb = new QCheckBox(gb);
2205             if (col != 3) {
2206                 theNotSpecials.append(cb);
2207             }
2208             cba[row][col] = cb;
2209             cb->setChecked(aPermissions & fperm[row][col]);
2210             if (aPartialPermissions & fperm[row][col]) {
2211                 cb->setTristate();
2212                 cb->setCheckState(Qt::PartiallyChecked);
2213             } else if (d->cbRecursive && d->cbRecursive->isChecked()) {
2214                 cb->setTristate();
2215             }
2216 
2217             cb->setEnabled(d->canChangePermissions);
2218             gl->addWidget(cb, row + 2, col + 1);
2219             switch (col) {
2220             case 0:
2221                 cb->setText(readLabel);
2222                 cb->setWhatsThis(readWhatsThis);
2223                 break;
2224             case 1:
2225                 cb->setText(writeLabel);
2226                 cb->setWhatsThis(writeWhatsThis);
2227                 break;
2228             case 2:
2229                 cb->setText(execLabel);
2230                 cb->setWhatsThis(execWhatsThis);
2231                 break;
2232             case 3:
2233                 switch (row) {
2234                 case 0:
2235                     cb->setText(i18n("Set UID"));
2236                     cb->setWhatsThis(setUidWhatsThis);
2237                     break;
2238                 case 1:
2239                     cb->setText(i18n("Set GID"));
2240                     cb->setWhatsThis(setGidWhatsThis);
2241                     break;
2242                 case 2:
2243                     cb->setText(i18nc("File permission", "Sticky"));
2244                     cb->setWhatsThis(stickyWhatsThis);
2245                     break;
2246                 }
2247                 break;
2248             }
2249         }
2250     }
2251     gl->setColumnStretch(6, 10);
2252 
2253 #if HAVE_POSIX_ACL
2254     KACLEditWidget *extendedACLs = nullptr;
2255 
2256     // FIXME make it work with partial entries
2257     if (properties->items().count() == 1) {
2258         QByteArray path = QFile::encodeName(properties->item().url().toLocalFile());
2259         d->fileSystemSupportsACLs = fileSystemSupportsACL(path);
2260     }
2261     if (d->fileSystemSupportsACLs) {
2262         std::for_each(theNotSpecials.begin(), theNotSpecials.end(), std::mem_fn(&QWidget::hide));
2263         extendedACLs = new KACLEditWidget(&dlg);
2264         extendedACLs->setEnabled(d->canChangePermissions);
2265         vbox->addWidget(extendedACLs);
2266         if (d->extendedACL.isValid() && d->extendedACL.isExtended()) {
2267             extendedACLs->setACL(d->extendedACL);
2268         } else {
2269             extendedACLs->setACL(KACL(aPermissions));
2270         }
2271 
2272         if (d->defaultACL.isValid()) {
2273             extendedACLs->setDefaultACL(d->defaultACL);
2274         }
2275 
2276         if (properties->items().constFirst().isDir()) {
2277             extendedACLs->setAllowDefaults(true);
2278         }
2279     }
2280 #endif
2281 
2282     QDialogButtonBox *buttonBox = new QDialogButtonBox(&dlg);
2283     buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
2284     connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
2285     connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
2286     vbox->addWidget(buttonBox);
2287 
2288     if (dlg.exec() != QDialog::Accepted) {
2289         return;
2290     }
2291 
2292     mode_t andPermissions = mode_t(~0);
2293     mode_t orPermissions = 0;
2294     for (int row = 0; row < 3; ++row) {
2295         for (int col = 0; col < 4; ++col) {
2296             switch (cba[row][col]->checkState()) {
2297             case Qt::Checked:
2298                 orPermissions |= fperm[row][col];
2299             // fall through
2300             case Qt::Unchecked:
2301                 andPermissions &= ~fperm[row][col];
2302                 break;
2303             case Qt::PartiallyChecked:
2304                 break;
2305             }
2306         }
2307     }
2308 
2309     const KFileItemList items = properties->items();
2310     d->isIrregular = std::any_of(items.cbegin(), items.cend(), [this, andPermissions, orPermissions](const KFileItem &item) {
2311         return isIrregular((item.permissions() & andPermissions) | orPermissions, item.isDir(), item.isLink());
2312     });
2313 
2314     d->permissions = orPermissions;
2315     d->partialPermissions = andPermissions;
2316 
2317 #if HAVE_POSIX_ACL
2318     // override with the acls, if present
2319     if (extendedACLs) {
2320         d->extendedACL = extendedACLs->getACL();
2321         d->defaultACL = extendedACLs->getDefaultACL();
2322         d->hasExtendedACL = d->extendedACL.isExtended() || d->defaultACL.isValid();
2323         d->permissions = d->extendedACL.basePermissions();
2324         d->permissions |= (andPermissions | orPermissions) & (S_ISUID | S_ISGID | S_ISVTX);
2325     }
2326 #endif
2327 
2328     updateAccessControls();
2329     Q_EMIT changed();
2330 }
2331 
2332 KFilePermissionsPropsPlugin::~KFilePermissionsPropsPlugin() = default;
2333 
2334 bool KFilePermissionsPropsPlugin::supports(const KFileItemList &items)
2335 {
2336     return std::any_of(items.cbegin(), items.cend(), [](const KFileItem &item) {
2337         return KProtocolManager::supportsPermissions(item.url());
2338     });
2339 }
2340 
2341 // sets a combo box in the Access Control frame
2342 void KFilePermissionsPropsPlugin::setComboContent(QComboBox *combo, PermissionsTarget target, mode_t permissions, mode_t partial)
2343 {
2344     combo->clear();
2345     if (d->isIrregular) { // #176876
2346         return;
2347     }
2348 
2349     if (d->pmode == PermissionsOnlyLinks) {
2350         combo->addItem(i18n("Link"));
2351         combo->setCurrentIndex(0);
2352         return;
2353     }
2354 
2355     mode_t tMask = permissionsMasks[target];
2356     int textIndex;
2357     for (textIndex = 0; standardPermissions[textIndex] != s_invalid_mode_t; ++textIndex) {
2358         if ((standardPermissions[textIndex] & tMask) == (permissions & tMask & (UniRead | UniWrite))) {
2359             break;
2360         }
2361     }
2362     Q_ASSERT(standardPermissions[textIndex] != s_invalid_mode_t); // must not happen, would be irreglar
2363 
2364     const auto permsTexts = permissionsTexts[static_cast<int>(d->pmode)];
2365     for (int i = 0; !permsTexts[i].isEmpty(); ++i) {
2366         combo->addItem(permsTexts[i].toString());
2367     }
2368 
2369     if (partial & tMask & ~UniExec) {
2370         combo->addItem(i18n("Varying (No Change)"));
2371         combo->setCurrentIndex(3);
2372     } else {
2373         combo->setCurrentIndex(textIndex);
2374     }
2375 }
2376 
2377 // permissions are irregular if they can't be displayed in a combo box.
2378 bool KFilePermissionsPropsPlugin::isIrregular(mode_t permissions, bool isDir, bool isLink)
2379 {
2380     if (isLink) { // links are always ok
2381         return false;
2382     }
2383 
2384     mode_t p = permissions;
2385     if (p & (S_ISUID | S_ISGID)) { // setuid/setgid -> irregular
2386         return true;
2387     }
2388     if (isDir) {
2389         p &= ~S_ISVTX; // ignore sticky on dirs
2390 
2391         // check supported flag combinations
2392         mode_t p0 = p & UniOwner;
2393         if ((p0 != 0) && (p0 != (S_IRUSR | S_IXUSR)) && (p0 != UniOwner)) {
2394             return true;
2395         }
2396         p0 = p & UniGroup;
2397         if ((p0 != 0) && (p0 != (S_IRGRP | S_IXGRP)) && (p0 != UniGroup)) {
2398             return true;
2399         }
2400         p0 = p & UniOthers;
2401         if ((p0 != 0) && (p0 != (S_IROTH | S_IXOTH)) && (p0 != UniOthers)) {
2402             return true;
2403         }
2404         return false;
2405     }
2406     if (p & S_ISVTX) { // sticky on file -> irregular
2407         return true;
2408     }
2409 
2410     // check supported flag combinations
2411     mode_t p0 = p & UniOwner;
2412     bool usrXPossible = !p0; // true if this file could be an executable
2413     if (p0 & S_IXUSR) {
2414         if ((p0 == S_IXUSR) || (p0 == (S_IWUSR | S_IXUSR))) {
2415             return true;
2416         }
2417         usrXPossible = true;
2418     } else if (p0 == S_IWUSR) {
2419         return true;
2420     }
2421 
2422     p0 = p & UniGroup;
2423     bool grpXPossible = !p0; // true if this file could be an executable
2424     if (p0 & S_IXGRP) {
2425         if ((p0 == S_IXGRP) || (p0 == (S_IWGRP | S_IXGRP))) {
2426             return true;
2427         }
2428         grpXPossible = true;
2429     } else if (p0 == S_IWGRP) {
2430         return true;
2431     }
2432     if (p0 == 0) {
2433         grpXPossible = true;
2434     }
2435 
2436     p0 = p & UniOthers;
2437     bool othXPossible = !p0; // true if this file could be an executable
2438     if (p0 & S_IXOTH) {
2439         if ((p0 == S_IXOTH) || (p0 == (S_IWOTH | S_IXOTH))) {
2440             return true;
2441         }
2442         othXPossible = true;
2443     } else if (p0 == S_IWOTH) {
2444         return true;
2445     }
2446 
2447     // check that there either all targets are executable-compatible, or none
2448     return (p & UniExec) && !(usrXPossible && grpXPossible && othXPossible);
2449 }
2450 
2451 // enables/disabled the widgets in the Access Control frame
2452 void KFilePermissionsPropsPlugin::enableAccessControls(bool enable)
2453 {
2454     d->ownerPermCombo->setEnabled(enable);
2455     d->groupPermCombo->setEnabled(enable);
2456     d->othersPermCombo->setEnabled(enable);
2457     if (d->extraCheckbox) {
2458         d->extraCheckbox->setEnabled(enable);
2459     }
2460     if (d->cbRecursive) {
2461         d->cbRecursive->setEnabled(enable);
2462     }
2463 }
2464 
2465 // updates all widgets in the Access Control frame
2466 void KFilePermissionsPropsPlugin::updateAccessControls()
2467 {
2468     setComboContent(d->ownerPermCombo, PermissionsOwner, d->permissions, d->partialPermissions);
2469     setComboContent(d->groupPermCombo, PermissionsGroup, d->permissions, d->partialPermissions);
2470     setComboContent(d->othersPermCombo, PermissionsOthers, d->permissions, d->partialPermissions);
2471 
2472     switch (d->pmode) {
2473     case PermissionsOnlyLinks:
2474         enableAccessControls(false);
2475         break;
2476     case PermissionsOnlyFiles:
2477         enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
2478         if (d->canChangePermissions) {
2479             d->explanationLabel->setText(
2480                 d->isIrregular || d->hasExtendedACL
2481                     ? i18np("This file uses advanced permissions", "These files use advanced permissions.", properties->items().count())
2482                     : QString());
2483         }
2484         if (d->partialPermissions & UniExec) {
2485             d->extraCheckbox->setTristate();
2486             d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
2487         } else {
2488             d->extraCheckbox->setTristate(false);
2489             d->extraCheckbox->setChecked(d->permissions & UniExec);
2490         }
2491         break;
2492     case PermissionsOnlyDirs:
2493         enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
2494         // if this is a dir, and we can change permissions, don't dis-allow
2495         // recursive, we can do that for ACL setting.
2496         if (d->cbRecursive) {
2497             d->cbRecursive->setEnabled(d->canChangePermissions && !d->isIrregular);
2498         }
2499 
2500         if (d->canChangePermissions) {
2501             d->explanationLabel->setText(
2502                 d->isIrregular || d->hasExtendedACL
2503                     ? i18np("This folder uses advanced permissions.", "These folders use advanced permissions.", properties->items().count())
2504                     : QString());
2505         }
2506         if (d->partialPermissions & S_ISVTX) {
2507             d->extraCheckbox->setTristate();
2508             d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
2509         } else {
2510             d->extraCheckbox->setTristate(false);
2511             d->extraCheckbox->setChecked(d->permissions & S_ISVTX);
2512         }
2513         break;
2514     case PermissionsMixed:
2515         enableAccessControls(d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
2516         if (d->canChangePermissions) {
2517             d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18n("These files use advanced permissions.") : QString());
2518         }
2519         if (d->partialPermissions & S_ISVTX) {
2520             d->extraCheckbox->setTristate();
2521             d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
2522         } else {
2523             d->extraCheckbox->setTristate(false);
2524             d->extraCheckbox->setChecked(d->permissions & S_ISVTX);
2525         }
2526         break;
2527     }
2528 }
2529 
2530 // gets masks for files and dirs from the Access Control frame widgets
2531 void KFilePermissionsPropsPlugin::getPermissionMasks(mode_t &andFilePermissions, mode_t &andDirPermissions, mode_t &orFilePermissions, mode_t &orDirPermissions)
2532 {
2533     andFilePermissions = mode_t(~UniSpecial);
2534     andDirPermissions = mode_t(~(S_ISUID | S_ISGID));
2535     orFilePermissions = 0;
2536     orDirPermissions = 0;
2537     if (d->isIrregular) {
2538         return;
2539     }
2540 
2541     mode_t m = standardPermissions[d->ownerPermCombo->currentIndex()];
2542     if (m != s_invalid_mode_t) {
2543         orFilePermissions |= m & UniOwner;
2544         if ((m & UniOwner)
2545             && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
2546             andFilePermissions &= ~(S_IRUSR | S_IWUSR);
2547         } else {
2548             andFilePermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR);
2549             if ((m & S_IRUSR) && (d->extraCheckbox->checkState() == Qt::Checked)) {
2550                 orFilePermissions |= S_IXUSR;
2551             }
2552         }
2553 
2554         orDirPermissions |= m & UniOwner;
2555         if (m & S_IRUSR) {
2556             orDirPermissions |= S_IXUSR;
2557         }
2558         andDirPermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR);
2559     }
2560 
2561     m = standardPermissions[d->groupPermCombo->currentIndex()];
2562     if (m != s_invalid_mode_t) {
2563         orFilePermissions |= m & UniGroup;
2564         if ((m & UniGroup)
2565             && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
2566             andFilePermissions &= ~(S_IRGRP | S_IWGRP);
2567         } else {
2568             andFilePermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP);
2569             if ((m & S_IRGRP) && (d->extraCheckbox->checkState() == Qt::Checked)) {
2570                 orFilePermissions |= S_IXGRP;
2571             }
2572         }
2573 
2574         orDirPermissions |= m & UniGroup;
2575         if (m & S_IRGRP) {
2576             orDirPermissions |= S_IXGRP;
2577         }
2578         andDirPermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP);
2579     }
2580 
2581     m = d->othersPermCombo->currentIndex() >= 0 ? standardPermissions[d->othersPermCombo->currentIndex()] : s_invalid_mode_t;
2582     if (m != s_invalid_mode_t) {
2583         orFilePermissions |= m & UniOthers;
2584         if ((m & UniOthers)
2585             && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
2586             andFilePermissions &= ~(S_IROTH | S_IWOTH);
2587         } else {
2588             andFilePermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH);
2589             if ((m & S_IROTH) && (d->extraCheckbox->checkState() == Qt::Checked)) {
2590                 orFilePermissions |= S_IXOTH;
2591             }
2592         }
2593 
2594         orDirPermissions |= m & UniOthers;
2595         if (m & S_IROTH) {
2596             orDirPermissions |= S_IXOTH;
2597         }
2598         andDirPermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH);
2599     }
2600 
2601     if (((d->pmode == PermissionsMixed) || (d->pmode == PermissionsOnlyDirs)) && (d->extraCheckbox->checkState() != Qt::PartiallyChecked)) {
2602         andDirPermissions &= ~S_ISVTX;
2603         if (d->extraCheckbox->checkState() == Qt::Checked) {
2604             orDirPermissions |= S_ISVTX;
2605         }
2606     }
2607 }
2608 
2609 void KFilePermissionsPropsPlugin::applyChanges()
2610 {
2611     mode_t orFilePermissions;
2612     mode_t orDirPermissions;
2613     mode_t andFilePermissions;
2614     mode_t andDirPermissions;
2615 
2616     if (!d->canChangePermissions) {
2617         properties->abortApplying();
2618         return;
2619     }
2620 
2621     if (!d->isIrregular) {
2622         getPermissionMasks(andFilePermissions, andDirPermissions, orFilePermissions, orDirPermissions);
2623     } else {
2624         orFilePermissions = d->permissions;
2625         andFilePermissions = d->partialPermissions;
2626         orDirPermissions = d->permissions;
2627         andDirPermissions = d->partialPermissions;
2628     }
2629 
2630     QString owner;
2631     QString group;
2632     if (d->usrEdit) {
2633         owner = d->usrEdit->text();
2634     }
2635     if (d->grpEdit) {
2636         group = d->grpEdit->text();
2637     } else if (d->grpCombo) {
2638         group = d->grpCombo->currentText();
2639     }
2640 
2641     const bool recursive = d->cbRecursive && d->cbRecursive->isChecked();
2642 
2643     if (!recursive) {
2644         if (owner == d->strOwner) {
2645             owner.clear();
2646         }
2647 
2648         if (group == d->strGroup) {
2649             group.clear();
2650         }
2651     }
2652 
2653     bool permissionChange = false;
2654 
2655     const KFileItemList items = properties->items();
2656     KFileItemList files;
2657     KFileItemList dirs;
2658     for (const auto &item : items) {
2659         const auto perms = item.permissions();
2660         if (item.isDir()) {
2661             dirs.append(item);
2662             if (!permissionChange && (recursive || perms != ((perms & andDirPermissions) | orDirPermissions))) {
2663                 permissionChange = true;
2664             }
2665             continue;
2666         }
2667 
2668         if (item.isFile()) {
2669             files.append(item);
2670             if (!permissionChange && perms != ((perms & andFilePermissions) | orFilePermissions)) {
2671                 permissionChange = true;
2672             }
2673         }
2674     }
2675 
2676     const bool ACLChange = (d->extendedACL != properties->item().ACL());
2677     const bool defaultACLChange = (d->defaultACL != properties->item().defaultACL());
2678 
2679     if (owner.isEmpty() && group.isEmpty() && !recursive && !permissionChange && !ACLChange && !defaultACLChange) {
2680         return;
2681     }
2682 
2683     auto processACLChanges = [this, ACLChange, defaultACLChange](KIO::ChmodJob *chmodJob) {
2684         if (!d->fileSystemSupportsACLs) {
2685             return;
2686         }
2687 
2688         if (ACLChange) {
2689             chmodJob->addMetaData(QStringLiteral("ACL_STRING"), d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE"));
2690         }
2691 
2692         if (defaultACLChange) {
2693             chmodJob->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE"));
2694         }
2695     };
2696 
2697     auto chmodDirs = [=]() {
2698         if (dirs.isEmpty()) {
2699             setDirty(false);
2700             Q_EMIT changesApplied();
2701             return;
2702         }
2703 
2704         auto *dirsJob = KIO::chmod(dirs, orDirPermissions, ~andDirPermissions, owner, group, recursive);
2705         processACLChanges(dirsJob);
2706 
2707         connect(dirsJob, &KJob::result, this, [this, dirsJob]() {
2708             if (dirsJob->error()) {
2709                 dirsJob->uiDelegate()->showErrorMessage();
2710             }
2711 
2712             setDirty(false);
2713             Q_EMIT changesApplied();
2714         });
2715     };
2716 
2717     // Change permissions in two steps, first files, then dirs
2718 
2719     if (!files.isEmpty()) {
2720         auto *filesJob = KIO::chmod(files, orFilePermissions, ~andFilePermissions, owner, group, false);
2721         processACLChanges(filesJob);
2722 
2723         connect(filesJob, &KJob::result, this, [=]() {
2724             if (filesJob->error()) {
2725                 filesJob->uiDelegate()->showErrorMessage();
2726             }
2727 
2728             chmodDirs();
2729         });
2730         return;
2731     }
2732 
2733     // No files to change? OK, now process dirs (if any)
2734     chmodDirs();
2735 }
2736 
2737 class KChecksumsPlugin::KChecksumsPluginPrivate
2738 {
2739 public:
2740     KChecksumsPluginPrivate()
2741     {
2742     }
2743 
2744     ~KChecksumsPluginPrivate()
2745     {
2746     }
2747 
2748     QWidget m_widget;
2749     Ui::ChecksumsWidget m_ui;
2750 
2751     QFileSystemWatcher fileWatcher;
2752     QString m_md5;
2753     QString m_sha1;
2754     QString m_sha256;
2755     QString m_sha512;
2756 };
2757 
2758 KChecksumsPlugin::KChecksumsPlugin(KPropertiesDialog *dialog)
2759     : KPropertiesDialogPlugin(dialog)
2760     , d(new KChecksumsPluginPrivate)
2761 {
2762     d->m_ui.setupUi(&d->m_widget);
2763     properties->addPage(&d->m_widget, i18nc("@title:tab", "C&hecksums"));
2764 
2765     d->m_ui.md5CopyButton->hide();
2766     d->m_ui.sha1CopyButton->hide();
2767     d->m_ui.sha256CopyButton->hide();
2768     d->m_ui.sha512CopyButton->hide();
2769 
2770     connect(d->m_ui.lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) {
2771         slotVerifyChecksum(text.toLower());
2772     });
2773 
2774     connect(d->m_ui.md5Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowMd5);
2775     connect(d->m_ui.sha1Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha1);
2776     connect(d->m_ui.sha256Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha256);
2777     connect(d->m_ui.sha512Button, &QPushButton::clicked, this, &KChecksumsPlugin::slotShowSha512);
2778 
2779     d->fileWatcher.addPath(properties->item().localPath());
2780     connect(&d->fileWatcher, &QFileSystemWatcher::fileChanged, this, &KChecksumsPlugin::slotInvalidateCache);
2781 
2782     auto clipboard = QApplication::clipboard();
2783     connect(d->m_ui.md5CopyButton, &QPushButton::clicked, this, [=]() {
2784         clipboard->setText(d->m_md5);
2785     });
2786 
2787     connect(d->m_ui.sha1CopyButton, &QPushButton::clicked, this, [=]() {
2788         clipboard->setText(d->m_sha1);
2789     });
2790 
2791     connect(d->m_ui.sha256CopyButton, &QPushButton::clicked, this, [=]() {
2792         clipboard->setText(d->m_sha256);
2793     });
2794 
2795     connect(d->m_ui.sha512CopyButton, &QPushButton::clicked, this, [=]() {
2796         clipboard->setText(d->m_sha512);
2797     });
2798 
2799     connect(d->m_ui.pasteButton, &QPushButton::clicked, this, [=]() {
2800         d->m_ui.lineEdit->setText(clipboard->text());
2801     });
2802 
2803     setDefaultState();
2804 }
2805 
2806 KChecksumsPlugin::~KChecksumsPlugin() = default;
2807 
2808 bool KChecksumsPlugin::supports(const KFileItemList &items)
2809 {
2810     if (items.count() != 1) {
2811         return false;
2812     }
2813 
2814     const KFileItem &item = items.first();
2815     return item.isFile() && !item.localPath().isEmpty() && item.isReadable() && !item.isDesktopFile() && !item.isLink();
2816 }
2817 
2818 void KChecksumsPlugin::slotInvalidateCache()
2819 {
2820     d->m_md5 = QString();
2821     d->m_sha1 = QString();
2822     d->m_sha256 = QString();
2823     d->m_sha512 = QString();
2824 }
2825 
2826 void KChecksumsPlugin::slotShowMd5()
2827 {
2828     auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget);
2829     label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2830 
2831     d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.md5Button, label);
2832     d->m_ui.md5Button->hide();
2833 
2834     showChecksum(QCryptographicHash::Md5, label, d->m_ui.md5CopyButton);
2835 }
2836 
2837 void KChecksumsPlugin::slotShowSha1()
2838 {
2839     auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget);
2840     label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2841 
2842     d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha1Button, label);
2843     d->m_ui.sha1Button->hide();
2844 
2845     showChecksum(QCryptographicHash::Sha1, label, d->m_ui.sha1CopyButton);
2846 }
2847 
2848 void KChecksumsPlugin::slotShowSha256()
2849 {
2850     auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget);
2851     label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2852 
2853     d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha256Button, label);
2854     d->m_ui.sha256Button->hide();
2855 
2856     showChecksum(QCryptographicHash::Sha256, label, d->m_ui.sha256CopyButton);
2857 }
2858 
2859 void KChecksumsPlugin::slotShowSha512()
2860 {
2861     auto label = new QLabel(i18nc("@action:button", "Calculating..."), &d->m_widget);
2862     label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2863 
2864     d->m_ui.calculateWidget->layout()->replaceWidget(d->m_ui.sha512Button, label);
2865     d->m_ui.sha512Button->hide();
2866 
2867     showChecksum(QCryptographicHash::Sha512, label, d->m_ui.sha512CopyButton);
2868 }
2869 
2870 void KChecksumsPlugin::slotVerifyChecksum(const QString &input)
2871 {
2872     auto algorithm = detectAlgorithm(input);
2873 
2874     // Input is not a supported hash algorithm.
2875     if (algorithm == QCryptographicHash::Md4) {
2876         if (input.isEmpty()) {
2877             setDefaultState();
2878         } else {
2879             setInvalidChecksumState();
2880         }
2881         return;
2882     }
2883 
2884     const QString checksum = cachedChecksum(algorithm);
2885 
2886     // Checksum already in cache.
2887     if (!checksum.isEmpty()) {
2888         const bool isMatch = (checksum == input);
2889         if (isMatch) {
2890             setMatchState();
2891         } else {
2892             setMismatchState();
2893         }
2894 
2895         return;
2896     }
2897 
2898     // Calculate checksum in another thread.
2899     auto futureWatcher = new QFutureWatcher<QString>(this);
2900     connect(futureWatcher, &QFutureWatcher<QString>::finished, this, [=]() {
2901         const QString checksum = futureWatcher->result();
2902         futureWatcher->deleteLater();
2903 
2904         cacheChecksum(checksum, algorithm);
2905 
2906         switch (algorithm) {
2907         case QCryptographicHash::Md5:
2908             slotShowMd5();
2909             break;
2910         case QCryptographicHash::Sha1:
2911             slotShowSha1();
2912             break;
2913         case QCryptographicHash::Sha256:
2914             slotShowSha256();
2915             break;
2916         case QCryptographicHash::Sha512:
2917             slotShowSha512();
2918             break;
2919         default:
2920             break;
2921         }
2922 
2923         const bool isMatch = (checksum == input);
2924         if (isMatch) {
2925             setMatchState();
2926         } else {
2927             setMismatchState();
2928         }
2929     });
2930 
2931     // Notify the user about the background computation.
2932     setVerifyState();
2933 
2934     auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath());
2935     futureWatcher->setFuture(future);
2936 }
2937 
2938 bool KChecksumsPlugin::isMd5(const QString &input)
2939 {
2940     QRegularExpression regex(QStringLiteral("^[a-f0-9]{32}$"));
2941     return regex.match(input).hasMatch();
2942 }
2943 
2944 bool KChecksumsPlugin::isSha1(const QString &input)
2945 {
2946     QRegularExpression regex(QStringLiteral("^[a-f0-9]{40}$"));
2947     return regex.match(input).hasMatch();
2948 }
2949 
2950 bool KChecksumsPlugin::isSha256(const QString &input)
2951 {
2952     QRegularExpression regex(QStringLiteral("^[a-f0-9]{64}$"));
2953     return regex.match(input).hasMatch();
2954 }
2955 
2956 bool KChecksumsPlugin::isSha512(const QString &input)
2957 {
2958     QRegularExpression regex(QStringLiteral("^[a-f0-9]{128}$"));
2959     return regex.match(input).hasMatch();
2960 }
2961 
2962 QString KChecksumsPlugin::computeChecksum(QCryptographicHash::Algorithm algorithm, const QString &path)
2963 {
2964     QFile file(path);
2965     if (!file.open(QIODevice::ReadOnly)) {
2966         return QString();
2967     }
2968 
2969     QCryptographicHash hash(algorithm);
2970     hash.addData(&file);
2971 
2972     return QString::fromLatin1(hash.result().toHex());
2973 }
2974 
2975 QCryptographicHash::Algorithm KChecksumsPlugin::detectAlgorithm(const QString &input)
2976 {
2977     if (isMd5(input)) {
2978         return QCryptographicHash::Md5;
2979     }
2980 
2981     if (isSha1(input)) {
2982         return QCryptographicHash::Sha1;
2983     }
2984 
2985     if (isSha256(input)) {
2986         return QCryptographicHash::Sha256;
2987     }
2988 
2989     if (isSha512(input)) {
2990         return QCryptographicHash::Sha512;
2991     }
2992 
2993     // Md4 used as negative error code.
2994     return QCryptographicHash::Md4;
2995 }
2996 
2997 void KChecksumsPlugin::setDefaultState()
2998 {
2999     QColor defaultColor = d->m_widget.palette().color(QPalette::Base);
3000 
3001     QPalette palette = d->m_widget.palette();
3002     palette.setColor(QPalette::Base, defaultColor);
3003 
3004     d->m_ui.feedbackLabel->hide();
3005     d->m_ui.lineEdit->setPalette(palette);
3006     d->m_ui.lineEdit->setToolTip(QString());
3007 }
3008 
3009 void KChecksumsPlugin::setInvalidChecksumState()
3010 {
3011     KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
3012     QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color();
3013 
3014     QPalette palette = d->m_widget.palette();
3015     palette.setColor(QPalette::Base, warningColor);
3016 
3017     d->m_ui.feedbackLabel->setText(i18n("Invalid checksum."));
3018     d->m_ui.feedbackLabel->show();
3019     d->m_ui.lineEdit->setPalette(palette);
3020     d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The given input is not a valid MD5, SHA1 or SHA256 checksum."));
3021 }
3022 
3023 void KChecksumsPlugin::setMatchState()
3024 {
3025     KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
3026     QColor positiveColor = colorScheme.background(KColorScheme::PositiveBackground).color();
3027 
3028     QPalette palette = d->m_widget.palette();
3029     palette.setColor(QPalette::Base, positiveColor);
3030 
3031     d->m_ui.feedbackLabel->setText(i18n("Checksums match."));
3032     d->m_ui.feedbackLabel->show();
3033     d->m_ui.lineEdit->setPalette(palette);
3034     d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum match."));
3035 }
3036 
3037 void KChecksumsPlugin::setMismatchState()
3038 {
3039     KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
3040     QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color();
3041 
3042     QPalette palette = d->m_widget.palette();
3043     palette.setColor(QPalette::Base, warningColor);
3044 
3045     d->m_ui.feedbackLabel->setText(
3046         i18n("<p>Checksums do not match.</p>"
3047              "This may be due to a faulty download. Try re-downloading the file.<br/>"
3048              "If the verification still fails, contact the source of the file."));
3049     d->m_ui.feedbackLabel->show();
3050     d->m_ui.lineEdit->setPalette(palette);
3051     d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum differ."));
3052 }
3053 
3054 void KChecksumsPlugin::setVerifyState()
3055 {
3056     // Users can paste a checksum at any time, so reset to default.
3057     setDefaultState();
3058 
3059     d->m_ui.feedbackLabel->setText(i18nc("notify the user about a computation in the background", "Verifying checksum..."));
3060     d->m_ui.feedbackLabel->show();
3061 }
3062 
3063 void KChecksumsPlugin::showChecksum(QCryptographicHash::Algorithm algorithm, QLabel *label, QPushButton *copyButton)
3064 {
3065     const QString checksum = cachedChecksum(algorithm);
3066 
3067     // Checksum in cache, nothing else to do.
3068     if (!checksum.isEmpty()) {
3069         label->setText(checksum);
3070         return;
3071     }
3072 
3073     // Calculate checksum in another thread.
3074     auto futureWatcher = new QFutureWatcher<QString>(this);
3075     connect(futureWatcher, &QFutureWatcher<QString>::finished, this, [=]() {
3076         const QString checksum = futureWatcher->result();
3077         futureWatcher->deleteLater();
3078 
3079         label->setText(checksum);
3080         cacheChecksum(checksum, algorithm);
3081 
3082         copyButton->show();
3083     });
3084 
3085     auto future = QtConcurrent::run(&KChecksumsPlugin::computeChecksum, algorithm, properties->item().localPath());
3086     futureWatcher->setFuture(future);
3087 }
3088 
3089 QString KChecksumsPlugin::cachedChecksum(QCryptographicHash::Algorithm algorithm) const
3090 {
3091     switch (algorithm) {
3092     case QCryptographicHash::Md5:
3093         return d->m_md5;
3094     case QCryptographicHash::Sha1:
3095         return d->m_sha1;
3096     case QCryptographicHash::Sha256:
3097         return d->m_sha256;
3098     case QCryptographicHash::Sha512:
3099         return d->m_sha512;
3100     default:
3101         break;
3102     }
3103 
3104     return QString();
3105 }
3106 
3107 void KChecksumsPlugin::cacheChecksum(const QString &checksum, QCryptographicHash::Algorithm algorithm)
3108 {
3109     switch (algorithm) {
3110     case QCryptographicHash::Md5:
3111         d->m_md5 = checksum;
3112         break;
3113     case QCryptographicHash::Sha1:
3114         d->m_sha1 = checksum;
3115         break;
3116     case QCryptographicHash::Sha256:
3117         d->m_sha256 = checksum;
3118         break;
3119     case QCryptographicHash::Sha512:
3120         d->m_sha512 = checksum;
3121         break;
3122     default:
3123         return;
3124     }
3125 }
3126 
3127 class KUrlPropsPlugin::KUrlPropsPluginPrivate
3128 {
3129 public:
3130     KUrlPropsPluginPrivate()
3131     {
3132     }
3133     ~KUrlPropsPluginPrivate()
3134     {
3135     }
3136 
3137     QFrame *m_frame;
3138     KUrlRequester *URLEdit;
3139     QString URLStr;
3140     bool fileNameReadOnly = false;
3141 };
3142 
3143 KUrlPropsPlugin::KUrlPropsPlugin(KPropertiesDialog *_props)
3144     : KPropertiesDialogPlugin(_props)
3145     , d(new KUrlPropsPluginPrivate)
3146 {
3147     d->m_frame = new QFrame();
3148     properties->addPage(d->m_frame, i18n("U&RL"));
3149     QVBoxLayout *layout = new QVBoxLayout(d->m_frame);
3150     layout->setContentsMargins(0, 0, 0, 0);
3151 
3152     QLabel *l;
3153     l = new QLabel(d->m_frame);
3154     l->setObjectName(QStringLiteral("Label_1"));
3155     l->setText(i18n("URL:"));
3156     layout->addWidget(l, Qt::AlignRight);
3157 
3158     d->URLEdit = new KUrlRequester(d->m_frame);
3159     layout->addWidget(d->URLEdit);
3160 
3161     KIO::StatJob *job = KIO::mostLocalUrl(properties->url());
3162     KJobWidgets::setWindow(job, properties);
3163     job->exec();
3164     QUrl url = job->mostLocalUrl();
3165 
3166     if (url.isLocalFile()) {
3167         QString path = url.toLocalFile();
3168 
3169         QFile f(path);
3170         if (!f.open(QIODevice::ReadOnly)) {
3171             return;
3172         }
3173 
3174         KDesktopFile config(path);
3175         const KConfigGroup dg = config.desktopGroup();
3176         d->URLStr = dg.readPathEntry("URL", QString());
3177 
3178         if (!d->URLStr.isEmpty()) {
3179             d->URLEdit->setUrl(QUrl(d->URLStr));
3180         }
3181     }
3182 
3183     connect(d->URLEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed);
3184 
3185     layout->addStretch(1);
3186 }
3187 
3188 KUrlPropsPlugin::~KUrlPropsPlugin() = default;
3189 
3190 void KUrlPropsPlugin::setFileNameReadOnly(bool ro)
3191 {
3192     d->fileNameReadOnly = ro;
3193 }
3194 
3195 bool KUrlPropsPlugin::supports(const KFileItemList &_items)
3196 {
3197     if (_items.count() != 1) {
3198         return false;
3199     }
3200     const KFileItem &item = _items.first();
3201     // check if desktop file
3202     if (!item.isDesktopFile()) {
3203         return false;
3204     }
3205 
3206     // open file and check type
3207     const auto [url, isLocal] = item.isMostLocalUrl();
3208     if (!isLocal) {
3209         return false;
3210     }
3211 
3212     KDesktopFile config(url.toLocalFile());
3213     return config.hasLinkType();
3214 }
3215 
3216 void KUrlPropsPlugin::applyChanges()
3217 {
3218     KIO::StatJob *job = KIO::mostLocalUrl(properties->url());
3219     KJobWidgets::setWindow(job, properties);
3220     job->exec();
3221     const QUrl url = job->mostLocalUrl();
3222 
3223     if (!url.isLocalFile()) {
3224         KMessageBox::error(nullptr, i18n("Could not save properties. Only entries on local file systems are supported."));
3225         properties->abortApplying();
3226         return;
3227     }
3228 
3229     QString path = url.toLocalFile();
3230     QFile f(path);
3231     if (!f.open(QIODevice::ReadWrite)) {
3232         KMessageBox::error(nullptr, couldNotSaveMsg(path));
3233         properties->abortApplying();
3234         return;
3235     }
3236 
3237     KDesktopFile config(path);
3238     KConfigGroup dg = config.desktopGroup();
3239     dg.writeEntry("Type", QStringLiteral("Link"));
3240     dg.writePathEntry("URL", d->URLEdit->url().toString());
3241     // Users can't create a Link .desktop file with a Name field,
3242     // but distributions can. Update the Name field in that case,
3243     // if the file name could have been changed.
3244     if (!d->fileNameReadOnly && dg.hasKey("Name")) {
3245         const QString nameStr = nameFromFileName(properties->url().fileName());
3246         dg.writeEntry("Name", nameStr);
3247         dg.writeEntry("Name", nameStr, KConfigBase::Persistent | KConfigBase::Localized);
3248     }
3249 
3250     setDirty(false);
3251 }
3252 
3253 /* ----------------------------------------------------
3254  *
3255  * KDesktopPropsPlugin
3256  *
3257  * -------------------------------------------------- */
3258 
3259 class KDesktopPropsPlugin::KDesktopPropsPluginPrivate
3260 {
3261 public:
3262     KDesktopPropsPluginPrivate()
3263         : w(new Ui_KPropertiesDesktopBase)
3264         , m_frame(new QFrame())
3265     {
3266     }
3267     ~KDesktopPropsPluginPrivate()
3268     {
3269         delete w;
3270     }
3271     QString command() const;
3272     Ui_KPropertiesDesktopBase *w;
3273     QWidget *m_frame = nullptr;
3274     std::unique_ptr<Ui_KPropertiesDesktopAdvBase> m_uiAdvanced;
3275 
3276     QString m_origCommandStr;
3277     QString m_terminalOptionStr;
3278     QString m_suidUserStr;
3279     QString m_origDesktopFile;
3280     bool m_terminalBool;
3281     bool m_suidBool;
3282     // Corresponds to "PrefersNonDefaultGPU=" (added in destop-entry-spec 1.4)
3283     // or to "X-KDE-RunOnDiscreteGpu=" for backwards compatibility
3284     bool m_runOnDiscreteGpuBool;
3285     bool m_startupBool;
3286 };
3287 
3288 KDesktopPropsPlugin::KDesktopPropsPlugin(KPropertiesDialog *_props)
3289     : KPropertiesDialogPlugin(_props)
3290     , d(new KDesktopPropsPluginPrivate)
3291 {
3292     QMimeDatabase db;
3293 
3294     d->w->setupUi(d->m_frame);
3295 
3296     properties->addPage(d->m_frame, i18n("&Application"));
3297 
3298     bool bKDesktopMode = properties->url().scheme() == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
3299 
3300     d->w->pathEdit->setMode(KFile::Directory | KFile::LocalOnly);
3301     d->w->pathEdit->lineEdit()->setAcceptDrops(false);
3302 
3303     connect(d->w->nameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3304     connect(d->w->genNameEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3305     connect(d->w->commentEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3306     connect(d->w->envarsEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3307     connect(d->w->programEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3308     connect(d->w->argumentsEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3309     connect(d->w->pathEdit, &KUrlRequester::textChanged, this, &KPropertiesDialogPlugin::changed);
3310 
3311     connect(d->w->browseButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotBrowseExec);
3312     connect(d->w->addFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAddFiletype);
3313     connect(d->w->delFiletypeButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotDelFiletype);
3314     connect(d->w->advancedButton, &QAbstractButton::clicked, this, &KDesktopPropsPlugin::slotAdvanced);
3315 
3316     // now populate the page
3317 
3318     KIO::StatJob *job = KIO::mostLocalUrl(_props->url());
3319     KJobWidgets::setWindow(job, _props);
3320     job->exec();
3321     QUrl url = job->mostLocalUrl();
3322 
3323     if (!url.isLocalFile()) {
3324         return;
3325     }
3326 
3327     d->m_origDesktopFile = url.toLocalFile();
3328 
3329     QFile f(d->m_origDesktopFile);
3330     if (!f.open(QIODevice::ReadOnly)) {
3331         return;
3332     }
3333 
3334     KDesktopFile _config(d->m_origDesktopFile);
3335     KConfigGroup config = _config.desktopGroup();
3336     QString nameStr = _config.readName();
3337     QString genNameStr = _config.readGenericName();
3338     QString commentStr = _config.readComment();
3339     QString commandStr = config.readEntry("Exec", QString());
3340 
3341     d->m_origCommandStr = commandStr;
3342     QString pathStr = config.readEntry("Path", QString()); // not readPathEntry, see kservice.cpp
3343     d->m_terminalBool = config.readEntry("Terminal", false);
3344     d->m_terminalOptionStr = config.readEntry("TerminalOptions");
3345     d->m_suidBool = config.readEntry("X-KDE-SubstituteUID", false);
3346     d->m_suidUserStr = config.readEntry("X-KDE-Username");
3347     if (KIO::hasDiscreteGpu()) {
3348         if (config.hasKey("PrefersNonDefaultGPU")) {
3349             d->m_runOnDiscreteGpuBool = config.readEntry("PrefersNonDefaultGPU", false);
3350         } else {
3351             d->m_runOnDiscreteGpuBool = config.readEntry("X-KDE-RunOnDiscreteGpu", false);
3352         }
3353     }
3354     if (config.hasKey("StartupNotify")) {
3355         d->m_startupBool = config.readEntry("StartupNotify", true);
3356     } else {
3357         d->m_startupBool = config.readEntry("X-KDE-StartupNotify", true);
3358     }
3359 
3360     const QStringList mimeTypes = config.readXdgListEntry("MimeType");
3361 
3362     if (nameStr.isEmpty() || bKDesktopMode) {
3363         // We'll use the file name if no name is specified
3364         // because we _need_ a Name for a valid file.
3365         // But let's do it in apply, not here, so that we pick up the right name.
3366         setDirty();
3367     }
3368     d->w->nameEdit->setText(nameStr);
3369     d->w->genNameEdit->setText(genNameStr);
3370     d->w->commentEdit->setText(commentStr);
3371 
3372     QStringList execLine = KShell::splitArgs(commandStr);
3373     QStringList enVars = {};
3374 
3375     if (!execLine.isEmpty()) {
3376         // check for apps that use the env executable
3377         // to set the environment
3378         if (execLine[0] == QLatin1String("env")) {
3379             execLine.pop_front();
3380         }
3381         for (auto env : execLine) {
3382             if (execLine.length() <= 1) {
3383                 // Don't empty out the list. If the last element contains an equal sign we have to treat it as part of the
3384                 // program name lest we have no program
3385                 // https://bugs.kde.org/show_bug.cgi?id=465290
3386                 break;
3387             }
3388             if (!env.contains(QLatin1String("="))) {
3389                 break;
3390             }
3391             enVars += env;
3392             execLine.pop_front();
3393         }
3394 
3395         d->w->programEdit->setText(execLine.takeFirst());
3396     } else {
3397         d->w->programEdit->clear();
3398     }
3399     d->w->argumentsEdit->setText(KShell::joinArgs(execLine));
3400     d->w->envarsEdit->setText(KShell::joinArgs(enVars));
3401 
3402     d->w->pathEdit->lineEdit()->setText(pathStr);
3403 
3404     // was: d->w->filetypeList->setFullWidth(true);
3405     //  d->w->filetypeList->header()->setStretchEnabled(true, d->w->filetypeList->columns()-1);
3406 
3407     for (QStringList::ConstIterator it = mimeTypes.begin(); it != mimeTypes.end();) {
3408         QMimeType p = db.mimeTypeForName(*it);
3409         ++it;
3410         QString preference;
3411         if (it != mimeTypes.end()) {
3412             bool numeric;
3413             (*it).toInt(&numeric);
3414             if (numeric) {
3415                 preference = *it;
3416                 ++it;
3417             }
3418         }
3419         if (p.isValid()) {
3420             QTreeWidgetItem *item = new QTreeWidgetItem();
3421             item->setText(0, p.name());
3422             item->setText(1, p.comment());
3423             item->setText(2, preference);
3424             d->w->filetypeList->addTopLevelItem(item);
3425         }
3426     }
3427     d->w->filetypeList->resizeColumnToContents(0);
3428 }
3429 
3430 KDesktopPropsPlugin::~KDesktopPropsPlugin() = default;
3431 
3432 void KDesktopPropsPlugin::slotAddFiletype()
3433 {
3434     QMimeDatabase db;
3435     KMimeTypeChooserDialog dlg(i18n("Add File Type for %1", properties->url().fileName()),
3436                                i18n("Select one or more file types to add:"),
3437                                QStringList(), // no preselected mimetypes
3438                                QString(),
3439                                QStringList(),
3440                                KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns,
3441                                d->m_frame);
3442 
3443     if (dlg.exec() == QDialog::Accepted) {
3444         const QStringList list = dlg.chooser()->mimeTypes();
3445         for (const QString &mimetype : list) {
3446             QMimeType p = db.mimeTypeForName(mimetype);
3447             if (!p.isValid()) {
3448                 continue;
3449             }
3450 
3451             bool found = false;
3452             int count = d->w->filetypeList->topLevelItemCount();
3453             for (int i = 0; !found && i < count; ++i) {
3454                 if (d->w->filetypeList->topLevelItem(i)->text(0) == mimetype) {
3455                     found = true;
3456                 }
3457             }
3458             if (!found) {
3459                 QTreeWidgetItem *item = new QTreeWidgetItem();
3460                 item->setText(0, p.name());
3461                 item->setText(1, p.comment());
3462                 d->w->filetypeList->addTopLevelItem(item);
3463             }
3464             d->w->filetypeList->resizeColumnToContents(0);
3465         }
3466     }
3467     Q_EMIT changed();
3468 }
3469 
3470 void KDesktopPropsPlugin::slotDelFiletype()
3471 {
3472     QTreeWidgetItem *cur = d->w->filetypeList->currentItem();
3473     if (cur) {
3474         delete cur;
3475         Q_EMIT changed();
3476     }
3477 }
3478 
3479 void KDesktopPropsPlugin::checkCommandChanged()
3480 {
3481     if (KIO::DesktopExecParser::executableName(d->command()) != KIO::DesktopExecParser::executableName(d->m_origCommandStr)) {
3482         d->m_origCommandStr = d->command();
3483     }
3484 }
3485 
3486 void KDesktopPropsPlugin::applyChanges()
3487 {
3488     // qDebug();
3489     KIO::StatJob *job = KIO::mostLocalUrl(properties->url());
3490     KJobWidgets::setWindow(job, properties);
3491     job->exec();
3492     const QUrl url = job->mostLocalUrl();
3493 
3494     if (!url.isLocalFile()) {
3495         KMessageBox::error(nullptr, i18n("Could not save properties. Only entries on local file systems are supported."));
3496         properties->abortApplying();
3497         return;
3498     }
3499 
3500     const QString path(url.toLocalFile());
3501 
3502     // make sure the directory exists
3503     QDir().mkpath(QFileInfo(path).absolutePath());
3504     QFile f(path);
3505     if (!f.open(QIODevice::ReadWrite)) {
3506         KMessageBox::error(nullptr, couldNotSaveMsg(path));
3507         properties->abortApplying();
3508         return;
3509     }
3510 
3511     // If the command is changed we reset certain settings that are strongly
3512     // coupled to the command.
3513     checkCommandChanged();
3514 
3515     KDesktopFile origConfig(d->m_origDesktopFile);
3516     std::unique_ptr<KDesktopFile> _config(origConfig.copyTo(path));
3517     KConfigGroup config = _config->desktopGroup();
3518     config.writeEntry("Type", QStringLiteral("Application"));
3519     config.writeEntry("Comment", d->w->commentEdit->text());
3520     config.writeEntry("Comment", d->w->commentEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
3521     config.writeEntry("GenericName", d->w->genNameEdit->text());
3522     config.writeEntry("GenericName", d->w->genNameEdit->text(), KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
3523     config.writeEntry("Exec", d->command());
3524     config.writeEntry("Path", d->w->pathEdit->lineEdit()->text()); // not writePathEntry, see kservice.cpp
3525 
3526     // Write mimeTypes
3527     QStringList mimeTypes;
3528     int count = d->w->filetypeList->topLevelItemCount();
3529     for (int i = 0; i < count; ++i) {
3530         QTreeWidgetItem *item = d->w->filetypeList->topLevelItem(i);
3531         QString preference = item->text(2);
3532         mimeTypes.append(item->text(0));
3533         if (!preference.isEmpty()) {
3534             mimeTypes.append(preference);
3535         }
3536     }
3537 
3538     // qDebug() << mimeTypes;
3539     config.writeXdgListEntry("MimeType", mimeTypes);
3540 
3541     if (!d->w->nameEdit->isHidden()) {
3542         QString nameStr = d->w->nameEdit->text();
3543         config.writeEntry("Name", nameStr);
3544         config.writeEntry("Name", nameStr, KConfigGroup::Persistent | KConfigGroup::Localized);
3545     }
3546 
3547     config.writeEntry("Terminal", d->m_terminalBool);
3548     config.writeEntry("TerminalOptions", d->m_terminalOptionStr);
3549     config.writeEntry("X-KDE-SubstituteUID", d->m_suidBool);
3550     config.writeEntry("X-KDE-Username", d->m_suidUserStr);
3551     if (KIO::hasDiscreteGpu()) {
3552         if (config.hasKey("PrefersNonDefaultGPU")) {
3553             config.writeEntry("PrefersNonDefaultGPU", d->m_runOnDiscreteGpuBool);
3554         } else {
3555             config.writeEntry("X-KDE-RunOnDiscreteGpu", d->m_runOnDiscreteGpuBool);
3556         }
3557     }
3558     config.writeEntry("StartupNotify", d->m_startupBool);
3559     config.sync();
3560 
3561     // KSycoca update needed?
3562     bool updateNeeded = !relativeAppsLocation(path).isEmpty();
3563     if (updateNeeded) {
3564         KBuildSycocaProgressDialog::rebuildKSycoca(d->m_frame);
3565     }
3566 
3567     setDirty(false);
3568 }
3569 
3570 void KDesktopPropsPlugin::slotBrowseExec()
3571 {
3572     QUrl f = QFileDialog::getOpenFileUrl(d->m_frame);
3573     if (f.isEmpty()) {
3574         return;
3575     }
3576 
3577     if (!f.isLocalFile()) {
3578         KMessageBox::information(d->m_frame, i18n("Only executables on local file systems are supported."));
3579         return;
3580     }
3581 
3582     const QString path = f.toLocalFile();
3583     d->w->programEdit->setText(path);
3584 }
3585 
3586 void KDesktopPropsPlugin::slotAdvanced()
3587 {
3588     auto *dlg = new QDialog(d->m_frame);
3589     dlg->setObjectName(QStringLiteral("KPropertiesDesktopAdv"));
3590     dlg->setModal(true);
3591     dlg->setAttribute(Qt::WA_DeleteOnClose);
3592     dlg->setWindowTitle(i18n("Advanced Options for %1", properties->url().fileName()));
3593 
3594     d->m_uiAdvanced.reset(new Ui_KPropertiesDesktopAdvBase);
3595     QWidget *mainWidget = new QWidget(dlg);
3596     d->m_uiAdvanced->setupUi(mainWidget);
3597 
3598     QDialogButtonBox *buttonBox = new QDialogButtonBox(dlg);
3599     buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
3600     connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
3601     connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
3602 
3603     QVBoxLayout *layout = new QVBoxLayout(dlg);
3604     layout->addWidget(mainWidget);
3605     layout->addWidget(buttonBox);
3606 
3607     // If the command is changed we reset certain settings that are strongly
3608     // coupled to the command.
3609     checkCommandChanged();
3610 
3611     // check to see if we use konsole if not do not add the nocloseonexit
3612     // because we don't know how to do this on other terminal applications
3613     KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
3614     QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
3615 
3616     bool terminalCloseBool = false;
3617 
3618     if (preferredTerminal == QLatin1String("konsole")) {
3619         terminalCloseBool = d->m_terminalOptionStr.contains(QLatin1String("--noclose"));
3620         d->m_uiAdvanced->terminalCloseCheck->setChecked(terminalCloseBool);
3621         d->m_terminalOptionStr.remove(QStringLiteral("--noclose"));
3622     } else {
3623         d->m_uiAdvanced->terminalCloseCheck->hide();
3624     }
3625 
3626     d->m_uiAdvanced->terminalCheck->setChecked(d->m_terminalBool);
3627     d->m_uiAdvanced->terminalEdit->setText(d->m_terminalOptionStr);
3628     d->m_uiAdvanced->terminalCloseCheck->setEnabled(d->m_terminalBool);
3629     d->m_uiAdvanced->terminalEdit->setEnabled(d->m_terminalBool);
3630     d->m_uiAdvanced->terminalEditLabel->setEnabled(d->m_terminalBool);
3631 
3632     d->m_uiAdvanced->suidCheck->setChecked(d->m_suidBool);
3633     d->m_uiAdvanced->suidEdit->setText(d->m_suidUserStr);
3634     d->m_uiAdvanced->suidEdit->setEnabled(d->m_suidBool);
3635     d->m_uiAdvanced->suidEditLabel->setEnabled(d->m_suidBool);
3636 
3637     if (KIO::hasDiscreteGpu()) {
3638         d->m_uiAdvanced->discreteGpuCheck->setChecked(d->m_runOnDiscreteGpuBool);
3639     } else {
3640         d->m_uiAdvanced->discreteGpuGroupBox->hide();
3641     }
3642 
3643     d->m_uiAdvanced->startupInfoCheck->setChecked(d->m_startupBool);
3644 
3645     // Provide username completion up to 1000 users.
3646     const int maxEntries = 1000;
3647     QStringList userNames = KUser::allUserNames(maxEntries);
3648     if (userNames.size() < maxEntries) {
3649         KCompletion *kcom = new KCompletion;
3650         kcom->setOrder(KCompletion::Sorted);
3651         d->m_uiAdvanced->suidEdit->setCompletionObject(kcom, true);
3652         d->m_uiAdvanced->suidEdit->setAutoDeleteCompletionObject(true);
3653         d->m_uiAdvanced->suidEdit->setCompletionMode(KCompletion::CompletionAuto);
3654         kcom->setItems(userNames);
3655     }
3656 
3657     connect(d->m_uiAdvanced->terminalEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3658     connect(d->m_uiAdvanced->terminalCloseCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed);
3659     connect(d->m_uiAdvanced->terminalCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed);
3660     connect(d->m_uiAdvanced->suidCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed);
3661     connect(d->m_uiAdvanced->suidEdit, &QLineEdit::textChanged, this, &KPropertiesDialogPlugin::changed);
3662     connect(d->m_uiAdvanced->discreteGpuCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed);
3663     connect(d->m_uiAdvanced->startupInfoCheck, &QAbstractButton::toggled, this, &KPropertiesDialogPlugin::changed);
3664 
3665     QObject::connect(dlg, &QDialog::accepted, this, [this]() {
3666         d->m_terminalOptionStr = d->m_uiAdvanced->terminalEdit->text().trimmed();
3667         d->m_terminalBool = d->m_uiAdvanced->terminalCheck->isChecked();
3668         d->m_suidBool = d->m_uiAdvanced->suidCheck->isChecked();
3669         d->m_suidUserStr = d->m_uiAdvanced->suidEdit->text().trimmed();
3670         if (KIO::hasDiscreteGpu()) {
3671             d->m_runOnDiscreteGpuBool = d->m_uiAdvanced->discreteGpuCheck->isChecked();
3672         }
3673         d->m_startupBool = d->m_uiAdvanced->startupInfoCheck->isChecked();
3674 
3675         if (d->m_uiAdvanced->terminalCloseCheck->isChecked()) {
3676             d->m_terminalOptionStr.append(QLatin1String(" --noclose"));
3677         }
3678     });
3679 
3680     dlg->show();
3681 }
3682 
3683 bool KDesktopPropsPlugin::supports(const KFileItemList &_items)
3684 {
3685     if (_items.count() != 1) {
3686         return false;
3687     }
3688 
3689     const KFileItem &item = _items.first();
3690 
3691     // check if desktop file
3692     if (!item.isDesktopFile()) {
3693         return false;
3694     }
3695 
3696     // open file and check type
3697     const auto [url, isLocal] = item.isMostLocalUrl();
3698     if (!isLocal) {
3699         return false;
3700     }
3701 
3702     KDesktopFile config(url.toLocalFile());
3703     return config.hasApplicationType() && KAuthorized::authorize(KAuthorized::RUN_DESKTOP_FILES) && KAuthorized::authorize(KAuthorized::SHELL_ACCESS);
3704 }
3705 
3706 QString KDesktopPropsPlugin::KDesktopPropsPluginPrivate::command() const
3707 {
3708     QStringList execSplit = KShell::splitArgs(w->envarsEdit->text()) + QStringList(w->programEdit->text()) + KShell::splitArgs(w->argumentsEdit->text());
3709 
3710     if (KShell::splitArgs(w->envarsEdit->text()).length()) {
3711         execSplit.push_front(QLatin1String("env"));
3712     }
3713 
3714     return KShell::joinArgs(execSplit);
3715 }
3716 
3717 #include "moc_kpropertiesdialog.cpp"
3718 #include "moc_kpropertiesdialog_p.cpp"