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"