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

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
0004     SPDX-FileCopyrightText: 1999, 2000 Preston Brown <pbrown@kde.org>
0005     SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
0006     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0007     SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
0008     SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
0009     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-or-later
0012 */
0013 
0014 /*
0015  * kpropertiesdialog.cpp
0016  * View/Edit Properties of files, locally or remotely
0017  *
0018  * some FilePermissionsPropsPlugin-changes by
0019  *  Henner Zeller <zeller@think.de>
0020  * some layout management by
0021  *  Bertrand Leconte <B.Leconte@mail.dotcom.fr>
0022  * the rest of the layout management, bug fixes, adaptation to libkio,
0023  * template feature by
0024  *  David Faure <faure@kde.org>
0025  * More layout, cleanups, and fixes by
0026  *  Preston Brown <pbrown@kde.org>
0027  * Plugin capability, cleanups and port to KDialog by
0028  *  Simon Hausmann <hausmann@kde.org>
0029  * KDesktopPropsPlugin by
0030  *  Waldo Bastian <bastian@kde.org>
0031  */
0032 
0033 #include "kpropertiesdialog.h"
0034 #include "../utils_p.h"
0035 #include "kio_widgets_debug.h"
0036 #include "kpropertiesdialogbuiltin_p.h"
0037 
0038 #include <config-kiowidgets.h>
0039 
0040 #include <kacl.h>
0041 #include <kio/global.h>
0042 #include <kio/statjob.h>
0043 #include <kioglobal_p.h>
0044 
0045 #include <KJobWidgets>
0046 #include <KLocalizedString>
0047 #include <KPluginFactory>
0048 #include <KPluginMetaData>
0049 
0050 #include <qplatformdefs.h>
0051 
0052 #include <QDebug>
0053 #include <QDir>
0054 #include <QList>
0055 #include <QMimeData>
0056 #include <QMimeDatabase>
0057 #include <QRegularExpression>
0058 #include <QStandardPaths>
0059 #include <QUrl>
0060 
0061 #include <algorithm>
0062 #include <functional>
0063 #include <vector>
0064 
0065 #ifdef Q_OS_WIN
0066 #include <process.h>
0067 #include <qt_windows.h>
0068 #include <shellapi.h>
0069 #ifdef __GNUC__
0070 #warning TODO: port completely to win32
0071 #endif
0072 #endif
0073 
0074 using namespace KDEPrivate;
0075 
0076 constexpr mode_t KFilePermissionsPropsPlugin::fperm[3][4] = {
0077     {S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID},
0078     {S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID},
0079     {S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX},
0080 };
0081 
0082 class KPropertiesDialogPrivate
0083 {
0084 public:
0085     explicit KPropertiesDialogPrivate(KPropertiesDialog *qq)
0086         : q(qq)
0087     {
0088     }
0089     ~KPropertiesDialogPrivate()
0090     {
0091         // qDeleteAll deletes the pages in order, this prevents crashes when closing the dialog
0092         qDeleteAll(m_pages);
0093     }
0094 
0095     /**
0096      * Common initialization for all constructors
0097      */
0098     void init();
0099     /**
0100      * Inserts all pages in the dialog.
0101      */
0102     void insertPages();
0103 
0104     void insertPlugin(KPropertiesDialogPlugin *plugin)
0105     {
0106         q->connect(plugin, &KPropertiesDialogPlugin::changed, plugin, [plugin]() {
0107             plugin->setDirty();
0108         });
0109         m_pages.push_back(plugin);
0110     }
0111 
0112     KPropertiesDialog *const q;
0113     bool m_aborted = false;
0114     KPageWidgetItem *fileSharePageItem = nullptr;
0115     KFilePropsPlugin *m_filePropsPlugin = nullptr;
0116     KFilePermissionsPropsPlugin *m_permissionsPropsPlugin = nullptr;
0117     KDesktopPropsPlugin *m_desktopPropsPlugin = nullptr;
0118     KUrlPropsPlugin *m_urlPropsPlugin = nullptr;
0119 
0120     /**
0121      * The URL of the props dialog (when shown for only one file)
0122      */
0123     QUrl m_singleUrl;
0124     /**
0125      * List of items this props dialog is shown for
0126      */
0127     KFileItemList m_items;
0128     /**
0129      * For templates
0130      */
0131     QString m_defaultName;
0132     QUrl m_currentDir;
0133 
0134     /**
0135      * List of all plugins inserted ( first one first )
0136      */
0137     std::vector<KPropertiesDialogPlugin *> m_pages;
0138 };
0139 
0140 KPropertiesDialog::KPropertiesDialog(const KFileItem &item, QWidget *parent)
0141     : KPageDialog(parent)
0142     , d(new KPropertiesDialogPrivate(this))
0143 {
0144     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(item.name())));
0145 
0146     Q_ASSERT(!item.isNull());
0147     d->m_items.append(item);
0148 
0149     d->m_singleUrl = item.url();
0150     Q_ASSERT(!d->m_singleUrl.isEmpty());
0151 
0152     d->init();
0153 }
0154 
0155 KPropertiesDialog::KPropertiesDialog(const QString &title, QWidget *parent)
0156     : KPageDialog(parent)
0157     , d(new KPropertiesDialogPrivate(this))
0158 {
0159     setWindowTitle(i18n("Properties for %1", title));
0160 
0161     d->init();
0162 }
0163 
0164 KPropertiesDialog::KPropertiesDialog(const KFileItemList &_items, QWidget *parent)
0165     : KPageDialog(parent)
0166     , d(new KPropertiesDialogPrivate(this))
0167 {
0168     if (_items.count() > 1) {
0169         setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", _items.count()));
0170     } else {
0171         setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_items.first().name())));
0172     }
0173 
0174     Q_ASSERT(!_items.isEmpty());
0175     d->m_singleUrl = _items.first().url();
0176     Q_ASSERT(!d->m_singleUrl.isEmpty());
0177 
0178     d->m_items = _items;
0179 
0180     d->init();
0181 }
0182 
0183 KPropertiesDialog::KPropertiesDialog(const QUrl &_url, QWidget *parent)
0184     : KPageDialog(parent)
0185     , d(new KPropertiesDialogPrivate(this))
0186 {
0187     d->m_singleUrl = _url.adjusted(QUrl::StripTrailingSlash);
0188 
0189     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(d->m_singleUrl.fileName())));
0190 
0191     KIO::StatJob *job = KIO::stat(d->m_singleUrl);
0192     KJobWidgets::setWindow(job, parent);
0193     job->exec();
0194     KIO::UDSEntry entry = job->statResult();
0195 
0196     d->m_items.append(KFileItem(entry, d->m_singleUrl));
0197     d->init();
0198 }
0199 
0200 KPropertiesDialog::KPropertiesDialog(const QList<QUrl> &urls, QWidget *parent)
0201     : KPageDialog(parent)
0202     , d(new KPropertiesDialogPrivate(this))
0203 {
0204     if (urls.count() > 1) {
0205         setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", urls.count()));
0206     } else {
0207         setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(urls.first().fileName())));
0208     }
0209 
0210     Q_ASSERT(!urls.isEmpty());
0211     d->m_singleUrl = urls.first();
0212     Q_ASSERT(!d->m_singleUrl.isEmpty());
0213 
0214     d->m_items.reserve(urls.size());
0215     for (const QUrl &url : urls) {
0216         KIO::StatJob *job = KIO::stat(url);
0217         KJobWidgets::setWindow(job, parent);
0218         job->exec();
0219         KIO::UDSEntry entry = job->statResult();
0220 
0221         d->m_items.append(KFileItem(entry, url));
0222     }
0223 
0224     d->init();
0225 }
0226 
0227 KPropertiesDialog::KPropertiesDialog(const QUrl &_tempUrl, const QUrl &_currentDir, const QString &_defaultName, QWidget *parent)
0228     : KPageDialog(parent)
0229     , d(new KPropertiesDialogPrivate(this))
0230 {
0231     setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_tempUrl.fileName())));
0232 
0233     d->m_singleUrl = _tempUrl;
0234     d->m_defaultName = _defaultName;
0235     d->m_currentDir = _currentDir;
0236     Q_ASSERT(!d->m_singleUrl.isEmpty());
0237 
0238     // Create the KFileItem for the _template_ file, in order to read from it.
0239     d->m_items.append(KFileItem(d->m_singleUrl));
0240     d->init();
0241 }
0242 
0243 #ifdef Q_OS_WIN
0244 bool showWin32FilePropertyDialog(const QString &fileName)
0245 {
0246     QString path_ = QDir::toNativeSeparators(QFileInfo(fileName).absoluteFilePath());
0247 
0248     SHELLEXECUTEINFOW execInfo;
0249 
0250     memset(&execInfo, 0, sizeof(execInfo));
0251     execInfo.cbSize = sizeof(execInfo);
0252     execInfo.fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
0253 
0254     const QString verb(QLatin1String("properties"));
0255     execInfo.lpVerb = (LPCWSTR)verb.utf16();
0256     execInfo.lpFile = (LPCWSTR)path_.utf16();
0257 
0258     return ShellExecuteExW(&execInfo);
0259 }
0260 #endif
0261 
0262 bool KPropertiesDialog::showDialog(const KFileItem &item, QWidget *parent, bool modal)
0263 {
0264     // TODO: do we really want to show the win32 property dialog?
0265     // This means we lose metainfo, support for .desktop files, etc. (DF)
0266 #ifdef Q_OS_WIN
0267     QString localPath = item.localPath();
0268     if (!localPath.isEmpty()) {
0269         return showWin32FilePropertyDialog(localPath);
0270     }
0271 #endif
0272     KPropertiesDialog *dlg = new KPropertiesDialog(item, parent);
0273     if (modal) {
0274         dlg->exec();
0275     } else {
0276         dlg->show();
0277     }
0278 
0279     return true;
0280 }
0281 
0282 bool KPropertiesDialog::showDialog(const QUrl &_url, QWidget *parent, bool modal)
0283 {
0284 #ifdef Q_OS_WIN
0285     if (_url.isLocalFile()) {
0286         return showWin32FilePropertyDialog(_url.toLocalFile());
0287     }
0288 #endif
0289     KPropertiesDialog *dlg = new KPropertiesDialog(_url, parent);
0290     if (modal) {
0291         dlg->exec();
0292     } else {
0293         dlg->show();
0294     }
0295 
0296     return true;
0297 }
0298 
0299 bool KPropertiesDialog::showDialog(const KFileItemList &_items, QWidget *parent, bool modal)
0300 {
0301     if (_items.count() == 1) {
0302         const KFileItem &item = _items.first();
0303         if (item.entry().count() == 0 && item.localPath().isEmpty()) // this remote item wasn't listed by a worker
0304                                                                      // Let's stat to get more info on the file
0305         {
0306             return KPropertiesDialog::showDialog(item.url(), parent, modal);
0307         } else {
0308             return KPropertiesDialog::showDialog(_items.first(), parent, modal);
0309         }
0310     }
0311     KPropertiesDialog *dlg = new KPropertiesDialog(_items, parent);
0312     if (modal) {
0313         dlg->exec();
0314     } else {
0315         dlg->show();
0316     }
0317     return true;
0318 }
0319 
0320 bool KPropertiesDialog::showDialog(const QList<QUrl> &urls, QWidget *parent, bool modal)
0321 {
0322     KPropertiesDialog *dlg = new KPropertiesDialog(urls, parent);
0323     if (modal) {
0324         dlg->exec();
0325     } else {
0326         dlg->show();
0327     }
0328     return true;
0329 }
0330 
0331 void KPropertiesDialogPrivate::init()
0332 {
0333     q->setFaceType(KPageDialog::Tabbed);
0334 
0335     insertPages();
0336 }
0337 
0338 void KPropertiesDialog::showFileSharingPage()
0339 {
0340     if (d->fileSharePageItem) {
0341         setCurrentPage(d->fileSharePageItem);
0342     }
0343 }
0344 
0345 void KPropertiesDialog::setFileSharingPage(QWidget *page)
0346 {
0347     d->fileSharePageItem = addPage(page, i18nc("@title:tab", "Share"));
0348 }
0349 
0350 void KPropertiesDialog::setFileNameReadOnly(bool ro)
0351 {
0352     if (d->m_filePropsPlugin) {
0353         d->m_filePropsPlugin->setFileNameReadOnly(ro);
0354     }
0355 
0356     if (d->m_urlPropsPlugin) {
0357         d->m_urlPropsPlugin->setFileNameReadOnly(ro);
0358     }
0359 }
0360 
0361 KPropertiesDialog::~KPropertiesDialog()
0362 {
0363 }
0364 
0365 QUrl KPropertiesDialog::url() const
0366 {
0367     return d->m_singleUrl;
0368 }
0369 
0370 KFileItem &KPropertiesDialog::item()
0371 {
0372     return d->m_items.first();
0373 }
0374 
0375 KFileItemList KPropertiesDialog::items() const
0376 {
0377     return d->m_items;
0378 }
0379 
0380 QUrl KPropertiesDialog::currentDir() const
0381 {
0382     return d->m_currentDir;
0383 }
0384 
0385 QString KPropertiesDialog::defaultName() const
0386 {
0387     return d->m_defaultName;
0388 }
0389 
0390 bool KPropertiesDialog::canDisplay(const KFileItemList &_items)
0391 {
0392     // TODO: cache the result of those calls. Currently we parse .desktop files far too many times
0393     /* clang-format off */
0394     return KFilePropsPlugin::supports(_items)
0395         || KFilePermissionsPropsPlugin::supports(_items)
0396         || KDesktopPropsPlugin::supports(_items)
0397         || KUrlPropsPlugin::supports(_items);
0398     /* clang-format on */
0399 }
0400 
0401 void KPropertiesDialog::accept()
0402 {
0403     d->m_aborted = false;
0404 
0405     auto acceptAndClose = [this]() {
0406         Q_EMIT applied();
0407         Q_EMIT propertiesClosed();
0408         deleteLater(); // Somewhat like Qt::WA_DeleteOnClose would do.
0409         KPageDialog::accept();
0410     };
0411 
0412     const bool isAnyDirty = std::any_of(d->m_pages.cbegin(), d->m_pages.cend(), [](const KPropertiesDialogPlugin *page) {
0413         return page->isDirty();
0414     });
0415 
0416     if (!isAnyDirty) { // No point going further
0417         acceptAndClose();
0418         return;
0419     }
0420 
0421     // If any page is dirty, then set the main one (KFilePropsPlugin) as
0422     // dirty too. This is what makes it possible to save changes to a global
0423     // desktop file into a local one. In other cases, it doesn't hurt.
0424     if (d->m_filePropsPlugin) {
0425         d->m_filePropsPlugin->setDirty(true);
0426     }
0427 
0428     // Changes are applied in the following order:
0429     // - KFilePropsPlugin changes, this is because in case of renaming an item or saving changes
0430     //   of a template or a .desktop file, the renaming or copying respectively, must be finished
0431     //   first, before applying the rest of the changes
0432     // - KFilePermissionsPropsPlugin changes, e.g. if the item was read-only and was changed to
0433     //   read/write, this must be applied first for other changes to work
0434     // - The rest of the changes from the other plugins/tabs
0435     // - KFilePropsPlugin::postApplyChanges()
0436 
0437     auto applyOtherChanges = [this, acceptAndClose]() {
0438         Q_ASSERT(!d->m_filePropsPlugin->isDirty());
0439         Q_ASSERT(!d->m_permissionsPropsPlugin->isDirty());
0440 
0441         // Apply the changes for the rest of the plugins
0442         for (auto *page : d->m_pages) {
0443             if (d->m_aborted) {
0444                 break;
0445             }
0446 
0447             if (page->isDirty()) {
0448                 // qDebug() << "applying changes for " << page->metaObject()->className();
0449                 page->applyChanges();
0450             }
0451             /* else {
0452                 qDebug() << "skipping page " << page->metaObject()->className();
0453             } */
0454         }
0455 
0456         if (!d->m_aborted && d->m_filePropsPlugin) {
0457             d->m_filePropsPlugin->postApplyChanges();
0458         }
0459 
0460         if (!d->m_aborted) {
0461             acceptAndClose();
0462         } // Else, keep dialog open for user to fix the problem.
0463     };
0464 
0465     auto applyPermissionsChanges = [this, applyOtherChanges]() {
0466         connect(d->m_permissionsPropsPlugin, &KFilePermissionsPropsPlugin::changesApplied, this, [applyOtherChanges]() {
0467             applyOtherChanges();
0468         });
0469 
0470         d->m_permissionsPropsPlugin->applyChanges();
0471     };
0472 
0473     if (d->m_filePropsPlugin && d->m_filePropsPlugin->isDirty()) {
0474         // changesApplied() is _not_ emitted if applying the changes was aborted
0475         connect(d->m_filePropsPlugin, &KFilePropsPlugin::changesApplied, this, [this, applyPermissionsChanges, applyOtherChanges]() {
0476             if (d->m_permissionsPropsPlugin && d->m_permissionsPropsPlugin->isDirty()) {
0477                 applyPermissionsChanges();
0478             } else {
0479                 applyOtherChanges();
0480             }
0481         });
0482 
0483         d->m_filePropsPlugin->applyChanges();
0484     }
0485 }
0486 
0487 void KPropertiesDialog::reject()
0488 {
0489     Q_EMIT canceled();
0490     Q_EMIT propertiesClosed();
0491 
0492     deleteLater();
0493     KPageDialog::reject();
0494 }
0495 
0496 void KPropertiesDialogPrivate::insertPages()
0497 {
0498     if (m_items.isEmpty()) {
0499         return;
0500     }
0501 
0502     if (KFilePropsPlugin::supports(m_items)) {
0503         m_filePropsPlugin = new KFilePropsPlugin(q);
0504         insertPlugin(m_filePropsPlugin);
0505     }
0506 
0507     if (KFilePermissionsPropsPlugin::supports(m_items)) {
0508         m_permissionsPropsPlugin = new KFilePermissionsPropsPlugin(q);
0509         insertPlugin(m_permissionsPropsPlugin);
0510     }
0511 
0512     if (KChecksumsPlugin::supports(m_items)) {
0513         KPropertiesDialogPlugin *p = new KChecksumsPlugin(q);
0514         insertPlugin(p);
0515     }
0516 
0517     if (KDesktopPropsPlugin::supports(m_items)) {
0518         m_desktopPropsPlugin = new KDesktopPropsPlugin(q);
0519         insertPlugin(m_desktopPropsPlugin);
0520     }
0521 
0522     if (KUrlPropsPlugin::supports(m_items)) {
0523         m_urlPropsPlugin = new KUrlPropsPlugin(q);
0524         insertPlugin(m_urlPropsPlugin);
0525     }
0526 
0527     if (m_items.count() != 1) {
0528         return;
0529     }
0530 
0531     const KFileItem item = m_items.first();
0532     const QString mimetype = item.mimetype();
0533 
0534     if (mimetype.isEmpty()) {
0535         return;
0536     }
0537 
0538     const auto scheme = item.url().scheme();
0539     const auto filter = [mimetype, scheme](const KPluginMetaData &metaData) {
0540         const auto supportedProtocols = metaData.value(QStringLiteral("X-KDE-Protocols"), QStringList());
0541         if (!supportedProtocols.isEmpty()) {
0542             const auto none = std::none_of(supportedProtocols.cbegin(), supportedProtocols.cend(), [scheme](const auto &protocol) {
0543                 return !protocol.isEmpty() && protocol == scheme;
0544             });
0545             if (none) {
0546                 return false;
0547             }
0548         }
0549 
0550         return metaData.mimeTypes().isEmpty() || metaData.supportsMimeType(mimetype);
0551     };
0552     const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/propertiesdialog"), filter);
0553     for (const auto &jsonMetadata : jsonPlugins) {
0554         if (auto plugin = KPluginFactory::instantiatePlugin<KPropertiesDialogPlugin>(jsonMetadata, q).plugin) {
0555             insertPlugin(plugin);
0556         }
0557     }
0558 }
0559 
0560 void KPropertiesDialog::updateUrl(const QUrl &_newUrl)
0561 {
0562     Q_ASSERT(d->m_items.count() == 1);
0563     // qDebug() << "KPropertiesDialog::updateUrl (pre)" << _newUrl;
0564     QUrl newUrl = _newUrl;
0565     Q_EMIT saveAs(d->m_singleUrl, newUrl);
0566     // qDebug() << "KPropertiesDialog::updateUrl (post)" << newUrl;
0567 
0568     d->m_singleUrl = newUrl;
0569     d->m_items.first().setUrl(newUrl);
0570     Q_ASSERT(!d->m_singleUrl.isEmpty());
0571     // If we have an Desktop page, set it dirty, so that a full file is saved locally
0572     // Same for a URL page (because of the Name= hack)
0573     if (d->m_urlPropsPlugin) {
0574         d->m_urlPropsPlugin->setDirty();
0575     } else if (d->m_desktopPropsPlugin) {
0576         d->m_desktopPropsPlugin->setDirty();
0577     }
0578 }
0579 
0580 void KPropertiesDialog::rename(const QString &_name)
0581 {
0582     Q_ASSERT(d->m_items.count() == 1);
0583     // qDebug() << "KPropertiesDialog::rename " << _name;
0584     QUrl newUrl;
0585     // if we're creating from a template : use currentdir
0586     if (!d->m_currentDir.isEmpty()) {
0587         newUrl = d->m_currentDir;
0588         newUrl.setPath(Utils::concatPaths(newUrl.path(), _name));
0589     } else {
0590         // It's a directory, so strip the trailing slash first
0591         newUrl = d->m_singleUrl.adjusted(QUrl::StripTrailingSlash);
0592         // Now change the filename
0593         newUrl = newUrl.adjusted(QUrl::RemoveFilename); // keep trailing slash
0594         newUrl.setPath(Utils::concatPaths(newUrl.path(), _name));
0595     }
0596     updateUrl(newUrl);
0597 }
0598 
0599 void KPropertiesDialog::abortApplying()
0600 {
0601     d->m_aborted = true;
0602 }
0603 
0604 #include "moc_kpropertiesdialog.cpp"