1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "widgetsaskuseractionhandler.h"
9
10#include <KConfig>
11#include <KConfigGroup>
12#include <KGuiItem>
13#include <KIO/WorkerBase>
14#include <KJob>
15#include <KJobWidgets>
16#include <KLocalizedString>
17#include <KMessageDialog>
18#include <KSharedConfig>
19#include <KSslInfoDialog>
20#include <KStandardGuiItem>
21#include <kio_widgets_debug.h>
22
23#include <QApplication>
24#include <QDialogButtonBox>
25#include <QPointer>
26#include <QRegularExpression>
27#include <QUrl>
28
29class KIO::WidgetsAskUserActionHandlerPrivate
30{
31public:
32 explicit WidgetsAskUserActionHandlerPrivate(WidgetsAskUserActionHandler *qq)
33 : q(qq)
34 {
35 }
36
37 // Creates a KSslInfoDialog or falls back to a generic Information dialog
38 void sslMessageBox(const QString &text, const KIO::MetaData &metaData, QWidget *parent);
39
40 bool gotPersistentUserReply(KIO::AskUserActionInterface::MessageDialogType type, const KConfigGroup &cg, const QString &dontAskAgainName);
41 void savePersistentUserReply(KIO::AskUserActionInterface::MessageDialogType type, KConfigGroup &cg, const QString &dontAskAgainName, int result);
42
43 WidgetsAskUserActionHandler *const q;
44 QPointer<QWidget> m_parentWidget = nullptr;
45
46 QWidget *getParentWidget(KJob *job);
47 QWidget *getParentWidget(QWidget *widget);
48};
49
50bool KIO::WidgetsAskUserActionHandlerPrivate::gotPersistentUserReply(KIO::AskUserActionInterface::MessageDialogType type,
51 const KConfigGroup &cg,
52 const QString &dontAskAgainName)
53{
54 // storage values matching the logic of FrameworkIntegration's KMessageBoxDontAskAgainConfigStorage
55 switch (type) {
56 case KIO::AskUserActionInterface::QuestionTwoActions:
57 case KIO::AskUserActionInterface::QuestionTwoActionsCancel:
58 case KIO::AskUserActionInterface::WarningTwoActions:
59 case KIO::AskUserActionInterface::WarningTwoActionsCancel: {
60 // storage holds "true" if persistent reply is "Yes", "false" for persistent "No",
61 // otherwise no persistent reply is present
62 const QString value = cg.readEntry(key: dontAskAgainName, aDefault: QString());
63 if ((value.compare(other: QLatin1String("yes"), cs: Qt::CaseInsensitive) == 0) || (value.compare(other: QLatin1String("true"), cs: Qt::CaseInsensitive) == 0)) {
64 Q_EMIT q->messageBoxResult(result: KIO::WorkerBase::PrimaryAction);
65 return true;
66 }
67 if ((value.compare(other: QLatin1String("no"), cs: Qt::CaseInsensitive) == 0) || (value.compare(other: QLatin1String("false"), cs: Qt::CaseInsensitive) == 0)) {
68 Q_EMIT q->messageBoxResult(result: KIO::WorkerBase::SecondaryAction);
69 return true;
70 }
71 break;
72 }
73 case KIO::AskUserActionInterface::WarningContinueCancel: {
74 // storage holds "false" if persistent reply is "Continue"
75 // otherwise no persistent reply is present
76 const bool value = cg.readEntry(key: dontAskAgainName, aDefault: true);
77 if (value == false) {
78 Q_EMIT q->messageBoxResult(result: KIO::WorkerBase::Continue);
79 return true;
80 }
81 break;
82 }
83 default:
84 break;
85 }
86
87 return false;
88}
89
90void KIO::WidgetsAskUserActionHandlerPrivate::savePersistentUserReply(KIO::AskUserActionInterface::MessageDialogType type,
91 KConfigGroup &cg,
92 const QString &dontAskAgainName,
93 int result)
94{
95 // see gotPersistentUserReply for values stored and why
96 switch (type) {
97 case KIO::AskUserActionInterface::QuestionTwoActions:
98 case KIO::AskUserActionInterface::QuestionTwoActionsCancel:
99 case KIO::AskUserActionInterface::WarningTwoActions:
100 case KIO::AskUserActionInterface::WarningTwoActionsCancel:
101 cg.writeEntry(key: dontAskAgainName, value: result == KIO::WorkerBase::PrimaryAction);
102 cg.sync();
103 break;
104 case KIO::AskUserActionInterface::WarningContinueCancel:
105 cg.writeEntry(key: dontAskAgainName, value: false);
106 cg.sync();
107 break;
108 default:
109 break;
110 }
111}
112
113QWidget *KIO::WidgetsAskUserActionHandlerPrivate::getParentWidget(KJob *job)
114{
115 // This needs to be in qpointer, otherwise copying process
116 // will crash if done in background and dolphin is closed
117 QPointer<QWidget> parentWidget = nullptr;
118
119 if (job) {
120 parentWidget = KJobWidgets::window(job);
121 }
122
123 return getParentWidget(widget: parentWidget);
124}
125
126QWidget *KIO::WidgetsAskUserActionHandlerPrivate::getParentWidget(QWidget *widget)
127{
128 // This needs to be in qpointer, otherwise copying process
129 // will crash if done in background and dolphin is closed
130 QPointer<QWidget> parentWidget = widget;
131
132 if (!parentWidget) {
133 parentWidget = this->m_parentWidget;
134 }
135
136 if (!parentWidget) {
137 parentWidget = qApp->activeWindow();
138 }
139
140 return parentWidget;
141}
142
143KIO::WidgetsAskUserActionHandler::WidgetsAskUserActionHandler(QObject *parent)
144 : KIO::AskUserActionInterface(parent)
145 , d(new WidgetsAskUserActionHandlerPrivate(this))
146{
147}
148
149KIO::WidgetsAskUserActionHandler::~WidgetsAskUserActionHandler()
150{
151}
152
153void KIO::WidgetsAskUserActionHandler::askUserRename(KJob *job,
154 const QString &title,
155 const QUrl &src,
156 const QUrl &dest,
157 KIO::RenameDialog_Options options,
158 KIO::filesize_t sizeSrc,
159 KIO::filesize_t sizeDest,
160 const QDateTime &ctimeSrc,
161 const QDateTime &ctimeDest,
162 const QDateTime &mtimeSrc,
163 const QDateTime &mtimeDest)
164{
165 QMetaObject::invokeMethod(qGuiApp, function: [=, this] {
166 auto *dlg = new KIO::RenameDialog(d->getParentWidget(job), title, src, dest, options, sizeSrc, sizeDest, ctimeSrc, ctimeDest, mtimeSrc, mtimeDest);
167
168 dlg->setAttribute(Qt::WA_DeleteOnClose);
169 dlg->setWindowModality(Qt::WindowModal);
170
171 connect(sender: job, signal: &KJob::finished, context: dlg, slot: &QDialog::reject);
172 connect(sender: dlg, signal: &QDialog::finished, context: this, slot: [this, job, dlg](const int exitCode) {
173 KIO::RenameDialog_Result result = static_cast<RenameDialog_Result>(exitCode);
174 const QUrl newUrl = result == Result_AutoRename ? dlg->autoDestUrl() : dlg->newDestUrl();
175 Q_EMIT askUserRenameResult(result, newUrl, parentJob: job);
176 });
177
178 dlg->show();
179 });
180}
181
182void KIO::WidgetsAskUserActionHandler::askUserSkip(KJob *job, KIO::SkipDialog_Options options, const QString &errorText)
183{
184 QMetaObject::invokeMethod(qGuiApp, function: [=, this] {
185 auto *dlg = new KIO::SkipDialog(d->getParentWidget(job), options, errorText);
186 dlg->setAttribute(Qt::WA_DeleteOnClose);
187 dlg->setWindowModality(Qt::WindowModal);
188
189 connect(sender: job, signal: &KJob::finished, context: dlg, slot: &QDialog::reject);
190 connect(sender: dlg, signal: &QDialog::finished, context: this, slot: [this, job](const int exitCode) {
191 Q_EMIT askUserSkipResult(result: static_cast<KIO::SkipDialog_Result>(exitCode), parentJob: job);
192 });
193
194 dlg->show();
195 });
196}
197
198struct ProcessAskDeleteResult {
199 QStringList prettyList;
200 KMessageDialog::Type dialogType = KMessageDialog::QuestionTwoActions;
201 KGuiItem acceptButton;
202 QString text;
203 QIcon icon;
204 QString title = i18n("Delete Permanently");
205 bool isSingleUrl = false;
206};
207
208using AskIface = KIO::AskUserActionInterface;
209static ProcessAskDeleteResult processAskDelete(const QList<QUrl> &urls, AskIface::DeletionType deletionType)
210{
211 ProcessAskDeleteResult res;
212 res.prettyList.reserve(asize: urls.size());
213 std::transform(first: urls.cbegin(), last: urls.cend(), result: std::back_inserter(x&: res.prettyList), unary_op: [](const auto &url) {
214 if (url.scheme() == QLatin1String("trash")) {
215 QString path = url.path();
216 // HACK (#98983): remove "0-foo". Note that it works better than
217 // displaying KFileItem::name(), for files under a subdir.
218 static const QRegularExpression re(QStringLiteral("^/[0-9]+-"));
219 path.remove(re);
220 return path;
221 } else {
222 return url.toDisplayString(QUrl::PreferLocalFile);
223 }
224 });
225
226 const int urlCount = res.prettyList.size();
227 res.isSingleUrl = urlCount == 1;
228
229 switch (deletionType) {
230 case AskIface::Delete: {
231 res.dialogType = KMessageDialog::QuestionTwoActions; // Using Question* so the Delete button is pre-selected. Bug 462845
232 res.icon = QIcon::fromTheme(QStringLiteral("dialog-warning"));
233 if (res.isSingleUrl) {
234 res.text = xi18nc("@info",
235 "Do you really want to permanently delete this item?<nl/><nl/>"
236 "<filename>%1</filename><nl/><nl/>"
237 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
238 res.prettyList.at(0));
239 } else {
240 res.text = xi18ncp("@info",
241 "Do you really want to permanently delete this %1 item?<nl/><nl/>"
242 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
243 "Do you really want to permanently delete these %1 items?<nl/><nl/>"
244 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
245 urlCount);
246 }
247 res.acceptButton = KGuiItem(i18nc("@action:button", "Delete Permanently"), QStringLiteral("edit-delete"));
248 break;
249 }
250 case AskIface::DeleteInsteadOfTrash: {
251 res.dialogType = KMessageDialog::WarningTwoActions;
252 if (res.isSingleUrl) {
253 res.text = xi18nc("@info",
254 "Moving this item to Trash failed as it is too large."
255 " Permanently delete it instead?<nl/><nl/>"
256 "<filename>%1</filename><nl/><nl/>"
257 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
258 res.prettyList.at(0));
259 } else {
260 res.text = xi18ncp("@info",
261 "Moving this %1 item to Trash failed as it is too large."
262 " Permanently delete it instead?<nl/>"
263 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
264 "Moving these %1 items to Trash failed as they are too large."
265 " Permanently delete them instead?<nl/><nl/>"
266 "<emphasis strong='true'>This action cannot be undone.</emphasis>",
267 urlCount);
268 }
269 res.acceptButton = KGuiItem(i18nc("@action:button", "Delete Permanently"), QStringLiteral("edit-delete"));
270 break;
271 }
272 case AskIface::EmptyTrash: {
273 res.dialogType = KMessageDialog::QuestionTwoActions; // Using Question* so the Delete button is pre-selected.
274 res.icon = QIcon::fromTheme(QStringLiteral("dialog-warning"));
275 res.text = xi18nc("@info",
276 "Do you want to permanently delete all items from the Trash?<nl/><nl/>"
277 "<emphasis strong='true'>This action cannot be undone.</emphasis>");
278 res.acceptButton = KGuiItem(i18nc("@action:button", "Empty Trash"), QStringLiteral("user-trash"));
279 break;
280 }
281 case AskIface::Trash: {
282 if (res.isSingleUrl) {
283 res.text = xi18nc("@info",
284 "Do you really want to move this item to the Trash?<nl/>"
285 "<filename>%1</filename>",
286 res.prettyList.at(0));
287 } else {
288 res.text =
289 xi18ncp("@info", "Do you really want to move this %1 item to the Trash?", "Do you really want to move these %1 items to the Trash?", urlCount);
290 }
291 res.title = i18n("Move to Trash");
292 res.acceptButton = KGuiItem(res.title, QStringLiteral("user-trash"));
293 break;
294 }
295 default:
296 break;
297 }
298 return res;
299}
300
301void KIO::WidgetsAskUserActionHandler::askUserDelete(const QList<QUrl> &urls, DeletionType deletionType, ConfirmationType confirmationType, QWidget *parent)
302{
303 QString keyName;
304 bool ask = (confirmationType == ForceConfirmation);
305 if (!ask) {
306 // The default value for confirmations is true for delete and false
307 // for trash. If you change this, please also update:
308 // dolphin/src/settings/general/confirmationssettingspage.cpp
309 bool defaultValue = true;
310
311 switch (deletionType) {
312 case DeleteInsteadOfTrash:
313 case Delete:
314 keyName = QStringLiteral("ConfirmDelete");
315 break;
316 case Trash:
317 keyName = QStringLiteral("ConfirmTrash");
318 defaultValue = false;
319 break;
320 case EmptyTrash:
321 keyName = QStringLiteral("ConfirmEmptyTrash");
322 break;
323 }
324
325 KSharedConfigPtr kioConfig = KSharedConfig::openConfig(QStringLiteral("kiorc"), mode: KConfig::NoGlobals);
326 ask = kioConfig->group(QStringLiteral("Confirmations")).readEntry(key: keyName, aDefault: defaultValue);
327 }
328
329 if (!ask) {
330 Q_EMIT askUserDeleteResult(allowDelete: true, urls, deletionType, parent);
331 return;
332 }
333
334 QMetaObject::invokeMethod(qGuiApp, function: [=, this] {
335 const auto &[prettyList, dialogType, acceptButton, text, icon, title, singleUrl] = processAskDelete(urls, deletionType);
336 KMessageDialog *dlg = new KMessageDialog(dialogType, text, parent);
337 dlg->setAttribute(Qt::WA_DeleteOnClose);
338 dlg->setCaption(title);
339 dlg->setIcon(icon);
340 dlg->setButtons(primaryAction: acceptButton, secondaryAction: KStandardGuiItem::cancel());
341 if (!singleUrl) {
342 dlg->setListWidgetItems(prettyList);
343 }
344 dlg->setDontAskAgainText(i18nc("@option:checkbox", "Do not ask again"));
345 // If we get here, !ask must be false
346 dlg->setDontAskAgainChecked(!ask);
347
348 connect(sender: dlg, signal: &QDialog::finished, context: this, slot: [=, this](const int buttonCode) {
349 const bool isDelete = (buttonCode == KMessageDialog::PrimaryAction);
350
351 Q_EMIT askUserDeleteResult(allowDelete: isDelete, urls, deletionType, parent);
352
353 if (isDelete) {
354 KSharedConfigPtr kioConfig = KSharedConfig::openConfig(QStringLiteral("kiorc"), mode: KConfig::NoGlobals);
355 KConfigGroup cg = kioConfig->group(QStringLiteral("Confirmations"));
356 cg.writeEntry(key: keyName, value: !dlg->isDontAskAgainChecked());
357 cg.sync();
358 }
359 });
360
361 dlg->setWindowModality(Qt::WindowModal);
362 dlg->show();
363 });
364}
365
366void KIO::WidgetsAskUserActionHandler::requestUserMessageBox(MessageDialogType type,
367 const QString &text,
368 const QString &title,
369 const QString &primaryActionText,
370 const QString &secondaryActionText,
371 const QString &primaryActionIconName,
372 const QString &secondaryActionIconName,
373 const QString &dontAskAgainName,
374 const QString &details,
375 QWidget *parent)
376{
377 if (d->gotPersistentUserReply(type,
378 cg: KSharedConfig::openConfig(QStringLiteral("kioslaverc"))->group(QStringLiteral("Notification Messages")),
379 dontAskAgainName)) {
380 return;
381 }
382
383 const KGuiItem primaryActionButton(primaryActionText, primaryActionIconName);
384 const KGuiItem secondaryActionButton(secondaryActionText, secondaryActionIconName);
385
386 // It's "Do not ask again" every where except with Information
387 QString dontAskAgainText = i18nc("@option:check", "Do not ask again");
388
389 KMessageDialog::Type dlgType;
390 bool hasCancelButton = false;
391
392 switch (type) {
393 case AskUserActionInterface::QuestionTwoActions:
394 dlgType = KMessageDialog::QuestionTwoActions;
395 break;
396 case AskUserActionInterface::QuestionTwoActionsCancel:
397 dlgType = KMessageDialog::QuestionTwoActionsCancel;
398 hasCancelButton = true;
399 break;
400 case AskUserActionInterface::WarningTwoActions:
401 dlgType = KMessageDialog::WarningTwoActions;
402 break;
403 case AskUserActionInterface::WarningTwoActionsCancel:
404 dlgType = KMessageDialog::WarningTwoActionsCancel;
405 hasCancelButton = true;
406 break;
407 case AskUserActionInterface::WarningContinueCancel:
408 dlgType = KMessageDialog::WarningContinueCancel;
409 hasCancelButton = true;
410 break;
411 case AskUserActionInterface::Information:
412 dlgType = KMessageDialog::Information;
413 dontAskAgainText = i18nc("@option:check", "Do not show this message again");
414 break;
415 case AskUserActionInterface::Error:
416 dlgType = KMessageDialog::Error;
417 dontAskAgainText = QString{}; // No dontAskAgain checkbox
418 break;
419 default:
420 qCWarning(KIO_WIDGETS) << "Unknown message dialog type" << type;
421 return;
422 }
423
424 QMetaObject::invokeMethod(qGuiApp, function: [=, this]() {
425 auto cancelButton = hasCancelButton ? KStandardGuiItem::cancel() : KGuiItem();
426 auto *dialog = new KMessageDialog(dlgType, text, d->getParentWidget(widget: parent));
427
428 dialog->setAttribute(Qt::WA_DeleteOnClose);
429 dialog->setCaption(title);
430 dialog->setIcon(QIcon{});
431 dialog->setButtons(primaryAction: primaryActionButton, secondaryAction: secondaryActionButton, cancelAction: cancelButton);
432 dialog->setDetails(details);
433 dialog->setDontAskAgainText(dontAskAgainText);
434 dialog->setDontAskAgainChecked(false);
435 dialog->setOpenExternalLinks(true); // Allow opening external links in the text labels
436
437 connect(sender: dialog, signal: &QDialog::finished, context: this, slot: [=, this](const int result) {
438 KIO::WorkerBase::ButtonCode btnCode;
439 switch (result) {
440 case KMessageDialog::PrimaryAction:
441 if (dlgType == KMessageDialog::WarningContinueCancel) {
442 btnCode = KIO::WorkerBase::Continue;
443 } else {
444 btnCode = KIO::WorkerBase::PrimaryAction;
445 }
446 break;
447 case KMessageDialog::SecondaryAction:
448 btnCode = KIO::WorkerBase::SecondaryAction;
449 break;
450 case KMessageDialog::Cancel:
451 btnCode = KIO::WorkerBase::Cancel;
452 break;
453 case KMessageDialog::Ok:
454 btnCode = KIO::WorkerBase::Ok;
455 break;
456 default:
457 qCWarning(KIO_WIDGETS) << "Unknown message dialog result" << result;
458 return;
459 }
460
461 Q_EMIT messageBoxResult(result: btnCode);
462
463 if ((result != KMessageDialog::Cancel) && dialog->isDontAskAgainChecked()) {
464 KSharedConfigPtr reqMsgConfig = KSharedConfig::openConfig(QStringLiteral("kioslaverc"));
465 KConfigGroup cg = reqMsgConfig->group(QStringLiteral("Notification Messages"));
466 d->savePersistentUserReply(type, cg, dontAskAgainName, result);
467 }
468 });
469
470 dialog->show();
471 });
472}
473
474void KIO::WidgetsAskUserActionHandler::setWindow(QWidget *window)
475{
476 d->m_parentWidget = window;
477}
478
479void KIO::WidgetsAskUserActionHandler::askIgnoreSslErrors(const QVariantMap &sslErrorData, QWidget *parent)
480{
481 QWidget *parentWidget = d->getParentWidget(widget: parent);
482
483 QString message = i18n("The server failed the authenticity check (%1).\n\n", sslErrorData[QLatin1String("hostname")].toString());
484
485 message += sslErrorData[QLatin1String("sslError")].toString();
486
487 auto *dialog = new KMessageDialog(KMessageDialog::WarningTwoActionsCancel, message, parentWidget);
488
489 dialog->setAttribute(Qt::WA_DeleteOnClose);
490 dialog->setCaption(i18n("Server Authentication"));
491 dialog->setIcon(QIcon{});
492 dialog->setButtons(primaryAction: KGuiItem{i18n("&Details"), QStringLiteral("documentinfo")}, secondaryAction: KStandardGuiItem::cont(), cancelAction: KStandardGuiItem::cancel());
493
494 connect(sender: dialog, signal: &KMessageDialog::finished, context: this, slot: [this, parentWidget, sslErrorData](int result) {
495 if (result == KMessageDialog::PrimaryAction) {
496 showSslDetails(sslErrorData, parentWidget);
497 } else if (result == KMessageDialog::SecondaryAction) {
498 // continue();
499 Q_EMIT askIgnoreSslErrorsResult(result: 1);
500 } else if (result == KMessageDialog::Cancel) {
501 // cancel();
502 Q_EMIT askIgnoreSslErrorsResult(result: 0);
503 }
504 });
505
506 dialog->show();
507}
508
509void KIO::WidgetsAskUserActionHandler::showSslDetails(const QVariantMap &sslErrorData, QWidget *parentWidget)
510{
511 const QStringList sslList = sslErrorData[QLatin1String("peerCertChain")].toStringList();
512
513 QList<QSslCertificate> certChain;
514 bool decodedOk = true;
515 for (const QString &str : sslList) {
516 certChain.append(t: QSslCertificate(str.toUtf8()));
517 if (certChain.last().isNull()) {
518 decodedOk = false;
519 break;
520 }
521 }
522
523 QMetaObject::invokeMethod(qGuiApp, function: [=, this] {
524 if (decodedOk) { // Use KSslInfoDialog
525 KSslInfoDialog *ksslDlg = new KSslInfoDialog(parentWidget);
526 ksslDlg->setSslInfo(
527 certificateChain: certChain,
528 ip: QString(),
529 host: sslErrorData[QLatin1String("hostname")].toString(),
530 sslProtocol: sslErrorData[QLatin1String("protocol")].toString(),
531 cipher: sslErrorData[QLatin1String("cipher")].toString(),
532 usedBits: sslErrorData[QLatin1String("usedBits")].toInt(),
533 bits: sslErrorData[QLatin1String("bits")].toInt(),
534 validationErrors: KSslInfoDialog::certificateErrorsFromString(errorsString: sslErrorData[QLatin1String("certificateErrors")].toStringList().join(sep: QLatin1Char('\n'))));
535
536 // KSslInfoDialog deletes itself by setting Qt::WA_DeleteOnClose
537
538 QObject::connect(sender: ksslDlg, signal: &QDialog::finished, context: this, slot: [this, sslErrorData, parentWidget]() {
539 // KSslInfoDialog only has one button, QDialogButtonBox::Close
540 askIgnoreSslErrors(sslErrorData, parent: parentWidget);
541 });
542
543 ksslDlg->show();
544 return;
545 }
546
547 // Fallback to a generic message box
548 auto *dialog = new KMessageDialog(KMessageDialog::Information, i18n("The peer SSL certificate chain appears to be corrupt."), parentWidget);
549
550 dialog->setAttribute(Qt::WA_DeleteOnClose);
551 dialog->setCaption(i18n("SSL"));
552 dialog->setButtons(primaryAction: KStandardGuiItem::ok());
553
554 QObject::connect(sender: dialog, signal: &QDialog::finished, context: this, slot: [this](const int result) {
555 Q_EMIT askIgnoreSslErrorsResult(result: result == KMessageDialog::Ok ? 1 : 0);
556 });
557
558 dialog->show();
559 });
560}
561
562#include "moc_widgetsaskuseractionhandler.cpp"
563

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