1 /*
2  * This file is part of the KDE project.
3  *
4  * Copyright (C) 2008 Dirk Mueller <mueller@kde.org>
5  * Copyright (C) 2008 Urs Wolfer <uwolfer @ kde.org>
6  * Copyright (C) 2008 Michael Howell <mhowell123@gmail.com>
7  * Copyright (C) 2009,2010 Dawit Alemayehu <adawit@kde.org>
8  *
9  * This library is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Library General Public
11  * License as published by the Free Software Foundation; either
12  * version 2 of the License, or (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Library General Public License for more details.
18  *
19  * You should have received a copy of the GNU Library General Public License
20  * along with this library; see the file COPYING.LIB.  If not, write to
21  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  * Boston, MA 02110-1301, USA.
23  *
24  */
25 
26 // Own
27 #include "kwebpage.h"
28 #include "kwebwallet.h"
29 
30 // Local
31 #include "kwebpluginfactory.h"
32 
33 // KDE
34 #include <kprotocolmanager.h>
35 #include <kjobuidelegate.h>
36 #include <krun.h>
37 #include <kjobwidgets.h>
38 
39 #include <kstandardshortcut.h>
40 #include <QDebug>
41 #include <kshell.h>
42 #include <kmimetypetrader.h>
43 #include <kio/accessmanager.h>
44 #include <kio/job.h>
45 #include <kio/copyjob.h>
46 #include <kio/jobuidelegate.h>
47 #include <KIO/CommandLauncherJob>
48 #include <KIO/OpenUrlJob>
49 #include <kio/renamedialog.h>
50 #include <kio/scheduler.h>
51 #include <kparts/browseropenorsavequestion.h>
52 
53 // Qt
54 #include <QPointer>
55 #include <QFileInfo>
56 #include <QCoreApplication>
57 #include <QAction>
58 #include <QFileDialog>
59 #include <QWebFrame>
60 #include <QNetworkReply>
61 #include <qtemporaryfile.h>
62 
63 #define QL1S(x)  QLatin1String(x)
64 #define QL1C(x)  QLatin1Char(x)
65 
reloadRequestWithoutDisposition(QNetworkReply * reply)66 static void reloadRequestWithoutDisposition(QNetworkReply *reply)
67 {
68     QNetworkRequest req(reply->request());
69     req.setRawHeader("x-kdewebkit-ignore-disposition", "true");
70 
71     QWebFrame *frame = qobject_cast<QWebFrame *> (req.originatingObject());
72     if (!frame) {
73         return;
74     }
75 
76     frame->load(req);
77 }
78 
isMimeTypeAssociatedWithSelf(const KService::Ptr & offer)79 static bool isMimeTypeAssociatedWithSelf(const KService::Ptr &offer)
80 {
81     if (!offer) {
82         return false;
83     }
84 
85     // qDebug() << offer->desktopEntryName();
86 
87     const QString &appName = QCoreApplication::applicationName();
88 
89     if (appName == offer->desktopEntryName() || offer->exec().trimmed().startsWith(appName)) {
90         return true;
91     }
92 
93     // konqueror exception since it uses kfmclient to open html content...
94     if (appName == QL1S("konqueror") && offer->exec().trimmed().startsWith(QL1S("kfmclient"))) {
95         return true;
96     }
97 
98     return false;
99 }
100 
extractMimeType(const QNetworkReply * reply,QString & mimeType)101 static void extractMimeType(const QNetworkReply *reply, QString &mimeType)
102 {
103     mimeType.clear();
104     const KIO::MetaData &metaData = reply->attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::MetaData)).toMap();
105     if (metaData.contains(QL1S("content-type"))) {
106         mimeType = metaData.value(QL1S("content-type"));
107     }
108 
109     if (!mimeType.isEmpty()) {
110         return;
111     }
112 
113     if (!reply->hasRawHeader("Content-Type")) {
114         return;
115     }
116 
117     const QString value(QL1S(reply->rawHeader("Content-Type").simplified().constData()));
118     const int index = value.indexOf(QL1C(';'));
119     mimeType = ((index == -1) ? value : value.left(index));
120 }
121 
downloadResource(const QUrl & srcUrl,const QString & suggestedName=QString (),QWidget * parent=nullptr,const KIO::MetaData & metaData=KIO::MetaData ())122 static bool downloadResource(const QUrl &srcUrl, const QString &suggestedName = QString(),
123                              QWidget *parent = nullptr, const KIO::MetaData &metaData = KIO::MetaData())
124 {
125     const QString fileName = suggestedName.isEmpty() ? srcUrl.fileName() : suggestedName;
126     // convert filename to URL using fromPath to avoid trouble with ':' in filenames (#184202)
127     QUrl destUrl = QUrl::fromLocalFile(QFileDialog::getSaveFileName(parent, QString(), fileName));
128     if (!destUrl.isValid()) {
129         return false;
130     }
131 
132     // Using KIO::copy rather than file_copy, to benefit from "dest already exists" dialogs.
133     KIO::Job *job = KIO::copy(srcUrl, destUrl);
134 
135     if (!metaData.isEmpty()) {
136         job->setMetaData(metaData);
137     }
138 
139     job->addMetaData(QL1S("MaxCacheSize"), QL1S("0")); // Don't store in http cache.
140     job->addMetaData(QL1S("cache"), QL1S("cache")); // Use entry from cache if available.
141     KJobWidgets::setWindow(job, parent ? parent->window() : nullptr);
142     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
143     return true;
144 }
145 
isReplyStatusOk(const QNetworkReply * reply)146 static bool isReplyStatusOk(const QNetworkReply *reply)
147 {
148     if (!reply || reply->error() != QNetworkReply::NoError) {
149         return false;
150     }
151 
152     // Check HTTP status code only for http and webdav protocols...
153     const QString scheme = reply->url().scheme();
154     if (scheme.startsWith(QLatin1String("http"), Qt::CaseInsensitive) ||
155             scheme.startsWith(QLatin1String("webdav"), Qt::CaseInsensitive)) {
156         bool ok = false;
157         const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok);
158         if (!ok || statusCode < 200 || statusCode > 299) {
159             return false;
160         }
161     }
162 
163     return true;
164 }
165 
166 class KWebPage::KWebPagePrivate
167 {
168 public:
KWebPagePrivate(KWebPage * page)169     KWebPagePrivate(KWebPage *page)
170         : q(page)
171         , inPrivateBrowsingMode(false)
172     {
173     }
174 
windowWidget()175     QWidget *windowWidget()
176     {
177         return (window ? window.data() : q->view());
178     }
179 
_k_copyResultToTempFile(KJob * job)180     void _k_copyResultToTempFile(KJob *job)
181     {
182         KIO::FileCopyJob *cJob = qobject_cast<KIO::FileCopyJob *>(job);
183         if (cJob && !cJob->error()) {
184             // Same as KRun::foundMimeType but with a different URL
185             auto *job = new KIO::OpenUrlJob(cJob->destUrl(), mimeType);
186             job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window));
187             job->start();
188         }
189     }
190 
_k_receivedContentType(KIO::Job * job,const QString & mimetype)191     void _k_receivedContentType(KIO::Job *job, const QString &mimetype)
192     {
193         KIO::TransferJob *tJob = qobject_cast<KIO::TransferJob *>(job);
194         if (tJob && !tJob->error()) {
195             tJob->putOnHold();
196             KIO::Scheduler::publishSlaveOnHold();
197             // Get suggested file name...
198             mimeType = mimetype;
199             const QString suggestedFileName(tJob->queryMetaData(QL1S("content-disposition-filename")));
200             // qDebug() << "suggested filename:" << suggestedFileName << ", mimetype:" << mimetype;
201             (void) downloadResource(tJob->url(), suggestedFileName, window, tJob->metaData());
202         }
203     }
204 
_k_contentTypeCheckFailed(KJob * job)205     void _k_contentTypeCheckFailed(KJob *job)
206     {
207         KIO::TransferJob *tJob = qobject_cast<KIO::TransferJob *>(job);
208         // On error simply call downloadResource which will probably fail as well.
209         if (tJob && tJob->error()) {
210             (void)downloadResource(tJob->url(), QString(), window, tJob->metaData());
211         }
212     }
213 
214     KWebPage *q;
215     QPointer<QWidget> window;
216     QString mimeType;
217     QPointer<KWebWallet> wallet;
218     bool inPrivateBrowsingMode;
219 };
220 
setActionIcon(QAction * action,const QIcon & icon)221 static void setActionIcon(QAction *action, const QIcon &icon)
222 {
223     if (action) {
224         action->setIcon(icon);
225     }
226 }
227 
setActionShortcut(QAction * action,const QList<QKeySequence> & shortcut)228 static void setActionShortcut(QAction *action, const QList<QKeySequence> &shortcut)
229 {
230     if (action) {
231         action->setShortcuts(shortcut);
232     }
233 }
234 
KWebPage(QObject * parent,Integration flags)235 KWebPage::KWebPage(QObject *parent, Integration flags)
236     : QWebPage(parent), d(new KWebPagePrivate(this))
237 {
238     // KDE KParts integration for <embed> tag...
239     if (!flags || (flags & KPartsIntegration)) {
240         setPluginFactory(new KWebPluginFactory(this));
241     }
242 
243     QWidget *parentWidget = qobject_cast<QWidget *>(parent);
244     d->window = (parentWidget ? parentWidget->window() : nullptr);
245 
246     // KDE IO (KIO) integration...
247     if (!flags || (flags & KIOIntegration)) {
248         KIO::Integration::AccessManager *manager = new KIO::Integration::AccessManager(this);
249         // Disable QtWebKit's internal cache to avoid duplication with the one in KIO...
250         manager->setCache(nullptr);
251         manager->setWindow(d->window);
252         manager->setEmitReadyReadOnMetaDataChange(true);
253         setNetworkAccessManager(manager);
254     }
255 
256     // KWallet integration...
257     if (!flags || (flags & KWalletIntegration)) {
258         setWallet(new KWebWallet(nullptr, (d->window ? d->window->winId() : 0)));
259     }
260 
261     setActionIcon(action(Back), QIcon::fromTheme("go-previous"));
262     setActionIcon(action(Forward), QIcon::fromTheme("go-next"));
263     setActionIcon(action(Reload), QIcon::fromTheme("view-refresh"));
264     setActionIcon(action(Stop), QIcon::fromTheme("process-stop"));
265     setActionIcon(action(Cut), QIcon::fromTheme("edit-cut"));
266     setActionIcon(action(Copy), QIcon::fromTheme("edit-copy"));
267     setActionIcon(action(Paste), QIcon::fromTheme("edit-paste"));
268     setActionIcon(action(Undo), QIcon::fromTheme("edit-undo"));
269     setActionIcon(action(Redo), QIcon::fromTheme("edit-redo"));
270     setActionIcon(action(SelectAll), QIcon::fromTheme("edit-select-all"));
271     setActionIcon(action(InspectElement), QIcon::fromTheme("view-process-all"));
272     setActionIcon(action(OpenLinkInNewWindow), QIcon::fromTheme("window-new"));
273     setActionIcon(action(OpenFrameInNewWindow), QIcon::fromTheme("window-new"));
274     setActionIcon(action(OpenImageInNewWindow), QIcon::fromTheme("window-new"));
275     setActionIcon(action(CopyLinkToClipboard), QIcon::fromTheme("edit-copy"));
276     setActionIcon(action(CopyImageToClipboard), QIcon::fromTheme("edit-copy"));
277     setActionIcon(action(ToggleBold), QIcon::fromTheme("format-text-bold"));
278     setActionIcon(action(ToggleItalic), QIcon::fromTheme("format-text-italic"));
279     setActionIcon(action(ToggleUnderline), QIcon::fromTheme("format-text-underline"));
280     setActionIcon(action(DownloadLinkToDisk), QIcon::fromTheme("document-save"));
281     setActionIcon(action(DownloadImageToDisk), QIcon::fromTheme("document-save"));
282 
283     settings()->setWebGraphic(QWebSettings::MissingPluginGraphic, QIcon::fromTheme("preferences-plugin").pixmap(32, 32));
284     settings()->setWebGraphic(QWebSettings::MissingImageGraphic, QIcon::fromTheme("image-missing").pixmap(32, 32));
285     settings()->setWebGraphic(QWebSettings::DefaultFrameIconGraphic, QIcon::fromTheme("applications-internet").pixmap(32, 32));
286 
287     setActionShortcut(action(Back), KStandardShortcut::back());
288     setActionShortcut(action(Forward), KStandardShortcut::forward());
289     setActionShortcut(action(Reload), KStandardShortcut::reload());
290     setActionShortcut(action(Stop), QList<QKeySequence>() << QKeySequence(Qt::Key_Escape));
291     setActionShortcut(action(Cut), KStandardShortcut::cut());
292     setActionShortcut(action(Copy), KStandardShortcut::copy());
293     setActionShortcut(action(Paste), KStandardShortcut::paste());
294     setActionShortcut(action(Undo), KStandardShortcut::undo());
295     setActionShortcut(action(Redo), KStandardShortcut::redo());
296     setActionShortcut(action(SelectAll), KStandardShortcut::selectAll());
297 }
298 
~KWebPage()299 KWebPage::~KWebPage()
300 {
301     delete d;
302 }
303 
isExternalContentAllowed() const304 bool KWebPage::isExternalContentAllowed() const
305 {
306     KIO::AccessManager *manager = qobject_cast<KIO::AccessManager *>(networkAccessManager());
307     if (manager) {
308         return manager->isExternalContentAllowed();
309     }
310     return true;
311 }
312 
wallet() const313 KWebWallet *KWebPage::wallet() const
314 {
315     return d->wallet;
316 }
317 
setAllowExternalContent(bool allow)318 void KWebPage::setAllowExternalContent(bool allow)
319 {
320     KIO::AccessManager *manager = qobject_cast<KIO::AccessManager *>(networkAccessManager());
321     if (manager) {
322         manager->setExternalContentAllowed(allow);
323     }
324 }
325 
setWallet(KWebWallet * wallet)326 void KWebPage::setWallet(KWebWallet *wallet)
327 {
328     // Delete the current wallet if this object is its parent...
329     if (d->wallet && this == d->wallet->parent()) {
330         delete d->wallet;
331     }
332 
333     d->wallet = wallet;
334 
335     if (d->wallet) {
336         d->wallet->setParent(this);
337     }
338 }
339 
downloadRequest(const QNetworkRequest & request)340 void KWebPage::downloadRequest(const QNetworkRequest &request)
341 {
342     KIO::TransferJob *job = KIO::get(request.url());
343     connect(job, &KIO::TransferJob::mimeTypeFound,
344             this, [this](KIO::Job *job, const QString &mimeType) { d->_k_receivedContentType(job, mimeType); });
345 
346     job->setMetaData(request.attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::MetaData)).toMap());
347     job->addMetaData(QL1S("MaxCacheSize"), QL1S("0")); // Don't store in http cache.
348     job->addMetaData(QL1S("cache"), QL1S("cache")); // Use entry from cache if available.
349     KJobWidgets::setWindow(job, d->windowWidget());
350 }
351 
downloadUrl(const QUrl & url)352 void KWebPage::downloadUrl(const QUrl &url)
353 {
354     downloadRequest(QNetworkRequest(url));
355 }
356 
downloadResponse(QNetworkReply * reply)357 void KWebPage::downloadResponse(QNetworkReply *reply)
358 {
359     Q_ASSERT(reply);
360 
361     if (!reply) {
362         return;
363     }
364 
365     // Put the job on hold only for the protocols we know about (read: http).
366     KIO::Integration::AccessManager::putReplyOnHold(reply);
367 
368     QString mimeType;
369     KIO::MetaData metaData;
370 
371     if (handleReply(reply, &mimeType, &metaData)) {
372         return;
373     }
374 
375     // Ask OpenUrlJob to handle the response when mimetype is unknown
376     // or when mimetype is inode/*
377     if (mimeType.isEmpty() || mimeType.startsWith(QL1S("inode/"), Qt::CaseInsensitive)) {
378         auto *job = new KIO::OpenUrlJob(reply->url(), mimeType);
379         job->setSuggestedFileName(metaData.value(QL1S("content-disposition-filename")));
380         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->windowWidget()));
381         job->start();
382         return;
383     }
384 }
385 
sessionMetaData(const QString & key) const386 QString KWebPage::sessionMetaData(const QString &key) const
387 {
388     QString value;
389 
390     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
391     if (manager) {
392         value = manager->sessionMetaData().value(key);
393     }
394 
395     return value;
396 }
397 
requestMetaData(const QString & key) const398 QString KWebPage::requestMetaData(const QString &key) const
399 {
400     QString value;
401 
402     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
403     if (manager) {
404         value = manager->requestMetaData().value(key);
405     }
406 
407     return value;
408 }
409 
setSessionMetaData(const QString & key,const QString & value)410 void KWebPage::setSessionMetaData(const QString &key, const QString &value)
411 {
412     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
413     if (manager) {
414         manager->sessionMetaData()[key] = value;
415     }
416 }
417 
setRequestMetaData(const QString & key,const QString & value)418 void KWebPage::setRequestMetaData(const QString &key, const QString &value)
419 {
420     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
421     if (manager) {
422         manager->requestMetaData()[key] = value;
423     }
424 }
425 
removeSessionMetaData(const QString & key)426 void KWebPage::removeSessionMetaData(const QString &key)
427 {
428     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
429     if (manager) {
430         manager->sessionMetaData().remove(key);
431     }
432 }
433 
removeRequestMetaData(const QString & key)434 void KWebPage::removeRequestMetaData(const QString &key)
435 {
436     KIO::Integration::AccessManager *manager = qobject_cast<KIO::Integration::AccessManager *>(networkAccessManager());
437     if (manager) {
438         manager->requestMetaData().remove(key);
439     }
440 }
441 
userAgentForUrl(const QUrl & _url) const442 QString KWebPage::userAgentForUrl(const QUrl &_url) const
443 {
444     const QUrl url(_url);
445     const QString userAgent = KProtocolManager::userAgentForHost((url.isLocalFile() ? QL1S("localhost") : url.host()));
446 
447     if (userAgent == KProtocolManager::defaultUserAgent()) {
448         return QWebPage::userAgentForUrl(_url);
449     }
450 
451     return userAgent;
452 }
453 
setDisableCookieJarStorage(QNetworkAccessManager * manager,bool status)454 static void setDisableCookieJarStorage(QNetworkAccessManager *manager, bool status)
455 {
456     if (manager) {
457         KIO::Integration::CookieJar *cookieJar = manager ? qobject_cast<KIO::Integration::CookieJar *>(manager->cookieJar()) : nullptr;
458         if (cookieJar) {
459             //qDebug() << "Store cookies ?" << !status;
460             cookieJar->setDisableCookieStorage(status);
461         }
462     }
463 }
464 
acceptNavigationRequest(QWebFrame * frame,const QNetworkRequest & request,NavigationType type)465 bool KWebPage::acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type)
466 {
467     // qDebug() << "url:" << request.url() << ", type:" << type << ", frame:" << frame;
468 
469     if (frame && d->wallet && type == QWebPage::NavigationTypeFormSubmitted) {
470         d->wallet->saveFormData(frame);
471     }
472 
473     // Make sure nothing is cached when private browsing mode is enabled...
474     if (settings()->testAttribute(QWebSettings::PrivateBrowsingEnabled)) {
475         if (!d->inPrivateBrowsingMode) {
476             setDisableCookieJarStorage(networkAccessManager(), true);
477             setSessionMetaData(QL1S("no-cache"), QL1S("true"));
478             d->inPrivateBrowsingMode = true;
479         }
480     } else  {
481         if (d->inPrivateBrowsingMode) {
482             setDisableCookieJarStorage(networkAccessManager(), false);
483             removeSessionMetaData(QL1S("no-cache"));
484             d->inPrivateBrowsingMode = false;
485         }
486     }
487 
488     /*
489       If the navigation request is from the main frame, set the cross-domain
490       meta-data value to the current url for proper integration with KCookieJar...
491     */
492     if (frame == mainFrame() && type != QWebPage::NavigationTypeReload) {
493         setSessionMetaData(QL1S("cross-domain"), request.url().toString());
494     }
495 
496     return QWebPage::acceptNavigationRequest(frame, request, type);
497 }
498 
handleReply(QNetworkReply * reply,QString * contentType,KIO::MetaData * metaData)499 bool KWebPage::handleReply(QNetworkReply *reply, QString *contentType, KIO::MetaData *metaData)
500 {
501     // Reply url...
502     const QUrl replyUrl(reply->url());
503 
504     // Get suggested file name...
505     const KIO::MetaData &data = reply->attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::MetaData)).toMap();
506     const QString suggestedFileName = data.value(QL1S("content-disposition-filename"));
507     if (metaData) {
508         *metaData = data;
509     }
510 
511     // Get the mime-type...
512     QString mimeType;
513     extractMimeType(reply, mimeType);
514     if (contentType) {
515         *contentType = mimeType;
516     }
517 
518     // Let the calling function deal with handling empty or inode/* mimetypes...
519     if (mimeType.isEmpty() || mimeType.startsWith(QL1S("inode/"), Qt::CaseInsensitive)) {
520         return false;
521     }
522 
523     // Convert executable text files to plain text...
524     if (KParts::BrowserRun::isTextExecutable(mimeType)) {
525         mimeType = QL1S("text/plain");
526     }
527 
528     //qDebug() << "Content-disposition:" << suggestedFileName;
529     //qDebug() << "Got unsupported content of type:" << mimeType << "URL:" << replyUrl;
530     //qDebug() << "Error code:" << reply->error() << reply->errorString();
531 
532     if (isReplyStatusOk(reply)) {
533         while (true) {
534             KParts::BrowserOpenOrSaveQuestion::Result result;
535             KParts::BrowserOpenOrSaveQuestion dlg(d->windowWidget(), replyUrl, mimeType);
536             dlg.setSuggestedFileName(suggestedFileName);
537             dlg.setFeatures(KParts::BrowserOpenOrSaveQuestion::ServiceSelection);
538             result = dlg.askOpenOrSave();
539 
540             switch (result) {
541             case KParts::BrowserOpenOrSaveQuestion::Open:
542                 // Handle Post operations that return content...
543                 if (reply->operation() == QNetworkAccessManager::PostOperation) {
544                     d->mimeType = mimeType;
545                     QFileInfo finfo(suggestedFileName.isEmpty() ? replyUrl.fileName() : suggestedFileName);
546                     QTemporaryFile tempFile(QDir::tempPath() + QLatin1String("/kwebpage_XXXXXX.") + finfo.suffix());
547                     tempFile.setAutoRemove(false);
548                     tempFile.open();
549                     const QUrl destUrl = QUrl::fromLocalFile(tempFile.fileName());
550                     KIO::Job *job = KIO::file_copy(replyUrl, destUrl, 0600, KIO::Overwrite);
551                     KJobWidgets::setWindow(job, d->windowWidget());
552                     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
553                     connect(job, SIGNAL(result(KJob*)),
554                             this, SLOT(_k_copyResultToTempFile(KJob*)));
555                     return true;
556                 }
557 
558                 // Ask before running any executables...
559                 if (KParts::BrowserRun::allowExecution(mimeType, replyUrl)) {
560                     KService::Ptr offer = dlg.selectedService();
561                     // HACK: The check below is necessary to break an infinite
562                     // recursion that occurs whenever this function is called as a result
563                     // of receiving content that can be rendered by the app using this engine.
564                     // For example a text/html header that containing a content-disposition
565                     // header is received by the app using this class.
566                     if (isMimeTypeAssociatedWithSelf(offer)) {
567                         reloadRequestWithoutDisposition(reply);
568                     } else {
569                         QList<QUrl> list;
570                         list.append(replyUrl);
571                         bool success = false;
572                         // qDebug() << "Suggested file name:" << suggestedFileName;
573                         if (offer) {
574                             success = KRun::runService(*offer, list, d->windowWidget(), false, suggestedFileName);
575                         } else {
576                             success = KRun::displayOpenWithDialog(list, d->windowWidget(), false, suggestedFileName);
577                             if (!success) {
578                                 break;
579                             }
580                         }
581                         // For non KIO apps and cancelled Open With dialog, remove slave on hold.
582                         if (!success || (offer && !offer->categories().contains(QL1S("KDE")))) {
583                             KIO::SimpleJob::removeOnHold(); // Remove any slave-on-hold...
584                         }
585                     }
586                     return true;
587                 }
588                 // TODO: Instead of silently failing when allowExecution fails, notify
589                 // the user why the requested action cannot be fulfilled...
590                 return false;
591             case KParts::BrowserOpenOrSaveQuestion::Save:
592                 // Do not download local files...
593                 if (!replyUrl.isLocalFile()) {
594                     QString downloadCmd(reply->property("DownloadManagerExe").toString());
595                     if (!downloadCmd.isEmpty()) {
596                         downloadCmd += QLatin1Char(' ');
597                         downloadCmd += KShell::quoteArg(replyUrl.url());
598                         if (!suggestedFileName.isEmpty()) {
599                             downloadCmd += QLatin1Char(' ');
600                             downloadCmd += KShell::quoteArg(suggestedFileName);
601                         }
602                         // qDebug() << "download command:" << downloadCmd;
603                         if (KRun::runCommand(downloadCmd, view())) {
604                             return true;
605                         }
606                     }
607                     if (!downloadResource(replyUrl, suggestedFileName, d->windowWidget())) {
608                         return true; // file dialog was cancelled, stop here
609                     }
610                 }
611                 return true;
612             case KParts::BrowserOpenOrSaveQuestion::Cancel:
613             default:
614                 KIO::SimpleJob::removeOnHold(); // Remove any slave-on-hold...
615                 return true;
616             }
617         }
618     } else {
619         KService::Ptr offer = KMimeTypeTrader::self()->preferredService(mimeType);
620         if (isMimeTypeAssociatedWithSelf(offer)) {
621             reloadRequestWithoutDisposition(reply);
622             return true;
623         }
624     }
625 
626     return false;
627 }
628 
629 #include "moc_kwebpage.cpp"
630 
631