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 "kfileitem.h"
15#include "kio_widgets_debug.h"
16
17#include <gpudetection_p.h>
18#include <kbuildsycocaprogressdialog.h>
19#include <kdirnotify.h>
20#include <kfileitemlistproperties.h>
21#include <kio/chmodjob.h>
22#include <kio/copyjob.h>
23#include <kio/desktopexecparser.h>
24#include <kio/directorysizejob.h>
25#include <kio/jobuidelegate.h>
26#include <kio/renamedialog.h>
27#include <kio/statjob.h>
28#include <kmountpoint.h>
29#include <kprotocolinfo.h>
30#include <kprotocolmanager.h>
31#include <kurlrequester.h>
32
33#include <KApplicationTrader>
34#include <KAuthorized>
35#include <KCapacityBar>
36#include <KColorScheme>
37#include <KCompletion>
38#include <KConfigGroup>
39#include <KDesktopFile>
40#include <KDialogJobUiDelegate>
41#include <KIO/ApplicationLauncherJob>
42#include <KIO/FileSystemFreeSpaceJob>
43#include <KIO/OpenFileManagerWindowJob>
44#include <KIconButton>
45#include <KJobUiDelegate>
46#include <KJobWidgets>
47#include <KLazyLocalizedString>
48#include <KLineEdit>
49#include <KMessageBox>
50#include <KMessageWidget>
51#include <KMimeTypeChooser>
52#include <KMimeTypeEditor>
53#include <KSeparator>
54#include <KService>
55#include <KSharedConfig>
56#include <KShell>
57#include <KSqueezedTextLabel>
58#include <KSycoca>
59
60#include <QCheckBox>
61#include <QClipboard>
62#include <QComboBox>
63
64#ifdef WITH_QTDBUS
65#include <QDBusConnection>
66#include <QDBusInterface>
67#include <QDBusReply>
68#endif
69
70#include <QDialogButtonBox>
71#include <QFile>
72#include <QFileDialog>
73#include <QFileInfo>
74#include <QFileSystemWatcher>
75#include <QFutureWatcher>
76#include <QLabel>
77#include <QLayout>
78#include <QLocale>
79#include <QMimeDatabase>
80#include <QProgressBar>
81#include <QPushButton>
82#include <QSizePolicy>
83#include <QStyle>
84#include <QtConcurrentRun>
85#include <QTextDocumentFragment>
86
87#include "ui_checksumswidget.h"
88#include "ui_kfilepropspluginwidget.h"
89#include "ui_kpropertiesdesktopadvbase.h"
90#include "ui_kpropertiesdesktopbase.h"
91
92#if HAVE_POSIX_ACL
93#include "kacleditwidget.h"
94#endif
95#include <cerrno>
96extern "C" {
97#if HAVE_SYS_XATTR_H
98#include <sys/xattr.h>
99#endif
100#if HAVE_SYS_EXTATTR_H
101#include <sys/extattr.h>
102#endif
103#if HAVE_SYS_MOUNT_H
104#include <sys/mount.h>
105#endif
106}
107
108using namespace KDEPrivate;
109
110static QString couldNotSaveMsg(const QString &path)
111{
112 return xi18nc("@info", "Could not save properties due to insufficient write access to:<nl/><filename>%1</filename>.", path);
113}
114static QString nameFromFileName(QString nameStr)
115{
116 if (nameStr.endsWith(s: QLatin1String(".desktop"))) {
117 nameStr.chop(n: 8);
118 }
119 // Make it human-readable (%2F => '/', ...)
120 nameStr = KIO::decodeFileName(str: nameStr);
121 return nameStr;
122}
123
124class KFilePropsPlugin::KFilePropsPluginPrivate
125{
126public:
127 KFilePropsPluginPrivate()
128 : m_ui(new Ui_KFilePropsPluginWidget())
129 {
130 m_ui->setupUi(&m_mainWidget);
131 }
132
133 ~KFilePropsPluginPrivate()
134 {
135 if (dirSizeJob) {
136 dirSizeJob->kill();
137 }
138 }
139
140 void hideMountPointLabels()
141 {
142 m_ui->fsLabel_Left->hide();
143 m_ui->fsLabel->hide();
144
145 m_ui->mountPointLabel_Left->hide();
146 m_ui->mountPointLabel->hide();
147
148 m_ui->mountSrcLabel_Left->hide();
149 m_ui->mountSrcLabel->hide();
150 }
151
152 QWidget m_mainWidget;
153 std::unique_ptr<Ui_KFilePropsPluginWidget> m_ui;
154 KIO::DirectorySizeJob *dirSizeJob = nullptr;
155 QTimer *dirSizeUpdateTimer = nullptr;
156 bool bMultiple;
157 bool bIconChanged;
158 bool bKDesktopMode;
159 bool bDesktopFile;
160 QString mimeType;
161 QString oldFileName;
162
163 QString m_sRelativePath;
164 bool m_bFromTemplate;
165
166 /*
167 * The initial filename
168 */
169 QString oldName;
170};
171
172KFilePropsPlugin::KFilePropsPlugin(KPropertiesDialog *_props)
173 : KPropertiesDialogPlugin(_props)
174 , d(new KFilePropsPluginPrivate)
175{
176 const auto itemsList = properties->items();
177 d->bMultiple = (itemsList.count() > 1);
178 d->bIconChanged = false;
179 d->bDesktopFile = KDesktopPropsPlugin::supports(items: itemsList);
180 // qDebug() << "KFilePropsPlugin::KFilePropsPlugin bMultiple=" << d->bMultiple;
181
182 // We set this data from the first item, and we'll
183 // check that the other items match against it, resetting when not.
184 const KFileItem firstItem = properties->item();
185 auto [url, isLocal] = firstItem.isMostLocalUrl();
186 bool isReallyLocal = firstItem.url().isLocalFile();
187 KMountPoint::Ptr mp;
188 if (isLocal || isReallyLocal) {
189 mp = KMountPoint::currentMountPoints(infoNeeded: KMountPoint::DetailsNeededFlag::NeedMountOptions).findByPath(path: url.toLocalFile());
190 if (!mp) {
191 qCWarning(KIO_WIDGETS) << "Could not find mount point for" << url;
192 }
193 }
194 bool bDesktopFile = firstItem.isDesktopFile();
195 mode_t mode = firstItem.mode();
196 bool hasDirs = firstItem.isDir() && !firstItem.isLink();
197 bool hasRoot = url.path() == QLatin1String("/");
198 bool hasAccessTime =
199 mp ? !(mp->mountOptions().contains(str: QLatin1String("noatime")) || (hasDirs && mp->mountOptions().contains(str: QLatin1String("nodiratime")))) : true;
200 QString iconStr = firstItem.iconName();
201 QString directory = properties->url().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
202 QString protocol = properties->url().scheme();
203 d->bKDesktopMode = protocol == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
204 QString mimeComment = firstItem.mimeComment();
205 d->mimeType = firstItem.mimetype();
206 KIO::filesize_t totalSize = firstItem.size();
207 QString magicMimeName;
208 QString magicMimeComment;
209 QMimeDatabase db;
210 if (isLocal) {
211 QMimeType magicMimeType = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchContent);
212 if (magicMimeType.isValid() && !magicMimeType.isDefault()) {
213 magicMimeName = magicMimeType.name();
214 magicMimeComment = magicMimeType.comment();
215 }
216 }
217#ifdef Q_OS_WIN
218 if (isReallyLocal) {
219 directory = QDir::toNativeSeparators(directory.mid(1));
220 }
221#endif
222
223 // Those things only apply to 'single file' mode
224 QString filename;
225 bool isTrash = false;
226 d->m_bFromTemplate = false;
227
228 // And those only to 'multiple' mode
229 uint iDirCount = hasDirs ? 1 : 0;
230 uint iFileCount = 1 - iDirCount;
231
232 properties->addPage(widget: &d->m_mainWidget, i18nc("@title:tab File properties", "&General"));
233
234 d->m_ui->symlinkTargetMessageWidget->hide();
235
236 if (!d->bMultiple) {
237 QString path;
238 if (!d->m_bFromTemplate) {
239 isTrash = (properties->url().scheme() == QLatin1String("trash"));
240 // Extract the full name, but without file: for local files
241 path = properties->url().toDisplayString(options: QUrl::PreferLocalFile);
242 } else {
243 path = Utils::concatPaths(path1: properties->currentDir().path(), path2: properties->defaultName());
244 directory = properties->currentDir().toDisplayString(options: QUrl::PreferLocalFile);
245 }
246
247 if (d->bDesktopFile) {
248 determineRelativePath(path);
249 }
250
251 // Extract the file name only
252 filename = properties->defaultName();
253 if (filename.isEmpty()) { // no template
254 const QFileInfo finfo(firstItem.name()); // this gives support for UDS_NAME, e.g. for kio_trash or kio_system
255 filename = finfo.fileName(); // Make sure only the file's name is displayed (#160964).
256 } else {
257 d->m_bFromTemplate = true;
258 setDirty(); // to enforce that the copy happens
259 }
260 d->oldFileName = filename;
261
262 // Make it human-readable
263 filename = nameFromFileName(nameStr: filename);
264 d->oldName = filename;
265 } else {
266 // Multiple items: see what they have in common
267 for (const auto &item : itemsList) {
268 if (item == firstItem) {
269 continue;
270 }
271
272 const QUrl url = item.url();
273 // qDebug() << "KFilePropsPlugin::KFilePropsPlugin " << url.toDisplayString();
274 // The list of things we check here should match the variables defined
275 // at the beginning of this method.
276 if (url.isLocalFile() != isLocal) {
277 isLocal = false; // not all local
278 }
279 if (bDesktopFile && item.isDesktopFile() != bDesktopFile) {
280 bDesktopFile = false; // not all desktop files
281 }
282 if (item.mode() != mode) {
283 mode = static_cast<mode_t>(0);
284 }
285 if (KIO::iconNameForUrl(url) != iconStr) {
286 iconStr = QStringLiteral("document-multiple");
287 }
288 if (url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() != directory) {
289 directory.clear();
290 }
291 if (url.scheme() != protocol) {
292 protocol.clear();
293 }
294 if (!mimeComment.isNull() && item.mimeComment() != mimeComment) {
295 mimeComment.clear();
296 }
297 if (isLocal && !magicMimeComment.isNull()) {
298 QMimeType magicMimeType = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchContent);
299 if (magicMimeType.isValid() && magicMimeType.comment() != magicMimeComment) {
300 magicMimeName.clear();
301 magicMimeComment.clear();
302 }
303 }
304
305 if (isLocal && url.path() == QLatin1String("/")) {
306 hasRoot = true;
307 }
308 if (item.isDir() && !item.isLink()) {
309 iDirCount++;
310 hasDirs = true;
311 } else {
312 iFileCount++;
313 totalSize += item.size();
314 }
315 }
316 }
317
318 if (!isReallyLocal && !protocol.isEmpty()) {
319 directory += QLatin1String(" (") + protocol + QLatin1Char(')');
320 }
321
322 if (!isTrash //
323 && (bDesktopFile || Utils::isDirMask(mode)) //
324 && !d->bMultiple // not implemented for multiple
325 && enableIconButton()) {
326 d->m_ui->iconLabel->hide();
327
328 const int bsize = 66 + (2 * d->m_ui->iconButton->style()->pixelMetric(metric: QStyle::PM_ButtonMargin));
329 d->m_ui->iconButton->setFixedSize(w: bsize, h: bsize);
330 d->m_ui->iconButton->setIconSize(48);
331 if (bDesktopFile && isLocal) {
332 const KDesktopFile config(url.toLocalFile());
333 if (config.hasDeviceType()) {
334 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Device);
335 } else {
336 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Application);
337 }
338 } else {
339 d->m_ui->iconButton->setIconType(group: KIconLoader::Desktop, context: KIconLoader::Place);
340 }
341
342 d->m_ui->iconButton->setIcon(iconStr);
343 connect(sender: d->m_ui->iconButton, signal: &KIconButton::iconChanged, context: this, slot: &KFilePropsPlugin::slotIconChanged);
344 } else {
345 d->m_ui->iconButton->hide();
346
347 const int bsize = 66 + (2 * d->m_ui->iconLabel->style()->pixelMetric(metric: QStyle::PM_ButtonMargin));
348 d->m_ui->iconLabel->setFixedSize(w: bsize, h: bsize);
349 d->m_ui->iconLabel->setPixmap(QIcon::fromTheme(name: iconStr, fallback: QIcon::fromTheme(QStringLiteral("unknown"))).pixmap(extent: 48));
350 }
351
352 KFileItemListProperties itemList(KFileItemList{firstItem});
353 if (d->bMultiple || isTrash || hasRoot || !(d->m_bFromTemplate || itemList.supportsMoving())) {
354 d->m_ui->fileNameLineEdit->hide();
355 setFileNameReadOnly(true);
356 if (d->bMultiple) {
357 d->m_ui->fileNameLabel->setText(KIO::itemsSummaryString(items: iFileCount + iDirCount, files: iFileCount, dirs: iDirCount, size: 0, showSize: false));
358 }
359 } else {
360 d->m_ui->fileNameLabel->hide();
361
362 d->m_ui->fileNameLineEdit->setText(filename);
363 connect(sender: d->m_ui->fileNameLineEdit, signal: &QLineEdit::textChanged, context: this, slot: &KFilePropsPlugin::nameFileChanged);
364 }
365 d->m_ui->fileNameLineEdit->setPlaceholderText(hasDirs ? i18nc("@info:placeholder", "Enter folder name") : i18nc("@info:placeholder", "Enter file name"));
366
367 // Mimetype widgets
368 if (!mimeComment.isEmpty() && !isTrash) {
369 d->m_ui->mimeCommentLabel->setText(mimeComment);
370 d->m_ui->mimeCommentLabel->setToolTip(d->mimeType);
371
372 const int hSpacing = properties->style()->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing);
373 d->m_ui->defaultHandlerLayout->setSpacing(hSpacing);
374
375#ifndef Q_OS_WIN
376 updateDefaultHandler(mimeType: d->mimeType);
377 connect(sender: KSycoca::self(), signal: &KSycoca::databaseChanged, context: this, slot: [this] {
378 updateDefaultHandler(mimeType: d->mimeType);
379 });
380
381 connect(sender: d->m_ui->configureMimeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotEditFileType);
382#endif
383
384 } else {
385 d->m_ui->typeLabel->hide();
386 d->m_ui->mimeCommentLabel->hide();
387 d->m_ui->configureMimeBtn->hide();
388
389 d->m_ui->defaultHandlerLabel_Left->hide();
390 d->m_ui->defaultHandlerIcon->hide();
391 d->m_ui->defaultHandlerLabel->hide();
392 }
393
394#ifdef Q_OS_WIN
395 d->m_ui->defaultHandlerLabel_Left->hide();
396 d->m_ui->defaultHandlerIcon->hide();
397 d->m_ui->defaultHandlerLabel->hide();
398#endif
399
400 if (!magicMimeComment.isEmpty() && magicMimeComment != mimeComment) {
401 d->m_ui->magicMimeCommentLabel->setText(magicMimeComment);
402 d->m_ui->magicMimeCommentLabel->setToolTip(magicMimeName);
403 } else {
404 d->m_ui->contentLabel->hide();
405 d->m_ui->magicMimeCommentLabel->hide();
406 }
407
408 d->m_ui->configureMimeBtn->setVisible(KAuthorized::authorizeAction(QStringLiteral("editfiletype")) && !d->m_ui->defaultHandlerLabel->isHidden());
409
410 // Location:
411 if (!directory.isEmpty()) {
412 d->m_ui->locationLabel->setText(directory);
413
414 // Layout direction for this label is always LTR; but if we are in RTL mode,
415 // align the text to the right, otherwise the text is on the wrong side of the dialog
416 if (properties->layoutDirection() == Qt::RightToLeft) {
417 d->m_ui->locationLabel->setAlignment(Qt::AlignRight);
418 }
419 }
420
421 // Size widgets
422 if (!hasDirs) { // Only files [and symlinks]
423 d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)").arg(args: KIO::convertSize(size: totalSize), args: QLocale().toString(i: totalSize)));
424 d->m_ui->sizeBtnWidget->hide();
425 } else { // Directory
426 connect(sender: d->m_ui->calculateSizeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeDetermine);
427 connect(sender: d->m_ui->stopCalculateSizeBtn, signal: &QAbstractButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeStop);
428
429 if (auto filelight = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"))) {
430 d->m_ui->sizeDetailsBtn->setText(i18nc("@action:button", "Explore in %1", filelight->name()));
431 d->m_ui->sizeDetailsBtn->setIcon(QIcon::fromTheme(name: filelight->icon()));
432 connect(sender: d->m_ui->sizeDetailsBtn, signal: &QPushButton::clicked, context: this, slot: &KFilePropsPlugin::slotSizeDetails);
433 } else {
434 d->m_ui->sizeDetailsBtn->hide();
435 }
436
437 // sizelay->addStretch(10); // so that the buttons don't grow horizontally
438
439 // auto-launch for local dirs only, and not for '/'
440 if (isLocal && !hasRoot) {
441 d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
442 slotSizeDetermine();
443 } else {
444 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
445 }
446 }
447
448 // Symlink widgets
449 if (!d->bMultiple && firstItem.isLink()) {
450 d->m_ui->symlinkTargetEdit->setPlaceholderText(i18nc("@info:placeholder", "Enter target location"));
451 d->m_ui->symlinkTargetEdit->setText(firstItem.linkDest());
452 connect(sender: d->m_ui->symlinkTargetEdit, signal: &QLineEdit::textChanged, context: this, slot: [this](const QString &text) {
453 setDirty();
454 d->m_ui->symlinkTargetOpenDir->setToolTip(xi18nc("@info:tooltip Go to path %1", "Show <filename>%1</filename>", text));
455 d->m_ui->symlinkTargetOpenDir->setAccessibleDescription(QTextDocumentFragment::fromHtml(html: d->m_ui->symlinkTargetOpenDir->toolTip()).toPlainText());
456 });
457 d->m_ui->symlinkTargetOpenDir->setToolTip(xi18nc("@info:tooltip Go to path %1", "Show <filename>%1</filename>", firstItem.linkDest()));
458 d->m_ui->symlinkTargetOpenDir->setAccessibleDescription(QTextDocumentFragment::fromHtml(html: d->m_ui->symlinkTargetOpenDir->toolTip()).toPlainText());
459
460 connect(sender: d->m_ui->symlinkTargetOpenDir, signal: &QPushButton::clicked, context: this, slot: [this] {
461 // The most local URL is needed to resolve symlinks in virtual locations like "desktop:/"
462 const QUrl url = properties->item().isMostLocalUrl().url;
463 const QUrl resolvedTargetLocation = url.resolved(relative: QUrl(d->m_ui->symlinkTargetEdit->text()));
464
465 KIO::StatJob *statJob = KIO::stat(url: resolvedTargetLocation, side: KIO::StatJob::SourceSide, details: KIO::StatNoDetails, flags: KIO::HideProgressInfo);
466 connect(sender: statJob, signal: &KJob::finished, context: this, slot: [this, statJob] {
467 if (statJob->error()) {
468 d->m_ui->symlinkTargetMessageWidget->setText(statJob->errorString());
469 d->m_ui->symlinkTargetMessageWidget->animatedShow();
470 return;
471 }
472
473 KIO::highlightInFileManager(urls: {statJob->url()});
474 properties->close();
475 });
476 });
477 } else {
478 d->m_ui->symlinkTargetLabel->hide();
479 d->m_ui->symlinkTargetEdit->hide();
480 d->m_ui->symlinkTargetOpenDir->hide();
481 }
482
483 // Time widgets
484 if (!d->bMultiple) {
485 QLocale locale;
486 if (const QDateTime dt = firstItem.time(which: KFileItem::CreationTime); !dt.isNull()) {
487 d->m_ui->createdTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
488 } else {
489 d->m_ui->createdTimeLabel->hide();
490 d->m_ui->createdTimeLabel_Left->hide();
491 }
492
493 if (const QDateTime dt = firstItem.time(which: KFileItem::ModificationTime); !dt.isNull()) {
494 d->m_ui->modifiedTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
495 } else {
496 d->m_ui->modifiedTimeLabel->hide();
497 d->m_ui->modifiedTimeLabel_Left->hide();
498 }
499
500 if (const QDateTime dt = firstItem.time(which: KFileItem::AccessTime); !dt.isNull() && hasAccessTime) {
501 d->m_ui->accessTimeLabel->setText(locale.toString(dateTime: dt, format: QLocale::LongFormat));
502 } else {
503 d->m_ui->accessTimeLabel->hide();
504 d->m_ui->accessTimeLabel_Left->hide();
505 }
506 } else {
507 d->m_ui->createdTimeLabel->hide();
508 d->m_ui->createdTimeLabel_Left->hide();
509 d->m_ui->modifiedTimeLabel->hide();
510 d->m_ui->modifiedTimeLabel_Left->hide();
511 d->m_ui->accessTimeLabel->hide();
512 d->m_ui->accessTimeLabel_Left->hide();
513 }
514
515 // File system and mount point widgets
516 if (hasDirs) { // only for directories
517 if (isLocal) {
518 if (mp) {
519 d->m_ui->fsLabel->setText(mp->mountType());
520 d->m_ui->mountPointLabel->setText(mp->mountPoint());
521 d->m_ui->mountSrcLabel->setText(mp->mountedFrom());
522 } else {
523 d->hideMountPointLabels();
524 }
525 } else {
526 d->hideMountPointLabels();
527 }
528
529 KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url);
530 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotFreeSpaceResult);
531 } else {
532 d->m_ui->fsSeparator->hide();
533 d->m_ui->freespaceLabel->hide();
534 d->m_ui->capacityBar->hide();
535 d->hideMountPointLabels();
536 }
537
538 // UDSEntry extra fields
539 // To determine extra fields, use the original URL, not the mostLocalUrl.
540 // e.g. trash:/foo will point to file:/...local/share/Trash/files/foo and therefore not have any fields.
541 if (const auto extraFields = KProtocolInfo::extraFields(url: firstItem.url()); !d->bMultiple && !extraFields.isEmpty()) {
542 int curRow = d->m_ui->gridLayout->rowCount();
543 KSeparator *sep = new KSeparator(Qt::Horizontal, &d->m_mainWidget);
544 d->m_ui->gridLayout->addWidget(sep, row: curRow++, column: 0, rowSpan: 1, columnSpan: 3);
545
546 QLocale locale;
547 for (int i = 0; i < extraFields.count(); ++i) {
548 const auto &field = extraFields.at(i);
549
550 QString text = firstItem.entry().stringValue(field: KIO::UDSEntry::UDS_EXTRA + i);
551 if (field.type == KProtocolInfo::ExtraField::Invalid || text.isEmpty()) {
552 continue;
553 }
554
555 if (field.type == KProtocolInfo::ExtraField::DateTime) {
556 const QDateTime date = QDateTime::fromString(string: text, format: Qt::ISODate);
557 if (!date.isValid()) {
558 continue;
559 }
560
561 text = locale.toString(dateTime: date, format: QLocale::LongFormat);
562 }
563
564 auto *label = new QLabel(i18n("%1:", field.name), &d->m_mainWidget);
565 d->m_ui->gridLayout->addWidget(label, row: curRow, column: 0, Qt::AlignRight);
566
567 auto *squeezedLabel = new KSqueezedTextLabel(text, &d->m_mainWidget);
568 if (properties->layoutDirection() == Qt::RightToLeft) {
569 squeezedLabel->setAlignment(Qt::AlignRight);
570 } else {
571 squeezedLabel->setLayoutDirection(Qt::LeftToRight);
572 }
573
574 d->m_ui->gridLayout->addWidget(squeezedLabel, row: curRow++, column: 1);
575 squeezedLabel->setTextInteractionFlags(Qt::TextBrowserInteraction | Qt::TextSelectableByKeyboard);
576 label->setBuddy(squeezedLabel);
577 }
578 }
579}
580
581bool KFilePropsPlugin::enableIconButton() const
582{
583 const KFileItem item = properties->item();
584
585 // desktop files are special, files in /usr/share/applications can be
586 // edited by overlaying them in .local/share/applications
587 // https://bugs.kde.org/show_bug.cgi?id=429613
588 if (item.isDesktopFile()) {
589 return true;
590 }
591
592 // If the current item is a directory, check if it's writable,
593 // so we can create/update a .directory
594 // Current item is a file, same thing: check if it is writable
595 if (item.isWritable()) {
596 // exclude remote dirs as changing the icon has no effect (bug 205954)
597 if (item.isLocalFile() || item.url().scheme() == QLatin1String("desktop")) {
598 return true;
599 }
600 }
601
602 return false;
603}
604
605void KFilePropsPlugin::setFileNameReadOnly(bool readOnly)
606{
607 Q_ASSERT(readOnly); // false isn't supported
608
609 if (readOnly) {
610 Q_ASSERT(!d->m_bFromTemplate);
611
612 d->m_ui->fileNameLineEdit->hide();
613
614 d->m_ui->fileNameLabel->show();
615 d->m_ui->fileNameLabel->setText(d->oldName); // will get overwritten if d->bMultiple
616 }
617}
618
619void KFilePropsPlugin::slotEditFileType()
620{
621 QString mime;
622 if (d->mimeType == QLatin1String("application/octet-stream")) {
623 const int pos = d->oldFileName.lastIndexOf(c: QLatin1Char('.'));
624 if (pos != -1) {
625 mime = QLatin1Char('*') + QStringView(d->oldFileName).mid(pos);
626 } else {
627 mime = QStringLiteral("*");
628 }
629 } else {
630 mime = d->mimeType;
631 }
632 KMimeTypeEditor::editMimeType(mimeType: mime, widget: properties->window());
633}
634
635void KFilePropsPlugin::slotIconChanged()
636{
637 d->bIconChanged = true;
638 Q_EMIT changed();
639}
640
641void KFilePropsPlugin::nameFileChanged(const QString &text)
642{
643 properties->buttonBox()->button(which: QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
644 Q_EMIT changed();
645}
646
647static QString relativeAppsLocation(const QString &file)
648{
649 // Don't resolve symlinks, so that editing /usr/share/applications/foo.desktop that is
650 // a symlink works
651 const QString absolute = QFileInfo(file).absoluteFilePath();
652 const QStringList dirs = QStandardPaths::standardLocations(type: QStandardPaths::ApplicationsLocation);
653 for (const QString &base : dirs) {
654 QDir base_dir = QDir(base);
655 if (base_dir.exists() && absolute.startsWith(s: base_dir.canonicalPath())) {
656 return absolute.mid(position: base.length() + 1);
657 }
658 }
659 return QString(); // return empty if the file is not in apps
660}
661
662void KFilePropsPlugin::determineRelativePath(const QString &path)
663{
664 // now let's make it relative
665 d->m_sRelativePath = relativeAppsLocation(file: path);
666}
667
668void KFilePropsPlugin::slotFreeSpaceResult(KJob *_job)
669{
670 const auto job = qobject_cast<KIO::FileSystemFreeSpaceJob *>(object: _job);
671 Q_ASSERT(job);
672 if (!job->error()) {
673 const qint64 size = job->size();
674 const qint64 available = job->availableSize();
675 const quint64 used = size - available;
676 const int percentUsed = (size == 0) ? 0 : qRound(d: 100.0 * qreal(used) / qreal(size));
677
678 d->m_ui->capacityBar->setText(i18nc("Available space out of total partition size (percent used)",
679 "%1 free of %2 (%3% used)",
680 KIO::convertSize(available),
681 KIO::convertSize(size),
682 percentUsed));
683
684 d->m_ui->capacityBar->setValue(percentUsed);
685 } else {
686 d->m_ui->capacityBar->setText(i18nc("@info:status", "Unknown size"));
687 d->m_ui->capacityBar->setValue(0);
688 }
689}
690
691void KFilePropsPlugin::slotDirSizeUpdate()
692{
693 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
694 KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
695 KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
696 d->m_ui->sizeLabel->setText(i18n("Calculating... %1 (%2)\n%3, %4",
697 KIO::convertSize(totalSize),
698 QLocale().toString(totalSize),
699 i18np("1 file", "%1 files", totalFiles),
700 i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
701}
702
703void KFilePropsPlugin::slotDirSizeFinished(KJob *job)
704{
705 if (job->error()) {
706 d->m_ui->sizeLabel->setText(job->errorString());
707 } else {
708 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
709 KIO::filesize_t totalFiles = d->dirSizeJob->totalFiles();
710 KIO::filesize_t totalSubdirs = d->dirSizeJob->totalSubdirs();
711 d->m_ui->sizeLabel->setText(QStringLiteral("%1 (%2)\n%3, %4")
712 .arg(args: KIO::convertSize(size: totalSize),
713 args: QLocale().toString(i: totalSize),
714 i18np("1 file", "%1 files", totalFiles),
715 i18np("1 sub-folder", "%1 sub-folders", totalSubdirs)));
716 }
717 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
718 // just in case you change something and try again :)
719 d->m_ui->calculateSizeBtn->setText(i18n("Refresh"));
720 d->m_ui->calculateSizeBtn->setEnabled(true);
721 d->dirSizeJob = nullptr;
722 delete d->dirSizeUpdateTimer;
723 d->dirSizeUpdateTimer = nullptr;
724}
725
726void KFilePropsPlugin::slotSizeDetermine()
727{
728 d->m_ui->sizeLabel->setText(i18n("Calculating...\n"));
729 // qDebug() << "properties->item()=" << properties->item() << "URL=" << properties->item().url();
730
731 d->dirSizeJob = KIO::directorySize(lstItems: properties->items());
732 d->dirSizeUpdateTimer = new QTimer(this);
733 connect(sender: d->dirSizeUpdateTimer, signal: &QTimer::timeout, context: this, slot: &KFilePropsPlugin::slotDirSizeUpdate);
734 d->dirSizeUpdateTimer->start(msec: 500);
735 connect(sender: d->dirSizeJob, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotDirSizeFinished);
736 d->m_ui->stopCalculateSizeBtn->setEnabled(true);
737 d->m_ui->calculateSizeBtn->setEnabled(false);
738
739 // also update the "Free disk space" display
740 if (!d->m_ui->capacityBar->isHidden()) {
741 const KFileItem item = properties->item();
742 KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(url: item.url());
743 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotFreeSpaceResult);
744 }
745}
746
747void KFilePropsPlugin::slotSizeStop()
748{
749 if (d->dirSizeJob) {
750 KIO::filesize_t totalSize = d->dirSizeJob->totalSize();
751 d->m_ui->sizeLabel->setText(i18n("At least %1\n", KIO::convertSize(totalSize)));
752 d->dirSizeJob->kill();
753 d->dirSizeJob = nullptr;
754 }
755 if (d->dirSizeUpdateTimer) {
756 d->dirSizeUpdateTimer->stop();
757 }
758
759 d->m_ui->stopCalculateSizeBtn->setEnabled(false);
760 d->m_ui->calculateSizeBtn->setEnabled(true);
761}
762
763void KFilePropsPlugin::slotSizeDetails()
764{
765 // Open the current folder in filelight
766 KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
767 if (service) {
768 auto *job = new KIO::ApplicationLauncherJob(service);
769 job->setUrls({properties->url()});
770 job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, properties));
771 job->start();
772 }
773}
774
775KFilePropsPlugin::~KFilePropsPlugin() = default;
776
777bool KFilePropsPlugin::supports(const KFileItemList & /*_items*/)
778{
779 return true;
780}
781
782void KFilePropsPlugin::applyChanges()
783{
784 if (d->dirSizeJob) {
785 slotSizeStop();
786 }
787
788 // qDebug() << "KFilePropsPlugin::applyChanges";
789
790 if (!d->m_ui->fileNameLineEdit->isHidden()) {
791 QString n = d->m_ui->fileNameLineEdit->text();
792 // Remove trailing spaces (#4345)
793 while (!n.isEmpty() && n[n.length() - 1].isSpace()) {
794 n.chop(n: 1);
795 }
796 if (n.isEmpty()) {
797 KMessageBox::error(parent: properties, i18n("The new file name is empty."));
798 properties->abortApplying();
799 return;
800 }
801
802 // Do we need to rename the file ?
803 // qDebug() << "oldname = " << d->oldName;
804 // qDebug() << "newname = " << n;
805 if (d->oldName != n || d->m_bFromTemplate) { // true for any from-template file
806 KIO::CopyJob *job = nullptr;
807 QUrl oldurl = properties->url();
808
809 QString newFileName = KIO::encodeFileName(str: n);
810 if (d->bDesktopFile && !newFileName.endsWith(s: QLatin1String(".desktop"))) {
811 newFileName += QLatin1String(".desktop");
812 }
813
814 // Tell properties. Warning, this changes the result of properties->url() !
815 properties->rename(name: newFileName);
816
817 // Update also relative path (for apps)
818 if (!d->m_sRelativePath.isEmpty()) {
819 determineRelativePath(path: properties->url().toLocalFile());
820 }
821
822 // qDebug() << "New URL = " << properties->url();
823 // qDebug() << "old = " << oldurl.url();
824
825 // Don't remove the template !!
826 if (!d->m_bFromTemplate) { // (normal renaming)
827 job = KIO::moveAs(src: oldurl, dest: properties->url());
828 } else { // Copying a template
829 job = KIO::copyAs(src: oldurl, dest: properties->url());
830 }
831 KJobWidgets::setWindow(job, widget: properties);
832 connect(sender: job, signal: &KJob::result, context: this, slot: &KFilePropsPlugin::slotCopyFinished);
833 connect(sender: job, signal: &KIO::CopyJob::renamed, context: this, slot: &KFilePropsPlugin::slotFileRenamed);
834 return;
835 }
836
837 properties->updateUrl(newUrl: properties->url());
838 // Update also relative path (for apps)
839 if (!d->m_sRelativePath.isEmpty()) {
840 determineRelativePath(path: properties->url().toLocalFile());
841 }
842 }
843
844 // No job, keep going
845 slotCopyFinished(nullptr);
846}
847
848void KFilePropsPlugin::slotCopyFinished(KJob *job)
849{
850 // qDebug() << "KFilePropsPlugin::slotCopyFinished";
851 if (job) {
852 if (job->error()) {
853 job->uiDelegate()->showErrorMessage();
854 // Didn't work. Revert the URL to the old one
855 properties->updateUrl(newUrl: static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst());
856 properties->abortApplying(); // Don't apply the changes to the wrong file !
857 return;
858 }
859 }
860
861 Q_ASSERT(!properties->item().isNull());
862 Q_ASSERT(!properties->item().url().isEmpty());
863
864 // Save the file locally
865 if (d->bDesktopFile && !d->m_sRelativePath.isEmpty()) {
866 // qDebug() << "KFilePropsPlugin::slotCopyFinished " << d->m_sRelativePath;
867 const QString newPath = QStandardPaths::writableLocation(type: QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + d->m_sRelativePath;
868 const QUrl newURL = QUrl::fromLocalFile(localfile: newPath);
869 // qDebug() << "KFilePropsPlugin::slotCopyFinished path=" << newURL;
870 properties->updateUrl(newUrl: newURL);
871 }
872
873 if (d->bKDesktopMode && d->bDesktopFile) {
874 // Renamed? Update Name field
875 // Note: The desktop KIO worker does this as well, but not when
876 // the file is copied from a template.
877 if (d->m_bFromTemplate) {
878 KIO::StatJob *job = KIO::stat(url: properties->url());
879 job->exec();
880 KIO::UDSEntry entry = job->statResult();
881
882 KFileItem item(entry, properties->url());
883 KDesktopFile config(item.localPath());
884 KConfigGroup cg = config.desktopGroup();
885 QString nameStr = nameFromFileName(nameStr: properties->url().fileName());
886 cg.writeEntry(key: "Name", value: nameStr);
887 cg.writeEntry(key: "Name", value: nameStr, pFlags: KConfigGroup::Persistent | KConfigGroup::Localized);
888 }
889 }
890
891 if (!d->m_ui->symlinkTargetEdit->isHidden() && !d->bMultiple) {
892 const KFileItem item = properties->item();
893 const QString newTarget = d->m_ui->symlinkTargetEdit->text();
894 if (newTarget != item.linkDest()) {
895 // qDebug() << "Updating target of symlink to" << newTarget;
896 KIO::Job *job = KIO::symlink(target: newTarget, dest: item.url(), flags: KIO::Overwrite);
897 job->uiDelegate()->setAutoErrorHandlingEnabled(true);
898 job->exec();
899 }
900 }
901
902 // "Link to Application" templates need to be made executable
903 // Instead of matching against a filename we check if the destination
904 // is an Application now.
905 if (d->m_bFromTemplate) {
906 // destination is not necessarily local, use the src template
907 KDesktopFile templateResult(static_cast<KIO::CopyJob *>(job)->srcUrls().constFirst().toLocalFile());
908 if (templateResult.hasApplicationType()) {
909 // We can either stat the file and add the +x bit or use the larger chmod() job
910 // with a umask designed to only touch u+x. This is only one KIO job, so let's
911 // do that.
912
913 KFileItem appLink(properties->item());
914 KFileItemList fileItemList;
915 fileItemList << appLink;
916
917 // first 0100 adds u+x, second 0100 only allows chmod to change u+x
918 KIO::Job *chmodJob = KIO::chmod(lstItems: fileItemList, permissions: 0100, mask: 0100, newOwner: QString(), newGroup: QString(), recursive: KIO::HideProgressInfo);
919 chmodJob->exec();
920 }
921 }
922
923 setDirty(false);
924 Q_EMIT changesApplied();
925}
926
927void KFilePropsPlugin::applyIconChanges()
928{
929 if (d->m_ui->iconButton->isHidden() || !d->bIconChanged) {
930 return;
931 }
932 // handle icon changes - only local files (or pseudo-local) for now
933 // TODO: Use KTempFile and KIO::file_copy with overwrite = true
934 QUrl url = properties->url();
935 KIO::StatJob *job = KIO::mostLocalUrl(url);
936 KJobWidgets::setWindow(job, widget: properties);
937 job->exec();
938 url = job->mostLocalUrl();
939
940 if (url.isLocalFile()) {
941 QString path;
942
943 if (Utils::isDirMask(mode: properties->item().mode())) {
944 path = url.toLocalFile() + QLatin1String("/.directory");
945 // don't call updateUrl because the other tabs (i.e. permissions)
946 // apply to the directory, not the .directory file.
947 } else {
948 path = url.toLocalFile();
949 }
950
951 // Get the default image
952 QMimeDatabase db;
953 const QString str = db.mimeTypeForFile(fileName: url.toLocalFile(), mode: QMimeDatabase::MatchExtension).iconName();
954 // Is it another one than the default ?
955 QString sIcon;
956 if (const QString currIcon = d->m_ui->iconButton->icon(); str != currIcon) {
957 sIcon = currIcon;
958 }
959 // (otherwise write empty value)
960
961 // qDebug() << "**" << path << "**";
962
963 // If default icon and no .directory file -> don't create one
964 if (!sIcon.isEmpty() || QFile::exists(fileName: path)) {
965 KDesktopFile cfg(path);
966 // qDebug() << "sIcon = " << (sIcon);
967 // qDebug() << "str = " << (str);
968 cfg.desktopGroup().writeEntry(key: "Icon", value: sIcon);
969 cfg.sync();
970
971 cfg.reparseConfiguration();
972 if (cfg.desktopGroup().readEntry(key: "Icon") != sIcon) {
973 properties->abortApplying();
974
975 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
976 }
977 }
978 }
979}
980
981void KFilePropsPlugin::updateDefaultHandler(const QString &mimeType)
982{
983 const bool isGeneric = d->mimeType == QLatin1String("application/octet-stream");
984
985 const auto service = KApplicationTrader::preferredService(mimeType);
986 if (!isGeneric && service) {
987 const int iconSize = properties->style()->pixelMetric(metric: QStyle::PM_SmallIconSize);
988 d->m_ui->defaultHandlerIcon->setPixmap(QIcon::fromTheme(name: service->icon()).pixmap(extent: iconSize));
989 d->m_ui->defaultHandlerIcon->show();
990 d->m_ui->defaultHandlerLabel->setText(service->name());
991 d->m_ui->defaultHandlerLabel->setDisabled(false);
992 } else {
993 d->m_ui->defaultHandlerIcon->hide();
994 if (isGeneric) {
995 d->m_ui->defaultHandlerLabel->setText(i18n("No registered file type"));
996 } else {
997 d->m_ui->defaultHandlerLabel->setText(i18n("No associated application"));
998 }
999 d->m_ui->defaultHandlerLabel->setDisabled(true);
1000 }
1001
1002 if (isGeneric) {
1003 d->m_ui->configureMimeBtn->setText(i18nc("@action:button Create new file type", "Create…"));
1004 d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
1005 d->m_ui->configureMimeBtn->setToolTip(QString());
1006 } else {
1007 d->m_ui->configureMimeBtn->setText(i18nc("@action:button", "Change…"));
1008 d->m_ui->configureMimeBtn->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
1009 d->m_ui->configureMimeBtn->setToolTip(i18nc("@info:tooltip", "Change the default application for opening files of type “%1”.", d->mimeType));
1010 }
1011}
1012
1013void KFilePropsPlugin::slotFileRenamed(KIO::Job *, const QUrl &, const QUrl &newUrl)
1014{
1015 // This is called in case of an existing local file during the copy/move operation,
1016 // if the user chooses Rename.
1017 properties->updateUrl(newUrl);
1018}
1019
1020void KFilePropsPlugin::postApplyChanges()
1021{
1022 // Save the icon only after applying the permissions changes (#46192)
1023 applyIconChanges();
1024
1025#ifdef WITH_QTDBUS
1026 const KFileItemList items = properties->items();
1027 const QList<QUrl> lst = items.urlList();
1028 org::kde::KDirNotify::emitFilesChanged(fileList: QList<QUrl>(lst));
1029#endif
1030}
1031
1032class KFilePermissionsPropsPlugin::KFilePermissionsPropsPluginPrivate
1033{
1034public:
1035 QFrame *m_frame = nullptr;
1036 QCheckBox *cbRecursive = nullptr;
1037 QLabel *explanationLabel = nullptr;
1038 QComboBox *ownerPermCombo = nullptr;
1039 QComboBox *groupPermCombo = nullptr;
1040 QComboBox *othersPermCombo = nullptr;
1041 QCheckBox *extraCheckbox = nullptr;
1042 mode_t partialPermissions;
1043 KFilePermissionsPropsPlugin::PermissionsMode pmode;
1044 bool canChangePermissions;
1045 bool isIrregular;
1046 bool hasExtendedACL;
1047 KACL extendedACL;
1048 KACL defaultACL;
1049 bool fileSystemSupportsACLs;
1050
1051 QComboBox *grpCombo = nullptr;
1052
1053 KLineEdit *usrEdit = nullptr;
1054 KLineEdit *grpEdit = nullptr;
1055
1056 // Old permissions
1057 mode_t permissions;
1058 // Old group
1059 QString strGroup;
1060 // Old owner
1061 QString strOwner;
1062};
1063
1064static constexpr mode_t UniOwner{S_IRUSR | S_IWUSR | S_IXUSR};
1065static constexpr mode_t UniGroup{S_IRGRP | S_IWGRP | S_IXGRP};
1066static constexpr mode_t UniOthers{S_IROTH | S_IWOTH | S_IXOTH};
1067static constexpr mode_t UniRead{S_IRUSR | S_IRGRP | S_IROTH};
1068static constexpr mode_t UniWrite{S_IWUSR | S_IWGRP | S_IWOTH};
1069static constexpr mode_t UniExec{S_IXUSR | S_IXGRP | S_IXOTH};
1070static constexpr mode_t UniSpecial{S_ISUID | S_ISGID | S_ISVTX};
1071static constexpr mode_t s_invalid_mode_t{static_cast<mode_t>(-1)};
1072
1073// synced with PermissionsTarget
1074constexpr mode_t KFilePermissionsPropsPlugin::permissionsMasks[3] = {UniOwner, UniGroup, UniOthers};
1075constexpr mode_t KFilePermissionsPropsPlugin::standardPermissions[4] = {0, UniRead, UniRead | UniWrite, s_invalid_mode_t};
1076
1077// synced with PermissionsMode and standardPermissions
1078static constexpr KLazyLocalizedString permissionsTexts[4][4] = {
1079 {kli18n(text: "No Access"), kli18n(text: "Can Only View"), kli18n(text: "Can View & Modify"), {}},
1080 {kli18n(text: "No Access"), kli18n(text: "Can Only View Content"), kli18n(text: "Can View & Modify Content"), {}},
1081 {{}, {}, {}, {}}, // no texts for links
1082 {kli18n(text: "No Access"), kli18n(text: "Can Only View/Read Content"), kli18n(text: "Can View/Read & Modify/Write"), {}}};
1083
1084KFilePermissionsPropsPlugin::KFilePermissionsPropsPlugin(KPropertiesDialog *_props)
1085 : KPropertiesDialogPlugin(_props)
1086 , d(new KFilePermissionsPropsPluginPrivate)
1087{
1088 const auto &[localUrl, isLocal] = properties->item().isMostLocalUrl();
1089 bool isTrash = (properties->url().scheme() == QLatin1String("trash"));
1090 KUser myself(KUser::UseEffectiveUID);
1091 const bool IamRoot = myself.isSuperUser();
1092
1093 const KFileItem firstItem = properties->item();
1094 bool isLink = firstItem.isLink();
1095 bool isDir = firstItem.isDir(); // all dirs
1096 bool hasDir = firstItem.isDir(); // at least one dir
1097 d->permissions = firstItem.permissions(); // common permissions to all files
1098 d->partialPermissions = d->permissions; // permissions that only some files have (at first we take everything)
1099 d->isIrregular = isIrregular(permissions: d->permissions, isDir, isLink);
1100 d->strOwner = firstItem.user();
1101 d->strGroup = firstItem.group();
1102 d->hasExtendedACL = firstItem.ACL().isExtended() || firstItem.defaultACL().isValid();
1103 d->extendedACL = firstItem.ACL();
1104 d->defaultACL = firstItem.defaultACL();
1105 d->fileSystemSupportsACLs = false;
1106
1107 if (properties->items().count() > 1) {
1108 // Multiple items: see what they have in common
1109 const KFileItemList items = properties->items();
1110 for (const auto &item : items) {
1111 if (item == firstItem) { // No need to check the first one again
1112 continue;
1113 }
1114
1115 const bool isItemDir = item.isDir();
1116 const bool isItemLink = item.isLink();
1117
1118 if (!d->isIrregular) {
1119 d->isIrregular |= isIrregular(permissions: item.permissions(), isDir: isItemDir == isDir, isLink: isItemLink == isLink);
1120 }
1121
1122 d->hasExtendedACL = d->hasExtendedACL || item.hasExtendedACL();
1123
1124 if (isItemLink != isLink) {
1125 isLink = false;
1126 }
1127
1128 if (isItemDir != isDir) {
1129 isDir = false;
1130 }
1131 hasDir |= isItemDir;
1132
1133 if (item.permissions() != d->permissions) {
1134 d->permissions &= item.permissions();
1135 d->partialPermissions |= item.permissions();
1136 }
1137
1138 if (item.user() != d->strOwner) {
1139 d->strOwner.clear();
1140 }
1141
1142 if (item.group() != d->strGroup) {
1143 d->strGroup.clear();
1144 }
1145 }
1146 }
1147
1148 if (isLink) {
1149 d->pmode = PermissionsOnlyLinks;
1150 } else if (isDir) {
1151 d->pmode = PermissionsOnlyDirs;
1152 } else if (hasDir) {
1153 d->pmode = PermissionsMixed;
1154 } else {
1155 d->pmode = PermissionsOnlyFiles;
1156 }
1157
1158 // keep only what's not in the common permissions
1159 d->partialPermissions = d->partialPermissions & ~d->permissions;
1160
1161 bool isMyFile = false;
1162
1163 if (isLocal && !d->strOwner.isEmpty()) { // local files, and all owned by the same person
1164 if (myself.isValid()) {
1165 isMyFile = (d->strOwner == myself.loginName());
1166 } else {
1167 qCWarning(KIO_WIDGETS) << "I don't exist ?! geteuid=" << KUserId::currentEffectiveUserId().toString();
1168 }
1169 } else {
1170 // We don't know, for remote files, if they are ours or not.
1171 // So we let the user change permissions, and
1172 // KIO::chmod will tell, if he had no right to do it.
1173 isMyFile = true;
1174 }
1175
1176 d->canChangePermissions = (isMyFile || IamRoot) && (!isLink);
1177
1178 // create GUI
1179
1180 d->m_frame = new QFrame();
1181 properties->addPage(widget: d->m_frame, i18n("&Permissions"));
1182
1183 QBoxLayout *box = new QVBoxLayout(d->m_frame);
1184
1185 QWidget *l;
1186 QString lbl;
1187 QGroupBox *gb;
1188 QFormLayout *gl;
1189 QPushButton *pbAdvancedPerm = nullptr;
1190
1191 /* Group: Access Permissions */
1192 gb = new QGroupBox(i18n("Access Permissions"), d->m_frame);
1193 gb->setFlat(true);
1194 box->addWidget(gb);
1195
1196 gl = new QFormLayout(gb);
1197 gl->setFormAlignment(Qt::AlignHCenter);
1198
1199 l = d->explanationLabel = new QLabel(gb);
1200 if (isLink) {
1201 d->explanationLabel->setText(
1202 i18np("This file is a link and does not have permissions.", "All files are links and do not have permissions.", properties->items().count()));
1203 } else if (!d->canChangePermissions) {
1204 d->explanationLabel->setText(i18n("Only the owner can change permissions."));
1205 } else {
1206 d->explanationLabel->setFixedHeight(0);
1207 }
1208 gl->addWidget(w: l);
1209
1210 l = d->ownerPermCombo = new QComboBox(gb);
1211 gl->addRow(i18n("O&wner:"), field: l);
1212 connect(sender: d->ownerPermCombo, signal: &QComboBox::activated, context: this, slot: &KPropertiesDialogPlugin::changed);
1213 l->setWhatsThis(i18n("Specifies the actions that the owner is allowed to do."));
1214
1215 l = d->groupPermCombo = new QComboBox(gb);
1216 gl->addRow(i18n("Gro&up:"), field: l);
1217 connect(sender: d->groupPermCombo, signal: &QComboBox::activated, context: this, slot: &KPropertiesDialogPlugin::changed);
1218 l->setWhatsThis(i18n("Specifies the actions that the members of the group are allowed to do."));
1219
1220 l = d->othersPermCombo = new QComboBox(gb);
1221 gl->addRow(i18n("O&thers:"), field: l);
1222 connect(sender: d->othersPermCombo, signal: &QComboBox::activated, context: this, slot: &KPropertiesDialogPlugin::changed);
1223 l->setWhatsThis(
1224 i18n("Specifies the actions that all users, who are neither "
1225 "owner nor in the group, are allowed to do."));
1226
1227 if (!isLink) {
1228 l = d->extraCheckbox = new QCheckBox(hasDir ? i18n("Only own&er can delete or rename contents") : i18n("Allow &executing file as program"), gb);
1229 connect(sender: d->extraCheckbox, signal: &QAbstractButton::clicked, context: this, slot: &KPropertiesDialogPlugin::changed);
1230 gl->addRow(labelText: hasDir ? i18n("Delete or rename:") : i18n("Execute:"), field: l);
1231 l->setWhatsThis(hasDir ? i18n("Enable this option to allow only the folder's owner to "
1232 "delete or rename the contained files and folders. Other "
1233 "users can only add new files, which requires the 'Modify "
1234 "Content' permission.")
1235 : i18n("Enable this option to mark the file as executable. This only makes "
1236 "sense for programs and scripts. It is required when you want to "
1237 "execute them."));
1238
1239 pbAdvancedPerm = new QPushButton(i18n("A&dvanced Permissions"), gb);
1240 gl->addWidget(w: pbAdvancedPerm);
1241 connect(sender: pbAdvancedPerm, signal: &QAbstractButton::clicked, context: this, slot: &KFilePermissionsPropsPlugin::slotShowAdvancedPermissions);
1242 } else {
1243 d->extraCheckbox = nullptr;
1244 }
1245
1246 /**** Group: Ownership ****/
1247 gb = new QGroupBox(i18n("Ownership"), d->m_frame);
1248 gb->setFlat(true);
1249 box->addWidget(gb);
1250
1251 gl = new QFormLayout(gb);
1252 gl->setFormAlignment(Qt::AlignHCenter);
1253
1254 /*** Set Owner ***/
1255 lbl = i18n("User:");
1256 /* GJ: Don't autocomplete more than 1000 users. This is a kind of random
1257 * value. Huge sites having 10.000+ user have a fair chance of using NIS,
1258 * (possibly) making this unacceptably slow.
1259 * OTOH, it is nice to offer this functionality for the standard user.
1260 */
1261 int maxEntries = 1000;
1262
1263 /* File owner: For root, offer a KLineEdit with autocompletion.
1264 * For a user, who can never chown() a file, offer a QLabel.
1265 */
1266 if (IamRoot && isLocal) {
1267 d->usrEdit = new KLineEdit(gb);
1268 KCompletion *kcom = d->usrEdit->completionObject();
1269 kcom->setOrder(KCompletion::Sorted);
1270 QStringList userNames = KUser::allUserNames(maxCount: maxEntries);
1271 kcom->setItems(userNames);
1272 d->usrEdit->setCompletionMode((userNames.size() < maxEntries) ? KCompletion::CompletionAuto : KCompletion::CompletionNone);
1273 d->usrEdit->setText(d->strOwner);
1274 gl->addRow(labelText: lbl, field: d->usrEdit);
1275 connect(sender: d->usrEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
1276 } else {
1277 l = new QLabel(d->strOwner, gb);
1278 qobject_cast<QLabel *>(object: l)->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
1279 l->setFocusPolicy(Qt::TabFocus);
1280 gl->addRow(labelText: lbl, field: l);
1281 }
1282
1283 /*** Set Group ***/
1284 QStringList groupList = myself.groupNames();
1285 const bool isMyGroup = groupList.contains(str: d->strGroup);
1286
1287 /* add the group the file currently belongs to ..
1288 * .. if it is not there already
1289 */
1290 if (!isMyGroup) {
1291 groupList += d->strGroup;
1292 }
1293 lbl = i18n("Group:");
1294
1295 /* Set group: if possible to change:
1296 * - Offer a KLineEdit for root, since he can change to any group.
1297 * - Offer a QComboBox for a normal user, since he can change to a fixed
1298 * (small) set of groups only.
1299 * If not changeable: offer a QLabel.
1300 */
1301 if (IamRoot && isLocal) {
1302 d->grpEdit = new KLineEdit(gb);
1303 KCompletion *kcom = new KCompletion;
1304 kcom->setItems(groupList);
1305 d->grpEdit->setCompletionObject(kcom, handle: true);
1306 d->grpEdit->setAutoDeleteCompletionObject(true);
1307 d->grpEdit->setCompletionMode(KCompletion::CompletionAuto);
1308 d->grpEdit->setText(d->strGroup);
1309 gl->addRow(labelText: lbl, field: d->grpEdit);
1310 connect(sender: d->grpEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
1311 } else if ((groupList.count() > 1) && isMyFile && isLocal) {
1312 d->grpCombo = new QComboBox(gb);
1313 d->grpCombo->setObjectName(QStringLiteral("combogrouplist"));
1314 d->grpCombo->addItems(texts: groupList);
1315 d->grpCombo->setCurrentIndex(groupList.indexOf(str: d->strGroup));
1316 gl->addRow(labelText: lbl, field: d->grpCombo);
1317 connect(sender: d->grpCombo, signal: &QComboBox::activated, context: this, slot: &KPropertiesDialogPlugin::changed);
1318 } else {
1319 l = new QLabel(d->strGroup, gb);
1320 qobject_cast<QLabel *>(object: l)->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
1321 l->setFocusPolicy(Qt::TabFocus);
1322 gl->addRow(labelText: lbl, field: l);
1323 }
1324
1325 // "Apply recursive" checkbox
1326 if (hasDir && !isLink && !isTrash) {
1327 d->cbRecursive = new QCheckBox(i18n("Apply changes to all subfolders and their contents"), d->m_frame);
1328 connect(sender: d->cbRecursive, signal: &QAbstractButton::clicked, context: this, slot: &KPropertiesDialogPlugin::changed);
1329 KSeparator *sep = new KSeparator();
1330 box->addWidget(sep);
1331 box->addWidget(d->cbRecursive);
1332 }
1333
1334 updateAccessControls();
1335
1336 if (isTrash) {
1337 // don't allow to change properties for file into trash
1338 enableAccessControls(enable: false);
1339 if (pbAdvancedPerm) {
1340 pbAdvancedPerm->setEnabled(false);
1341 }
1342 }
1343}
1344
1345#if HAVE_POSIX_ACL
1346static bool fileSystemSupportsACL(const QByteArray &path)
1347{
1348 bool fileSystemSupportsACLs = false;
1349#ifdef Q_OS_FREEBSD
1350 // FIXME: unbreak and enable this
1351 // Maybe use pathconf(2) to perform this check?
1352 // struct statfs buf;
1353 // fileSystemSupportsACLs = (statfs(path.data(), &buf) == 0) && (buf.f_flags & MNT_ACLS);
1354 Q_UNUSED(path);
1355#elif defined Q_OS_MACOS
1356 fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0, 0, XATTR_NOFOLLOW) >= 0 || errno == ENODATA;
1357#else
1358 fileSystemSupportsACLs = getxattr(path.data(), "system.posix_acl_access", nullptr, 0) >= 0 || errno == ENODATA;
1359#endif
1360 return fileSystemSupportsACLs;
1361}
1362#endif
1363
1364void KFilePermissionsPropsPlugin::slotShowAdvancedPermissions()
1365{
1366 bool isDir = (d->pmode == PermissionsOnlyDirs) || (d->pmode == PermissionsMixed);
1367 QDialog dlg(properties);
1368 dlg.setWindowModality(Qt::WindowModal);
1369 dlg.setModal(true);
1370 dlg.setWindowTitle(i18n("Advanced Permissions"));
1371
1372 QLabel *l;
1373 QLabel *cl[3];
1374 QGroupBox *gb;
1375 QGridLayout *gl;
1376
1377 auto *vbox = new QVBoxLayout(&dlg);
1378
1379 // Group: Access Permissions
1380 gb = new QGroupBox(i18n("Access Permissions"), &dlg);
1381 gb->setFlat(true);
1382 vbox->addWidget(gb);
1383 vbox->setAlignment(Qt::AlignHCenter);
1384
1385 gl = new QGridLayout(gb);
1386 gl->addItem(item: new QSpacerItem(0, 10), row: 0, column: 0);
1387
1388 QList<QWidget *> theNotSpecials;
1389
1390 l = new QLabel(i18n("Class"), gb);
1391 gl->addWidget(l, row: 1, column: 0);
1392 theNotSpecials.append(t: l);
1393
1394 QString readWhatsThis;
1395 QString readLabel;
1396 if (isDir) {
1397 readLabel = i18n("Show\nEntries");
1398 readWhatsThis = i18n("This flag allows viewing the content of the folder.");
1399 } else {
1400 readLabel = i18n("Read");
1401 readWhatsThis = i18n("The Read flag allows viewing the content of the file.");
1402 }
1403
1404 QString writeWhatsThis;
1405 QString writeLabel;
1406 if (isDir) {
1407 writeLabel = i18n("Write\nEntries");
1408 writeWhatsThis = i18n(
1409 "This flag allows adding, renaming and deleting of files. "
1410 "Note that deleting and renaming can be limited using the Sticky flag.");
1411 } else {
1412 writeLabel = i18n("Write");
1413 writeWhatsThis = i18n("The Write flag allows modifying the content of the file.");
1414 }
1415
1416 QString execLabel;
1417 QString execWhatsThis;
1418 if (isDir) {
1419 execLabel = i18nc("Enter folder", "Enter");
1420 execWhatsThis = i18n("Enable this flag to allow entering the folder.");
1421 } else {
1422 execLabel = i18n("Exec");
1423 execWhatsThis = i18n("Enable this flag to allow executing the file as a program.");
1424 }
1425 // GJ: Add space between normal and special modes
1426 QSize size = l->sizeHint();
1427 size.setWidth(size.width() + 15);
1428 l->setFixedSize(size);
1429 gl->addWidget(l, row: 1, column: 3);
1430
1431 l = new QLabel(i18n("Special"), gb);
1432 gl->addWidget(l, row: 1, column: 4, rowSpan: 1, columnSpan: 1);
1433 QString specialWhatsThis;
1434 if (isDir) {
1435 specialWhatsThis = i18n(
1436 "Special flag. Valid for the whole folder, the exact "
1437 "meaning of the flag can be seen in the right hand column.");
1438 } else {
1439 specialWhatsThis = i18n(
1440 "Special flag. The exact meaning of the flag can be seen "
1441 "in the right hand column.");
1442 }
1443 l->setWhatsThis(specialWhatsThis);
1444
1445 cl[0] = new QLabel(i18n("User"), gb);
1446 gl->addWidget(cl[0], row: 2, column: 0);
1447 theNotSpecials.append(t: cl[0]);
1448
1449 cl[1] = new QLabel(i18n("Group"), gb);
1450 gl->addWidget(cl[1], row: 3, column: 0);
1451 theNotSpecials.append(t: cl[1]);
1452
1453 cl[2] = new QLabel(i18n("Others"), gb);
1454 gl->addWidget(cl[2], row: 4, column: 0);
1455 theNotSpecials.append(t: cl[2]);
1456
1457 QString setUidWhatsThis;
1458 if (isDir) {
1459 setUidWhatsThis = i18n(
1460 "If this flag is set, the owner of this folder will be "
1461 "the owner of all new files.");
1462 } else {
1463 setUidWhatsThis = i18n(
1464 "If this file is an executable and the flag is set, it will "
1465 "be executed with the permissions of the owner.");
1466 }
1467
1468 QString setGidWhatsThis;
1469 if (isDir) {
1470 setGidWhatsThis = i18n(
1471 "If this flag is set, the group of this folder will be "
1472 "set for all new files.");
1473 } else {
1474 setGidWhatsThis = i18n(
1475 "If this file is an executable and the flag is set, it will "
1476 "be executed with the permissions of the group.");
1477 }
1478
1479 QString stickyWhatsThis;
1480 if (isDir) {
1481 stickyWhatsThis = i18n(
1482 "If the Sticky flag is set on a folder, only the owner "
1483 "and root can delete or rename files. Otherwise everybody "
1484 "with write permissions can do this.");
1485 } else {
1486 stickyWhatsThis = i18n(
1487 "The Sticky flag on a file is ignored on Linux, but may "
1488 "be used on some systems");
1489 }
1490 mode_t aPermissions = 0;
1491 mode_t aPartialPermissions = 0;
1492 mode_t dummy1 = 0;
1493 mode_t dummy2 = 0;
1494
1495 if (!d->isIrregular) {
1496 switch (d->pmode) {
1497 case PermissionsOnlyFiles:
1498 getPermissionMasks(andFilePermissions&: aPartialPermissions, andDirPermissions&: dummy1, orFilePermissions&: aPermissions, orDirPermissions&: dummy2);
1499 break;
1500 case PermissionsOnlyDirs:
1501 case PermissionsMixed:
1502 getPermissionMasks(andFilePermissions&: dummy1, andDirPermissions&: aPartialPermissions, orFilePermissions&: dummy2, orDirPermissions&: aPermissions);
1503 break;
1504 case PermissionsOnlyLinks:
1505 aPermissions = UniRead | UniWrite | UniExec | UniSpecial;
1506 break;
1507 }
1508 } else {
1509 aPermissions = d->permissions;
1510 aPartialPermissions = d->partialPermissions;
1511 }
1512
1513 // Draw Checkboxes
1514 QCheckBox *cba[3][4];
1515 for (int row = 0; row < 3; ++row) {
1516 for (int col = 0; col < 4; ++col) {
1517 auto *cb = new QCheckBox(gb);
1518 if (col != 3) {
1519 theNotSpecials.append(t: cb);
1520 }
1521 cba[row][col] = cb;
1522 cb->setChecked(aPermissions & fperm[row][col]);
1523 if (aPartialPermissions & fperm[row][col]) {
1524 cb->setTristate();
1525 cb->setCheckState(Qt::PartiallyChecked);
1526 } else if (d->cbRecursive && d->cbRecursive->isChecked()) {
1527 cb->setTristate();
1528 }
1529
1530 cb->setEnabled(d->canChangePermissions);
1531 gl->addWidget(cb, row: row + 2, column: col + 1);
1532 switch (col) {
1533 case 0:
1534 cb->setText(readLabel);
1535 cb->setWhatsThis(readWhatsThis);
1536 break;
1537 case 1:
1538 cb->setText(writeLabel);
1539 cb->setWhatsThis(writeWhatsThis);
1540 break;
1541 case 2:
1542 cb->setText(execLabel);
1543 cb->setWhatsThis(execWhatsThis);
1544 break;
1545 case 3:
1546 switch (row) {
1547 case 0:
1548 cb->setText(i18n("Set UID"));
1549 cb->setWhatsThis(setUidWhatsThis);
1550 break;
1551 case 1:
1552 cb->setText(i18n("Set GID"));
1553 cb->setWhatsThis(setGidWhatsThis);
1554 break;
1555 case 2:
1556 cb->setText(i18nc("File permission", "Sticky"));
1557 cb->setWhatsThis(stickyWhatsThis);
1558 break;
1559 }
1560 break;
1561 }
1562 }
1563 }
1564 gl->setColumnStretch(column: 6, stretch: 10);
1565 vbox->addStretch(stretch: 1);
1566
1567#if HAVE_POSIX_ACL
1568 KACLEditWidget *extendedACLs = nullptr;
1569
1570 // FIXME make it work with partial entries
1571 if (properties->items().count() == 1) {
1572 QByteArray path = QFile::encodeName(properties->item().url().toLocalFile());
1573 d->fileSystemSupportsACLs = fileSystemSupportsACL(path);
1574 }
1575 if (d->fileSystemSupportsACLs) {
1576 std::for_each(theNotSpecials.begin(), theNotSpecials.end(), std::mem_fn(&QWidget::hide));
1577 extendedACLs = new KACLEditWidget(&dlg);
1578 extendedACLs->setEnabled(d->canChangePermissions);
1579 vbox->addWidget(extendedACLs);
1580 if (d->extendedACL.isValid() && d->extendedACL.isExtended()) {
1581 extendedACLs->setACL(d->extendedACL);
1582 } else {
1583 extendedACLs->setACL(KACL(aPermissions));
1584 }
1585
1586 if (d->defaultACL.isValid()) {
1587 extendedACLs->setDefaultACL(d->defaultACL);
1588 }
1589
1590 if (properties->items().constFirst().isDir()) {
1591 extendedACLs->setAllowDefaults(true);
1592 }
1593 }
1594#endif
1595 // Separator above buttons
1596 auto *buttonSep = new KSeparator();
1597 vbox->addWidget(buttonSep);
1598
1599 auto *buttonBox = new QDialogButtonBox(&dlg);
1600 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
1601 connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: &dlg, slot: &QDialog::accept);
1602 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: &dlg, slot: &QDialog::reject);
1603 vbox->addWidget(buttonBox);
1604
1605 if (dlg.exec() != QDialog::Accepted) {
1606 return;
1607 }
1608
1609 mode_t andPermissions = mode_t(~0);
1610 mode_t orPermissions = 0;
1611 for (int row = 0; row < 3; ++row) {
1612 for (int col = 0; col < 4; ++col) {
1613 switch (cba[row][col]->checkState()) {
1614 case Qt::Checked:
1615 orPermissions |= fperm[row][col];
1616 // fall through
1617 case Qt::Unchecked:
1618 andPermissions &= ~fperm[row][col];
1619 break;
1620 case Qt::PartiallyChecked:
1621 break;
1622 }
1623 }
1624 }
1625
1626 const KFileItemList items = properties->items();
1627 d->isIrregular = std::any_of(first: items.cbegin(), last: items.cend(), pred: [this, andPermissions, orPermissions](const KFileItem &item) {
1628 return isIrregular(permissions: (item.permissions() & andPermissions) | orPermissions, isDir: item.isDir(), isLink: item.isLink());
1629 });
1630
1631 d->permissions = orPermissions;
1632 d->partialPermissions = andPermissions;
1633
1634#if HAVE_POSIX_ACL
1635 // override with the acls, if present
1636 if (extendedACLs) {
1637 d->extendedACL = extendedACLs->getACL();
1638 d->defaultACL = extendedACLs->getDefaultACL();
1639 d->hasExtendedACL = d->extendedACL.isExtended() || d->defaultACL.isValid();
1640 d->permissions = d->extendedACL.basePermissions();
1641 d->permissions |= (andPermissions | orPermissions) & (S_ISUID | S_ISGID | S_ISVTX);
1642 }
1643#endif
1644
1645 dlg.setMinimumSize(dlg.sizeHint());
1646
1647 updateAccessControls();
1648 Q_EMIT changed();
1649}
1650
1651KFilePermissionsPropsPlugin::~KFilePermissionsPropsPlugin() = default;
1652
1653bool KFilePermissionsPropsPlugin::supports(const KFileItemList &items)
1654{
1655 return std::any_of(first: items.cbegin(), last: items.cend(), pred: [](const KFileItem &item) {
1656 return KProtocolManager::supportsPermissions(url: item.url());
1657 });
1658}
1659
1660// sets a combo box in the Access Control frame
1661void KFilePermissionsPropsPlugin::setComboContent(QComboBox *combo, PermissionsTarget target, mode_t permissions, mode_t partial)
1662{
1663 combo->clear();
1664 if (d->isIrregular) { // #176876
1665 return;
1666 }
1667
1668 if (d->pmode == PermissionsOnlyLinks) {
1669 combo->addItem(i18n("Link"));
1670 combo->setCurrentIndex(0);
1671 return;
1672 }
1673
1674 mode_t tMask = permissionsMasks[target];
1675 int textIndex;
1676 for (textIndex = 0; standardPermissions[textIndex] != s_invalid_mode_t; ++textIndex) {
1677 if ((standardPermissions[textIndex] & tMask) == (permissions & tMask & (UniRead | UniWrite))) {
1678 break;
1679 }
1680 }
1681 Q_ASSERT(standardPermissions[textIndex] != s_invalid_mode_t); // must not happen, would be irreglar
1682
1683 const auto permsTexts = permissionsTexts[static_cast<int>(d->pmode)];
1684 for (int i = 0; !permsTexts[i].isEmpty(); ++i) {
1685 combo->addItem(atext: permsTexts[i].toString());
1686 }
1687
1688 if (partial & tMask & ~UniExec) {
1689 combo->addItem(i18n("Varying (No Change)"));
1690 combo->setCurrentIndex(3);
1691 } else {
1692 combo->setCurrentIndex(textIndex);
1693 }
1694}
1695
1696// permissions are irregular if they can't be displayed in a combo box.
1697bool KFilePermissionsPropsPlugin::isIrregular(mode_t permissions, bool isDir, bool isLink)
1698{
1699 if (isLink) { // links are always ok
1700 return false;
1701 }
1702
1703 mode_t p = permissions;
1704 if (p & (S_ISUID | S_ISGID)) { // setuid/setgid -> irregular
1705 return true;
1706 }
1707 if (isDir) {
1708 p &= ~S_ISVTX; // ignore sticky on dirs
1709
1710 // check supported flag combinations
1711 mode_t p0 = p & UniOwner;
1712 if ((p0 != 0) && (p0 != (S_IRUSR | S_IXUSR)) && (p0 != UniOwner)) {
1713 return true;
1714 }
1715 p0 = p & UniGroup;
1716 if ((p0 != 0) && (p0 != (S_IRGRP | S_IXGRP)) && (p0 != UniGroup)) {
1717 return true;
1718 }
1719 p0 = p & UniOthers;
1720 if ((p0 != 0) && (p0 != (S_IROTH | S_IXOTH)) && (p0 != UniOthers)) {
1721 return true;
1722 }
1723 return false;
1724 }
1725 if (p & S_ISVTX) { // sticky on file -> irregular
1726 return true;
1727 }
1728
1729 // check supported flag combinations
1730 mode_t p0 = p & UniOwner;
1731 bool usrXPossible = !p0; // true if this file could be an executable
1732 if (p0 & S_IXUSR) {
1733 if ((p0 == S_IXUSR) || (p0 == (S_IWUSR | S_IXUSR))) {
1734 return true;
1735 }
1736 usrXPossible = true;
1737 } else if (p0 == S_IWUSR) {
1738 return true;
1739 }
1740
1741 p0 = p & UniGroup;
1742 bool grpXPossible = !p0; // true if this file could be an executable
1743 if (p0 & S_IXGRP) {
1744 if ((p0 == S_IXGRP) || (p0 == (S_IWGRP | S_IXGRP))) {
1745 return true;
1746 }
1747 grpXPossible = true;
1748 } else if (p0 == S_IWGRP) {
1749 return true;
1750 }
1751 if (p0 == 0) {
1752 grpXPossible = true;
1753 }
1754
1755 p0 = p & UniOthers;
1756 bool othXPossible = !p0; // true if this file could be an executable
1757 if (p0 & S_IXOTH) {
1758 if ((p0 == S_IXOTH) || (p0 == (S_IWOTH | S_IXOTH))) {
1759 return true;
1760 }
1761 othXPossible = true;
1762 } else if (p0 == S_IWOTH) {
1763 return true;
1764 }
1765
1766 // check that there either all targets are executable-compatible, or none
1767 return (p & UniExec) && !(usrXPossible && grpXPossible && othXPossible);
1768}
1769
1770// enables/disabled the widgets in the Access Control frame
1771void KFilePermissionsPropsPlugin::enableAccessControls(bool enable)
1772{
1773 d->ownerPermCombo->setEnabled(enable);
1774 d->groupPermCombo->setEnabled(enable);
1775 d->othersPermCombo->setEnabled(enable);
1776 if (d->extraCheckbox) {
1777 d->extraCheckbox->setEnabled(enable);
1778 }
1779 if (d->cbRecursive) {
1780 d->cbRecursive->setEnabled(enable);
1781 }
1782}
1783
1784// updates all widgets in the Access Control frame
1785void KFilePermissionsPropsPlugin::updateAccessControls()
1786{
1787 setComboContent(combo: d->ownerPermCombo, target: PermissionsOwner, permissions: d->permissions, partial: d->partialPermissions);
1788 setComboContent(combo: d->groupPermCombo, target: PermissionsGroup, permissions: d->permissions, partial: d->partialPermissions);
1789 setComboContent(combo: d->othersPermCombo, target: PermissionsOthers, permissions: d->permissions, partial: d->partialPermissions);
1790
1791 switch (d->pmode) {
1792 case PermissionsOnlyLinks:
1793 enableAccessControls(enable: false);
1794 break;
1795 case PermissionsOnlyFiles:
1796 enableAccessControls(enable: d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
1797 if (d->canChangePermissions) {
1798 d->explanationLabel->setText(
1799 d->isIrregular || d->hasExtendedACL
1800 ? i18np("This file uses advanced permissions", "These files use advanced permissions.", properties->items().count())
1801 : QString());
1802 }
1803 if (d->partialPermissions & UniExec) {
1804 d->extraCheckbox->setTristate();
1805 d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
1806 } else {
1807 d->extraCheckbox->setTristate(false);
1808 d->extraCheckbox->setChecked(d->permissions & UniExec);
1809 }
1810 break;
1811 case PermissionsOnlyDirs:
1812 enableAccessControls(enable: d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
1813 // if this is a dir, and we can change permissions, don't dis-allow
1814 // recursive, we can do that for ACL setting.
1815 if (d->cbRecursive) {
1816 d->cbRecursive->setEnabled(d->canChangePermissions && !d->isIrregular);
1817 }
1818
1819 if (d->canChangePermissions) {
1820 d->explanationLabel->setText(
1821 d->isIrregular || d->hasExtendedACL
1822 ? i18np("This folder uses advanced permissions.", "These folders use advanced permissions.", properties->items().count())
1823 : QString());
1824 }
1825 if (d->partialPermissions & S_ISVTX) {
1826 d->extraCheckbox->setTristate();
1827 d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
1828 } else {
1829 d->extraCheckbox->setTristate(false);
1830 d->extraCheckbox->setChecked(d->permissions & S_ISVTX);
1831 }
1832 break;
1833 case PermissionsMixed:
1834 enableAccessControls(enable: d->canChangePermissions && !d->isIrregular && !d->hasExtendedACL);
1835 if (d->canChangePermissions) {
1836 d->explanationLabel->setText(d->isIrregular || d->hasExtendedACL ? i18n("These files use advanced permissions.") : QString());
1837 }
1838 if (d->partialPermissions & S_ISVTX) {
1839 d->extraCheckbox->setTristate();
1840 d->extraCheckbox->setCheckState(Qt::PartiallyChecked);
1841 } else {
1842 d->extraCheckbox->setTristate(false);
1843 d->extraCheckbox->setChecked(d->permissions & S_ISVTX);
1844 }
1845 break;
1846 }
1847}
1848
1849// gets masks for files and dirs from the Access Control frame widgets
1850void KFilePermissionsPropsPlugin::getPermissionMasks(mode_t &andFilePermissions, mode_t &andDirPermissions, mode_t &orFilePermissions, mode_t &orDirPermissions)
1851{
1852 andFilePermissions = mode_t(~UniSpecial);
1853 andDirPermissions = mode_t(~(S_ISUID | S_ISGID));
1854 orFilePermissions = 0;
1855 orDirPermissions = 0;
1856 if (d->isIrregular) {
1857 return;
1858 }
1859
1860 mode_t m = standardPermissions[d->ownerPermCombo->currentIndex()];
1861 if (m != s_invalid_mode_t) {
1862 orFilePermissions |= m & UniOwner;
1863 if ((m & UniOwner)
1864 && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
1865 andFilePermissions &= ~(S_IRUSR | S_IWUSR);
1866 } else {
1867 andFilePermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR);
1868 if ((m & S_IRUSR) && (d->extraCheckbox->checkState() == Qt::Checked)) {
1869 orFilePermissions |= S_IXUSR;
1870 }
1871 }
1872
1873 orDirPermissions |= m & UniOwner;
1874 if (m & S_IRUSR) {
1875 orDirPermissions |= S_IXUSR;
1876 }
1877 andDirPermissions &= ~(S_IRUSR | S_IWUSR | S_IXUSR);
1878 }
1879
1880 m = standardPermissions[d->groupPermCombo->currentIndex()];
1881 if (m != s_invalid_mode_t) {
1882 orFilePermissions |= m & UniGroup;
1883 if ((m & UniGroup)
1884 && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
1885 andFilePermissions &= ~(S_IRGRP | S_IWGRP);
1886 } else {
1887 andFilePermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP);
1888 if ((m & S_IRGRP) && (d->extraCheckbox->checkState() == Qt::Checked)) {
1889 orFilePermissions |= S_IXGRP;
1890 }
1891 }
1892
1893 orDirPermissions |= m & UniGroup;
1894 if (m & S_IRGRP) {
1895 orDirPermissions |= S_IXGRP;
1896 }
1897 andDirPermissions &= ~(S_IRGRP | S_IWGRP | S_IXGRP);
1898 }
1899
1900 m = d->othersPermCombo->currentIndex() >= 0 ? standardPermissions[d->othersPermCombo->currentIndex()] : s_invalid_mode_t;
1901 if (m != s_invalid_mode_t) {
1902 orFilePermissions |= m & UniOthers;
1903 if ((m & UniOthers)
1904 && ((d->pmode == PermissionsMixed) || ((d->pmode == PermissionsOnlyFiles) && (d->extraCheckbox->checkState() == Qt::PartiallyChecked)))) {
1905 andFilePermissions &= ~(S_IROTH | S_IWOTH);
1906 } else {
1907 andFilePermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH);
1908 if ((m & S_IROTH) && (d->extraCheckbox->checkState() == Qt::Checked)) {
1909 orFilePermissions |= S_IXOTH;
1910 }
1911 }
1912
1913 orDirPermissions |= m & UniOthers;
1914 if (m & S_IROTH) {
1915 orDirPermissions |= S_IXOTH;
1916 }
1917 andDirPermissions &= ~(S_IROTH | S_IWOTH | S_IXOTH);
1918 }
1919
1920 if (((d->pmode == PermissionsMixed) || (d->pmode == PermissionsOnlyDirs)) && (d->extraCheckbox->checkState() != Qt::PartiallyChecked)) {
1921 andDirPermissions &= ~S_ISVTX;
1922 if (d->extraCheckbox->checkState() == Qt::Checked) {
1923 orDirPermissions |= S_ISVTX;
1924 }
1925 }
1926}
1927
1928void KFilePermissionsPropsPlugin::applyChanges()
1929{
1930 mode_t orFilePermissions;
1931 mode_t orDirPermissions;
1932 mode_t andFilePermissions;
1933 mode_t andDirPermissions;
1934
1935 if (!d->canChangePermissions) {
1936 properties->abortApplying();
1937 return;
1938 }
1939
1940 if (!d->isIrregular) {
1941 getPermissionMasks(andFilePermissions, andDirPermissions, orFilePermissions, orDirPermissions);
1942 } else {
1943 orFilePermissions = d->permissions;
1944 andFilePermissions = d->partialPermissions;
1945 orDirPermissions = d->permissions;
1946 andDirPermissions = d->partialPermissions;
1947 }
1948
1949 QString owner;
1950 QString group;
1951 if (d->usrEdit) {
1952 owner = d->usrEdit->text();
1953 }
1954 if (d->grpEdit) {
1955 group = d->grpEdit->text();
1956 } else if (d->grpCombo) {
1957 group = d->grpCombo->currentText();
1958 }
1959
1960 const bool recursive = d->cbRecursive && d->cbRecursive->isChecked();
1961
1962 if (!recursive) {
1963 if (owner == d->strOwner) {
1964 owner.clear();
1965 }
1966
1967 if (group == d->strGroup) {
1968 group.clear();
1969 }
1970 }
1971
1972 bool permissionChange = false;
1973
1974 const KFileItemList items = properties->items();
1975 KFileItemList files;
1976 KFileItemList dirs;
1977 for (const auto &item : items) {
1978 const auto perms = item.permissions();
1979 if (item.isDir()) {
1980 dirs.append(t: item);
1981 if (!permissionChange && (recursive || perms != ((perms & andDirPermissions) | orDirPermissions))) {
1982 permissionChange = true;
1983 }
1984 continue;
1985 }
1986
1987 if (item.isFile()) {
1988 files.append(t: item);
1989 if (!permissionChange && perms != ((perms & andFilePermissions) | orFilePermissions)) {
1990 permissionChange = true;
1991 }
1992 }
1993 }
1994
1995 const bool ACLChange = (d->extendedACL != properties->item().ACL());
1996 const bool defaultACLChange = (d->defaultACL != properties->item().defaultACL());
1997
1998 if (owner.isEmpty() && group.isEmpty() && !recursive && !permissionChange && !ACLChange && !defaultACLChange) {
1999 return;
2000 }
2001
2002 auto processACLChanges = [this, ACLChange, defaultACLChange](KIO::ChmodJob *chmodJob) {
2003 if (!d->fileSystemSupportsACLs) {
2004 return;
2005 }
2006
2007 if (ACLChange) {
2008 chmodJob->addMetaData(QStringLiteral("ACL_STRING"), value: d->extendedACL.isValid() ? d->extendedACL.asString() : QStringLiteral("ACL_DELETE"));
2009 }
2010
2011 if (defaultACLChange) {
2012 chmodJob->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), value: d->defaultACL.isValid() ? d->defaultACL.asString() : QStringLiteral("ACL_DELETE"));
2013 }
2014 };
2015
2016 auto chmodDirs = [=, this]() {
2017 if (dirs.isEmpty()) {
2018 setDirty(false);
2019 Q_EMIT changesApplied();
2020 return;
2021 }
2022
2023 auto *dirsJob = KIO::chmod(lstItems: dirs, permissions: orDirPermissions, mask: ~andDirPermissions, newOwner: owner, newGroup: group, recursive);
2024 processACLChanges(dirsJob);
2025
2026 connect(sender: dirsJob, signal: &KJob::result, context: this, slot: [this, dirsJob]() {
2027 if (dirsJob->error()) {
2028 dirsJob->uiDelegate()->showErrorMessage();
2029 }
2030
2031 setDirty(false);
2032 Q_EMIT changesApplied();
2033 });
2034 };
2035
2036 // Change permissions in two steps, first files, then dirs
2037
2038 if (!files.isEmpty()) {
2039 auto *filesJob = KIO::chmod(lstItems: files, permissions: orFilePermissions, mask: ~andFilePermissions, newOwner: owner, newGroup: group, recursive: false);
2040 processACLChanges(filesJob);
2041
2042 connect(sender: filesJob, signal: &KJob::result, context: this, slot: [=]() {
2043 if (filesJob->error()) {
2044 filesJob->uiDelegate()->showErrorMessage();
2045 }
2046
2047 chmodDirs();
2048 });
2049 return;
2050 }
2051
2052 // No files to change? OK, now process dirs (if any)
2053 chmodDirs();
2054}
2055
2056class KChecksumsPlugin::KChecksumsPluginPrivate
2057{
2058public:
2059 QWidget m_widget;
2060 Ui::ChecksumsWidget m_ui;
2061
2062 QFileSystemWatcher fileWatcher;
2063 QString m_md5;
2064 QString m_sha1;
2065 QString m_sha256;
2066 QString m_sha512;
2067
2068 bool m_multiFileMode{false};
2069 bool m_md5Matches{false};
2070 bool m_sha1Matches{false};
2071 bool m_sha256Matches{false};
2072 bool m_sha512Matches{false};
2073};
2074
2075KChecksumsPlugin::KChecksumsPlugin(KPropertiesDialog *dialog)
2076 : KPropertiesDialogPlugin(dialog)
2077 , d(new KChecksumsPluginPrivate)
2078{
2079 d->m_ui.setupUi(&d->m_widget);
2080 properties->addPage(widget: &d->m_widget, i18nc("@title:tab", "C&hecksums"));
2081
2082 d->m_ui.md5CopyButton->hide();
2083 d->m_ui.sha1CopyButton->hide();
2084 d->m_ui.sha256CopyButton->hide();
2085 d->m_ui.sha512CopyButton->hide();
2086
2087 connect(sender: d->m_ui.lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this](const QString &text) {
2088 slotVerifyChecksum(input: text.toLower());
2089 });
2090
2091 connect(sender: d->m_ui.md5Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowMd5);
2092 connect(sender: d->m_ui.sha1Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha1);
2093 connect(sender: d->m_ui.sha256Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha256);
2094 connect(sender: d->m_ui.sha512Button, signal: &QPushButton::clicked, context: this, slot: &KChecksumsPlugin::slotShowSha512);
2095
2096 // NOTE we only watch the first file for changes
2097 // - if there is only one file, then this is the only path
2098 // - if there are multiple file, the first one is the one we're comparing against
2099 // - properly watching all paths would be complicated because we would have to
2100 // work around limits to the number of files supported by QFileSystemWatcher...
2101 d->fileWatcher.addPath(file: properties->item().localPath());
2102 connect(sender: &d->fileWatcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &KChecksumsPlugin::slotInvalidateCache);
2103
2104 auto clipboard = QApplication::clipboard();
2105 connect(sender: d->m_ui.md5CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2106 clipboard->setText(d->m_md5);
2107 });
2108
2109 connect(sender: d->m_ui.sha1CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2110 clipboard->setText(d->m_sha1);
2111 });
2112
2113 connect(sender: d->m_ui.sha256CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2114 clipboard->setText(d->m_sha256);
2115 });
2116
2117 connect(sender: d->m_ui.sha512CopyButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2118 clipboard->setText(d->m_sha512);
2119 });
2120
2121 connect(sender: d->m_ui.pasteButton, signal: &QPushButton::clicked, context: this, slot: [=, this]() {
2122 d->m_ui.lineEdit->setText(clipboard->text());
2123 });
2124
2125 setDefaultState();
2126
2127 if (properties->items().count() == 1 && detectAlgorithm(input: clipboard->text()) != QCryptographicHash::Md4) {
2128 d->m_ui.lineEdit->setText(clipboard->text());
2129 }
2130}
2131
2132KChecksumsPlugin::~KChecksumsPlugin() = default;
2133
2134bool KChecksumsPlugin::supports(const KFileItemList &items)
2135{
2136 if (items.count() < 1) {
2137 return false;
2138 }
2139
2140 for (const auto &i : items) {
2141 if (!i.isFile() || i.localPath().isEmpty() || !i.isReadable() || i.isLink()) {
2142 return false;
2143 }
2144 }
2145
2146 return true;
2147}
2148
2149void KChecksumsPlugin::slotInvalidateCache()
2150{
2151 d->m_md5 = QString();
2152 d->m_sha1 = QString();
2153 d->m_sha256 = QString();
2154 d->m_sha512 = QString();
2155}
2156
2157void KChecksumsPlugin::slotShowMd5()
2158{
2159 d->m_ui.md5LineEdit->show(); // Show before hiding. This way keyboard focus goes from md5Button to md5LineEdit.
2160 d->m_ui.md5Button->hide();
2161 d->m_ui.md5Label->setBuddy(d->m_ui.md5LineEdit);
2162 d->m_ui.horizontalSpacerMd5->changeSize(w: 0, h: 0);
2163
2164 showChecksum(algorithm: QCryptographicHash::Md5, label: d->m_ui.md5LineEdit, copyButton: d->m_ui.md5CopyButton);
2165}
2166
2167void KChecksumsPlugin::slotShowSha1()
2168{
2169 d->m_ui.sha1LineEdit->show(); // Show before hiding. This way keyboard focus goes from sha1Button to sha1LineEdit.
2170 d->m_ui.sha1Button->hide();
2171 d->m_ui.sha1Label->setBuddy(d->m_ui.sha1LineEdit);
2172 d->m_ui.horizontalSpacerSha1->changeSize(w: 0, h: 0);
2173
2174 showChecksum(algorithm: QCryptographicHash::Sha1, label: d->m_ui.sha1LineEdit, copyButton: d->m_ui.sha1CopyButton);
2175}
2176
2177void KChecksumsPlugin::slotShowSha256()
2178{
2179 d->m_ui.sha256LineEdit->show();
2180 d->m_ui.sha256Button->hide();
2181 d->m_ui.sha256Label->setBuddy(d->m_ui.sha256LineEdit);
2182 d->m_ui.horizontalSpacerSha256->changeSize(w: 0, h: 0);
2183
2184 showChecksum(algorithm: QCryptographicHash::Sha256, label: d->m_ui.sha256LineEdit, copyButton: d->m_ui.sha256CopyButton);
2185}
2186
2187void KChecksumsPlugin::slotShowSha512()
2188{
2189 d->m_ui.sha512LineEdit->show();
2190 d->m_ui.sha512Button->hide();
2191 d->m_ui.sha512Label->setBuddy(d->m_ui.sha512LineEdit);
2192 d->m_ui.horizontalSpacerSha512->changeSize(w: 0, h: 0);
2193
2194 showChecksum(algorithm: QCryptographicHash::Sha512, label: d->m_ui.sha512LineEdit, copyButton: d->m_ui.sha512CopyButton);
2195}
2196
2197void KChecksumsPlugin::slotVerifyChecksum(const QString &input)
2198{
2199 auto algorithm = detectAlgorithm(input);
2200
2201 // Input is not a supported hash algorithm.
2202 if (algorithm == QCryptographicHash::Md4) {
2203 if (input.isEmpty()) {
2204 setDefaultState();
2205 } else {
2206 setInvalidChecksumState();
2207 }
2208 return;
2209 }
2210
2211 const QString checksum = cachedChecksum(algorithm);
2212
2213 // Checksum already in cache.
2214 if (!checksum.isEmpty()) {
2215 const bool isMatch = (checksum == input);
2216 if (isMatch) {
2217 setMatchState();
2218 } else {
2219 setMismatchState();
2220 }
2221
2222 return;
2223 }
2224
2225 // Calculate checksum in another thread.
2226 using CheckType = QPair<QString, bool>;
2227
2228 auto futureWatcher = new QFutureWatcher<CheckType>(this);
2229 connect(sender: futureWatcher, signal: &QFutureWatcher<CheckType>::finished, context: this, slot: [=, this]() {
2230 const QString checksum = futureWatcher->result().first;
2231 futureWatcher->deleteLater();
2232
2233 cacheChecksum(checksum, algorithm);
2234
2235 switch (algorithm) {
2236 case QCryptographicHash::Md5:
2237 slotShowMd5();
2238 break;
2239 case QCryptographicHash::Sha1:
2240 slotShowSha1();
2241 break;
2242 case QCryptographicHash::Sha256:
2243 slotShowSha256();
2244 break;
2245 case QCryptographicHash::Sha512:
2246 slotShowSha512();
2247 break;
2248 default:
2249 break;
2250 }
2251
2252 const bool isMatch = (checksum == input);
2253 if (isMatch) {
2254 setMatchState();
2255 } else {
2256 setMismatchState();
2257 }
2258 });
2259
2260 // Notify the user about the background computation.
2261 setVerifyState();
2262
2263 auto future = QtConcurrent::run(f: &KChecksumsPlugin::computeChecksum, args&: algorithm, args: properties->items());
2264 futureWatcher->setFuture(future);
2265}
2266
2267bool KChecksumsPlugin::isMd5(const QString &input)
2268{
2269 QRegularExpression regex(QStringLiteral("^[a-f0-9]{32}$"));
2270 return regex.match(subject: input).hasMatch();
2271}
2272
2273bool KChecksumsPlugin::isSha1(const QString &input)
2274{
2275 QRegularExpression regex(QStringLiteral("^[a-f0-9]{40}$"));
2276 return regex.match(subject: input).hasMatch();
2277}
2278
2279bool KChecksumsPlugin::isSha256(const QString &input)
2280{
2281 QRegularExpression regex(QStringLiteral("^[a-f0-9]{64}$"));
2282 return regex.match(subject: input).hasMatch();
2283}
2284
2285bool KChecksumsPlugin::isSha512(const QString &input)
2286{
2287 QRegularExpression regex(QStringLiteral("^[a-f0-9]{128}$"));
2288 return regex.match(subject: input).hasMatch();
2289}
2290
2291QPair<QString, bool> KChecksumsPlugin::computeChecksum(QCryptographicHash::Algorithm algorithm, const KFileItemList &items)
2292{
2293 auto getChecksum = [&](const QString &path) {
2294 QFile file(path);
2295 if (!file.open(flags: QIODevice::ReadOnly)) {
2296 return QString();
2297 }
2298
2299 QCryptographicHash hash(algorithm);
2300 hash.addData(device: &file);
2301
2302 return QString::fromLatin1(ba: hash.result().toHex());
2303 };
2304
2305 QString comparedSum = getChecksum(items.first().localPath());
2306 bool matches = true;
2307
2308 for (qsizetype i = 1; i < items.count(); ++i) {
2309 auto sum = getChecksum(items[i].localPath());
2310
2311 if (sum != comparedSum) {
2312 matches = false;
2313 break;
2314 }
2315 }
2316
2317 return {comparedSum, matches};
2318}
2319
2320QCryptographicHash::Algorithm KChecksumsPlugin::detectAlgorithm(const QString &input)
2321{
2322 if (isMd5(input)) {
2323 return QCryptographicHash::Md5;
2324 }
2325
2326 if (isSha1(input)) {
2327 return QCryptographicHash::Sha1;
2328 }
2329
2330 if (isSha256(input)) {
2331 return QCryptographicHash::Sha256;
2332 }
2333
2334 if (isSha512(input)) {
2335 return QCryptographicHash::Sha512;
2336 }
2337
2338 // Md4 used as negative error code.
2339 return QCryptographicHash::Md4;
2340}
2341
2342void KChecksumsPlugin::setDefaultState()
2343{
2344 QColor defaultColor = d->m_widget.palette().color(cr: QPalette::Base);
2345
2346 QPalette palette = d->m_widget.palette();
2347 palette.setColor(acr: QPalette::Base, acolor: defaultColor);
2348
2349 d->m_ui.feedbackLabel->hide();
2350 d->m_ui.lineEdit->setPalette(palette);
2351 d->m_ui.lineEdit->setToolTip(QString());
2352
2353 if (properties->items().count() > 1) {
2354 d->m_multiFileMode = true;
2355
2356 d->m_ui.label->hide();
2357 d->m_ui.kseparator->hide();
2358 d->m_ui.lineEdit->hide();
2359 d->m_ui.pasteButton->hide();
2360 }
2361}
2362
2363void KChecksumsPlugin::setInvalidChecksumState()
2364{
2365 KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
2366 QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color();
2367
2368 QPalette palette = d->m_widget.palette();
2369 palette.setColor(acr: QPalette::Base, acolor: warningColor);
2370
2371 d->m_ui.feedbackLabel->setText(i18n("Invalid checksum."));
2372 d->m_ui.feedbackLabel->show();
2373 d->m_ui.lineEdit->setPalette(palette);
2374 d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The given input is not a valid MD5, SHA1 or SHA256 checksum."));
2375}
2376
2377void KChecksumsPlugin::setMatchState()
2378{
2379 KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
2380 QColor positiveColor = colorScheme.background(KColorScheme::PositiveBackground).color();
2381
2382 QPalette palette = d->m_widget.palette();
2383 palette.setColor(acr: QPalette::Base, acolor: positiveColor);
2384
2385 d->m_ui.feedbackLabel->setText(i18n("Checksums match."));
2386 d->m_ui.feedbackLabel->show();
2387 d->m_ui.lineEdit->setPalette(palette);
2388 d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum match."));
2389}
2390
2391void KChecksumsPlugin::setMismatchState()
2392{
2393 KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
2394 QColor warningColor = colorScheme.background(KColorScheme::NegativeBackground).color();
2395
2396 QPalette palette = d->m_widget.palette();
2397 palette.setColor(acr: QPalette::Base, acolor: warningColor);
2398
2399 d->m_ui.feedbackLabel->setText(
2400 i18n("<p>Checksums do not match.</p>"
2401 "This may be due to a faulty download. Try re-downloading the file.<br/>"
2402 "If the verification still fails, contact the source of the file."));
2403 d->m_ui.feedbackLabel->show();
2404 d->m_ui.feedbackLabel->setWordWrap(true);
2405 d->m_ui.lineEdit->setPalette(palette);
2406 d->m_ui.lineEdit->setToolTip(i18nc("@info:tooltip", "The computed checksum and the expected checksum differ."));
2407}
2408
2409void KChecksumsPlugin::setVerifyState()
2410{
2411 // Users can paste a checksum at any time, so reset to default.
2412 setDefaultState();
2413
2414 d->m_ui.feedbackLabel->setText(i18nc("@info:progress computation in the background", "Verifying checksum…"));
2415 d->m_ui.feedbackLabel->show();
2416}
2417
2418void KChecksumsPlugin::showChecksum(QCryptographicHash::Algorithm algorithm, QLineEdit *label, QPushButton *copyButton)
2419{
2420 const QString checksum = cachedChecksum(algorithm);
2421
2422 // Reset colors before calculating
2423 KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
2424 QPalette palette = d->m_widget.palette();
2425 QColor defaultColor = d->m_widget.palette().color(cr: QPalette::Base);
2426 palette.setColor(acr: QPalette::Base, acolor: defaultColor);
2427 label->setPalette(palette);
2428
2429 // Checksum in cache, nothing else to do
2430 if (!checksum.isEmpty()) {
2431 label->setText(checksum);
2432 label->setCursorPosition(0);
2433 copyButton->show();
2434
2435 if (d->m_multiFileMode) {
2436 d->m_ui.feedbackLabel->show();
2437 d->m_ui.kseparator->show();
2438
2439 if (cachedMultiFileMatch(algorithm)) {
2440 QColor positiveColor = colorScheme.background(KColorScheme::PositiveBackground).color();
2441 palette.setColor(acr: QPalette::Base, acolor: positiveColor);
2442 label->setPalette(palette);
2443 d->m_ui.feedbackLabel->setText(i18n("The checksums of all files are identical."));
2444 } else {
2445 QColor negativeColor = colorScheme.background(KColorScheme::NegativeBackground).color();
2446 palette.setColor(acr: QPalette::Base, acolor: negativeColor);
2447 label->setPalette(palette);
2448 d->m_ui.feedbackLabel->setText(i18n("The selected files have different checksums."));
2449 }
2450 }
2451
2452 return;
2453 } else {
2454 label->setText(i18nc("@info:progress", "Calculating…"));
2455 }
2456
2457 // Calculate checksum in another thread
2458 using CheckType = QPair<QString, bool>;
2459
2460 auto futureWatcher = new QFutureWatcher<CheckType>(this);
2461 connect(sender: futureWatcher, signal: &QFutureWatcher<CheckType>::finished, context: this, slot: [=, this]() {
2462 const CheckType result = futureWatcher->result();
2463 futureWatcher->deleteLater();
2464
2465 cacheChecksum(checksum: result.first, algorithm);
2466 cacheMultiFileMatch(isMatch: result.second, algorithm);
2467
2468 showChecksum(algorithm, label, copyButton); // actually show cached result
2469 });
2470
2471 auto future = QtConcurrent::run(f: &KChecksumsPlugin::computeChecksum, args&: algorithm, args: properties->items());
2472 futureWatcher->setFuture(future);
2473}
2474
2475QString KChecksumsPlugin::cachedChecksum(QCryptographicHash::Algorithm algorithm) const
2476{
2477 switch (algorithm) {
2478 case QCryptographicHash::Md5:
2479 return d->m_md5;
2480 case QCryptographicHash::Sha1:
2481 return d->m_sha1;
2482 case QCryptographicHash::Sha256:
2483 return d->m_sha256;
2484 case QCryptographicHash::Sha512:
2485 return d->m_sha512;
2486 default:
2487 break;
2488 }
2489
2490 return QString();
2491}
2492
2493void KChecksumsPlugin::cacheChecksum(const QString &checksum, QCryptographicHash::Algorithm algorithm)
2494{
2495 switch (algorithm) {
2496 case QCryptographicHash::Md5:
2497 d->m_md5 = checksum;
2498 break;
2499 case QCryptographicHash::Sha1:
2500 d->m_sha1 = checksum;
2501 break;
2502 case QCryptographicHash::Sha256:
2503 d->m_sha256 = checksum;
2504 break;
2505 case QCryptographicHash::Sha512:
2506 d->m_sha512 = checksum;
2507 break;
2508 default:
2509 return;
2510 }
2511}
2512
2513bool KChecksumsPlugin::cachedMultiFileMatch(QCryptographicHash::Algorithm algorithm) const
2514{
2515 switch (algorithm) {
2516 case QCryptographicHash::Md5:
2517 return d->m_md5Matches;
2518 case QCryptographicHash::Sha1:
2519 return d->m_sha1Matches;
2520 case QCryptographicHash::Sha256:
2521 return d->m_sha256Matches;
2522 case QCryptographicHash::Sha512:
2523 return d->m_sha512Matches;
2524 default:
2525 break;
2526 }
2527
2528 return false;
2529}
2530
2531void KChecksumsPlugin::cacheMultiFileMatch(const bool &isMatch, QCryptographicHash::Algorithm algorithm)
2532{
2533 switch (algorithm) {
2534 case QCryptographicHash::Md5:
2535 d->m_md5Matches = isMatch;
2536 break;
2537 case QCryptographicHash::Sha1:
2538 d->m_sha1Matches = isMatch;
2539 break;
2540 case QCryptographicHash::Sha256:
2541 d->m_sha256Matches = isMatch;
2542 break;
2543 case QCryptographicHash::Sha512:
2544 d->m_sha512Matches = isMatch;
2545 break;
2546 default:
2547 return;
2548 }
2549}
2550
2551class KUrlPropsPlugin::KUrlPropsPluginPrivate
2552{
2553public:
2554 QFrame *m_frame;
2555 KUrlRequester *URLEdit;
2556 QString URLStr;
2557 bool fileNameReadOnly = false;
2558};
2559
2560KUrlPropsPlugin::KUrlPropsPlugin(KPropertiesDialog *_props)
2561 : KPropertiesDialogPlugin(_props)
2562 , d(new KUrlPropsPluginPrivate)
2563{
2564 d->m_frame = new QFrame();
2565 properties->addPage(widget: d->m_frame, i18n("U&RL"));
2566 QVBoxLayout *layout = new QVBoxLayout(d->m_frame);
2567
2568 QLabel *l;
2569 l = new QLabel(d->m_frame);
2570 l->setObjectName(QStringLiteral("Label_1"));
2571 l->setText(i18n("URL:"));
2572 layout->addWidget(l);
2573
2574 d->URLEdit = new KUrlRequester(d->m_frame);
2575 layout->addWidget(d->URLEdit);
2576
2577 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2578 KJobWidgets::setWindow(job, widget: properties);
2579 job->exec();
2580 QUrl url = job->mostLocalUrl();
2581
2582 if (url.isLocalFile()) {
2583 QString path = url.toLocalFile();
2584
2585 QFile f(path);
2586 if (!f.open(flags: QIODevice::ReadOnly)) {
2587 return;
2588 }
2589
2590 KDesktopFile config(path);
2591 const KConfigGroup dg = config.desktopGroup();
2592 d->URLStr = dg.readPathEntry(key: "URL", aDefault: QString());
2593
2594 if (!d->URLStr.isEmpty()) {
2595 d->URLEdit->setUrl(QUrl(d->URLStr));
2596 }
2597 }
2598
2599 connect(sender: d->URLEdit, signal: &KUrlRequester::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2600
2601 layout->addStretch(stretch: 1);
2602}
2603
2604KUrlPropsPlugin::~KUrlPropsPlugin() = default;
2605
2606void KUrlPropsPlugin::setFileNameReadOnly(bool ro)
2607{
2608 d->fileNameReadOnly = ro;
2609}
2610
2611bool KUrlPropsPlugin::supports(const KFileItemList &_items)
2612{
2613 if (_items.count() != 1) {
2614 return false;
2615 }
2616 const KFileItem &item = _items.first();
2617 // check if desktop file
2618 if (!item.isDesktopFile()) {
2619 return false;
2620 }
2621
2622 // open file and check type
2623 const auto [url, isLocal] = item.isMostLocalUrl();
2624 if (!isLocal) {
2625 return false;
2626 }
2627
2628 KDesktopFile config(url.toLocalFile());
2629 return config.hasLinkType();
2630}
2631
2632void KUrlPropsPlugin::applyChanges()
2633{
2634 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2635 KJobWidgets::setWindow(job, widget: properties);
2636 job->exec();
2637 const QUrl url = job->mostLocalUrl();
2638
2639 if (!url.isLocalFile()) {
2640 KMessageBox::error(parent: nullptr, i18n("Could not save properties. Only entries on local file systems are supported."));
2641 properties->abortApplying();
2642 return;
2643 }
2644
2645 QString path = url.toLocalFile();
2646 QFile f(path);
2647 if (!f.open(flags: QIODevice::ReadWrite)) {
2648 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
2649 properties->abortApplying();
2650 return;
2651 }
2652
2653 KDesktopFile config(path);
2654 KConfigGroup dg = config.desktopGroup();
2655 dg.writeEntry(key: "Type", QStringLiteral("Link"));
2656 dg.writePathEntry(Key: "URL", path: d->URLEdit->url().toString());
2657 // Users can't create a Link .desktop file with a Name field,
2658 // but distributions can. Update the Name field in that case,
2659 // if the file name could have been changed.
2660 if (!d->fileNameReadOnly && dg.hasKey(key: "Name")) {
2661 const QString nameStr = nameFromFileName(nameStr: properties->url().fileName());
2662 dg.writeEntry(key: "Name", value: nameStr);
2663 dg.writeEntry(key: "Name", value: nameStr, pFlags: KConfigBase::Persistent | KConfigBase::Localized);
2664 }
2665
2666 setDirty(false);
2667}
2668
2669/* ----------------------------------------------------
2670 *
2671 * KDesktopPropsPlugin
2672 *
2673 * -------------------------------------------------- */
2674
2675class KDesktopPropsPlugin::KDesktopPropsPluginPrivate
2676{
2677public:
2678 KDesktopPropsPluginPrivate()
2679 : w(new Ui_KPropertiesDesktopBase)
2680 , m_frame(new QFrame())
2681 {
2682 }
2683 ~KDesktopPropsPluginPrivate()
2684 {
2685 delete w;
2686 }
2687 QString command() const;
2688 Ui_KPropertiesDesktopBase *w;
2689 QWidget *m_frame = nullptr;
2690 std::unique_ptr<Ui_KPropertiesDesktopAdvBase> m_uiAdvanced;
2691
2692 QString m_origCommandStr;
2693 QString m_terminalOptionStr;
2694 QString m_suidUserStr;
2695 QString m_origDesktopFile;
2696 bool m_terminalBool;
2697 bool m_suidBool;
2698 // Corresponds to "PrefersNonDefaultGPU=" (added in destop-entry-spec 1.4)
2699 bool m_runOnDiscreteGpuBool;
2700 bool m_startupBool;
2701};
2702
2703KDesktopPropsPlugin::KDesktopPropsPlugin(KPropertiesDialog *_props)
2704 : KPropertiesDialogPlugin(_props)
2705 , d(new KDesktopPropsPluginPrivate)
2706{
2707 QMimeDatabase db;
2708
2709 d->w->setupUi(d->m_frame);
2710
2711 properties->addPage(widget: d->m_frame, i18n("&Application"));
2712
2713 bool bKDesktopMode = properties->url().scheme() == QLatin1String("desktop") || properties->currentDir().scheme() == QLatin1String("desktop");
2714
2715 d->w->pathEdit->setMode(KFile::Directory | KFile::LocalOnly);
2716 d->w->pathEdit->lineEdit()->setAcceptDrops(false);
2717
2718 connect(sender: d->w->nameEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2719 connect(sender: d->w->genNameEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2720 connect(sender: d->w->commentEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2721 connect(sender: d->w->envarsEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2722 connect(sender: d->w->programEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2723 connect(sender: d->w->argumentsEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2724 connect(sender: d->w->pathEdit, signal: &KUrlRequester::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
2725
2726 connect(sender: d->w->browseButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotBrowseExec);
2727 connect(sender: d->w->addFiletypeButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotAddFiletype);
2728 connect(sender: d->w->delFiletypeButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotDelFiletype);
2729 connect(sender: d->w->advancedButton, signal: &QAbstractButton::clicked, context: this, slot: &KDesktopPropsPlugin::slotAdvanced);
2730
2731 // now populate the page
2732
2733 KIO::StatJob *job = KIO::mostLocalUrl(url: _props->url());
2734 KJobWidgets::setWindow(job, widget: _props);
2735 job->exec();
2736 QUrl url = job->mostLocalUrl();
2737
2738 if (!url.isLocalFile()) {
2739 return;
2740 }
2741
2742 d->m_origDesktopFile = url.toLocalFile();
2743
2744 QFile f(d->m_origDesktopFile);
2745 if (!f.open(flags: QIODevice::ReadOnly)) {
2746 return;
2747 }
2748
2749 KDesktopFile _config(d->m_origDesktopFile);
2750 KConfigGroup config = _config.desktopGroup();
2751 QString nameStr = _config.readName();
2752 QString genNameStr = _config.readGenericName();
2753 QString commentStr = _config.readComment();
2754 QString commandStr = config.readEntry(key: "Exec", aDefault: QString());
2755
2756 d->m_origCommandStr = commandStr;
2757 QString pathStr = config.readEntry(key: "Path", aDefault: QString()); // not readPathEntry, see kservice.cpp
2758 d->m_terminalBool = config.readEntry(key: "Terminal", defaultValue: false);
2759 d->m_terminalOptionStr = config.readEntry(key: "TerminalOptions");
2760 d->m_suidBool = config.readEntry(key: "X-KDE-SubstituteUID", defaultValue: false);
2761 d->m_suidUserStr = config.readEntry(key: "X-KDE-Username");
2762 if (KIO::hasDiscreteGpu()) {
2763 if (config.hasKey(key: "PrefersNonDefaultGPU")) {
2764 d->m_runOnDiscreteGpuBool = config.readEntry(key: "PrefersNonDefaultGPU", defaultValue: false);
2765 } else {
2766 d->m_runOnDiscreteGpuBool = config.readEntry(key: "X-KDE-RunOnDiscreteGpu", defaultValue: false);
2767 }
2768 }
2769 if (config.hasKey(key: "StartupNotify")) {
2770 d->m_startupBool = config.readEntry(key: "StartupNotify", defaultValue: true);
2771 } else {
2772 d->m_startupBool = config.readEntry(key: "X-KDE-StartupNotify", defaultValue: true);
2773 }
2774
2775 const QStringList mimeTypes = config.readXdgListEntry(key: "MimeType");
2776
2777 if (nameStr.isEmpty() || bKDesktopMode) {
2778 // We'll use the file name if no name is specified
2779 // because we _need_ a Name for a valid file.
2780 // But let's do it in apply, not here, so that we pick up the right name.
2781 setDirty();
2782 }
2783 d->w->nameEdit->setText(nameStr);
2784 d->w->genNameEdit->setText(genNameStr);
2785 d->w->commentEdit->setText(commentStr);
2786
2787 QStringList execLine = KShell::splitArgs(cmd: commandStr);
2788 QStringList enVars = {};
2789
2790 if (!execLine.isEmpty()) {
2791 // check for apps that use the env executable
2792 // to set the environment
2793 if (execLine[0] == QLatin1String("env")) {
2794 execLine.pop_front();
2795 }
2796 for (const auto &env : execLine) {
2797 if (execLine.length() <= 1) {
2798 // Don't empty out the list. If the last element contains an equal sign we have to treat it as part of the
2799 // program name lest we have no program
2800 // https://bugs.kde.org/show_bug.cgi?id=465290
2801 break;
2802 }
2803 if (!env.contains(s: QLatin1String("="))) {
2804 break;
2805 }
2806 enVars += env;
2807 execLine.pop_front();
2808 }
2809
2810 Q_ASSERT(!execLine.isEmpty());
2811 d->w->programEdit->setText(execLine.takeFirst());
2812 } else {
2813 d->w->programEdit->clear();
2814 }
2815 d->w->argumentsEdit->setText(KShell::joinArgs(args: execLine));
2816 d->w->envarsEdit->setText(KShell::joinArgs(args: enVars));
2817
2818 d->w->pathEdit->lineEdit()->setText(pathStr);
2819
2820 // was: d->w->filetypeList->setFullWidth(true);
2821 // d->w->filetypeList->header()->setStretchEnabled(true, d->w->filetypeList->columns()-1);
2822
2823 for (QStringList::ConstIterator it = mimeTypes.begin(); it != mimeTypes.end();) {
2824 QMimeType p = db.mimeTypeForName(nameOrAlias: *it);
2825 ++it;
2826 QString preference;
2827 if (it != mimeTypes.end()) {
2828 bool numeric;
2829 (*it).toInt(ok: &numeric);
2830 if (numeric) {
2831 preference = *it;
2832 ++it;
2833 }
2834 }
2835 if (p.isValid()) {
2836 QTreeWidgetItem *item = new QTreeWidgetItem();
2837 item->setText(column: 0, atext: p.name());
2838 item->setText(column: 1, atext: p.comment());
2839 item->setText(column: 2, atext: preference);
2840 d->w->filetypeList->addTopLevelItem(item);
2841 }
2842 }
2843 d->w->filetypeList->resizeColumnToContents(column: 0);
2844}
2845
2846KDesktopPropsPlugin::~KDesktopPropsPlugin() = default;
2847
2848void KDesktopPropsPlugin::slotAddFiletype()
2849{
2850 QMimeDatabase db;
2851 KMimeTypeChooserDialog dlg(i18n("Add File Type for %1", properties->url().fileName()),
2852 i18n("Select one or more file types to add:"),
2853 QStringList(), // no preselected mimetypes
2854 QString(),
2855 QStringList(),
2856 KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns,
2857 d->m_frame);
2858
2859 if (dlg.exec() == QDialog::Accepted) {
2860 const QStringList list = dlg.chooser()->mimeTypes();
2861 for (const QString &mimetype : list) {
2862 QMimeType p = db.mimeTypeForName(nameOrAlias: mimetype);
2863 if (!p.isValid()) {
2864 continue;
2865 }
2866
2867 bool found = false;
2868 int count = d->w->filetypeList->topLevelItemCount();
2869 for (int i = 0; !found && i < count; ++i) {
2870 if (d->w->filetypeList->topLevelItem(index: i)->text(column: 0) == mimetype) {
2871 found = true;
2872 }
2873 }
2874 if (!found) {
2875 QTreeWidgetItem *item = new QTreeWidgetItem();
2876 item->setText(column: 0, atext: p.name());
2877 item->setText(column: 1, atext: p.comment());
2878 d->w->filetypeList->addTopLevelItem(item);
2879 }
2880 d->w->filetypeList->resizeColumnToContents(column: 0);
2881 }
2882 }
2883 Q_EMIT changed();
2884}
2885
2886void KDesktopPropsPlugin::slotDelFiletype()
2887{
2888 QTreeWidgetItem *cur = d->w->filetypeList->currentItem();
2889 if (cur) {
2890 delete cur;
2891 Q_EMIT changed();
2892 }
2893}
2894
2895void KDesktopPropsPlugin::checkCommandChanged()
2896{
2897 if (KIO::DesktopExecParser::executableName(execLine: d->command()) != KIO::DesktopExecParser::executableName(execLine: d->m_origCommandStr)) {
2898 d->m_origCommandStr = d->command();
2899 }
2900}
2901
2902void KDesktopPropsPlugin::applyChanges()
2903{
2904 // qDebug();
2905 KIO::StatJob *job = KIO::mostLocalUrl(url: properties->url());
2906 KJobWidgets::setWindow(job, widget: properties);
2907 job->exec();
2908 const QUrl url = job->mostLocalUrl();
2909
2910 if (!url.isLocalFile()) {
2911 KMessageBox::error(parent: nullptr, i18n("Could not save properties. Only entries on local file systems are supported."));
2912 properties->abortApplying();
2913 return;
2914 }
2915
2916 const QString path(url.toLocalFile());
2917
2918 // make sure the directory exists
2919 QDir().mkpath(dirPath: QFileInfo(path).absolutePath());
2920 QFile f(path);
2921 if (!f.open(flags: QIODevice::ReadWrite)) {
2922 KMessageBox::error(parent: nullptr, text: couldNotSaveMsg(path));
2923 properties->abortApplying();
2924 return;
2925 }
2926
2927 // If the command is changed we reset certain settings that are strongly
2928 // coupled to the command.
2929 checkCommandChanged();
2930
2931 KDesktopFile origConfig(d->m_origDesktopFile);
2932 std::unique_ptr<KDesktopFile> _config(origConfig.copyTo(file: path));
2933 KConfigGroup config = _config->desktopGroup();
2934 config.writeEntry(key: "Type", QStringLiteral("Application"));
2935 config.writeEntry(key: "Comment", value: d->w->commentEdit->text());
2936 config.writeEntry(key: "Comment", value: d->w->commentEdit->text(), pFlags: KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
2937 config.writeEntry(key: "GenericName", value: d->w->genNameEdit->text());
2938 config.writeEntry(key: "GenericName", value: d->w->genNameEdit->text(), pFlags: KConfigGroup::Persistent | KConfigGroup::Localized); // for compat
2939 config.writeEntry(key: "Exec", value: d->command());
2940 config.writeEntry(key: "Path", value: d->w->pathEdit->lineEdit()->text()); // not writePathEntry, see kservice.cpp
2941
2942 // Write mimeTypes
2943 QStringList mimeTypes;
2944 int count = d->w->filetypeList->topLevelItemCount();
2945 for (int i = 0; i < count; ++i) {
2946 QTreeWidgetItem *item = d->w->filetypeList->topLevelItem(index: i);
2947 QString preference = item->text(column: 2);
2948 mimeTypes.append(t: item->text(column: 0));
2949 if (!preference.isEmpty()) {
2950 mimeTypes.append(t: preference);
2951 }
2952 }
2953
2954 // qDebug() << mimeTypes;
2955 config.writeXdgListEntry(key: "MimeType", value: mimeTypes);
2956
2957 if (!d->w->nameEdit->isHidden()) {
2958 QString nameStr = d->w->nameEdit->text();
2959 config.writeEntry(key: "Name", value: nameStr);
2960 config.writeEntry(key: "Name", value: nameStr, pFlags: KConfigGroup::Persistent | KConfigGroup::Localized);
2961 }
2962
2963 config.writeEntry(key: "Terminal", value: d->m_terminalBool);
2964 config.writeEntry(key: "TerminalOptions", value: d->m_terminalOptionStr);
2965 config.writeEntry(key: "X-KDE-SubstituteUID", value: d->m_suidBool);
2966 config.writeEntry(key: "X-KDE-Username", value: d->m_suidUserStr);
2967 if (KIO::hasDiscreteGpu()) {
2968 config.writeEntry(key: "PrefersNonDefaultGPU", value: d->m_runOnDiscreteGpuBool);
2969 }
2970 config.writeEntry(key: "StartupNotify", value: d->m_startupBool);
2971 config.sync();
2972
2973 // KSycoca update needed?
2974 bool updateNeeded = !relativeAppsLocation(file: path).isEmpty();
2975 if (updateNeeded) {
2976 KBuildSycocaProgressDialog::rebuildKSycoca(parent: d->m_frame);
2977 }
2978
2979 setDirty(false);
2980}
2981
2982void KDesktopPropsPlugin::slotBrowseExec()
2983{
2984 QUrl f = QFileDialog::getOpenFileUrl(parent: d->m_frame);
2985 if (f.isEmpty()) {
2986 return;
2987 }
2988
2989 if (!f.isLocalFile()) {
2990 KMessageBox::information(parent: d->m_frame, i18n("Only executables on local file systems are supported."));
2991 return;
2992 }
2993
2994 const QString path = f.toLocalFile();
2995 d->w->programEdit->setText(path);
2996}
2997
2998void KDesktopPropsPlugin::slotAdvanced()
2999{
3000 auto *dlg = new QDialog(d->m_frame);
3001 dlg->setObjectName(QStringLiteral("KPropertiesDesktopAdv"));
3002 dlg->setWindowModality(Qt::WindowModal);
3003 dlg->setModal(true);
3004 dlg->setAttribute(Qt::WA_DeleteOnClose);
3005 dlg->setWindowTitle(i18n("Advanced Options for %1", properties->url().fileName()));
3006
3007 d->m_uiAdvanced.reset(p: new Ui_KPropertiesDesktopAdvBase);
3008 QWidget *mainWidget = new QWidget(dlg);
3009 d->m_uiAdvanced->setupUi(mainWidget);
3010
3011 QDialogButtonBox *buttonBox = new QDialogButtonBox(dlg);
3012 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
3013 connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: dlg, slot: &QDialog::accept);
3014 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: dlg, slot: &QDialog::reject);
3015
3016 QVBoxLayout *layout = new QVBoxLayout(dlg);
3017 layout->addWidget(mainWidget);
3018 layout->addWidget(buttonBox);
3019
3020 // If the command is changed we reset certain settings that are strongly
3021 // coupled to the command.
3022 checkCommandChanged();
3023
3024 // check to see if we use konsole if not do not add the nocloseonexit
3025 // because we don't know how to do this on other terminal applications
3026 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
3027 QString preferredTerminal = confGroup.readPathEntry(key: "TerminalApplication", QStringLiteral("konsole"));
3028
3029 bool terminalCloseBool = false;
3030
3031 if (preferredTerminal == QLatin1String("konsole")) {
3032 terminalCloseBool = d->m_terminalOptionStr.contains(s: QLatin1String("--noclose"));
3033 d->m_uiAdvanced->terminalCloseCheck->setChecked(terminalCloseBool);
3034 d->m_terminalOptionStr.remove(QStringLiteral("--noclose"));
3035 } else {
3036 d->m_uiAdvanced->terminalCloseCheck->hide();
3037 }
3038
3039 d->m_uiAdvanced->terminalCheck->setChecked(d->m_terminalBool);
3040 d->m_uiAdvanced->terminalEdit->setText(d->m_terminalOptionStr);
3041 d->m_uiAdvanced->terminalCloseCheck->setEnabled(d->m_terminalBool);
3042 d->m_uiAdvanced->terminalEdit->setEnabled(d->m_terminalBool);
3043 d->m_uiAdvanced->terminalEditLabel->setEnabled(d->m_terminalBool);
3044
3045 d->m_uiAdvanced->suidCheck->setChecked(d->m_suidBool);
3046 d->m_uiAdvanced->suidEdit->setText(d->m_suidUserStr);
3047 d->m_uiAdvanced->suidEdit->setEnabled(d->m_suidBool);
3048 d->m_uiAdvanced->suidEditLabel->setEnabled(d->m_suidBool);
3049
3050 if (KIO::hasDiscreteGpu()) {
3051 d->m_uiAdvanced->discreteGpuCheck->setChecked(d->m_runOnDiscreteGpuBool);
3052 } else {
3053 d->m_uiAdvanced->discreteGpuGroupBox->hide();
3054 }
3055
3056 d->m_uiAdvanced->startupInfoCheck->setChecked(d->m_startupBool);
3057
3058 // Provide username completion up to 1000 users.
3059 const int maxEntries = 1000;
3060 QStringList userNames = KUser::allUserNames(maxCount: maxEntries);
3061 if (userNames.size() < maxEntries) {
3062 KCompletion *kcom = new KCompletion;
3063 kcom->setOrder(KCompletion::Sorted);
3064 d->m_uiAdvanced->suidEdit->setCompletionObject(kcom, handle: true);
3065 d->m_uiAdvanced->suidEdit->setAutoDeleteCompletionObject(true);
3066 d->m_uiAdvanced->suidEdit->setCompletionMode(KCompletion::CompletionAuto);
3067 kcom->setItems(userNames);
3068 }
3069
3070 connect(sender: d->m_uiAdvanced->terminalEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
3071 connect(sender: d->m_uiAdvanced->terminalCloseCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
3072 connect(sender: d->m_uiAdvanced->terminalCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
3073 connect(sender: d->m_uiAdvanced->suidCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
3074 connect(sender: d->m_uiAdvanced->suidEdit, signal: &QLineEdit::textChanged, context: this, slot: &KPropertiesDialogPlugin::changed);
3075 connect(sender: d->m_uiAdvanced->discreteGpuCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
3076 connect(sender: d->m_uiAdvanced->startupInfoCheck, signal: &QAbstractButton::toggled, context: this, slot: &KPropertiesDialogPlugin::changed);
3077
3078 QObject::connect(sender: dlg, signal: &QDialog::accepted, context: this, slot: [this]() {
3079 d->m_terminalOptionStr = d->m_uiAdvanced->terminalEdit->text().trimmed();
3080 d->m_terminalBool = d->m_uiAdvanced->terminalCheck->isChecked();
3081 d->m_suidBool = d->m_uiAdvanced->suidCheck->isChecked();
3082 d->m_suidUserStr = d->m_uiAdvanced->suidEdit->text().trimmed();
3083 if (KIO::hasDiscreteGpu()) {
3084 d->m_runOnDiscreteGpuBool = d->m_uiAdvanced->discreteGpuCheck->isChecked();
3085 }
3086 d->m_startupBool = d->m_uiAdvanced->startupInfoCheck->isChecked();
3087
3088 if (d->m_uiAdvanced->terminalCloseCheck->isChecked()) {
3089 d->m_terminalOptionStr.append(s: QLatin1String(" --noclose"));
3090 }
3091 });
3092
3093 dlg->show();
3094}
3095
3096bool KDesktopPropsPlugin::supports(const KFileItemList &_items)
3097{
3098 if (_items.count() != 1) {
3099 return false;
3100 }
3101
3102 const KFileItem &item = _items.first();
3103
3104 // check if desktop file
3105 if (!item.isDesktopFile()) {
3106 return false;
3107 }
3108
3109 // open file and check type
3110 const auto [url, isLocal] = item.isMostLocalUrl();
3111 if (!isLocal) {
3112 return false;
3113 }
3114
3115 KDesktopFile config(url.toLocalFile());
3116 return config.hasApplicationType() && KAuthorized::authorize(action: KAuthorized::RUN_DESKTOP_FILES) && KAuthorized::authorize(action: KAuthorized::SHELL_ACCESS);
3117}
3118
3119QString KDesktopPropsPlugin::KDesktopPropsPluginPrivate::command() const
3120{
3121 QStringList execSplit = KShell::splitArgs(cmd: w->envarsEdit->text()) + QStringList(w->programEdit->text()) + KShell::splitArgs(cmd: w->argumentsEdit->text());
3122
3123 if (KShell::splitArgs(cmd: w->envarsEdit->text()).length()) {
3124 execSplit.push_front(t: QLatin1String("env"));
3125 }
3126
3127 return KShell::joinArgs(args: execSplit);
3128}
3129
3130#include "moc_kpropertiesdialogbuiltin_p.cpp"
3131

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