File indexing completed on 2024-12-01 03:41:18

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