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

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