Fix crash if key bindings specified in profile cannot be found. Improve
[personal-kdebase.git] / apps / konqueror / client / kfmclient.cpp
blob8d3ab29e974fdad6ca59e4995ba8b56175b9c0ab
1 /* This file is part of the KDE project
2 Copyright (C) 1999-2006 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 as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
20 #include "kfmclient.h"
22 #include <ktoolinvocation.h>
23 #include <kio/job.h>
24 #include <kio/jobuidelegate.h>
25 #include <kcmdlineargs.h>
26 #include <klocale.h>
27 #include <kprocess.h>
28 #include <kstandarddirs.h>
29 #include <kmessagebox.h>
30 #include <kmimetypetrader.h>
31 #include <kmimetype.h>
32 #include <kdebug.h>
33 #include <kservice.h>
34 #include <krun.h>
35 #include <kcomponentdata.h>
36 #include <KStartupInfoId>
38 #include <konq_mainwindow_interface.h>
39 #include <konq_main_interface.h>
41 #include <QtCore/QDir>
42 #include <QtCore/QRegExp>
44 #include <stdlib.h>
45 #include <stdio.h>
46 #include <signal.h>
47 #include <unistd.h>
49 #ifdef Q_WS_X11
50 #include <X11/Xlib.h>
51 #include <QX11Info>
52 #endif
54 static const char appName[] = "kfmclient";
55 static const char programName[] = I18N_NOOP("kfmclient");
56 static const char description[] = I18N_NOOP("KDE tool for opening URLs from the command line");
57 static const char version[] = "2.0";
59 QByteArray ClientApp::startup_id_str;
60 bool ClientApp::m_ok = true;
61 bool s_interactive = true;
63 K_GLOBAL_STATIC_WITH_ARGS(KComponentData, s_instance, ("kfmclient"))
65 static void needInstance();
67 extern "C" KDE_EXPORT int kdemain( int argc, char **argv )
69 KCmdLineArgs::init(argc, argv, appName, 0, ki18n(programName), version, ki18n(description), false);
72 KCmdLineOptions options;
74 options.add("noninteractive", ki18n("Non interactive use: no message boxes"));
76 options.add("commands", ki18n("Show available commands"));
78 options.add("+command", ki18n("Command (see --commands)"));
80 options.add("+[URL(s)]", ki18n("Arguments for command"));
82 KCmdLineArgs::addCmdLineOptions( options );
83 KCmdLineArgs::addTempFileOption();
85 KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
87 if (argc == 1 || args->isSet("commands") )
89 KCmdLineArgs::enable_i18n();
90 puts(i18n("\nSyntax:\n").toLocal8Bit());
91 puts(i18n(" kfmclient openURL 'url' ['mimetype']\n"
92 " # Opens a window showing 'url'.\n"
93 " # 'url' may be a relative path\n"
94 " # or file name, such as . or subdir/\n"
95 " # If 'url' is omitted, $HOME is used instead.\n\n").toLocal8Bit());
96 puts(i18n(" # If 'mimetype' is specified, it will be used to determine the\n"
97 " # component that Konqueror should use. For instance, set it to\n"
98 " # text/html for a web page, to make it appear faster\n\n").toLocal8Bit());
100 puts(i18n(" kfmclient newTab 'url' ['mimetype']\n"
101 " # Same as above but opens a new tab with 'url' in an existing Konqueror\n"
102 " # window on the current active desktop if possible.\n\n").toLocal8Bit());
104 puts(i18n(" kfmclient openProfile 'profile' ['url']\n"
105 " # Opens a window using the given profile.\n"
106 " # 'profile' is a file under ~/.kde/share/apps/konqueror/profiles.\n"
107 " # 'url' is an optional URL to open.\n\n").toLocal8Bit());
109 return 0;
112 // Use kfmclient from the session KDE version
113 if(( args->arg( 0 ) == "openURL" || args->arg( 0 ) == "newTab" )
114 && getenv( "KDE_FULL_SESSION" ) != NULL )
116 int version = KDE_VERSION_MAJOR;
117 if( getenv( "KDE_SESSION_VERSION" ) == NULL ) // this is KDE3
118 version = 3;
119 else
120 version = atoi( getenv( "KDE_SESSION_VERSION" ));
121 if( version != 0 && version != KDE_VERSION_MAJOR )
123 kDebug( 1202 ) << "Forwarding to kfmclient from KDE version " << version;
124 char wrapper[ 10 ];
125 sprintf( wrapper, "kde%d", version );
126 char** newargv = new char*[ argc + 2 ];
127 newargv[ 0 ] = wrapper;
128 for( int i = 0;
129 i < argc;
130 ++i )
131 newargv[ i + 1 ] = argv[ i ];
132 newargv[ argc + 1 ] = NULL;
133 execvp( wrapper, newargv );
134 // just continue if failed
138 // ClientApp internally uses KConfig and hence needs a valid KComponentData
139 needInstance();
140 return ClientApp::doIt() ? 0 /*no error*/ : 1 /*error*/;
143 // Call needInstance before any use of KConfig
144 static void needInstance()
146 KComponentData *tmp = s_instance; // inits the global static if referenced for the first time
147 Q_UNUSED(tmp);
151 Whether to start a new konqueror or reuse an existing process.
153 First of all, this concept is actually broken, as the view used to show
154 the data may change at any time, and therefore Konqy reused to browse
155 "safe" data may eventually browse something completely different.
156 Moreover, it's quite difficult to find out when to reuse, and thus this
157 function is an ugly hack. You've been warned.
159 Kfmclient will attempt to find an instance for reusing if either reusing
160 is configured to reuse always,
161 or it's not configured to never reuse, and the URL to-be-opened is "safe".
162 The URL is safe, if the view used to view it is listed in the allowed KPart's.
163 In order to find out the part, mimetype is needed, and KMimeTypeTrader is needed.
164 If mimetype is not known, KMimeType is used (which doesn't work e.g. for remote
165 URLs, but oh well). Since this function may be running without a KApplication
166 instance, I'm actually quite surprised it works, and it may sooner or later break.
167 Nice, isn't it?
169 If a profile is being used, and no url has been explicitly given, it needs to be
170 read from the profile. If there's more than one URL listed in the profile, no reusing
171 will be done (oh well), if there's no URL, no reusing will be done either (also
172 because the webbrowsing profile doesn't have any URL listed).
174 static bool startNewKonqueror( QString url, QString mimetype, const QString& profile )
176 needInstance();
177 KConfig konqCfg( QLatin1String( "konquerorrc" ) );
178 const KConfigGroup reusingGroup( &konqCfg, "Reusing" );
179 QStringList allowed_parts;
180 // is duplicated in ../KonquerorAdaptor.cpp
181 allowed_parts << QLatin1String( "dolphinpart.desktop" )
182 << QLatin1String( "konq_sidebartng.desktop" );
183 if( reusingGroup.hasKey( "SafeParts" )
184 && reusingGroup.readEntry( "SafeParts" ) != QLatin1String( "SAFE" ))
185 allowed_parts = reusingGroup.readEntry( "SafeParts",QStringList() );
186 if( allowed_parts.count() == 1 && allowed_parts.first() == QLatin1String( "ALL" ))
187 return false; // all parts allowed
188 if( url.isEmpty())
190 if( profile.isEmpty())
191 return true;
192 QString profilepath = KStandardDirs::locate( "data", QLatin1String("konqueror/profiles/") + profile );
193 if( profilepath.isEmpty())
194 return true;
195 KConfig cfg( profilepath );
196 KConfigGroup profileGroup( &cfg, "Profile" );
197 const QMap< QString, QString > entries = profileGroup.entryMap();
198 QRegExp urlregexp( QLatin1String( "^View[0-9]*_URL$" ));
199 QStringList urls;
200 for( QMap< QString, QString >::ConstIterator it = entries.begin();
201 it != entries.end();
202 ++it )
204 // don't read value from map, dollar expansion is needed
205 QString value = profileGroup.readEntry( it.key(), QString());
206 if( urlregexp.indexIn( it.key()) >= 0 && !value.isEmpty())
207 urls << value;
209 if( urls.count() != 1 )
210 return true;
211 url = urls.first();
212 mimetype.clear();
214 if (mimetype.isEmpty())
215 mimetype = KMimeType::findByUrl(KUrl(url))->name();
216 if (mimetype == "application/octet-stream")
217 return true;
218 KService::List offers = KMimeTypeTrader::self()->query( mimetype, QLatin1String( "KParts/ReadOnlyPart" ) );
219 KService::Ptr serv;
220 if( offers.count() > 0 )
221 serv = offers.first();
222 return !serv || !allowed_parts.contains( serv->desktopEntryName() + QLatin1String(".desktop") );
225 static int currentScreen()
227 #ifdef Q_WS_X11
228 QX11Info info;
229 if( QX11Info::display() != NULL )
230 return info.screen();
231 // case when there's no KApplication instance
232 const char* env = getenv( "DISPLAY" );
233 if( env == NULL )
234 return 0;
235 const char* dotpos = strrchr( env, '.' );
236 const char* colonpos = strrchr( env, ':' );
237 if( dotpos != NULL && colonpos != NULL && dotpos > colonpos )
238 return atoi( dotpos + 1 );
239 #endif
240 return 0;
243 static bool s_dbus_initialized = false;
244 static void needDBus()
246 if ( !s_dbus_initialized ) {
247 extern void qDBusBindToApplication();
248 qDBusBindToApplication();
249 if (!QDBusConnection::sessionBus().isConnected())
250 kFatal(101) << "Session bus not found" ;
251 s_dbus_initialized = true;
255 // when reusing a preloaded konqy, make sure your always use a DBus call which opens a profile !
257 static QString getPreloadedKonqy()
259 needInstance();
260 KConfig konqCfg( QLatin1String( "konquerorrc" ) );
261 const KConfigGroup reusingGroup( &konqCfg, "Reusing" );
262 if( reusingGroup.readEntry( "MaxPreloadCount", 1 ) == 0 )
263 return QString();
264 needDBus();
265 QDBusInterface ref( "org.kde.kded", "/modules/konqy_preloader", "org.kde.konqueror.Preloader", QDBusConnection::sessionBus() );
266 // ## used to have NoEventLoop and 3s timeout with dcop
267 QDBusReply<QString> reply = ref.call( "getPreloadedKonqy", currentScreen() );
268 if ( reply.isValid() )
269 return reply;
270 return QString();
273 static QString konqyToReuse( const QString& url, const QString& mimetype, const QString& profile )
274 { // prefer(?) preloaded ones
276 QString ret = getPreloadedKonqy();
277 if( !ret.isEmpty())
278 return ret;
279 if( startNewKonqueror( url, mimetype, profile ))
280 return QString();
281 needDBus();
282 QDBusConnection dbus = QDBusConnection::sessionBus();
283 QDBusReply<QStringList> reply = dbus.interface()->registeredServiceNames();
284 if ( !reply.isValid() )
285 return QString();
287 const QStringList allServices = reply;
288 const int screen = currentScreen();
289 for ( QStringList::const_iterator it = allServices.begin(), end = allServices.end() ; it != end ; ++it ) {
290 const QString service = *it;
291 if ( service.startsWith( "org.kde.konqueror" ) ) {
292 org::kde::Konqueror::Main konq( service, "/KonqMain", dbus );
293 QDBusReply<bool> reuse = konq.processCanBeReused( screen );
294 if ( reuse.isValid() && reuse )
295 return service;
299 return QString();
302 void ClientApp::sendASNChange()
304 #ifdef Q_WS_X11
305 KStartupInfoId id;
306 id.initId( startup_id_str );
307 KStartupInfoData data;
308 data.addPid( 0 ); // say there's another process for this ASN with unknown PID
309 data.setHostname(); // ( no need to bother to get this konqy's PID )
310 Display* dpy = QX11Info::display();
311 if( dpy == NULL ) // we may be running without QApplication here
312 dpy = XOpenDisplay( NULL );
313 if( dpy != NULL )
314 KStartupInfo::sendChangeX( dpy, id, data );
315 if( dpy != NULL && dpy != QX11Info::display())
316 XCloseDisplay( dpy );
317 #endif
320 static bool krun_has_error = false;
322 bool ClientApp::createNewWindow(const KUrl & url, bool newTab, bool tempFile, const QString & mimetype)
324 kDebug( 1202 ) << url << "mimetype=" << mimetype;
325 needInstance();
327 if (url.protocol().startsWith(QLatin1String("http")))
329 KConfig config(QLatin1String("kfmclientrc"));
330 KConfigGroup generalGroup(&config, "General");
331 if (!generalGroup.readEntry("BrowserApplication").isEmpty())
333 kDebug() << "Using external browser" << generalGroup.readEntry( "BrowserApplication" );
334 Q_ASSERT( qApp );
335 //ClientApp app;
336 #ifdef Q_WS_X11
337 KStartupInfo::appStarted();
338 #endif
340 // TODO we don't handle tempFile here, but most likely the external browser doesn't support it,
341 // so we should sleep and delete it ourselves....
342 KRun * run = new KRun( url, 0L, false, false /* no progress window */ );
343 QObject::connect( run, SIGNAL( finished() ), qApp, SLOT( delayedQuit() ));
344 QObject::connect( run, SIGNAL( error() ), qApp, SLOT( delayedQuit() ));
345 qApp->exec();
346 return !krun_has_error;
350 needDBus();
351 QDBusConnection dbus = QDBusConnection::sessionBus();
352 KConfig cfg( QLatin1String( "konquerorrc" ) );
353 KConfigGroup fmSettings = cfg.group( "FMSettings" );
354 if ( newTab || fmSettings.readEntry( "KonquerorTabforExternalURL", false) ) {
356 QString foundApp;
357 QDBusObjectPath foundObj;
358 QDBusReply<QStringList> reply = dbus.interface()->registeredServiceNames();
359 if ( reply.isValid() ) {
360 const QStringList allServices = reply;
361 for ( QStringList::const_iterator it = allServices.begin(), end = allServices.end() ; it != end ; ++it ) {
362 const QString service = *it;
363 if ( service.startsWith( "org.kde.konqueror" ) ) {
364 org::kde::Konqueror::Main konq( service, "/KonqMain", dbus );
365 QDBusReply<QDBusObjectPath> windowReply = konq.windowForTab();
366 if ( windowReply.isValid() ) {
367 QDBusObjectPath path = windowReply;
368 // "/" is the indicator for "no object found", since we can't use an empty path
369 if ( path.path() != "/" ) {
370 foundApp = service;
371 foundObj = path;
378 if ( !foundApp.isEmpty() ) {
379 org::kde::Konqueror::MainWindow konqWindow( foundApp, foundObj.path(), dbus );
380 QDBusReply<void> newTabReply = konqWindow.newTabASN( url.url(), startup_id_str, tempFile );
381 if ( newTabReply.isValid() ) {
382 sendASNChange();
383 return true;
388 QString appId = konqyToReuse( url.url(), mimetype, QString() );
389 if( !appId.isEmpty())
391 kDebug( 1202 ) << "ClientApp::createNewWindow using existing konqueror";
392 org::kde::Konqueror::Main konq( appId, "/KonqMain", dbus );
393 konq.createNewWindow( url.url(), mimetype, startup_id_str, tempFile );
394 sendASNChange();
396 else
398 QString error;
399 /* Well, we can't pass a mimetype through startServiceByDesktopPath !
400 if ( KToolInvocation::startServiceByDesktopPath( QLatin1String("konqueror.desktop"),
401 url.url(), &error ) > 0 )
403 kError() << "Couldn't start konqueror from konqueror.desktop: " << error << endl;
405 // pass kfmclient's startup id to konqueror using kshell
406 #ifdef Q_WS_X11
407 KStartupInfoId id;
408 id.initId( startup_id_str );
409 id.setupStartupEnv();
410 #endif
411 QStringList args;
412 args << QLatin1String("konqueror");
413 if ( !mimetype.isEmpty() )
414 args << "-mimetype" << mimetype;
415 if ( tempFile )
416 args << "-tempfile";
417 args << url.url();
418 KProcess::startDetached(QLatin1String("kshell4"), args);
419 #ifdef Q_WS_X11
420 KStartupInfo::resetStartupEnv();
421 #endif
422 kDebug( 1202 ) << "ClientApp::createNewWindow KProcess started";
425 return true;
428 bool ClientApp::openProfile( const QString & profileName, const QString & url, const QString & mimetype )
430 needInstance();
431 QString appId = konqyToReuse( url, mimetype, profileName );
432 if( appId.isEmpty())
434 QString error;
435 if ( KToolInvocation::startServiceByDesktopPath( QLatin1String("konqueror.desktop"),
436 QLatin1String("--silent"), &error, &appId, NULL, startup_id_str ) > 0 )
438 kError() << "Couldn't start konqueror from konqueror.desktop: " << error << endl;
439 return false;
441 // startServiceByDesktopPath waits for the app to register with DBus
442 // so when we arrive here, konq is up and running already, and appId contains the identification
445 QString profile = KStandardDirs::locate( "data", QLatin1String("konqueror/profiles/") + profileName );
446 if ( profile.isEmpty() )
448 fprintf( stderr, "%s", i18n("Profile %1 not found\n", profileName).toLocal8Bit().data() );
449 ::exit( 0 );
451 needDBus();
452 org::kde::Konqueror::Main konqy( appId, "/KonqMain", QDBusConnection::sessionBus() );
453 if ( url.isEmpty() )
454 konqy.createBrowserWindowFromProfile( profile, profileName, startup_id_str );
455 else if ( mimetype.isEmpty() )
456 konqy.createBrowserWindowFromProfileAndUrl( profile, profileName, url, startup_id_str );
457 else
458 konqy.createBrowserWindowFromProfileUrlAndMimeType( profile, profileName, url, mimetype, startup_id_str );
459 sleep(2); // Martin Schenk <martin@schenk.com> says this is necessary to let the server read from the socket
460 // ######## so those methods should probably not be ASYNC
461 sendASNChange();
462 return true;
465 void ClientApp::delayedQuit()
467 // Quit in 2 seconds. This leaves time for KRun to pop up
468 // "app not found" in KProcessRunner, if that was the case.
469 QTimer::singleShot( 2000, this, SLOT(deref()) );
470 // don't access the KRun instance later, it will be deleted after calling slots
471 if( static_cast< const KRun* >( sender())->hasError())
472 krun_has_error = true;
475 static void checkArgumentCount(int count, int min, int max)
477 if (count < min)
479 fputs( i18n("Syntax Error: Not enough arguments\n").toLocal8Bit(), stderr );
480 ::exit(1);
482 if (max && (count > max))
484 fputs( i18n("Syntax Error: Too many arguments\n").toLocal8Bit(), stderr );
485 ::exit(1);
489 bool ClientApp::doIt()
491 KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
492 int argc = args->count();
493 checkArgumentCount(argc, 1, 0);
495 if ( !args->isSet( "ninteractive" ) ) {
496 s_interactive = false;
498 QString command = args->arg(0);
500 #ifdef Q_WS_X11
501 // read ASN env. variable for non-KApp cases
502 startup_id_str = KStartupInfo::currentStartupIdEnv().id();
503 #endif
505 kDebug() << "Creating ClientApp";
506 int fake_argc = 0;
507 char** fake_argv = 0;
508 ClientApp app( fake_argc, fake_argv );
511 if ( command == "openURL" || command == "newTab" )
513 checkArgumentCount(argc, 1, 3);
514 bool tempFile = KCmdLineArgs::isTempFileSet();
515 if ( argc == 1 )
517 KUrl url;
518 url.setPath(QDir::homePath());
519 return createNewWindow( url, command == "newTab", tempFile );
521 if ( argc == 2 )
523 return createNewWindow( args->url(1), command == "newTab", tempFile );
525 if ( argc == 3 )
527 return createNewWindow( args->url(1), command == "newTab", tempFile, args->arg(2) );
530 else if ( command == "openProfile" )
532 checkArgumentCount(argc, 2, 3);
533 QString url;
534 if ( argc == 3 )
535 url = args->url(2).url();
536 return openProfile( args->arg(1), url );
538 else if ( command == "exec" && argc >= 2)
540 // compatibility with KDE 3 and xdg-open
541 QStringList kioclientArgs;
542 kioclientArgs << "exec" << args->arg(1);
543 if (argc == 3)
544 kioclientArgs << args->arg(2);
546 int ret = KProcess::execute("kioclient", kioclientArgs);
547 return ret == 0;
549 else
551 fprintf( stderr, "%s", i18n("Syntax Error: Unknown command '%1'\n", command).toLocal8Bit().data() );
552 return false;
554 return true;
557 void ClientApp::slotResult( KJob * job )
559 if (job->error() && s_interactive)
561 static_cast<KIO::Job*>(job)->ui()->setWindow(0);
562 static_cast<KIO::Job*>(job)->ui()->showErrorMessage();
564 m_ok = !job->error();
565 quit();
568 void ClientApp::slotDialogCanceled()
570 m_ok = false;
571 quit();
574 #include "kfmclient.moc"