1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
4 SPDX-FileCopyrightText: 1999, 2000 Preston Brown <pbrown@kde.org>
5 SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org>
6 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
7 SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
8 SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com>
9 SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
10
11 SPDX-License-Identifier: LGPL-2.0-or-later
12*/
13
14/*
15 * kpropertiesdialog.cpp
16 * View/Edit Properties of files, locally or remotely
17 *
18 * some FilePermissionsPropsPlugin-changes by
19 * Henner Zeller <zeller@think.de>
20 * some layout management by
21 * Bertrand Leconte <B.Leconte@mail.dotcom.fr>
22 * the rest of the layout management, bug fixes, adaptation to libkio,
23 * template feature by
24 * David Faure <faure@kde.org>
25 * More layout, cleanups, and fixes by
26 * Preston Brown <pbrown@kde.org>
27 * Plugin capability, cleanups and port to KDialog by
28 * Simon Hausmann <hausmann@kde.org>
29 * KDesktopPropsPlugin by
30 * Waldo Bastian <bastian@kde.org>
31 */
32
33#include "kpropertiesdialog.h"
34#include "../utils_p.h"
35#include "kio_widgets_debug.h"
36#include "kpropertiesdialogbuiltin_p.h"
37
38#include <config-kiowidgets.h>
39
40#include <kacl.h>
41#include <kio/global.h>
42#include <kio/statjob.h>
43#include <kioglobal_p.h>
44
45#include <KJobWidgets>
46#include <KLocalizedString>
47#include <KPluginFactory>
48#include <KPluginMetaData>
49
50#include <qplatformdefs.h>
51
52#include <QDebug>
53#include <QDir>
54#include <QList>
55#include <QMimeData>
56#include <QMimeDatabase>
57#include <QRegularExpression>
58#include <QStandardPaths>
59#include <QUrl>
60
61#include <algorithm>
62#include <functional>
63#include <vector>
64
65#ifdef Q_OS_WIN
66#include <process.h>
67#include <qt_windows.h>
68#include <shellapi.h>
69#ifdef __GNUC__
70#warning TODO: port completely to win32
71#endif
72#endif
73
74using namespace KDEPrivate;
75
76constexpr mode_t KFilePermissionsPropsPlugin::fperm[3][4] = {
77 {S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID},
78 {S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID},
79 {S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX},
80};
81
82class KPropertiesDialogPrivate
83{
84public:
85 explicit KPropertiesDialogPrivate(KPropertiesDialog *qq)
86 : q(qq)
87 {
88 }
89 ~KPropertiesDialogPrivate()
90 {
91 // qDeleteAll deletes the pages in order, this prevents crashes when closing the dialog
92 qDeleteAll(c: m_pages);
93 }
94
95 /**
96 * Common initialization for all constructors
97 */
98 void init();
99 /**
100 * Inserts all pages in the dialog.
101 */
102 void insertPages();
103
104 void insertPlugin(KPropertiesDialogPlugin *plugin)
105 {
106 q->connect(sender: plugin, signal: &KPropertiesDialogPlugin::changed, context: plugin, slot: [plugin]() {
107 plugin->setDirty();
108 });
109 m_pages.push_back(x: plugin);
110 }
111
112 KPropertiesDialog *const q;
113 bool m_aborted = false;
114 KPageWidgetItem *fileSharePageItem = nullptr;
115 KFilePropsPlugin *m_filePropsPlugin = nullptr;
116 KFilePermissionsPropsPlugin *m_permissionsPropsPlugin = nullptr;
117 KDesktopPropsPlugin *m_desktopPropsPlugin = nullptr;
118 KUrlPropsPlugin *m_urlPropsPlugin = nullptr;
119
120 /**
121 * The URL of the props dialog (when shown for only one file)
122 */
123 QUrl m_singleUrl;
124 /**
125 * List of items this props dialog is shown for
126 */
127 KFileItemList m_items;
128 /**
129 * For templates
130 */
131 QString m_defaultName;
132 QUrl m_currentDir;
133
134 /**
135 * List of all plugins inserted ( first one first )
136 */
137 std::vector<KPropertiesDialogPlugin *> m_pages;
138};
139
140KPropertiesDialog::KPropertiesDialog(const KFileItem &item, QWidget *parent)
141 : KPageDialog(parent)
142 , d(new KPropertiesDialogPrivate(this))
143{
144 setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(item.name())));
145
146 Q_ASSERT(!item.isNull());
147 d->m_items.append(t: item);
148
149 d->m_singleUrl = item.url();
150 Q_ASSERT(!d->m_singleUrl.isEmpty());
151
152 d->init();
153}
154
155KPropertiesDialog::KPropertiesDialog(const QString &title, QWidget *parent)
156 : KPageDialog(parent)
157 , d(new KPropertiesDialogPrivate(this))
158{
159 setWindowTitle(i18n("Properties for %1", title));
160
161 d->init();
162}
163
164KPropertiesDialog::KPropertiesDialog(const KFileItemList &_items, QWidget *parent)
165 : KPageDialog(parent)
166 , d(new KPropertiesDialogPrivate(this))
167{
168 if (_items.count() > 1) {
169 setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", _items.count()));
170 } else {
171 setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_items.first().name())));
172 }
173
174 Q_ASSERT(!_items.isEmpty());
175 d->m_singleUrl = _items.first().url();
176 Q_ASSERT(!d->m_singleUrl.isEmpty());
177
178 d->m_items = _items;
179
180 d->init();
181}
182
183KPropertiesDialog::KPropertiesDialog(const QUrl &_url, QWidget *parent)
184 : KPageDialog(parent)
185 , d(new KPropertiesDialogPrivate(this))
186{
187 d->m_singleUrl = _url.adjusted(options: QUrl::StripTrailingSlash);
188
189 setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(d->m_singleUrl.fileName())));
190
191 KIO::StatJob *job = KIO::stat(url: d->m_singleUrl);
192 KJobWidgets::setWindow(job, widget: parent);
193 job->exec();
194 KIO::UDSEntry entry = job->statResult();
195
196 d->m_items.append(t: KFileItem(entry, d->m_singleUrl));
197 d->init();
198}
199
200KPropertiesDialog::KPropertiesDialog(const QList<QUrl> &urls, QWidget *parent)
201 : KPageDialog(parent)
202 , d(new KPropertiesDialogPrivate(this))
203{
204 if (urls.count() > 1) {
205 setWindowTitle(i18np("Properties for 1 item", "Properties for %1 Selected Items", urls.count()));
206 } else {
207 setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(urls.first().fileName())));
208 }
209
210 Q_ASSERT(!urls.isEmpty());
211 d->m_singleUrl = urls.first();
212 Q_ASSERT(!d->m_singleUrl.isEmpty());
213
214 d->m_items.reserve(asize: urls.size());
215 for (const QUrl &url : urls) {
216 KIO::StatJob *job = KIO::stat(url);
217 KJobWidgets::setWindow(job, widget: parent);
218 job->exec();
219 KIO::UDSEntry entry = job->statResult();
220
221 d->m_items.append(t: KFileItem(entry, url));
222 }
223
224 d->init();
225}
226
227KPropertiesDialog::KPropertiesDialog(const QUrl &_tempUrl, const QUrl &_currentDir, const QString &_defaultName, QWidget *parent)
228 : KPageDialog(parent)
229 , d(new KPropertiesDialogPrivate(this))
230{
231 setWindowTitle(i18n("Properties for %1", KIO::decodeFileName(_tempUrl.fileName())));
232
233 d->m_singleUrl = _tempUrl;
234 d->m_defaultName = _defaultName;
235 d->m_currentDir = _currentDir;
236 Q_ASSERT(!d->m_singleUrl.isEmpty());
237
238 // Create the KFileItem for the _template_ file, in order to read from it.
239 d->m_items.append(t: KFileItem(d->m_singleUrl));
240 d->init();
241}
242
243#ifdef Q_OS_WIN
244bool showWin32FilePropertyDialog(const QString &fileName)
245{
246 QString path_ = QDir::toNativeSeparators(QFileInfo(fileName).absoluteFilePath());
247
248 SHELLEXECUTEINFOW execInfo;
249
250 memset(&execInfo, 0, sizeof(execInfo));
251 execInfo.cbSize = sizeof(execInfo);
252 execInfo.fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
253
254 const QString verb(QLatin1String("properties"));
255 execInfo.lpVerb = (LPCWSTR)verb.utf16();
256 execInfo.lpFile = (LPCWSTR)path_.utf16();
257
258 return ShellExecuteExW(&execInfo);
259}
260#endif
261
262bool KPropertiesDialog::showDialog(const KFileItem &item, QWidget *parent, bool modal)
263{
264 // TODO: do we really want to show the win32 property dialog?
265 // This means we lose metainfo, support for .desktop files, etc. (DF)
266#ifdef Q_OS_WIN
267 QString localPath = item.localPath();
268 if (!localPath.isEmpty()) {
269 return showWin32FilePropertyDialog(localPath);
270 }
271#endif
272 KPropertiesDialog *dlg = new KPropertiesDialog(item, parent);
273 if (modal) {
274 dlg->exec();
275 } else {
276 dlg->show();
277 }
278
279 return true;
280}
281
282bool KPropertiesDialog::showDialog(const QUrl &_url, QWidget *parent, bool modal)
283{
284#ifdef Q_OS_WIN
285 if (_url.isLocalFile()) {
286 return showWin32FilePropertyDialog(_url.toLocalFile());
287 }
288#endif
289 KPropertiesDialog *dlg = new KPropertiesDialog(_url, parent);
290 if (modal) {
291 dlg->exec();
292 } else {
293 dlg->show();
294 }
295
296 return true;
297}
298
299bool KPropertiesDialog::showDialog(const KFileItemList &_items, QWidget *parent, bool modal)
300{
301 if (_items.count() == 1) {
302 const KFileItem &item = _items.first();
303 if (item.entry().count() == 0 && item.localPath().isEmpty()) // this remote item wasn't listed by a worker
304 // Let's stat to get more info on the file
305 {
306 return KPropertiesDialog::showDialog(url: item.url(), parent, modal);
307 } else {
308 return KPropertiesDialog::showDialog(item: _items.first(), parent, modal);
309 }
310 }
311 KPropertiesDialog *dlg = new KPropertiesDialog(_items, parent);
312 if (modal) {
313 dlg->exec();
314 } else {
315 dlg->show();
316 }
317 return true;
318}
319
320bool KPropertiesDialog::showDialog(const QList<QUrl> &urls, QWidget *parent, bool modal)
321{
322 KPropertiesDialog *dlg = new KPropertiesDialog(urls, parent);
323 if (modal) {
324 dlg->exec();
325 } else {
326 dlg->show();
327 }
328 return true;
329}
330
331void KPropertiesDialogPrivate::init()
332{
333 q->setFaceType(KPageDialog::Tabbed);
334
335 insertPages();
336}
337
338void KPropertiesDialog::showFileSharingPage()
339{
340 if (d->fileSharePageItem) {
341 setCurrentPage(d->fileSharePageItem);
342 }
343}
344
345void KPropertiesDialog::setFileSharingPage(QWidget *page)
346{
347 d->fileSharePageItem = addPage(widget: page, i18nc("@title:tab", "Share"));
348}
349
350void KPropertiesDialog::setFileNameReadOnly(bool ro)
351{
352 if (d->m_filePropsPlugin) {
353 d->m_filePropsPlugin->setFileNameReadOnly(ro);
354 }
355
356 if (d->m_urlPropsPlugin) {
357 d->m_urlPropsPlugin->setFileNameReadOnly(ro);
358 }
359}
360
361KPropertiesDialog::~KPropertiesDialog()
362{
363}
364
365QUrl KPropertiesDialog::url() const
366{
367 return d->m_singleUrl;
368}
369
370KFileItem &KPropertiesDialog::item()
371{
372 return d->m_items.first();
373}
374
375KFileItemList KPropertiesDialog::items() const
376{
377 return d->m_items;
378}
379
380QUrl KPropertiesDialog::currentDir() const
381{
382 return d->m_currentDir;
383}
384
385QString KPropertiesDialog::defaultName() const
386{
387 return d->m_defaultName;
388}
389
390bool KPropertiesDialog::canDisplay(const KFileItemList &_items)
391{
392 // TODO: cache the result of those calls. Currently we parse .desktop files far too many times
393 /* clang-format off */
394 return KFilePropsPlugin::supports(_items)
395 || KFilePermissionsPropsPlugin::supports(_items)
396 || KDesktopPropsPlugin::supports(_items)
397 || KUrlPropsPlugin::supports(_items);
398 /* clang-format on */
399}
400
401void KPropertiesDialog::accept()
402{
403 d->m_aborted = false;
404
405 auto acceptAndClose = [this]() {
406 Q_EMIT applied();
407 Q_EMIT propertiesClosed();
408 deleteLater(); // Somewhat like Qt::WA_DeleteOnClose would do.
409 KPageDialog::accept();
410 };
411
412 const bool isAnyDirty = std::any_of(first: d->m_pages.cbegin(), last: d->m_pages.cend(), pred: [](const KPropertiesDialogPlugin *page) {
413 return page->isDirty();
414 });
415
416 if (!isAnyDirty) { // No point going further
417 acceptAndClose();
418 return;
419 }
420
421 // If any page is dirty, then set the main one (KFilePropsPlugin) as
422 // dirty too. This is what makes it possible to save changes to a global
423 // desktop file into a local one. In other cases, it doesn't hurt.
424 if (d->m_filePropsPlugin) {
425 d->m_filePropsPlugin->setDirty(true);
426 }
427
428 // Changes are applied in the following order:
429 // - KFilePropsPlugin changes, this is because in case of renaming an item or saving changes
430 // of a template or a .desktop file, the renaming or copying respectively, must be finished
431 // first, before applying the rest of the changes
432 // - KFilePermissionsPropsPlugin changes, e.g. if the item was read-only and was changed to
433 // read/write, this must be applied first for other changes to work
434 // - The rest of the changes from the other plugins/tabs
435 // - KFilePropsPlugin::postApplyChanges()
436
437 auto applyOtherChanges = [this, acceptAndClose]() {
438 Q_ASSERT(!d->m_filePropsPlugin->isDirty());
439 Q_ASSERT(!d->m_permissionsPropsPlugin->isDirty());
440
441 // Apply the changes for the rest of the plugins
442 for (auto *page : d->m_pages) {
443 if (d->m_aborted) {
444 break;
445 }
446
447 if (page->isDirty()) {
448 // qDebug() << "applying changes for " << page->metaObject()->className();
449 page->applyChanges();
450 }
451 /* else {
452 qDebug() << "skipping page " << page->metaObject()->className();
453 } */
454 }
455
456 if (!d->m_aborted && d->m_filePropsPlugin) {
457 d->m_filePropsPlugin->postApplyChanges();
458 }
459
460 if (!d->m_aborted) {
461 acceptAndClose();
462 } // Else, keep dialog open for user to fix the problem.
463 };
464
465 auto applyPermissionsChanges = [this, applyOtherChanges]() {
466 connect(sender: d->m_permissionsPropsPlugin, signal: &KFilePermissionsPropsPlugin::changesApplied, context: this, slot: [applyOtherChanges]() {
467 applyOtherChanges();
468 });
469
470 d->m_permissionsPropsPlugin->applyChanges();
471 };
472
473 if (d->m_filePropsPlugin && d->m_filePropsPlugin->isDirty()) {
474 // changesApplied() is _not_ emitted if applying the changes was aborted
475 connect(sender: d->m_filePropsPlugin, signal: &KFilePropsPlugin::changesApplied, context: this, slot: [this, applyPermissionsChanges, applyOtherChanges]() {
476 if (d->m_permissionsPropsPlugin && d->m_permissionsPropsPlugin->isDirty()) {
477 applyPermissionsChanges();
478 } else {
479 applyOtherChanges();
480 }
481 });
482
483 d->m_filePropsPlugin->applyChanges();
484 }
485}
486
487void KPropertiesDialog::reject()
488{
489 Q_EMIT canceled();
490 Q_EMIT propertiesClosed();
491
492 deleteLater();
493 KPageDialog::reject();
494}
495
496void KPropertiesDialogPrivate::insertPages()
497{
498 if (m_items.isEmpty()) {
499 return;
500 }
501
502 if (KFilePropsPlugin::supports(items: m_items)) {
503 m_filePropsPlugin = new KFilePropsPlugin(q);
504 insertPlugin(plugin: m_filePropsPlugin);
505 }
506
507 if (KFilePermissionsPropsPlugin::supports(items: m_items)) {
508 m_permissionsPropsPlugin = new KFilePermissionsPropsPlugin(q);
509 insertPlugin(plugin: m_permissionsPropsPlugin);
510 }
511
512 if (KChecksumsPlugin::supports(items: m_items)) {
513 KPropertiesDialogPlugin *p = new KChecksumsPlugin(q);
514 insertPlugin(plugin: p);
515 }
516
517 if (KDesktopPropsPlugin::supports(items: m_items)) {
518 m_desktopPropsPlugin = new KDesktopPropsPlugin(q);
519 insertPlugin(plugin: m_desktopPropsPlugin);
520 }
521
522 if (KUrlPropsPlugin::supports(items: m_items)) {
523 m_urlPropsPlugin = new KUrlPropsPlugin(q);
524 insertPlugin(plugin: m_urlPropsPlugin);
525 }
526
527 if (m_items.count() != 1) {
528 return;
529 }
530
531 const KFileItem item = m_items.first();
532 const QString mimetype = item.mimetype();
533
534 if (mimetype.isEmpty()) {
535 return;
536 }
537
538 const auto scheme = item.url().scheme();
539 const auto filter = [mimetype, scheme](const KPluginMetaData &metaData) {
540 const auto supportedProtocols = metaData.value(QStringLiteral("X-KDE-Protocols"), defaultValue: QStringList());
541 if (!supportedProtocols.isEmpty()) {
542 const auto none = std::none_of(first: supportedProtocols.cbegin(), last: supportedProtocols.cend(), pred: [scheme](const auto &protocol) {
543 return !protocol.isEmpty() && protocol == scheme;
544 });
545 if (none) {
546 return false;
547 }
548 }
549
550 return metaData.mimeTypes().isEmpty() || metaData.supportsMimeType(mimeType: mimetype);
551 };
552 const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/propertiesdialog"), filter);
553 for (const auto &jsonMetadata : jsonPlugins) {
554 if (auto plugin = KPluginFactory::instantiatePlugin<KPropertiesDialogPlugin>(data: jsonMetadata, parent: q).plugin) {
555 insertPlugin(plugin);
556 }
557 }
558}
559
560void KPropertiesDialog::updateUrl(const QUrl &_newUrl)
561{
562 Q_ASSERT(d->m_items.count() == 1);
563 // qDebug() << "KPropertiesDialog::updateUrl (pre)" << _newUrl;
564 QUrl newUrl = _newUrl;
565 Q_EMIT saveAs(oldUrl: d->m_singleUrl, newUrl);
566 // qDebug() << "KPropertiesDialog::updateUrl (post)" << newUrl;
567
568 d->m_singleUrl = newUrl;
569 d->m_items.first().setUrl(newUrl);
570 Q_ASSERT(!d->m_singleUrl.isEmpty());
571 // If we have an Desktop page, set it dirty, so that a full file is saved locally
572 // Same for a URL page (because of the Name= hack)
573 if (d->m_urlPropsPlugin) {
574 d->m_urlPropsPlugin->setDirty();
575 } else if (d->m_desktopPropsPlugin) {
576 d->m_desktopPropsPlugin->setDirty();
577 }
578}
579
580void KPropertiesDialog::rename(const QString &_name)
581{
582 Q_ASSERT(d->m_items.count() == 1);
583 // qDebug() << "KPropertiesDialog::rename " << _name;
584 QUrl newUrl;
585 // if we're creating from a template : use currentdir
586 if (!d->m_currentDir.isEmpty()) {
587 newUrl = d->m_currentDir;
588 newUrl.setPath(path: Utils::concatPaths(path1: newUrl.path(), path2: _name));
589 } else {
590 // It's a directory, so strip the trailing slash first
591 newUrl = d->m_singleUrl.adjusted(options: QUrl::StripTrailingSlash);
592 // Now change the filename
593 newUrl = newUrl.adjusted(options: QUrl::RemoveFilename); // keep trailing slash
594 newUrl.setPath(path: Utils::concatPaths(path1: newUrl.path(), path2: _name));
595 }
596 updateUrl(newUrl: newUrl);
597}
598
599void KPropertiesDialog::abortApplying()
600{
601 d->m_aborted = true;
602}
603
604#include "moc_kpropertiesdialog.cpp"
605

source code of kio/src/widgets/kpropertiesdialog.cpp