1/* This file is part of the KDE project
2 *
3 * Copyright (C) 2002 David Faure <faure@kde.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License version 2, as published by the Free Software Foundation.
7 *
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * Library General Public License for more details.
12 *
13 * You should have received a copy of the GNU Library General Public License
14 * along with this library; see the file COPYING.LIB. If not, write to
15 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#include "browserrun.h"
20#include "browserrun_p.h"
21
22#include <kmessagebox.h>
23#include <kfiledialog.h>
24#include <kio/job.h>
25#include <kio/jobuidelegate.h>
26#include <kio/scheduler.h>
27#include <kio/copyjob.h>
28#include <klocale.h>
29#include <kshell.h>
30#include <kstringhandler.h>
31#include <kmimetypetrader.h>
32#include <ktemporaryfile.h>
33#include <kdebug.h>
34#include <kde_file.h>
35#include <kstandarddirs.h>
36#include <kdatetime.h>
37#include "browseropenorsavequestion.h"
38#include <kprotocolmanager.h>
39
40using namespace KParts;
41
42class BrowserRun::BrowserRunPrivate
43{
44public:
45 bool m_bHideErrorDialog;
46 bool m_bRemoveReferrer;
47 bool m_bTrustedSource;
48 KParts::OpenUrlArguments m_args;
49 KParts::BrowserArguments m_browserArgs;
50
51 KParts::ReadOnlyPart *m_part; // QGuardedPtr?
52 QPointer<QWidget> m_window;
53 QString m_mimeType;
54 QString m_contentDisposition;
55};
56
57BrowserRun::BrowserRun( const KUrl& url, const KParts::OpenUrlArguments& args,
58 const KParts::BrowserArguments& browserArgs,
59 KParts::ReadOnlyPart *part, QWidget* window,
60 bool removeReferrer, bool trustedSource, bool hideErrorDialog )
61 : KRun( url, window, 0 /*mode*/, false /*is_local_file known*/, false /* no GUI */ ),
62 d(new BrowserRunPrivate)
63{
64 d->m_bHideErrorDialog = hideErrorDialog;
65 d->m_bRemoveReferrer = removeReferrer;
66 d->m_bTrustedSource = trustedSource;
67 d->m_args = args;
68 d->m_browserArgs = browserArgs;
69 d->m_part = part;
70 d->m_window = window;
71}
72
73BrowserRun::~BrowserRun()
74{
75 delete d;
76}
77
78KParts::ReadOnlyPart* BrowserRun::part() const
79{
80 return d->m_part;
81}
82
83KUrl BrowserRun::url() const
84{
85 return KRun::url();
86}
87
88void BrowserRun::init()
89{
90 if ( d->m_bHideErrorDialog )
91 {
92 // ### KRun doesn't call a virtual method when it finds out that the URL
93 // is either malformed, or points to a non-existing local file...
94 // So we need to reimplement some of the checks, to handle d->m_bHideErrorDialog
95 if ( !KRun::url().isValid() ) {
96 redirectToError( KIO::ERR_MALFORMED_URL, KRun::url().url() );
97 return;
98 }
99 if ( !isLocalFile() && !hasError() && KRun::url().isLocalFile() )
100 setIsLocalFile( true );
101
102 if ( isLocalFile() ) {
103 KDE_struct_stat buff;
104 if ( KDE::stat( KRun::url().toLocalFile(), &buff ) == -1 )
105 {
106 kDebug(1000) << KRun::url().toLocalFile() << "doesn't exist.";
107 redirectToError( KIO::ERR_DOES_NOT_EXIST, KRun::url().toLocalFile() );
108 return;
109 }
110 setMode( buff.st_mode ); // while we're at it, save it for KRun::init() to use it
111 }
112 }
113 KRun::init();
114}
115
116void BrowserRun::scanFile()
117{
118 kDebug(1000) << KRun::url();
119
120 // Let's check for well-known extensions
121 // Not when there is a query in the URL, in any case.
122 // Optimization for http/https, findByURL doesn't trust extensions over http.
123 QString protocol = KRun::url().protocol();
124
125 if (!KProtocolInfo::proxiedBy(protocol).isEmpty()) {
126 QString dummy;
127 protocol = KProtocolManager::slaveProtocol(KRun::url(), dummy);
128 }
129
130 if ( KRun::url().query().isEmpty() && !protocol.startsWith(QLatin1String("http")))
131 {
132 KMimeType::Ptr mime = KMimeType::findByUrl( KRun::url() );
133 Q_ASSERT( mime );
134 if ( !mime->isDefault() || isLocalFile() )
135 {
136 kDebug(1000) << "MIME TYPE is" << mime->name();
137 mimeTypeDetermined( mime->name() );
138 return;
139 }
140 }
141
142 QMap<QString, QString>& metaData = d->m_args.metaData();
143 if ( d->m_part ) {
144 const QString proto = d->m_part->url().protocol();
145
146 if (proto == "https" || proto == "webdavs") {
147 metaData.insert("main_frame_request", "TRUE" );
148 metaData.insert("ssl_was_in_use", "TRUE" );
149 // metaData.insert("ssl_activate_warnings", "TRUE" );
150 } else if (proto == "http" || proto == "webdav") {
151 // metaData.insert("ssl_activate_warnings", "TRUE" );
152 metaData.insert("ssl_was_in_use", "FALSE" );
153 }
154
155 // Set the PropagateHttpHeader meta-data if it has not already been set...
156 if (!metaData.contains("PropagateHttpHeader"))
157 metaData.insert("PropagateHttpHeader", "TRUE");
158 }
159
160 KIO::TransferJob *job;
161 if ( d->m_browserArgs.doPost() && KRun::url().protocol().startsWith(QLatin1String("http"))) {
162 job = KIO::http_post( KRun::url(), d->m_browserArgs.postData, KIO::HideProgressInfo );
163 job->addMetaData( "content-type", d->m_browserArgs.contentType() );
164 } else {
165 job = KIO::get(KRun::url(),
166 d->m_args.reload() ? KIO::Reload : KIO::NoReload,
167 KIO::HideProgressInfo);
168 }
169
170 if ( d->m_bRemoveReferrer )
171 metaData.remove("referrer");
172
173 job->addMetaData( metaData );
174 job->ui()->setWindow( d->m_window );
175 connect( job, SIGNAL(result(KJob*)),
176 this, SLOT(slotBrowserScanFinished(KJob*)));
177 connect( job, SIGNAL(mimetype(KIO::Job*,QString)),
178 this, SLOT(slotBrowserMimetype(KIO::Job*,QString)));
179 setJob( job );
180}
181
182void BrowserRun::slotBrowserScanFinished(KJob *job)
183{
184 kDebug(1000) << job->error();
185 if ( job->error() == KIO::ERR_IS_DIRECTORY )
186 {
187 // It is in fact a directory. This happens when HTTP redirects to FTP.
188 // Due to the "protocol doesn't support listing" code in BrowserRun, we
189 // assumed it was a file.
190 kDebug(1000) << "It is in fact a directory!";
191 // Update our URL in case of a redirection
192 KRun::setUrl( static_cast<KIO::TransferJob *>(job)->url() );
193 setJob( 0 );
194 mimeTypeDetermined( "inode/directory" );
195 }
196 else
197 {
198 if ( job->error() )
199 handleError( job );
200 else
201 KRun::slotScanFinished(job);
202 }
203}
204
205static KMimeType::Ptr fixupMimeType (const QString& mimeType, const QString& fileName)
206{
207 KMimeType::Ptr mime = KMimeType::mimeType(mimeType);
208 if ((!mime || mime->isDefault()) && !fileName.isEmpty()) {
209 mime = KMimeType::findByUrl(fileName, 0, false, true);
210 }
211 return mime;
212}
213
214void BrowserRun::slotBrowserMimetype( KIO::Job *_job, const QString &type )
215{
216 Q_ASSERT( _job == KRun::job() ); Q_UNUSED(_job)
217 KIO::TransferJob *job = static_cast<KIO::TransferJob *>(KRun::job());
218 // Update our URL in case of a redirection
219 //kDebug(1000) << "old URL=" << KRun::url();
220 //kDebug(1000) << "new URL=" << job->url();
221 setUrl( job->url() );
222
223 if (job->isErrorPage()) {
224 d->m_mimeType = type;
225 handleError(job);
226 setJob( 0 );
227 } else {
228 kDebug(1000) << "found" << type << "for" << KRun::url();
229
230 // Suggested filename given by the server (e.g. HTTP content-disposition)
231 // When set, we should really be saving instead of embedding
232 const QString suggestedFileName = job->queryMetaData("content-disposition-filename");
233 setSuggestedFileName(suggestedFileName); // store it (in KRun)
234 //kDebug(1000) << "suggestedFileName=" << suggestedFileName;
235 d->m_contentDisposition = job->queryMetaData("content-disposition-type");
236
237 const QString modificationTime = job->queryMetaData("content-disposition-modification-date");
238 if (!modificationTime.isEmpty()) {
239 d->m_args.metaData().insert(QLatin1String("content-disposition-modification-date"), modificationTime);
240 }
241
242 QMapIterator<QString,QString> it (job->metaData());
243 while (it.hasNext()) {
244 it.next();
245 if (it.key().startsWith(QLatin1String("ssl_"), Qt::CaseInsensitive))
246 d->m_args.metaData().insert(it.key(), it.value());
247 }
248
249 // Make a copy to avoid a dead reference
250 QString _type = type;
251 job->putOnHold();
252 setJob( 0 );
253
254 // If the current mime-type is the default mime-type, then attempt to
255 // determine the "real" mimetype from the file name.
256 KMimeType::Ptr mimePtr = fixupMimeType(_type, suggestedFileName.isEmpty() ? url().fileName() : suggestedFileName);
257 if (mimePtr && mimePtr->name() != _type) {
258 _type = mimePtr->name();
259 }
260
261 mimeTypeDetermined( _type );
262 }
263}
264
265BrowserRun::NonEmbeddableResult BrowserRun::handleNonEmbeddable(const QString& mimeType)
266{
267 return handleNonEmbeddable(mimeType, NULL);
268}
269
270BrowserRun::NonEmbeddableResult BrowserRun::handleNonEmbeddable(const QString& _mimeType, KService::Ptr* selectedService)
271{
272 QString mimeType( _mimeType );
273 Q_ASSERT( !hasFinished() ); // only come here if the mimetype couldn't be embedded
274 // Support for saving remote files.
275 if ( mimeType != "inode/directory" && // dirs can't be saved
276 !KRun::url().isLocalFile() )
277 {
278 if ( isTextExecutable(mimeType) )
279 mimeType = QLatin1String("text/plain"); // view, don't execute
280 // ... -> ask whether to save
281 BrowserOpenOrSaveQuestion question(d->m_window, KRun::url(), mimeType);
282 question.setSuggestedFileName(suggestedFileName());
283 if (selectedService)
284 question.setFeatures(BrowserOpenOrSaveQuestion::ServiceSelection);
285 BrowserOpenOrSaveQuestion::Result res = question.askOpenOrSave();
286 if (res == BrowserOpenOrSaveQuestion::Save) {
287 save( KRun::url(), suggestedFileName() );
288 kDebug(1000) << "Save: returning Handled";
289 setFinished( true );
290 return Handled;
291 }
292 else if (res == BrowserOpenOrSaveQuestion::Cancel) {
293 // saving done or canceled
294 kDebug(1000) << "Cancel: returning Handled";
295 setFinished( true );
296 return Handled;
297 }
298 else // "Open" chosen (done by KRun::foundMimeType, called when returning NotHandled)
299 {
300 // If we were in a POST, we can't just pass a URL to an external application.
301 // We must save the data to a tempfile first.
302 if ( d->m_browserArgs.doPost() )
303 {
304 kDebug(1000) << "request comes from a POST, can't pass a URL to another app, need to save";
305 d->m_mimeType = mimeType;
306 QString extension;
307 QString fileName = suggestedFileName().isEmpty() ? KRun::url().fileName() : suggestedFileName();
308 int extensionPos = fileName.lastIndexOf( '.' );
309 if ( extensionPos != -1 )
310 extension = fileName.mid( extensionPos ); // keep the '.'
311 KTemporaryFile tempFile;
312 tempFile.setSuffix(extension);
313 tempFile.setAutoRemove(false);
314 tempFile.open();
315 KUrl destURL;
316 destURL.setPath( tempFile.fileName() );
317 KIO::Job *job = KIO::file_copy( KRun::url(), destURL, 0600, KIO::Overwrite );
318 job->ui()->setWindow(d->m_window);
319 connect( job, SIGNAL(result(KJob*)),
320 this, SLOT(slotCopyToTempFileResult(KJob*)) );
321 return Delayed; // We'll continue after the job has finished
322 }
323 if (selectedService && question.selectedService()) {
324 *selectedService = question.selectedService();
325 // KRun will use this when starting an app
326 KRun::setPreferredService(question.selectedService()->desktopEntryName());
327 }
328 }
329 }
330
331 // Check if running is allowed
332 if ( !d->m_bTrustedSource && // ... and untrusted source...
333 !allowExecution( mimeType, KRun::url() ) ) // ...and the user said no (for executables etc.)
334 {
335 setFinished( true );
336 return Handled;
337 }
338
339 KIO::Scheduler::publishSlaveOnHold(); // publish any slave on hold so it can be reused.
340 return NotHandled;
341}
342
343//static
344bool BrowserRun::allowExecution( const QString &mimeType, const KUrl &url )
345{
346 if ( !KRun::isExecutable( mimeType ) )
347 return true;
348
349 if ( !url.isLocalFile() ) // Don't permit to execute remote files
350 return false;
351
352 return ( KMessageBox::warningContinueCancel( 0,
353 i18n( "Do you really want to execute '%1'?", url.prettyUrl() ),
354 i18n("Execute File?"), KGuiItem(i18n("Execute")) ) == KMessageBox::Continue );
355}
356
357//static, deprecated
358#ifndef KDE_NO_DEPRECATED
359BrowserRun::AskSaveResult BrowserRun::askSave( const KUrl & url, KService::Ptr offer, const QString& mimeType, const QString & suggestedFileName )
360{
361 Q_UNUSED(offer);
362 BrowserOpenOrSaveQuestion question(0, url, mimeType);
363 question.setSuggestedFileName(suggestedFileName);
364 const BrowserOpenOrSaveQuestion::Result result = question.askOpenOrSave();
365 return result == BrowserOpenOrSaveQuestion::Save ? Save
366 : BrowserOpenOrSaveQuestion::Open ? Open
367 : Cancel;
368}
369#endif
370
371//static, deprecated
372#ifndef KDE_NO_DEPRECATED
373BrowserRun::AskSaveResult BrowserRun::askEmbedOrSave( const KUrl & url, const QString& mimeType, const QString & suggestedFileName, int flags )
374{
375 BrowserOpenOrSaveQuestion question(0, url, mimeType);
376 question.setSuggestedFileName(suggestedFileName);
377 const BrowserOpenOrSaveQuestion::Result result = question.askEmbedOrSave(flags);
378 return result == BrowserOpenOrSaveQuestion::Save ? Save
379 : BrowserOpenOrSaveQuestion::Embed ? Open
380 : Cancel;
381}
382#endif
383
384// Default implementation, overridden in KHTMLRun
385void BrowserRun::save( const KUrl & url, const QString & suggestedFileName )
386{
387 saveUrl(url, suggestedFileName, d->m_window, d->m_args);
388}
389
390// static
391void BrowserRun::simpleSave( const KUrl & url, const QString & suggestedFileName,
392 QWidget* window )
393{
394 saveUrl(url, suggestedFileName, window, KParts::OpenUrlArguments());
395}
396
397void KParts::BrowserRun::saveUrl(const KUrl & url, const QString & suggestedFileName,
398 QWidget* window, const KParts::OpenUrlArguments& args)
399{
400 // DownloadManager <-> konqueror integration
401 // find if the integration is enabled
402 // the empty key means no integration
403 // only use the downloadmanager for non-local urls
404 if ( !url.isLocalFile() )
405 {
406 KConfigGroup cfg = KSharedConfig::openConfig("konquerorrc", KConfig::NoGlobals)->group("HTML Settings");
407 QString downloadManger = cfg.readPathEntry("DownloadManager", QString());
408 if (!downloadManger.isEmpty())
409 {
410 // then find the download manager location
411 kDebug(1000) << "Using: "<<downloadManger <<" as Download Manager";
412 QString cmd=KStandardDirs::findExe(downloadManger);
413 if (cmd.isEmpty())
414 {
415 QString errMsg=i18n("The Download Manager (%1) could not be found in your $PATH ", downloadManger);
416 QString errMsgEx= i18n("Try to reinstall it \n\nThe integration with Konqueror will be disabled.");
417 KMessageBox::detailedSorry(0,errMsg,errMsgEx);
418 cfg.writePathEntry("DownloadManager",QString());
419 cfg.sync ();
420 }
421 else
422 {
423 // ### suggestedFileName not taken into account. Fix this (and
424 // the duplicated code) with shiny new KDownload class for 3.2 (pfeiffer)
425 // Until the shiny new class comes about, send the suggestedFileName
426 // along with the actual URL to download. (DA)
427 cmd += ' ' + KShell::quoteArg(url.url());
428 if ( !suggestedFileName.isEmpty() )
429 cmd += ' ' + KShell::quoteArg(suggestedFileName);
430
431 kDebug(1000) << "Calling command" << cmd;
432 // slave is already on hold (slotBrowserMimetype())
433 KIO::Scheduler::publishSlaveOnHold();
434 KRun::runCommand(cmd, window);
435 return;
436 }
437 }
438 }
439
440 // no download manager available, let's do it ourself
441 KFileDialog *dlg = new KFileDialog( QString(), QString() /*all files*/,
442 window);
443 dlg->setOperationMode( KFileDialog::Saving );
444 dlg->setCaption(i18n("Save As"));
445 dlg->setConfirmOverwrite(true);
446
447 QString name;
448 if ( !suggestedFileName.isEmpty() )
449 name = suggestedFileName;
450 else
451 name = url.fileName(KUrl::ObeyTrailingSlash); // can be empty, e.g. in case http://www.kde.org/
452
453 dlg->setSelection(name);
454 if ( dlg->exec() )
455 {
456 KUrl destURL( dlg->selectedUrl() );
457 if ( destURL.isValid() )
458 {
459 saveUrlUsingKIO(url, destURL, window, args.metaData());
460 }
461 }
462 delete dlg;
463}
464
465void BrowserRun::saveUrlUsingKIO(const KUrl & srcUrl, const KUrl& destUrl,
466 QWidget* window, const QMap<QString, QString> &metaData)
467{
468 KIO::FileCopyJob *job = KIO::file_copy(srcUrl, destUrl, -1, KIO::Overwrite);
469
470 const QString modificationTime = metaData[QLatin1String("content-disposition-modification-date")];
471 if (!modificationTime.isEmpty()) {
472 job->setModificationTime(KDateTime::fromString(modificationTime, KDateTime::RFCDate).dateTime());
473 }
474 job->setMetaData(metaData);
475 job->addMetaData("MaxCacheSize", "0"); // Don't store in http cache.
476 job->addMetaData("cache", "cache"); // Use entry from cache if available.
477 job->ui()->setWindow(window);
478 job->ui()->setAutoErrorHandlingEnabled( true );
479 new DownloadJobWatcher(job, metaData);
480}
481
482void BrowserRun::slotStatResult( KJob *job )
483{
484 if ( job->error() ) {
485 kDebug(1000) << job->errorString();
486 handleError( job );
487 } else
488 KRun::slotStatResult( job );
489}
490
491void BrowserRun::handleError( KJob * job )
492{
493 if ( !job ) { // Shouldn't happen, see docu.
494 kWarning(1000) << "handleError called with job=0! hideErrorDialog=" << d->m_bHideErrorDialog;
495 return;
496 }
497
498 KIO::TransferJob *tjob = qobject_cast<KIO::TransferJob *>(job);
499 if (tjob && tjob->isErrorPage() && !job->error()) {
500 // The default handling of error pages is to show them like normal pages
501 // But this is done here in handleError so that KHTMLRun can reimplement it
502 tjob->putOnHold();
503 setJob(0);
504 if (!d->m_mimeType.isEmpty())
505 mimeTypeDetermined(d->m_mimeType);
506 return;
507 }
508
509 if (d->m_bHideErrorDialog && job->error() != KIO::ERR_NO_CONTENT)
510 {
511 redirectToError( job->error(), job->errorText() );
512 return;
513 }
514
515 // Reuse code in KRun, to benefit from d->m_showingError etc.
516 KRun::slotStatResult( job );
517}
518
519// static
520KUrl BrowserRun::makeErrorUrl(int error, const QString& errorText, const QString& initialUrl)
521{
522 /*
523 * The format of the error:/ URL is error:/?query#url,
524 * where two variables are passed in the query:
525 * error = int kio error code, errText = QString error text from kio
526 * The sub-url is the URL that we were trying to open.
527 */
528 KUrl newURL(QString("error:/?error=%1&errText=%2")
529 .arg( error )
530 .arg( QString::fromUtf8( QUrl::toPercentEncoding( errorText ) ) ) );
531
532 QString cleanedOrigUrl = initialUrl;
533 KUrl runURL = cleanedOrigUrl;
534 if (runURL.isValid()) {
535 runURL.setPass( QString() ); // don't put the password in the error URL
536 cleanedOrigUrl = runURL.url();
537 }
538
539 newURL.setFragment(cleanedOrigUrl);
540 return newURL;
541
542 // The kde3 approach broke with invalid urls, now that they become empty in qt4.
543 //KUrl::List lst;
544 //lst << newURL << runURL;
545 //return KUrl::join(lst);
546}
547
548void BrowserRun::redirectToError( int error, const QString& errorText )
549{
550 /**
551 * To display this error in KHTMLPart instead of inside a dialog box,
552 * we tell konq that the mimetype is text/html, and we redirect to
553 * an error:/ URL that sends the info to khtml.
554 */
555 KRun::setUrl(makeErrorUrl(error, errorText, url().url()));
556 setJob( 0 );
557 mimeTypeDetermined( "text/html" );
558}
559
560void BrowserRun::slotCopyToTempFileResult(KJob *job)
561{
562 if ( job->error() ) {
563 job->uiDelegate()->showErrorMessage();
564 } else {
565 // Same as KRun::foundMimeType but with a different URL
566 (void) (KRun::runUrl( static_cast<KIO::FileCopyJob *>(job)->destUrl(), d->m_mimeType, d->m_window ));
567 }
568 setError( true ); // see above
569 setFinished( true );
570}
571
572bool BrowserRun::isTextExecutable( const QString &mimeType )
573{
574 return ( mimeType == "application/x-desktop" ||
575 mimeType == "application/x-shellscript" );
576}
577
578bool BrowserRun::hideErrorDialog() const
579{
580 return d->m_bHideErrorDialog;
581}
582
583QString BrowserRun::contentDisposition() const
584{
585 return d->m_contentDisposition;
586}
587
588bool BrowserRun::serverSuggestsSave() const
589{
590 // RfC 2183, section 2.8:
591 // Unrecognized disposition types should be treated as `attachment'.
592 return !contentDisposition().isEmpty() && (contentDisposition() != "inline");
593}
594
595KParts::OpenUrlArguments& KParts::BrowserRun::arguments()
596{
597 return d->m_args;
598}
599
600KParts::BrowserArguments& KParts::BrowserRun::browserArguments()
601{
602 return d->m_browserArgs;
603}
604
605#include "browserrun.moc"
606#include "browserrun_p.moc"
607