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>
24 #include <kio/jobuidelegate.h>
25 #include <kcmdlineargs.h>
28 #include <kstandarddirs.h>
29 #include <kmessagebox.h>
30 #include <kmimetypetrader.h>
31 #include <kmimetype.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>
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());
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
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
;
125 sprintf( wrapper
, "kde%d", version
);
126 char** newargv
= new char*[ argc
+ 2 ];
127 newargv
[ 0 ] = wrapper
;
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
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
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.
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
)
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
190 if( profile
.isEmpty())
192 QString profilepath
= KStandardDirs::locate( "data", QLatin1String("konqueror/profiles/") + profile
);
193 if( profilepath
.isEmpty())
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$" ));
200 for( QMap
< QString
, QString
>::ConstIterator it
= entries
.begin();
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())
209 if( urls
.count() != 1 )
214 if (mimetype
.isEmpty())
215 mimetype
= KMimeType::findByUrl(KUrl(url
))->name();
216 if (mimetype
== "application/octet-stream")
218 KService::List offers
= KMimeTypeTrader::self()->query( mimetype
, QLatin1String( "KParts/ReadOnlyPart" ) );
220 if( offers
.count() > 0 )
221 serv
= offers
.first();
222 return !serv
|| !allowed_parts
.contains( serv
->desktopEntryName() + QLatin1String(".desktop") );
225 static int currentScreen()
229 if( QX11Info::display() != NULL
)
230 return info
.screen();
231 // case when there's no KApplication instance
232 const char* env
= getenv( "DISPLAY" );
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 );
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()
260 KConfig
konqCfg( QLatin1String( "konquerorrc" ) );
261 const KConfigGroup
reusingGroup( &konqCfg
, "Reusing" );
262 if( reusingGroup
.readEntry( "MaxPreloadCount", 1 ) == 0 )
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() )
273 static QString
konqyToReuse( const QString
& url
, const QString
& mimetype
, const QString
& profile
)
274 { // prefer(?) preloaded ones
276 QString ret
= getPreloadedKonqy();
279 if( startNewKonqueror( url
, mimetype
, profile
))
282 QDBusConnection dbus
= QDBusConnection::sessionBus();
283 QDBusReply
<QStringList
> reply
= dbus
.interface()->registeredServiceNames();
284 if ( !reply
.isValid() )
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
)
302 void ClientApp::sendASNChange()
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
);
314 KStartupInfo::sendChangeX( dpy
, id
, data
);
315 if( dpy
!= NULL
&& dpy
!= QX11Info::display())
316 XCloseDisplay( dpy
);
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
;
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" );
337 KStartupInfo::appStarted();
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() ));
346 return !krun_has_error
;
351 QDBusConnection dbus
= QDBusConnection::sessionBus();
352 KConfig
cfg( QLatin1String( "konquerorrc" ) );
353 KConfigGroup fmSettings
= cfg
.group( "FMSettings" );
354 if ( newTab
|| fmSettings
.readEntry( "KonquerorTabforExternalURL", false) ) {
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() != "/" ) {
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() ) {
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
);
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
408 id
.initId( startup_id_str
);
409 id
.setupStartupEnv();
412 args
<< QLatin1String("konqueror");
413 if ( !mimetype
.isEmpty() )
414 args
<< "-mimetype" << mimetype
;
418 KProcess::startDetached(QLatin1String("kshell4"), args
);
420 KStartupInfo::resetStartupEnv();
422 kDebug( 1202 ) << "ClientApp::createNewWindow KProcess started";
428 bool ClientApp::openProfile( const QString
& profileName
, const QString
& url
, const QString
& mimetype
)
431 QString appId
= konqyToReuse( url
, mimetype
, profileName
);
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
;
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() );
452 org::kde::Konqueror::Main
konqy( appId
, "/KonqMain", QDBusConnection::sessionBus() );
454 konqy
.createBrowserWindowFromProfile( profile
, profileName
, startup_id_str
);
455 else if ( mimetype
.isEmpty() )
456 konqy
.createBrowserWindowFromProfileAndUrl( profile
, profileName
, url
, startup_id_str
);
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
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
)
479 fputs( i18n("Syntax Error: Not enough arguments\n").toLocal8Bit(), stderr
);
482 if (max
&& (count
> max
))
484 fputs( i18n("Syntax Error: Too many arguments\n").toLocal8Bit(), stderr
);
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);
501 // read ASN env. variable for non-KApp cases
502 startup_id_str
= KStartupInfo::currentStartupIdEnv().id();
505 kDebug() << "Creating ClientApp";
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();
518 url
.setPath(QDir::homePath());
519 return createNewWindow( url
, command
== "newTab", tempFile
);
523 return createNewWindow( args
->url(1), command
== "newTab", tempFile
);
527 return createNewWindow( args
->url(1), command
== "newTab", tempFile
, args
->arg(2) );
530 else if ( command
== "openProfile" )
532 checkArgumentCount(argc
, 2, 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);
544 kioclientArgs
<< args
->arg(2);
546 int ret
= KProcess::execute("kioclient", kioclientArgs
);
551 fprintf( stderr
, "%s", i18n("Syntax Error: Unknown command '%1'\n", command
).toLocal8Bit().data() );
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();
568 void ClientApp::slotDialogCanceled()
574 #include "kfmclient.moc"