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
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10#include "kpropertiesdialogbuiltin_p.h"
11
12#include "../utils_p.h"
13#include "config-kiowidgets.h"
14#include "kio_widgets_debug.h"
15
16#include <gpudetection_p.h>
17#include <kbuildsycocaprogressdialog.h>
18#include <kdirnotify.h>
19#include <kfileitemlistproperties.h>
20#include <kio/chmodjob.h>
21#include <kio/copyjob.h>
22#include <kio/desktopexecparser.h>
23#include <kio/directorysizejob.h>
24#include <kio/jobuidelegate.h>
25#include <kio/renamedialog.h>
26#include <kio/statjob.h>
27#include <kmountpoint.h>
28#include <kprotocolinfo.h>
29#include <kprotocolmanager.h>
30#include <kurlrequester.h>
31
32#include <KApplicationTrader>
33#include <KAuthorized>
34#include <KCapacityBar>
35#include <KColorScheme>
36#include <KCompletion>
37#include <KConfigGroup>
38#include <KDesktopFile>
39#include <KDialogJobUiDelegate>
40#include <KIO/ApplicationLauncherJob>
41#include <KIO/FileSystemFreeSpaceJob>
42#include <KIO/OpenFileManagerWindowJob>
43#include <KIconButton>
44#include <KJobUiDelegate>
45#include <KJobWidgets>
46#include <KLazyLocalizedString>
47#include <KLineEdit>
48#include <KMessageBox>
49#include <KMessageWidget>
50#include <KMimeTypeChooser>
51#include <KMimeTypeEditor>
52#include <KSeparator>
53#include <KService>
54#include <KSharedConfig>
55#include <KShell>
56#include <KSqueezedTextLabel>
57#include <KSycoca>
58
59#include <QCheckBox>
60#include <QClipboard>
61#include <QComboBox>
62#include <QDBusConnection>
63#include <QDBusInterface>
64#include <QDBusReply>
65#include <QDialogButtonBox>
66#include <QFile>
67#include <QFileDialog>
68#include <QFileInfo>
69#include <QFileSystemWatcher>
70#include <QFutureWatcher>
71#include <QLabel>
72#include <QLayout>
73#include <QLocale>
74#include <QMimeDatabase>
75#include <QProgressBar>
76#include <QPushButton>
77#include <QStyle>
78#include <QtConcurrentRun>
79
80#include "ui_checksumswidget.h"
81#include "ui_kfilepropspluginwidget.h"
82#include "ui_kpropertiesdesktopadvbase.h"
83#include "ui_kpropertiesdesktopbase.h"
84
85#if HAVE_POSIX_ACL
86#include "kacleditwidget.h"
87#endif
88#include <cerrno>
89extern "C" {
90#if HAVE_SYS_XATTR_H
91#include <sys/xattr.h>
92#endif
93#if HAVE_SYS_EXTATTR_H
94#include <sys/extattr.h>
95#endif
96#if HAVE_SYS_MOUNT_H
97#include <sys/mount.h>
98#endif
99}
100
101using namespace KDEPrivate;
102
103static QString couldNotSaveMsg(const QString &path)
104{
105 return xi18nc("@info", "Could not save properties due to insufficient write access to:<nl/><filename>%1</filename>.", path);
106}
107static QString nameFromFileName(QString nameStr)
108{
109 if (nameStr.endsWith(s: QLatin1String(".desktop"))) {
110 nameStr.chop(n: 8);
111 }
112 // Make it human-readable (%2F => '/', ...)
113 nameStr = KIO::decodeFileName(str: nameStr);
114 return nameStr;
115}
116
117class KFilePropsPlugin::KFilePropsPluginPrivate
118{
119public:
120 KFilePropsPluginPrivate()
121 : m_ui(new Ui_KFilePropsPluginWidget())
122 {
123 m_ui->setupUi(&m_mainWidget);
124 }
125
126 ~KFilePropsPluginPrivate()
127 {
128 if (dirSizeJob) {
129 dirSizeJob->kill();
130 }
131 }
132
133 void hideMountPointLabels()
134 {
135 m_ui->fsLabel_Left->hide();
136 m_ui->fsLabel->hide();
137
138 m_ui->mountPointLabel_Left->hide();
139 m_ui->mountPointLabel->hide();
140
141 m_ui->mountSrcLabel_Left->hide();
142 m_ui->mountSrcLabel->hide();
143 }
144
145 QWidget m_mainWidget;
146 std::unique_ptr<Ui_KFilePropsPluginWidget> m_ui;
147 KIO::DirectorySizeJob *dirSizeJob = nullptr;
148 QTimer *dirSizeUpdateTimer = nullptr;
149 bool bMultiple;
150 bool bIconChanged;
151 bool bKDesktopMode;
152 bool bDesktopFile;
153 QString mimeType;
154 QString oldFileName;
155
156 QString m_sRelativePath;
157 bool m_bFromTemplate;
158
159 /**
160 * The initial filename
161 */
162 QString oldName;
163};
164
165KFilePropsPlugin::KFilePropsPlugin(KPropertiesDialog *_props)
166 : KPropertiesDialogPlugin(_props)
167 , d(new KFilePropsPluginPrivate)
168{
169 const auto itemsList = properties->items();
170 d->bMultiple = (itemsList.count() > 1);
171 d->bIconChanged = false;
172 d->bDesktopFile = KDesktopPropsPlugin::supports(items: itemsList);
173 // qDebug() << "KFilePropsPlugin::KFilePropsPlugin bMultiple=" << d->bMultiple;
174
175 // We set this data from the first item, and we'll
176 // check that the other items match against it, resetting when not.
177 const KFileItem firstItem = properties->item();
178 auto [url, isLocal] = firstItem.isMostLocalUrl();
179 bool isReallyLocal = firstItem.url().isLocalFile();
180 bool bDesktopFile = firstItem.isDesktopFile();
181 mode_t mode = firstItem.mode();
182 bool hasDirs = firstItem.isDir() && !firstItem.isLink();
183 bool hasRoot = url.path() == QLatin1String("/");
184 QString iconStr = firstItem.iconName();
185 QString directory = properties->url().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
186 QString protocol = properties->url().scheme();
187 d->bKDesktopMode = protocol == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
188 QString mimeComment = firstItem.mimeComment();
189 d->mimeType = firstItem.mimetype();
190 KIO::filesize_t totalSize = firstItem.size();
191 QString magicMimeName;
192 QString magicMimeComment;
193 QMimeDatabase db;
194 if (isLocal) {
195 QMimeType magicMimeType = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchContent);
196 if (magicMimeType.isValid() && !magicMimeType.isDefault()) {
197 magicMimeName = magicMimeType.name();
198 magicMimeComment = magicMimeType.comment();
199 }
200 }
201#ifdef Q_OS_WIN
202 if (isReallyLocal) {
203 directory = QDir::toNativeSeparators(directory.mid(1));
204 }
205#endif
206
207 // Those things only apply to 'single file' mode
208 QString filename;
209 bool isTrash = false;
210 d->m_bFromTemplate = false;
211
212 // And those only to 'multiple' mode
213 uint iDirCount = hasDirs ? 1 : 0;
214 uint iFileCount = 1 - iDirCount;
215
216 properties->addPage(widget: &d->m_mainWidget, i18nc("@title:tab File properties", "&General"));
217
218 d->m_ui->symlinkTargetMessageWidget->hide();
219
220 if (!d->bMultiple) {
221 QString path;
222 if (!d->m_bFromTemplate) {
223 isTrash = (properties->url().scheme() == QLatin1String("trash"));
224 // Extract the full name, but without file: for local files
225 path = properties->url().toDisplayString(options: QUrl::PreferLocalFile);
226 } else {
227 path = Utils::concatPaths(path1: properties->currentDir().path(), path2: properties->defaultName());
228 directory = properties->currentDir().toDisplayString(options: QUrl::PreferLocalFile);
229 }
230
231 if (d->bDesktopFile) {
232 determineRelativePath(path);
233 }
234
235 // Extract the file name only
236 filename = properties->defaultName();
237 if (filename.isEmpty()) { // no template
238 const QFileInfo finfo(firstItem.name()); // this gives support for UDS_NAME, e.g. for kio_trash or kio_system
239 filename = finfo.fileName(); // Make sure only the file's name is displayed (#160964).
240 } else {
241 d->m_bFromTemplate = true;
242 setDirty(); // to enforce that the copy happens
243 }
244 d->oldFileName = filename;
245
246 // Make it human-readable
247 filename = nameFromFileName(nameStr: filename);
248 d->oldName = filename;
249 } else {
250 // Multiple items: see what they have in common
251 for (const auto &item : itemsList) {
252 if (item == firstItem) {
253 continue;
254 }
255
256 const QUrl url = item.url();
257 // qDebug() << "KFilePropsPlugin::KFilePropsPlugin " << url.toDisplayString();
258 // The list of things we check here should match the variables defined
259 // at the beginning of this method.
260 if (url.isLocalFile() != isLocal) {
261 isLocal = false; // not all local
262 }
263 if (bDesktopFile && item.isDesktopFile() != bDesktopFile) {
264 bDesktopFile = false; // not all desktop files
265 }
266 if (item.mode() != mode) {
267 mode = static_cast<mode_t>(0);
268 }
269 if (KIO::iconNameForUrl(url) != iconStr) {
270 iconStr = QStringLiteral("document-multiple");
271 }
272 if (url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() != directory) {
273 directory.clear();
274 }
275 if (url.scheme() != protocol) {
276 protocol.clear();
277 }
278 if (!mimeComment.isNull() && item.mimeComment() != mimeComment) {
279 mimeComment.clear();
280 }
281 if (isLocal && !magicMimeComment.isNull()) {
282 QMimeType magicMimeType = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchContent);
283 if (magicMimeType.isValid() && magicMimeType.comment() != magicMimeComment) {
284 magicMimeName.clear();
285 magicMimeComment.clear();
286 }
287 }
288
289 if (isLocal && url.path() == QLatin1String("/")) {
290 hasRoot = true;
291 }
292 if (item.isDir() && !item.isLink()) {
293 iDirCount++;
294 hasDirs = true;
295 } else {
296 iFileCount++;
297 totalSize += item.size();
298 }
299 }
300 }
301
302 if (!isReallyLocal && !protocol.isEmpty()) {
303 directory += QLatin1String(" (") + protocol + QLatin1Char(')');
304 }
305
306 if (!isTrash //
307 && (bDesktopFile || Utils::isDirMask(mode)) //
308 && !d->bMultiple // not implemented for multiple
309 && enableIconButton()) {
310 d->m_ui->iconLabel->hide();
311
312 const int bsize = 66 + (2 * d->m_ui->iconButton->style()->pixelMetric(metric: QStyle::PM_ButtonMargin));
313 d->m_ui->iconButton->setFixedSize(w: bsize, h: bsize);
314 d->m_ui->iconButton->setIconSize(48);
315 if (bDesktopFile && isLocal) {
316 const KDesktopFile config(url.toLocalFile());
317 if (config.hasDeviceType()) {
318 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Device);
319 } else {
320 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Application);
321 }
322 } else {
323 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Place);
324 }
325
326 d->m_ui->iconButton->setIcon(iconStr);
327 connect(sender: d->m_ui->iconButton, signal: &KIconButton::iconChanged, context: this, slot: &KFilePropsPlugin::slotIconChanged);
328 } else {
329 d->m_ui->iconButton->hide();
330
331 const int bsize = 66 + (2 * d->m_ui->iconLabel->style()->pixelMetric(metric: QStyle::PM_ButtonMargin));
332 d->m_ui->iconLabel->setFixedSize(w: bsize, h: bsize);
333 d->m_ui->iconLabel->setPixmap(QIcon::fromTheme(name: iconStr).pixmap(extent: 48));
334 }
335
336 KFileItemListProperties itemList(KFileItemList{firstItem});
337 if (d->bMultiple || isTrash || hasRoot || !(d->m_bFromTemplate || itemList.supportsMoving())) {
338 d->m_ui->fileNameLineEdit->hide();
339 setFileNameReadOnly(true);
340 if (d->bMultiple) {
341 d->m_ui->fileNameLabel->setText(KIO::itemsSummaryString(items: iFileCount + iDirCount, files: iFileCount, dirs: iDirCount, size: 0, showSize: false));
342 }
343 } else {
344 d->m_ui->fileNameLabel->hide();
345
346 d->m_ui->fileNameLineEdit->setText(filename);
347 connect(sender: d->m_ui->fileNameLineEdit, signal: &QLineEdit::textChanged, context: this, slot: &KFilePropsPlugin::nameFileChanged);
348 }
349
350 // Mimetype widgets
351 if (!mimeComment.isEmpty() && !isTrash) {
352 d->m_ui->mimeCommentLabel->setText(mimeComment);
353 d->m_ui->mimeCommentLabel->setToolTip(d->mimeType);
354
355 const int hSpacing = properties->style()->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing);
356 d->m_ui->defaultHandlerLayout->setSpacing(hSpacing);
357
358#ifndef Q_OS_WIN
359 updateDefaultHandler(mimeType: d->mimeType);
360 connect(sender: KSycoca::self(), signal: &KSycoca::databaseChanged, context: this, slot: [this] {
361 updateDefaultHandler(mimeType: d->mimeType);
362 });
363
364 connect(sender: d->m_ui->configureMimeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotEditFileType);
365#endif
366
367 } else {
368 d->m_ui->typeLabel->hide();
369 d->m_ui->mimeCommentLabel->hide();
370 d->m_ui->configureMimeBtn->hide();
371
372 d->m_ui->defaultHandlerLabel_Left->hide();
373 d->m_ui->defaultHandlerIcon->hide();
374 d->m_ui->defaultHandlerLabel->hide();
375 }
376
377#ifdef Q_OS_WIN
378 d->m_ui->defaultHandlerLabel_Left->hide();
379 d->m_ui->defaultHandlerIcon->hide();
380 d->m_ui->defaultHandlerLabel->hide();
381#endif
382
383 if (!magicMimeComment.isEmpty() && magicMimeComment != mimeComment) {
384 d->m_ui->magicMimeCommentLabel->setText(magicMimeComment);
385 d->m_ui->magicMimeCommentLabel->setToolTip(magicMimeName);
386 } else {
387 d->m_ui->contentLabel->hide();
388 d->m_ui->magicMimeCommentLabel->hide();
389 }
390
391 d->m_ui->configureMimeBtn->setVisible(KAuthorized::authorizeAction(QStringLiteral("editfiletype")) && !d->m_ui->defaultHandlerLabel->isHidden());
392
393 // Location:
394 if (!directory.isEmpty()) {
395 d->m_ui->locationLabel->setText(directory);
396
397 // Layout direction for this label is always LTR; but if we are in RTL mode,
398 // align the text to the right, otherwise the text is on the wrong side of the dialog
399 if (properties->layoutDirection() == Qt::RightToLeft) {
400 d->m_ui->locationLabel->setAlignment(Qt::AlignRight);
401 }
402 }
403
404 // Size widgets
405 if (!hasDirs) { // Only files [and symlinks]
406 d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)").arg(args: KIO::convertSize(size: totalSize), args: QLocale().toString(i: totalSize)));
407 d->m_ui->sizeBtnWidget->hide();
408 } else { // Directory
409 connect(sender: d->m_ui->calculateSizeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeDetermine);
410 connect(sender: d->m_ui->stopCalculateSizeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeStop);
411
412 if (auto filelight = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"))) {
413 d->m_ui->sizeDetailsBtn->setText(i18nc("@action:button", "Explore in %1", filelight->name()));
414 d->m_ui->sizeDetailsBtn->setIcon(QIcon::fromTheme(name: filelight->icon()));
415 connect(sender: d->m_ui->sizeDetailsBtn, signal: &QPushButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeDetails);
416 } else {
417 d->m_ui->sizeDetailsBtn->hide();
418 }
419
420 // sizelay->addStretch(10); // so that the buttons don't grow horizontally
421
422 // auto-launch for local dirs only, and not for '/'
423 if (isLocal && !hasRoot) {
424 d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
425 slotSizeDetermine();
426 } else {
427 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
428 }
429 }
430
431 // Symlink widgets
432 if (!d->bMultiple && firstItem.isLink()) {
433 d->m_ui->symlinkTargetEdit->setText(firstItem.linkDest());
434 connect(sender: d->m_ui->symlinkTargetEdit, signal: &QLineEdit::textChanged, context: this, slot: [this]() {
435 setDirty();
436 });
437
438 connect(sender: d->m_ui->symlinkTargetOpenDir, signal: &QPushButton::clicked, context: this, slot: [this] {
439 const QUrl resolvedTargetLocation = properties->item().url().resolved(relative: QUrl(d->m_ui->symlinkTargetEdit->text()));
440
441 KIO::StatJob *statJob = KIO::stat(url: resolvedTargetLocation, side: KIO::StatJob::SourceSide, details: KIO::StatNoDetails, flags: KIO::HideProgressInfo);
442 connect(sender: statJob, signal: &KJob::finished, context: this, slot: [this, statJob] {
443 if (statJob->error()) {
444 d->m_ui->symlinkTargetMessageWidget->setText(statJob->errorString());
445 d->m_ui->symlinkTargetMessageWidget->animatedShow();
446 return;
447 }
448
449 KIO::highlightInFileManager(urls: {statJob->url()});
450 properties->close();
451 });
452 });
453 } else {
454 d->m_ui->symlinkTargetLabel->hide();
455 d->m_ui->symlinkTargetEdit->hide();
456 d->m_ui->symlinkTargetOpenDir->hide();
457 }
458
459 // Time widgets
460 if (!d->bMultiple) {
461 QLocale locale;
462 if (const QDateTime dt = firstItem.time(which: KFileItem::CreationTime); !dt.isNull()) {
463 d->m_ui->createdTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
464 } else {
465 d->m_ui->createdTimeLabel->hide();
466 d->m_ui->createdTimeLabel_Left->hide();
467 }
468
469 if (const QDateTime dt = firstItem.time(which: KFileItem::ModificationTime); !dt.isNull()) {
470 d->m_ui->modifiedTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
471 } else {
472 d->m_ui->modifiedTimeLabel->hide();
473 d->m_ui->modifiedTimeLabel_Left->hide();
474 }
475
476 if (const QDateTime dt = firstItem.time(which: KFileItem::AccessTime); !dt.isNull()) {
477 d->m_ui->accessTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
478 } else {
479 d->m_ui->accessTimeLabel->hide();
480 d->m_ui->accessTimeLabel_Left->hide();
481 }
482 } else {
483 d->m_ui->createdTimeLabel->hide();
484 d->m_ui->createdTimeLabel_Left->hide();
485 d->m_ui->modifiedTimeLabel->hide();
486 d->m_ui->modifiedTimeLabel_Left->hide();
487 d->m_ui->accessTimeLabel->hide();
488 d->m_ui->accessTimeLabel_Left->hide();
489 }
490
491 // File system and mount point widgets
492 if (hasDirs) { // only for directories
493 if (isLocal) {
494 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: url.toLocalFile());
495
496 if (mp) {
497 d->m_ui->fsLabel->setText(mp->mountType());
498 d->m_ui->mountPointLabel->setText(mp->mountPoint());
499 d->m_ui->mountSrcLabel->setText(mp->mountedFrom());
500 } else {
501 qCWarning(KIO_WIDGETS) << "Could not find mount point for" << url;
502 d->hideMountPointLabels();
503 }
504 } else {
505 d->hideMountPointLabels();
506 }
507
508 KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url);
509 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotFreeSpaceResult);
510 } else {
511 d->m_ui->fsSeparator->hide();
512 d->m_ui->freespaceLabel->hide();
513 d->m_ui->capacityBar->hide();
514 d->hideMountPointLabels();
515 }
516
517 // UDSEntry extra fields
518 if (const auto extraFields = KProtocolInfo::extraFields(url); !d->bMultiple && !extraFields.isEmpty()) {
519 int curRow = d->m_ui->gridLayout->rowCount();
520 KSeparator *sep = new KSeparator(Qt::Horizontal, &d->m_mainWidget);
521 d->m_ui->gridLayout->addWidget(sep, row: curRow++, column: 0, rowSpan: 1, columnSpan: 3);
522
523 QLocale locale;
524 for (int i = 0; i < extraFields.count(); ++i) {
525 const auto &field = extraFields.at(i);
526
527 QString text = firstItem.entry().stringValue(field: KIO::UDSEntry::UDS_EXTRA + i);
528 if (field.type == KProtocolInfo::ExtraField::Invalid || text.isEmpty()) {
529 continue;
530 }
531
532 if (field.type == KProtocolInfo::ExtraField::DateTime) {
533 const QDateTime date = QDateTime::fromString(string: text, format: Qt::ISODate);
534 if (!date.isValid()) {
535 continue;
536 }
537
538 text = locale.toString(dateTime: date, format: QLocale::LongFormat);
539 }
540
541 auto *label = new QLabel(i18n("%1:", field.name), &d->m_mainWidget);
542 d->m_ui->gridLayout->addWidget(label, row: curRow, column: 0, Qt::AlignRight);
543
544 auto *squeezedLabel = new KSqueezedTextLabel(text, &d->m_mainWidget);
545 if (properties->layoutDirection() == Qt::RightToLeft) {
546 squeezedLabel->setAlignment(Qt::AlignRight);
547 } else {
548 squeezedLabel->setLayoutDirection(Qt::LeftToRight);
549 }
550
551 d->m_ui->gridLayout->addWidget(squeezedLabel, row: curRow++, column: 1);
552 squeezedLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
553 }
554 }
555}
556
557bool KFilePropsPlugin::enableIconButton() const
558{
559 const KFileItem item = properties->item();
560
561 // desktop files are special, files in /usr/share/applications can be
562 // edited by overlaying them in .local/share/applications
563 // https://bugs.kde.org/show_bug.cgi?id=429613
564 if (item.isDesktopFile()) {
565 return true;
566 }
567
568 // If the current item is a directory, check if it's writable,
569 // so we can create/update a .directory
570 // Current item is a file, same thing: check if it is writable
571 if (item.isWritable()) {
572 // exclude remote dirs as changing the icon has no effect (bug 205954)
573 if (item.isLocalFile() || item.url().scheme() == QLatin1String("desktop")) {
574 return true;
575 }
576 }
577
578 return false;
579}
580
581void KFilePropsPlugin::setFileNameReadOnly(bool readOnly)
582{
583 Q_ASSERT(readOnly); // false isn't supported
584
585 if (readOnly) {
586 Q_ASSERT(!d->m_bFromTemplate);
587
588 d->m_ui->fileNameLineEdit->hide();
589
590 d->m_ui->fileNameLabel->show();
591 d->m_ui->fileNameLabel->setText(d->oldName); // will get overwritten if d->bMultiple
592 }
593}
594
595void KFilePropsPlugin::slotEditFileType()
596{
597 QString mime;
598 if (d->mimeType == QLatin1String("application/octet-stream")) {
599 const int pos = d->oldFileName.lastIndexOf(c: QLatin1Char('.'));
600 if (pos != -1) {
601 mime = QLatin1Char('*') + QStringView(d->oldFileName).mid(pos);
602 } else {
603 mime = QStringLiteral("*");
604 }
605 } else {
606 mime = d->mimeType;
607 }
608 KMimeTypeEditor::editMimeType(mimeType: mime, widget: properties->window());
609}
610
611void KFilePropsPlugin::slotIconChanged()
612{
613 d->bIconChanged = true;
614 Q_EMIT changed();
615}
616
617void KFilePropsPlugin::nameFileChanged(const QString &text)
618{
619 properties->buttonBox()->button(which: QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
620 Q_EMIT changed();
621}
622
623static QString relativeAppsLocation(const QString &file)
624{
625 // Don't resolve symlinks, so that editing /usr/share/applications/foo.desktop that is
626 // a symlink works
627 const QString absolute = QFileInfo(file).absoluteFilePath();
628 const QStringList dirs = QStandardPaths::standardLocations(type: QStandardPaths::ApplicationsLocation);
629 for (const QString &base : dirs) {
630 QDir base_dir = QDir(base);
631 if (base_dir.exists() && absolute.startsWith(s: base_dir.canonicalPath())) {
632 return absolute.mid(position: base.length() + 1);
633 }
634 }
635 return QString(); // return empty if the file is not in apps
636}
637
638void KFilePropsPlugin::determineRelativePath(const QString &path)
639{
640 // now let's make it relative
641 d->m_sRelativePath = relativeAppsLocation(file: path);
642}
643
644void KFilePropsPlugin::slotFreeSpaceResult(KJob *_job)
645{
646 const auto job = qobject_cast<KIO::FileSystemFreeSpaceJob *>(object: _job);
647 Q_ASSERT(job);
648 if (!job->error()) {
649 const qint64 size = job->size();
650 const qint64 available = job->availableSize();
651 const quint64 used = size - available;
652 const int percentUsed = qRound(d: 100.0 * qreal(used) / qreal(size));
653
654 d->m_ui->capacityBar->setText(i18nc("Available space out of total partition size (percent used)",
655 "%1 free of %2 (%3% used)",
656 KIO::convertSize(available),
657 KIO::convertSize(size),
658 percentUsed));
659
660 d->m_ui->capacityBar->setValue(percentUsed);
661 } else {
662 d->m_ui->capacityBar->setText(i18nc("@info:status", "Unknown size"));
663 d->m_ui->capacityBar->setValue(0);
664 }
665}
666
667void KFilePropsPlugin::slotDirSizeUpdate()
668{
669 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
670 KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
671 KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
672 d->m_ui->sizeLabel->setText(i18n("Calculating... %1 (%2)\n%3, %4",
673 KIO::convertSize(totalSize),
674 QLocale().toString(totalSize),
675 i18np("1 file", "%1 files", totalFiles),
676 i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
677}
678
679void KFilePropsPlugin::slotDirSizeFinished(KJob *job)
680{
681 if (job->error()) {
682 d->m_ui->sizeLabel->setText(job->errorString());
683 } else {
684 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
685 KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
686 KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
687 d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)\n%3, %4")
688 .arg(args: KIO::convertSize(size: totalSize),
689 args: QLocale().toString(i: totalSize),
690 i18np("1 file", "%1 files", totalFiles),
691 i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
692 }
693 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
694 // just in case you change something and try again :)
695 d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
696 d->m_ui->calculateSizeBtn->setEnabled(true);
697 d->dirSizeJob = nullptr;
698 delete d->dirSizeUpdateTimer;
699 d->dirSizeUpdateTimer = nullptr;
700}
701
702void KFilePropsPlugin::slotSizeDetermine()
703{
704 d->m_ui->sizeLabel->setText(i18n("Calculating...\n"));
705 // qDebug() << "properties->item()=" << properties->item() << "URL=" << properties->item().url();
706
707 d->dirSizeJob = KIO::directorySize(lstItems: properties->items());
708 d->dirSizeUpdateTimer = new QTimer(this);
709 connect(sender: d->dirSizeUpdateTimer, signal: &QTimer::timeout, context: this, slot: &KFilePropsPlugin::slotDirSizeUpdate);
710 d->dirSizeUpdateTimer->start(msec: 500);
711 connect(sender: d->dirSizeJob, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotDirSizeFinished);
712 d->m_ui->stopCalculateSizeBtn->setEnabled(true);
713 d->m_ui->calculateSizeBtn->setEnabled(false);
714
715 // also update the "Free disk space" display
716 if (!d->m_ui->capacityBar->isHidden()) {
717 const KFileItem item = properties->item();
718 KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url: item.url());
719 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotFreeSpaceResult);
720 }
721}
722
723void KFilePropsPlugin::slotSizeStop()
724{
725 if (d->dirSizeJob) {
726 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
727 d->m_ui->sizeLabel->setText(i18n("At least %1\n", KIO::convertSize(totalSize)));
728 d->dirSizeJob->kill();
729 d->dirSizeJob = nullptr;
730 }
731 if (d->dirSizeUpdateTimer) {
732 d->dirSizeUpdateTimer->stop();
733 }
734
735 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
736 d->m_ui->calculateSizeBtn->setEnabled(true);
737}
738
739void KFilePropsPlugin::slotSizeDetails()
740{
741 // Open the current folder in filelight
742 KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
743 if (service) {
744 auto *job = new KIO::ApplicationLauncherJob(service);
745 job->setUrls({properties->url()});
746 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, properties));
747 job->start();
748 }
749}
750
751KFilePropsPlugin::~KFilePropsPlugin() = default;
752
753bool KFilePropsPlugin::supports(const KFileItemList & /*_items*/)
754{
755 return true;
756}
757
758void KFilePropsPlugin::applyChanges()
759{
760 if (d->dirSizeJob) {
761 slotSizeStop();
762 }
763
764 // qDebug() << "KFilePropsPlugin::applyChanges";
765
766 if (!d->m_ui->fileNameLineEdit->isHidden()) {
767 QString n = d->m_ui->fileNameLineEdit->text();
768 // Remove trailing spaces (#4345)
769 while (!n.isEmpty() && n[n.length() - 1].isSpace()) {
770 n.chop(n: 1);
771 }
772 if (n.isEmpty()) {
773 KMessageBox::error(parent: properties, i18n("The new file name is empty."));
774 properties->abortApplying();
775 return;
776 }
777
778 // Do we need to rename the file ?
779 // qDebug() << "oldname = " << d->oldName;
780 // qDebug() << "newname = " << n;
781 if (d->oldName != n || d->m_bFromTemplate) { // true for any from-template file
782 KIO::CopyJob *job = nullptr;
783 QUrl oldurl = properties->url();
784
785 QString newFileName = KIO::encodeFileName(str: n);
786 if (d->bDesktopFile && !newFileName.endsWith(s: QLatin1String(".desktop"))) {
787 newFileName += QLatin1String(".desktop");
788 }
789
790 // Tell properties. Warning, this changes the result of properties->url() !
791 properties->rename(name: newFileName);
792
793 // Update also relative path (for apps)
794 if (!d->m_sRelativePath.isEmpty()) {
795 determineRelativePath(path: properties->url().toLocalFile());
796 }
797
798 // qDebug() << "New URL = " << properties->url();
799 // qDebug() << "old = " << oldurl.url();
800
801 // Don't remove the template !!
802 if (!d->m_bFromTemplate) { // (normal renaming)
803 job = KIO::moveAs(src: oldurl, dest: properties->url());
804 } else { // Copying a template
805 job = KIO::copyAs(src: oldurl, dest: properties->url());
806 }
807 KJobWidgets::setWindow(job, widget: properties);
808 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotCopyFinished);
809 connect(sender: job, signal: &KIO::CopyJob::renamed, context: this, slot: &KFilePropsPlugin::slotFileRenamed);
810 return;
811 }
812
813 properties->updateUrl(newUrl: properties->url());
814 // Update also relative path (for apps)
815 if (!d->m_sRelativePath.isEmpty()) {
816 determineRelativePath(path: properties->url().toLocalFile());
817 }
818 }
819
820 // No job, keep going
821 slotCopyFinished(nullptr);
822}
823
824void KFilePropsPlugin::slotCopyFinished(KJob *job)
825{
826 // qDebug() << "KFilePropsPlugin::slotCopyFinished";
827 if (job) {
828 if (job->error()) {
829 job->uiDelegate()->showErrorMessage();
830 // Didn't work. Revert the URL to the old one
831 properties->updateUrl(newUrl: static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst());
832 properties->abortApplying(); // Don't apply the changes to the wrong file !
833 return;
834 }
835 }
836
837 Q_ASSERT(!properties->item().isNull());
838 Q_ASSERT(!properties->item().url().isEmpty());
839
840 // Save the file locally
841 if (d->bDesktopFile && !d->m_sRelativePath.isEmpty()) {
842 // qDebug() << "KFilePropsPlugin::slotCopyFinished " << d->m_sRelativePath;
843 const QString newPath = QStandardPaths::writableLocation(type: QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + d->m_sRelativePath;
844 const QUrl newURL = QUrl::fromLocalFile(localfile: newPath);
845 // qDebug() << "KFilePropsPlugin::slotCopyFinished path=" << newURL;
846 properties->updateUrl(newUrl: newURL);
847 }
848
849 if (d->bKDesktopMode && d->bDesktopFile) {
850 // Renamed? Update Name field
851 // Note: The desktop KIO worker does this as well, but not when
852 // the file is copied from a template.
853 if (d->m_bFromTemplate) {
854 KIO::StatJob *job = KIO::stat(url: properties->url());
855 job->exec();
856 KIO::UDSEntry entry = job->statResult();
857
858 KFileItem item(entry, properties->url());
859 KDesktopFile config(item.localPath());
860 KConfigGroup cg = config.desktopGroup();
861 QString nameStr = nameFromFileName(nameStr: properties->url().fileName());
862 cg.writeEntry(key: "Name", value: nameStr);
863 cg.writeEntry(key: "Name", value: nameStr, pFlags: KConfigGroup::Persistent | KConfigGroup::Localized);
864 }
865 }
866
867 if (!d->m_ui->symlinkTargetEdit->isHidden() && !d->bMultiple) {
868 const KFileItem item = properties->item();
869 const QString newTarget = d->m_ui->symlinkTargetEdit->text();
870 if (newTarget != item.linkDest()) {
871 // qDebug() << "Updating target of symlink to" << newTarget;
872 KIO::Job *job = KIO::symlink(target: newTarget, dest: item.url(), flags: KIO::Overwrite);
873 job->uiDelegate()->setAutoErrorHandlingEnabled(true);
874 job->exec();
875 }
876 }
877
878 // "Link to Application" templates need to be made executable
879 // Instead of matching against a filename we check if the destination
880 // is an Application now.
881 if (d->m_bFromTemplate) {
882 // destination is not necessarily local, use the src template
883 KDesktopFile templateResult(static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst().toLocalFile());
884 if (templateResult.hasApplicationType()) {
885 // We can either stat the file and add the +x bit or use the larger chmod() job
886 // with a umask designed to only touch u+x. This is only one KIO job, so let's
887 // do that.
888
889 KFileItem appLink(properties->item());
890 KFileItemList fileItemList;
891 fileItemList << appLink;
892
893 // first 0100 adds u+x, second 0100 only allows chmod to change u+x
894 KIO::Job *chmodJob = KIO::chmod(lstItems: fileItemList, permissions: 0100, mask: 0100, newOwner: QString(), newGroup: QString(), recursive: KIO::HideProgressInfo);
895 chmodJob->exec();
896 }
897 }
898
899 setDirty(false);
900 Q_EMIT changesApplied();
901}
902
903void KFilePropsPlugin::applyIconChanges()
904{
905 if (d->m_ui->iconButton->isHidden() || !d->bIconChanged) {
906 return;
907 }
908 // handle icon changes - only local files (or pseudo-local) for now
909 // TODO: Use KTempFile and KIO::file_copy with overwrite = true
910 QUrl url = properties->url();
911 KIO::StatJob *job = KIO::mostLocalUrl(url);
912 KJobWidgets::setWindow(job, widget: properties);
913 job->exec();
914 url = job->mostLocalUrl();
915
916 if (url.isLocalFile()) {
917 QString path;
918
919 if (Utils::isDirMask(mode: properties->item().mode())) {
920 path = url.toLocalFile() + QLatin1String("/.directory");
921 // don't call updateUrl because the other tabs (i.e. permissions)
922 // apply to the directory, not the .directory file.
923 } else {
924 path = url.toLocalFile();
925 }
926
927 // Get the default image
928 QMimeDatabase db;
929 const QString str = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchExtension).iconName();
930 // Is it another one than the default ?
931 QString sIcon;
932 if (const QString currIcon = d->m_ui->iconButton->icon(); str != currIcon) {
933 sIcon = currIcon;
934 }
935 // (otherwise write empty value)
936
937 // qDebug() << "**" << path << "**";
938
939 // If default icon and no .directory file -> don't create one
940 if (!sIcon.isEmpty() || QFile::exists(fileName: path)) {
941 KDesktopFile cfg(path);
942 // qDebug() << "sIcon = " << (sIcon);
943 // qDebug() << "str = " << (str);
944 cfg.desktopGroup().writeEntry(key: "Icon", value: sIcon);
945 cfg.sync();
946
947 cfg.reparseConfiguration();
948 if (cfg.desktopGroup().readEntry(key: "Icon") != sIcon) {
949 properties->abortApplying();
950
951 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
952 }
953 }
954 }
955}
956
957void KFilePropsPlugin::updateDefaultHandler(const QString &mimeType)
958{
959 const bool isGeneric = d->mimeType == QLatin1String("application/octet-stream");
960
961 const auto service = KApplicationTrader::preferredService(mimeType);
962 if (!isGeneric && service) {
963 const int iconSize = properties->style()->pixelMetric(metric: QStyle::PM_SmallIconSize);
964 d->m_ui->defaultHandlerIcon->setPixmap(QIcon::fromTheme(name: service->icon()).pixmap(extent: iconSize));
965 d->m_ui->defaultHandlerIcon->show();
966 d->m_ui->defaultHandlerLabel->setText(service->name());
967 d->m_ui->defaultHandlerLabel->setDisabled(false);
968 } else {
969 d->m_ui->defaultHandlerIcon->hide();
970 if (isGeneric) {
971 d->m_ui->defaultHandlerLabel->setText(i18n("No registered file type"));
972 } else {
973 d->m_ui->defaultHandlerLabel->setText(i18n("No associated application"));
974 }
975 d->m_ui->defaultHandlerLabel->setDisabled(true);
976 }
977
978 if (isGeneric) {
979 d->m_ui->configureMimeBtn->setText(i18nc("@action:button Create new file type", "Create…"));
980 d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
981 } else {
982 d->m_ui->configureMimeBtn->setText(i18nc("@action:button", "Change…"));
983 d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
984 }
985}
986
987void KFilePropsPlugin::slotFileRenamed(KIO::Job *, const QUrl &, const QUrl &newUrl)
988{
989 // This is called in case of an existing local file during the copy/move operation,
990 // if the user chooses Rename.
991 properties->updateUrl(newUrl);
992}
993
994void KFilePropsPlugin::postApplyChanges()
995{
996 // Save the icon only after applying the permissions changes (#46192)
997 applyIconChanges();
998
999 const KFileItemList items = properties->items();
1000 const QList<QUrl> lst = items.urlList();
1001 org::kde::KDirNotify::emitFilesChanged(fileList: QList<QUrl>(lst));
1002}
1003
1004class KFilePermissionsPropsPlugin::KFilePermissionsPropsPluginPrivate
1005{
1006public:
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
1036static constexpr mode_t UniOwner{S_IRUSR | S_IWUSR | S_IXUSR};
1037static constexpr mode_t UniGroup{S_IRGRP | S_IWGRP | S_IXGRP};
1038static constexpr mode_t UniOthers{S_IROTH | S_IWOTH | S_IXOTH};
1039static constexpr mode_t UniRead{S_IRUSR | S_IRGRP | S_IROTH};
1040static constexpr mode_t UniWrite{S_IWUSR | S_IWGRP | S_IWOTH};
1041static constexpr mode_t UniExec{S_IXUSR | S_IXGRP | S_IXOTH};
1042static constexpr mode_t UniSpecial{S_ISUID | S_ISGID | S_ISVTX};
1043static constexpr mode_t s_invalid_mode_t{static_cast<mode_t>(-1)};
1044
1045// synced with PermissionsTarget
1046constexpr mode_t KFilePermissionsPropsPlugin::permissionsMasks[3] = {UniOwner, UniGroup, UniOthers};
1047constexpr mode_t KFilePermissionsPropsPlugin::standardPermissions[4] = {0, UniRead, UniRead | UniWrite, s_invalid_mode_t};
1048
1049// synced with PermissionsMode and standardPermissions
1050static constexpr KLazyLocalizedString permissionsTexts[4][4] = {
1051 {kli18n(text: "No Access"), kli18n(text: "Can Only View"), kli18n(text: "Can View & Modify"), {}},
1052 {kli18n(text: "No Access"), kli18n(text: "Can Only View Content"), kli18n(text: "Can View & Modify Content"), {}},
1053 {{}, {}, {}, {}}, // no texts for links
1054 {kli18n(text: "No Access"), kli18n(text: "Can Only View/Read Content"), kli18n(text: "Can View/Read & Modify/Write"), {}}};
1055
1056KFilePermissionsPropsPlugin::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(permissions: 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(permissions: item.permissions(), isDir: isItemDir == isDir, isLink: 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(widget: d->m_frame, i18n("&Permissions"));
1154
1155 QBoxLayout *box = new QVBoxLayout(d->m_frame);
1156 box->setContentsMargins(left: 0, top: 0, right: 0, bottom: 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(column: 1, stretch: 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, row: 0, column: 0, rowSpan: 1, columnSpan: 2);
1179
1180 lbl = new QLabel(i18n("O&wner:"), gb);
1181 gl->addWidget(lbl, row: 1, column: 0, Qt::AlignRight);
1182 l = d->ownerPermCombo = new QComboBox(gb);
1183 lbl->setBuddy(l);
1184 gl->addWidget(l, row: 1, column: 1);
1185 connect(sender: d->ownerPermCombo, signal: &QComboBox::activated, context: this, slot: &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, row: 2, column: 0, Qt::AlignRight);
1190 l = d->groupPermCombo = new QComboBox(gb);
1191 lbl->setBuddy(l);
1192 gl->addWidget(l, row: 2, column: 1);
1193 connect(sender: d->groupPermCombo, signal: &QComboBox::activated, context: this, slot: &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, row: 3, column: 0, Qt::AlignRight);
1198 l = d->othersPermCombo = new QComboBox(gb);
1199 lbl->setBuddy(l);
1200 gl->addWidget(l, row: 3, column: 1);
1201 connect(sender: d->othersPermCombo, signal: &QComboBox::activated, context: this, slot: &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(sender: d->extraCheckbox, signal: &QAbstractButton::clicked, context: this, slot: &KPropertiesDialogPlugin::changed);
1209 gl->addWidget(l, row: 4, column: 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(item: spacer, row: 5, column: 0, rowSpan: 1, columnSpan: 3);
1220
1221 pbAdvancedPerm = new QPushButton(i18n("A&dvanced Permissions"), gb);
1222 gl->addWidget(pbAdvancedPerm, row: 6, column: 0, rowSpan: 1, columnSpan: 2, Qt::AlignRight);
1223 connect(sender: pbAdvancedPerm, signal: &QAbstractButton::clicked, context: this, slot: &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(item: new QSpacerItem(0, 10), row: 0, column: 0);
1234
1235 /*** Set Owner ***/
1236 l = new QLabel(i18n("User:"), gb);
1237 gl->addWidget(l, row: 1, column: 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(maxCount: 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, row: 1, column: 1);
1258 connect(sender: d->usrEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
1259 } else {
1260 l = new QLabel(d->strOwner, gb);
1261 gl->addWidget(l, row: 1, column: 1);
1262 }
1263
1264 /*** Set Group ***/
1265 QStringList groupList = myself.groupNames();
1266 const bool isMyGroup = groupList.contains(str: 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, row: 2, column: 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, handle: true);
1289 d->grpEdit->setAutoDeleteCompletionObject(true);
1290 d->grpEdit->setCompletionMode(KCompletion::CompletionAuto);
1291 d->grpEdit->setText(d->strGroup);
1292 gl->addWidget(d->grpEdit, row: 2, column: 1);
1293 connect(sender: d->grpEdit, signal: &QLineEdit::textChanged, context: this, slot: &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(texts: groupList);
1298 d->grpCombo->setCurrentIndex(groupList.indexOf(str: d->strGroup));
1299 gl->addWidget(d->grpCombo, row: 2, column: 1);
1300 connect(sender: d->grpCombo, signal: &QComboBox::activated, context: this, slot: &KPropertiesDialogPlugin::changed);
1301 } else {
1302 l = new QLabel(d->strGroup, gb);
1303 gl->addWidget(l, row: 2, column: 1);
1304 }
1305
1306 gl->setColumnStretch(column: 2, stretch: 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(sender: d->cbRecursive, signal: &QAbstractButton::clicked, context: this, slot: &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(enable: false);
1320 if (pbAdvancedPerm) {
1321 pbAdvancedPerm->setEnabled(false);
1322 }
1323 }
1324
1325 box->addStretch(stretch: 10);
1326}
1327
1328#if HAVE_POSIX_ACL
1329static 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
1347void 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(item: new QSpacerItem(0, 10), row: 0, column: 0);
1366
1367 QList<QWidget *> theNotSpecials;
1368
1369 l = new QLabel(i18n("Class"), gb);
1370 gl->addWidget(l, row: 1, column: 0);
1371 theNotSpecials.append(t: 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, row: 1, column: 3);
1409
1410 l = new QLabel(i18n("Special"), gb);
1411 gl->addWidget(l, row: 1, column: 4, rowSpan: 1, columnSpan: 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], row: 2, column: 0);
1426 theNotSpecials.append(t: cl[0]);
1427
1428 cl[1] = new QLabel(i18n("Group"), gb);
1429 gl->addWidget(cl[1], row: 3, column: 0);
1430 theNotSpecials.append(t: cl[1]);
1431
1432 cl[2] = new QLabel(i18n("Others"), gb);
1433 gl->addWidget(cl[2], row: 4, column: 0);
1434 theNotSpecials.append(t: 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(andFilePermissions&: aPartialPermissions, andDirPermissions&: dummy1, orFilePermissions&: aPermissions, orDirPermissions&: dummy2);
1478 break;
1479 case PermissionsOnlyDirs:
1480 case PermissionsMixed:
1481 getPermissionMasks(andFilePermissions&: dummy1, andDirPermissions&: aPartialPermissions, orFilePermissions&: dummy2, orDirPermissions&: 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(t: 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: row + 2, column: 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(column: 6, stretch: 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(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: &dlg, slot: &QDialog::accept);
1577 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: &dlg, slot: &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(first: items.cbegin(), last: items.cend(), pred: [this, andPermissions, orPermissions](const KFileItem &item) {
1603 return isIrregular(permissions: (item.permissions() & andPermissions) | orPermissions, isDir: item.isDir(), isLink: 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
1624KFilePermissionsPropsPlugin::~KFilePermissionsPropsPlugin() = default;
1625
1626bool KFilePermissionsPropsPlugin::supports(const KFileItemList &items)
1627{
1628 return std::any_of(first: items.cbegin(), last: items.cend(), pred: [](const KFileItem &item) {
1629 return KProtocolManager::supportsPermissions(url: item.url());
1630 });
1631}
1632
1633// sets a combo box in the Access Control frame
1634void 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(atext: 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.
1670bool 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
1744void 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
1758void KFilePermissionsPropsPlugin::updateAccessControls()
1759{
1760 setComboContent(combo: d->ownerPermCombo, target: PermissionsOwner, permissions: d->permissions, partial: d->partialPermissions);
1761 setComboContent(combo: d->groupPermCombo, target: PermissionsGroup, permissions: d->permissions, partial: d->partialPermissions);
1762 setComboContent(combo: d->othersPermCombo, target: PermissionsOthers, permissions: d->permissions, partial: d->partialPermissions);
1763
1764 switch (d->pmode) {
1765 case PermissionsOnlyLinks:
1766 enableAccessControls(enable: false);
1767 break;
1768 case PermissionsOnlyFiles:
1769 enableAccessControls(enable: 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(enable: 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(enable: 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
1823void 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
1901void 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(t: item);
1954 if (!permissionChange && (recursive || perms != ((perms & andDirPermissions) | orDirPermissions))) {
1955 permissionChange = true;
1956 }
1957 continue;
1958 }
1959
1960 if (item.isFile()) {
1961 files.append(t: 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"), value: d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE"));
1982 }
1983
1984 if (defaultACLChange) {
1985 chmodJob->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), value: d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE"));
1986 }
1987 };
1988
1989 auto chmodDirs = [=, this]() {
1990 if (dirs.isEmpty()) {
1991 setDirty(false);
1992 Q_EMIT changesApplied();
1993 return;
1994 }
1995
1996 auto *dirsJob = KIO::chmod(lstItems: dirs, permissions: orDirPermissions, mask: ~andDirPermissions, newOwner: owner, newGroup: group, recursive);
1997 processACLChanges(dirsJob);
1998
1999 connect(sender: dirsJob, signal: &KJob::result, context: this, slot: [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(lstItems: files, permissions: orFilePermissions, mask: ~andFilePermissions, newOwner: owner, newGroup: group, recursive: false);
2013 processACLChanges(filesJob);
2014
2015 connect(sender: filesJob, signal: &KJob::result, context: this, slot: [=]() {
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
2029class KChecksumsPlugin::KChecksumsPluginPrivate
2030{
2031public:
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
2042KChecksumsPlugin::KChecksumsPlugin(KPropertiesDialog *dialog)
2043 : KPropertiesDialogPlugin(dialog)
2044 , d(new KChecksumsPluginPrivate)
2045{
2046 d->m_ui.setupUi(&d->m_widget);
2047 properties->addPage(widget: &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(sender: d->m_ui.lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this](const QString &text) {
2055 slotVerifyChecksum(input: text.toLower());
2056 });
2057
2058 connect(sender: d->m_ui.md5Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowMd5);
2059 connect(sender: d->m_ui.sha1Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha1);
2060 connect(sender: d->m_ui.sha256Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha256);
2061 connect(sender: d->m_ui.sha512Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha512);
2062
2063 d->fileWatcher.addPath(file: properties->item().localPath());
2064 connect(sender: &d->fileWatcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &KChecksumsPlugin::slotInvalidateCache);
2065
2066 auto clipboard = QApplication::clipboard();
2067 connect(sender: d->m_ui.md5CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2068 clipboard->setText(d->m_md5);
2069 });
2070
2071 connect(sender: d->m_ui.sha1CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2072 clipboard->setText(d->m_sha1);
2073 });
2074
2075 connect(sender: d->m_ui.sha256CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2076 clipboard->setText(d->m_sha256);
2077 });
2078
2079 connect(sender: d->m_ui.sha512CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2080 clipboard->setText(d->m_sha512);
2081 });
2082
2083 connect(sender: d->m_ui.pasteButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2084 d->m_ui.lineEdit->setText(clipboard->text());
2085 });
2086
2087 setDefaultState();
2088}
2089
2090KChecksumsPlugin::~KChecksumsPlugin() = default;
2091
2092bool 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
2102void 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
2110void 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(from: d->m_ui.md5Button, to: label);
2116 d->m_ui.md5Button->hide();
2117
2118 showChecksum(algorithm: QCryptographicHash::Md5, label, copyButton: d->m_ui.md5CopyButton);
2119}
2120
2121void 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(from: d->m_ui.sha1Button, to: label);
2127 d->m_ui.sha1Button->hide();
2128
2129 showChecksum(algorithm: QCryptographicHash::Sha1, label, copyButton: d->m_ui.sha1CopyButton);
2130}
2131
2132void 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(from: d->m_ui.sha256Button, to: label);
2138 d->m_ui.sha256Button->hide();
2139
2140 showChecksum(algorithm: QCryptographicHash::Sha256, label, copyButton: d->m_ui.sha256CopyButton);
2141}
2142
2143void 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(from: d->m_ui.sha512Button, to: label);
2149 d->m_ui.sha512Button->hide();
2150
2151 showChecksum(algorithm: QCryptographicHash::Sha512, label, copyButton: d->m_ui.sha512CopyButton);
2152}
2153
2154void 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(sender: futureWatcher, signal: &QFutureWatcher<QString>::finished, context: this, slot: [=, 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(f: &KChecksumsPlugin::computeChecksum, args&: algorithm, args: properties->item().localPath());
2219 futureWatcher->setFuture(future);
2220}
2221
2222bool KChecksumsPlugin::isMd5(const QString &input)
2223{
2224 QRegularExpression regex(QStringLiteral("^[a-f0-9]{32}$"));
2225 return regex.match(subject: input).hasMatch();
2226}
2227
2228bool KChecksumsPlugin::isSha1(const QString &input)
2229{
2230 QRegularExpression regex(QStringLiteral("^[a-f0-9]{40}$"));
2231 return regex.match(subject: input).hasMatch();
2232}
2233
2234bool KChecksumsPlugin::isSha256(const QString &input)
2235{
2236 QRegularExpression regex(QStringLiteral("^[a-f0-9]{64}$"));
2237 return regex.match(subject: input).hasMatch();
2238}
2239
2240bool KChecksumsPlugin::isSha512(const QString &input)
2241{
2242 QRegularExpression regex(QStringLiteral("^[a-f0-9]{128}$"));
2243 return regex.match(subject: input).hasMatch();
2244}
2245
2246QString KChecksumsPlugin::computeChecksum(QCryptographicHash::Algorithm algorithm, const QString &path)
2247{
2248 QFile file(path);
2249 if (!file.open(flags: QIODevice::ReadOnly)) {
2250 return QString();
2251 }
2252
2253 QCryptographicHash hash(algorithm);
2254 hash.addData(device: &file);
2255
2256 return QString::fromLatin1(ba: hash.result().toHex());
2257}
2258
2259QCryptographicHash::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
2281void KChecksumsPlugin::setDefaultState()
2282{
2283 QColor defaultColor = d->m_widget.palette().color(cr: QPalette::Base);
2284
2285 QPalette palette = d->m_widget.palette();
2286 palette.setColor(acr: QPalette::Base, acolor: defaultColor);
2287
2288 d->m_ui.feedbackLabel->hide();
2289 d->m_ui.lineEdit->setPalette(palette);
2290 d->m_ui.lineEdit->setToolTip(QString());
2291}
2292
2293void 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(acr: QPalette::Base, acolor: 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
2307void 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(acr: QPalette::Base, acolor: 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
2321void 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(acr: QPalette::Base, acolor: 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
2338void 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
2347void 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(sender: futureWatcher, signal: &QFutureWatcher<QString>::finished, context: this, slot: [=, 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(f: &KChecksumsPlugin::computeChecksum, args&: algorithm, args: properties->item().localPath());
2370 futureWatcher->setFuture(future);
2371}
2372
2373QString 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
2391void 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
2411class KUrlPropsPlugin::KUrlPropsPluginPrivate
2412{
2413public:
2414 QFrame *m_frame;
2415 KUrlRequester *URLEdit;
2416 QString URLStr;
2417 bool fileNameReadOnly = false;
2418};
2419
2420KUrlPropsPlugin::KUrlPropsPlugin(KPropertiesDialog *_props)
2421 : KPropertiesDialogPlugin(_props)
2422 , d(new KUrlPropsPluginPrivate)
2423{
2424 d->m_frame = new QFrame();
2425 properties->addPage(widget: d->m_frame, i18n("U&RL"));
2426 QVBoxLayout *layout = new QVBoxLayout(d->m_frame);
2427 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 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, stretch: Qt::AlignRight);
2434
2435 d->URLEdit = new KUrlRequester(d->m_frame);
2436 layout->addWidget(d->URLEdit);
2437
2438 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2439 KJobWidgets::setWindow(job, widget: 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(flags: QIODevice::ReadOnly)) {
2448 return;
2449 }
2450
2451 KDesktopFile config(path);
2452 const KConfigGroup dg = config.desktopGroup();
2453 d->URLStr = dg.readPathEntry(key: "URL", aDefault: QString());
2454
2455 if (!d->URLStr.isEmpty()) {
2456 d->URLEdit->setUrl(QUrl(d->URLStr));
2457 }
2458 }
2459
2460 connect(sender: d->URLEdit, signal: &KUrlRequester::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2461
2462 layout->addStretch(stretch: 1);
2463}
2464
2465KUrlPropsPlugin::~KUrlPropsPlugin() = default;
2466
2467void KUrlPropsPlugin::setFileNameReadOnly(bool ro)
2468{
2469 d->fileNameReadOnly = ro;
2470}
2471
2472bool 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
2493void KUrlPropsPlugin::applyChanges()
2494{
2495 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2496 KJobWidgets::setWindow(job, widget: properties);
2497 job->exec();
2498 const QUrl url = job->mostLocalUrl();
2499
2500 if (!url.isLocalFile()) {
2501 KMessageBox::error(parent: 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(flags: QIODevice::ReadWrite)) {
2509 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
2510 properties->abortApplying();
2511 return;
2512 }
2513
2514 KDesktopFile config(path);
2515 KConfigGroup dg = config.desktopGroup();
2516 dg.writeEntry(key: "Type", QStringLiteral("Link"));
2517 dg.writePathEntry(Key: "URL", path: 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(key: "Name")) {
2522 const QString nameStr = nameFromFileName(nameStr: properties->url().fileName());
2523 dg.writeEntry(key: "Name", value: nameStr);
2524 dg.writeEntry(key: "Name", value: nameStr, pFlags: KConfigBase::Persistent | KConfigBase::Localized);
2525 }
2526
2527 setDirty(false);
2528}
2529
2530/* ----------------------------------------------------
2531 *
2532 * KDesktopPropsPlugin
2533 *
2534 * -------------------------------------------------- */
2535
2536class KDesktopPropsPlugin::KDesktopPropsPluginPrivate
2537{
2538public:
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 bool m_runOnDiscreteGpuBool;
2561 bool m_startupBool;
2562};
2563
2564KDesktopPropsPlugin::KDesktopPropsPlugin(KPropertiesDialog *_props)
2565 : KPropertiesDialogPlugin(_props)
2566 , d(new KDesktopPropsPluginPrivate)
2567{
2568 QMimeDatabase db;
2569
2570 d->w->setupUi(d->m_frame);
2571
2572 properties->addPage(widget: d->m_frame, i18n("&Application"));
2573
2574 bool bKDesktopMode = properties->url().scheme() == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
2575
2576 d->w->pathEdit->setMode(KFile::Directory | KFile::LocalOnly);
2577 d->w->pathEdit->lineEdit()->setAcceptDrops(false);
2578
2579 connect(sender: d->w->nameEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2580 connect(sender: d->w->genNameEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2581 connect(sender: d->w->commentEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2582 connect(sender: d->w->envarsEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2583 connect(sender: d->w->programEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2584 connect(sender: d->w->argumentsEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2585 connect(sender: d->w->pathEdit, signal: &KUrlRequester::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2586
2587 connect(sender: d->w->browseButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotBrowseExec);
2588 connect(sender: d->w->addFiletypeButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotAddFiletype);
2589 connect(sender: d->w->delFiletypeButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotDelFiletype);
2590 connect(sender: d->w->advancedButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotAdvanced);
2591
2592 // now populate the page
2593
2594 KIO::StatJob *job = KIO::mostLocalUrl(url: _props->url());
2595 KJobWidgets::setWindow(job, widget: _props);
2596 job->exec();
2597 QUrl url = job->mostLocalUrl();
2598
2599 if (!url.isLocalFile()) {
2600 return;
2601 }
2602
2603 d->m_origDesktopFile = url.toLocalFile();
2604
2605 QFile f(d->m_origDesktopFile);
2606 if (!f.open(flags: QIODevice::ReadOnly)) {
2607 return;
2608 }
2609
2610 KDesktopFile _config(d->m_origDesktopFile);
2611 KConfigGroup config = _config.desktopGroup();
2612 QString nameStr = _config.readName();
2613 QString genNameStr = _config.readGenericName();
2614 QString commentStr = _config.readComment();
2615 QString commandStr = config.readEntry(key: "Exec", aDefault: QString());
2616
2617 d->m_origCommandStr = commandStr;
2618 QString pathStr = config.readEntry(key: "Path", aDefault: QString()); // not readPathEntry, see kservice.cpp
2619 d->m_terminalBool = config.readEntry(key: "Terminal", defaultValue: false);
2620 d->m_terminalOptionStr = config.readEntry(key: "TerminalOptions");
2621 d->m_suidBool = config.readEntry(key: "X-KDE-SubstituteUID", defaultValue: false);
2622 d->m_suidUserStr = config.readEntry(key: "X-KDE-Username");
2623 if (KIO::hasDiscreteGpu()) {
2624 if (config.hasKey(key: "PrefersNonDefaultGPU")) {
2625 d->m_runOnDiscreteGpuBool = config.readEntry(key: "PrefersNonDefaultGPU", defaultValue: false);
2626 } else {
2627 d->m_runOnDiscreteGpuBool = config.readEntry(key: "X-KDE-RunOnDiscreteGpu", defaultValue: false);
2628 }
2629 }
2630 if (config.hasKey(key: "StartupNotify")) {
2631 d->m_startupBool = config.readEntry(key: "StartupNotify", defaultValue: true);
2632 } else {
2633 d->m_startupBool = config.readEntry(key: "X-KDE-StartupNotify", defaultValue: true);
2634 }
2635
2636 const QStringList mimeTypes = config.readXdgListEntry(key: "MimeType");
2637
2638 if (nameStr.isEmpty() || bKDesktopMode) {
2639 // We'll use the file name if no name is specified
2640 // because we _need_ a Name for a valid file.
2641 // But let's do it in apply, not here, so that we pick up the right name.
2642 setDirty();
2643 }
2644 d->w->nameEdit->setText(nameStr);
2645 d->w->genNameEdit->setText(genNameStr);
2646 d->w->commentEdit->setText(commentStr);
2647
2648 QStringList execLine = KShell::splitArgs(cmd: commandStr);
2649 QStringList enVars = {};
2650
2651 if (!execLine.isEmpty()) {
2652 // check for apps that use the env executable
2653 // to set the environment
2654 if (execLine[0] == QLatin1String("env")) {
2655 execLine.pop_front();
2656 }
2657 for (const auto &env : execLine) {
2658 if (execLine.length() <= 1) {
2659 // Don't empty out the list. If the last element contains an equal sign we have to treat it as part of the
2660 // program name lest we have no program
2661 // https://bugs.kde.org/show_bug.cgi?id=465290
2662 break;
2663 }
2664 if (!env.contains(s: QLatin1String("="))) {
2665 break;
2666 }
2667 enVars += env;
2668 execLine.pop_front();
2669 }
2670
2671 Q_ASSERT(!execLine.isEmpty());
2672 d->w->programEdit->setText(execLine.takeFirst());
2673 } else {
2674 d->w->programEdit->clear();
2675 }
2676 d->w->argumentsEdit->setText(KShell::joinArgs(args: execLine));
2677 d->w->envarsEdit->setText(KShell::joinArgs(args: enVars));
2678
2679 d->w->pathEdit->lineEdit()->setText(pathStr);
2680
2681 // was: d->w->filetypeList->setFullWidth(true);
2682 // d->w->filetypeList->header()->setStretchEnabled(true, d->w->filetypeList->columns()-1);
2683
2684 for (QStringList::ConstIterator it = mimeTypes.begin(); it != mimeTypes.end();) {
2685 QMimeType p = db.mimeTypeForName(nameOrAlias: *it);
2686 ++it;
2687 QString preference;
2688 if (it != mimeTypes.end()) {
2689 bool numeric;
2690 (*it).toInt(ok: &numeric);
2691 if (numeric) {
2692 preference = *it;
2693 ++it;
2694 }
2695 }
2696 if (p.isValid()) {
2697 QTreeWidgetItem *item = new QTreeWidgetItem();
2698 item->setText(column: 0, atext: p.name());
2699 item->setText(column: 1, atext: p.comment());
2700 item->setText(column: 2, atext: preference);
2701 d->w->filetypeList->addTopLevelItem(item);
2702 }
2703 }
2704 d->w->filetypeList->resizeColumnToContents(column: 0);
2705}
2706
2707KDesktopPropsPlugin::~KDesktopPropsPlugin() = default;
2708
2709void KDesktopPropsPlugin::slotAddFiletype()
2710{
2711 QMimeDatabase db;
2712 KMimeTypeChooserDialog dlg(i18n("Add File Type for %1", properties->url().fileName()),
2713 i18n("Select one or more file types to add:"),
2714 QStringList(), // no preselected mimetypes
2715 QString(),
2716 QStringList(),
2717 KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns,
2718 d->m_frame);
2719
2720 if (dlg.exec() == QDialog::Accepted) {
2721 const QStringList list = dlg.chooser()->mimeTypes();
2722 for (const QString &mimetype : list) {
2723 QMimeType p = db.mimeTypeForName(nameOrAlias: mimetype);
2724 if (!p.isValid()) {
2725 continue;
2726 }
2727
2728 bool found = false;
2729 int count = d->w->filetypeList->topLevelItemCount();
2730 for (int i = 0; !found && i < count; ++i) {
2731 if (d->w->filetypeList->topLevelItem(index: i)->text(column: 0) == mimetype) {
2732 found = true;
2733 }
2734 }
2735 if (!found) {
2736 QTreeWidgetItem *item = new QTreeWidgetItem();
2737 item->setText(column: 0, atext: p.name());
2738 item->setText(column: 1, atext: p.comment());
2739 d->w->filetypeList->addTopLevelItem(item);
2740 }
2741 d->w->filetypeList->resizeColumnToContents(column: 0);
2742 }
2743 }
2744 Q_EMIT changed();
2745}
2746
2747void KDesktopPropsPlugin::slotDelFiletype()
2748{
2749 QTreeWidgetItem *cur = d->w->filetypeList->currentItem();
2750 if (cur) {
2751 delete cur;
2752 Q_EMIT changed();
2753 }
2754}
2755
2756void KDesktopPropsPlugin::checkCommandChanged()
2757{
2758 if (KIO::DesktopExecParser::executableName(execLine: d->command()) != KIO::DesktopExecParser::executableName(execLine: d->m_origCommandStr)) {
2759 d->m_origCommandStr = d->command();
2760 }
2761}
2762
2763void KDesktopPropsPlugin::applyChanges()
2764{
2765 // qDebug();
2766 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2767 KJobWidgets::setWindow(job, widget: properties);
2768 job->exec();
2769 const QUrl url = job->mostLocalUrl();
2770
2771 if (!url.isLocalFile()) {
2772 KMessageBox::error(parent: nullptr, i18n("Could not save properties. Only entries on local file systems are supported."));
2773 properties->abortApplying();
2774 return;
2775 }
2776
2777 const QString path(url.toLocalFile());
2778
2779 // make sure the directory exists
2780 QDir().mkpath(dirPath: QFileInfo(path).absolutePath());
2781 QFile f(path);
2782 if (!f.open(flags: QIODevice::ReadWrite)) {
2783 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
2784 properties->abortApplying();
2785 return;
2786 }
2787
2788 // If the command is changed we reset certain settings that are strongly
2789 // coupled to the command.
2790 checkCommandChanged();
2791
2792 KDesktopFile origConfig(d->m_origDesktopFile);
2793 std::unique_ptr<KDesktopFile> _config(origConfig.copyTo(file: path));
2794 KConfigGroup config = _config->desktopGroup();
2795 config.writeEntry(key: "Type", QStringLiteral("Application"));
2796 config.writeEntry(key: "Comment", value: d->w->commentEdit->text());
2797 config.writeEntry(key: "Comment", value: d->w->commentEdit->text(), pFlags: KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
2798 config.writeEntry(key: "GenericName", value: d->w->genNameEdit->text());
2799 config.writeEntry(key: "GenericName", value: d->w->genNameEdit->text(), pFlags: KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
2800 config.writeEntry(key: "Exec", value: d->command());
2801 config.writeEntry(key: "Path", value: d->w->pathEdit->lineEdit()->text()); // not writePathEntry, see kservice.cpp
2802
2803 // Write mimeTypes
2804 QStringList mimeTypes;
2805 int count = d->w->filetypeList->topLevelItemCount();
2806 for (int i = 0; i < count; ++i) {
2807 QTreeWidgetItem *item = d->w->filetypeList->topLevelItem(index: i);
2808 QString preference = item->text(column: 2);
2809 mimeTypes.append(t: item->text(column: 0));
2810 if (!preference.isEmpty()) {
2811 mimeTypes.append(t: preference);
2812 }
2813 }
2814
2815 // qDebug() << mimeTypes;
2816 config.writeXdgListEntry(key: "MimeType", value: mimeTypes);
2817
2818 if (!d->w->nameEdit->isHidden()) {
2819 QString nameStr = d->w->nameEdit->text();
2820 config.writeEntry(key: "Name", value: nameStr);
2821 config.writeEntry(key: "Name", value: nameStr, pFlags: KConfigGroup::Persistent | KConfigGroup::Localized);
2822 }
2823
2824 config.writeEntry(key: "Terminal", value: d->m_terminalBool);
2825 config.writeEntry(key: "TerminalOptions", value: d->m_terminalOptionStr);
2826 config.writeEntry(key: "X-KDE-SubstituteUID", value: d->m_suidBool);
2827 config.writeEntry(key: "X-KDE-Username", value: d->m_suidUserStr);
2828 if (KIO::hasDiscreteGpu()) {
2829 config.writeEntry(key: "PrefersNonDefaultGPU", value: d->m_runOnDiscreteGpuBool);
2830 }
2831 config.writeEntry(key: "StartupNotify", value: d->m_startupBool);
2832 config.sync();
2833
2834 // KSycoca update needed?
2835 bool updateNeeded = !relativeAppsLocation(file: path).isEmpty();
2836 if (updateNeeded) {
2837 KBuildSycocaProgressDialog::rebuildKSycoca(parent: d->m_frame);
2838 }
2839
2840 setDirty(false);
2841}
2842
2843void KDesktopPropsPlugin::slotBrowseExec()
2844{
2845 QUrl f = QFileDialog::getOpenFileUrl(parent: d->m_frame);
2846 if (f.isEmpty()) {
2847 return;
2848 }
2849
2850 if (!f.isLocalFile()) {
2851 KMessageBox::information(parent: d->m_frame, i18n("Only executables on local file systems are supported."));
2852 return;
2853 }
2854
2855 const QString path = f.toLocalFile();
2856 d->w->programEdit->setText(path);
2857}
2858
2859void KDesktopPropsPlugin::slotAdvanced()
2860{
2861 auto *dlg = new QDialog(d->m_frame);
2862 dlg->setObjectName(QStringLiteral("KPropertiesDesktopAdv"));
2863 dlg->setModal(true);
2864 dlg->setAttribute(Qt::WA_DeleteOnClose);
2865 dlg->setWindowTitle(i18n("Advanced Options for %1", properties->url().fileName()));
2866
2867 d->m_uiAdvanced.reset(p: new Ui_KPropertiesDesktopAdvBase);
2868 QWidget *mainWidget = new QWidget(dlg);
2869 d->m_uiAdvanced->setupUi(mainWidget);
2870
2871 QDialogButtonBox *buttonBox = new QDialogButtonBox(dlg);
2872 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
2873 connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: dlg, slot: &QDialog::accept);
2874 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: dlg, slot: &QDialog::reject);
2875
2876 QVBoxLayout *layout = new QVBoxLayout(dlg);
2877 layout->addWidget(mainWidget);
2878 layout->addWidget(buttonBox);
2879
2880 // If the command is changed we reset certain settings that are strongly
2881 // coupled to the command.
2882 checkCommandChanged();
2883
2884 // check to see if we use konsole if not do not add the nocloseonexit
2885 // because we don't know how to do this on other terminal applications
2886 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
2887 QString preferredTerminal = confGroup.readPathEntry(key: "TerminalApplication", QStringLiteral("konsole"));
2888
2889 bool terminalCloseBool = false;
2890
2891 if (preferredTerminal == QLatin1String("konsole")) {
2892 terminalCloseBool = d->m_terminalOptionStr.contains(s: QLatin1String("--noclose"));
2893 d->m_uiAdvanced->terminalCloseCheck->setChecked(terminalCloseBool);
2894 d->m_terminalOptionStr.remove(QStringLiteral("--noclose"));
2895 } else {
2896 d->m_uiAdvanced->terminalCloseCheck->hide();
2897 }
2898
2899 d->m_uiAdvanced->terminalCheck->setChecked(d->m_terminalBool);
2900 d->m_uiAdvanced->terminalEdit->setText(d->m_terminalOptionStr);
2901 d->m_uiAdvanced->terminalCloseCheck->setEnabled(d->m_terminalBool);
2902 d->m_uiAdvanced->terminalEdit->setEnabled(d->m_terminalBool);
2903 d->m_uiAdvanced->terminalEditLabel->setEnabled(d->m_terminalBool);
2904
2905 d->m_uiAdvanced->suidCheck->setChecked(d->m_suidBool);
2906 d->m_uiAdvanced->suidEdit->setText(d->m_suidUserStr);
2907 d->m_uiAdvanced->suidEdit->setEnabled(d->m_suidBool);
2908 d->m_uiAdvanced->suidEditLabel->setEnabled(d->m_suidBool);
2909
2910 if (KIO::hasDiscreteGpu()) {
2911 d->m_uiAdvanced->discreteGpuCheck->setChecked(d->m_runOnDiscreteGpuBool);
2912 } else {
2913 d->m_uiAdvanced->discreteGpuGroupBox->hide();
2914 }
2915
2916 d->m_uiAdvanced->startupInfoCheck->setChecked(d->m_startupBool);
2917
2918 // Provide username completion up to 1000 users.
2919 const int maxEntries = 1000;
2920 QStringList userNames = KUser::allUserNames(maxCount: maxEntries);
2921 if (userNames.size() < maxEntries) {
2922 KCompletion *kcom = new KCompletion;
2923 kcom->setOrder(KCompletion::Sorted);
2924 d->m_uiAdvanced->suidEdit->setCompletionObject(kcom, handle: true);
2925 d->m_uiAdvanced->suidEdit->setAutoDeleteCompletionObject(true);
2926 d->m_uiAdvanced->suidEdit->setCompletionMode(KCompletion::CompletionAuto);
2927 kcom->setItems(userNames);
2928 }
2929
2930 connect(sender: d->m_uiAdvanced->terminalEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2931 connect(sender: d->m_uiAdvanced->terminalCloseCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
2932 connect(sender: d->m_uiAdvanced->terminalCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
2933 connect(sender: d->m_uiAdvanced->suidCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
2934 connect(sender: d->m_uiAdvanced->suidEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2935 connect(sender: d->m_uiAdvanced->discreteGpuCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
2936 connect(sender: d->m_uiAdvanced->startupInfoCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
2937
2938 QObject::connect(sender: dlg, signal: &QDialog::accepted, context: this, slot: [this]() {
2939 d->m_terminalOptionStr = d->m_uiAdvanced->terminalEdit->text().trimmed();
2940 d->m_terminalBool = d->m_uiAdvanced->terminalCheck->isChecked();
2941 d->m_suidBool = d->m_uiAdvanced->suidCheck->isChecked();
2942 d->m_suidUserStr = d->m_uiAdvanced->suidEdit->text().trimmed();
2943 if (KIO::hasDiscreteGpu()) {
2944 d->m_runOnDiscreteGpuBool = d->m_uiAdvanced->discreteGpuCheck->isChecked();
2945 }
2946 d->m_startupBool = d->m_uiAdvanced->startupInfoCheck->isChecked();
2947
2948 if (d->m_uiAdvanced->terminalCloseCheck->isChecked()) {
2949 d->m_terminalOptionStr.append(s: QLatin1String(" --noclose"));
2950 }
2951 });
2952
2953 dlg->show();
2954}
2955
2956bool KDesktopPropsPlugin::supports(const KFileItemList &_items)
2957{
2958 if (_items.count() != 1) {
2959 return false;
2960 }
2961
2962 const KFileItem &item = _items.first();
2963
2964 // check if desktop file
2965 if (!item.isDesktopFile()) {
2966 return false;
2967 }
2968
2969 // open file and check type
2970 const auto [url, isLocal] = item.isMostLocalUrl();
2971 if (!isLocal) {
2972 return false;
2973 }
2974
2975 KDesktopFile config(url.toLocalFile());
2976 return config.hasApplicationType() && KAuthorized::authorize(action: KAuthorized::RUN_DESKTOP_FILES) && KAuthorized::authorize(action: KAuthorized::SHELL_ACCESS);
2977}
2978
2979QString KDesktopPropsPlugin::KDesktopPropsPluginPrivate::command() const
2980{
2981 QStringList execSplit = KShell::splitArgs(cmd: w->envarsEdit->text()) + QStringList(w->programEdit->text()) + KShell::splitArgs(cmd: w->argumentsEdit->text());
2982
2983 if (KShell::splitArgs(cmd: w->envarsEdit->text()).length()) {
2984 execSplit.push_front(t: QLatin1String("env"));
2985 }
2986
2987 return KShell::joinArgs(args: execSplit);
2988}
2989
2990#include "moc_kpropertiesdialogbuiltin_p.cpp"
2991

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