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