File indexing completed on 2024-04-14 04:52:46

0001 /* This file is part of the KDE project
0002     SPDX-FileCopyrightText: 1999-2006 David Faure <faure@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kfmclient.h"
0008 
0009 #include <kio/job.h>
0010 
0011 #include <KLocalizedString>
0012 #include <kprocess.h>
0013 #include <config-konqueror.h>
0014 
0015 #include <kmessagebox.h>
0016 #include <kservice.h>
0017 #include <KIO/CommandLauncherJob>
0018 #include <KIO/ApplicationLauncherJob>
0019 #include <KStartupInfo>
0020 #include <kurifilter.h>
0021 #include <KConfig>
0022 #include <KConfigGroup>
0023 #include <KService>
0024 #include <KAboutData>
0025 #include <KWindowSystem>
0026 #include <KShell>
0027 #include <KSharedConfig>
0028 
0029 #include <kcoreaddons_version.h>
0030 
0031 #include <QApplication>
0032 #include <QDBusConnection>
0033 #include <QDir>
0034 #include <QMimeDatabase>
0035 #include <QUrl>
0036 #include <QCommandLineParser>
0037 #include <QCommandLineOption>
0038 #include <QTimer>
0039 
0040 #ifdef WIN32
0041 #include <process.h>
0042 #endif
0043 
0044 #include <unistd.h>
0045 
0046 #include "konqclientrequest.h"
0047 #include "kfmclient_debug.h"
0048 
0049 static const char appName[] = "kfmclient";
0050 static const char version[] = "2.0";
0051 
0052 #if QT_VERSION_MAJOR < 6
0053 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
0054 #else
0055 int main(int argc, char **argv)
0056 #endif
0057 {
0058     QApplication app(argc, argv);
0059     KLocalizedString::setApplicationDomain("kfmclient");
0060 
0061     KAboutData aboutData(appName, i18n("kfmclient"), QLatin1String(version));
0062     aboutData.setShortDescription(i18n("KDE tool for opening URLs from the command line"));
0063     KAboutData::setApplicationData(aboutData);
0064 
0065     QCommandLineParser parser;
0066     aboutData.setupCommandLine(&parser);
0067 
0068     //qCDebug(KFMCLIENT_LOG) << "kfmclient starting" << QTime::currentTime();
0069 
0070     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("noninteractive"), i18n("Non interactive use: no message boxes")));
0071 
0072     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("commands"), i18n("Show available commands")));
0073 
0074     //This option is needed to fix a bug caused by the fact that Plasma inserts kfmclient_html in the Favorites section of the K menu.
0075     //kfmclient_html calls "kfmclient_openURL %u text/html", where %u is replaced by an URL. However, when the user
0076     //activates it from the K menu, there's no URL, so the command becomes "kfmclient openURL text/html", which causes
0077     //kfmclient to attempt to open the URL text/html. Adding this option allows to change the exec line in kfmclient_html
0078     //to kfmclient --mimetype text/html openURL %u, allowing an easy fix of the bug (see doIt()).
0079     //TODO: remove the old syntax of specifying the mimetype after the URL when building for KF6
0080     parser.addOption({{QStringLiteral("mimetype"), QStringLiteral("t")},
0081         i18n("The mimetype of the URL. Allows Konqueror to determine in advance which component to use, making it start faster."),
0082         i18nc("the name for a the value of an option on the command line help", "type"), QString()});
0083 
0084     parser.addPositionalArgument(QStringLiteral("command"), i18n("Command (see --commands)"));
0085 
0086     parser.addPositionalArgument(QStringLiteral("[URL(s)]"), i18n("Arguments for command"));
0087 
0088     parser.addOption(QCommandLineOption(QStringList{"tempfile"}, i18n("The files/URLs opened by the application will be deleted after use")));
0089 
0090     parser.process(app);
0091     aboutData.processCommandLine(&parser);
0092 
0093     const QStringList args = parser.positionalArguments();
0094 
0095     if (args.isEmpty() || parser.isSet(QStringLiteral("commands"))) {
0096         QTextStream ts(stdout, QIODevice::WriteOnly);
0097         ts << i18n("\nSyntax:\n");
0098         ts << i18n("  kfmclient openURL 'url' ['mimetype']\n"
0099                   "            # Opens a window showing 'url'.\n"
0100                   "            #  'url' may be a relative path\n"
0101                   "            #   or file name, such as . or subdir/\n"
0102                   "            #   If 'url' is omitted, the start page is shown.\n\n");
0103         ts << i18n("            # If 'mimetype' is specified, it will be used to determine the\n"
0104                   "            #   component that Konqueror should use. For instance, set it to\n"
0105                   "            #   text/html for a web page, to make it appear faster\n"
0106                   "            # Note: this way of specifying mimetype is deprecated.\n"
0107                   "            #   Please use the --mimetype option\n\n");
0108         ts << i18n("  kfmclient newTab 'url' ['mimetype']\n"
0109                   "            # Same as above but opens a new tab with 'url' in an existing Konqueror\n"
0110                   "            #   window on the current active desktop if possible.\n\n");
0111         return 0;
0112     }
0113 
0114     // Use kfmclient from the session KDE version
0115     if ((args.at(0) == QLatin1String("openURL") || args.at(0) == QLatin1String("newTab"))
0116             && qEnvironmentVariableIsSet("KDE_FULL_SESSION")) {
0117         const int version = qEnvironmentVariableIntValue("KDE_SESSION_VERSION");
0118         if (version != 0 && version != KCOREADDONS_VERSION_MAJOR) {
0119             qCDebug(KFMCLIENT_LOG) << "Forwarding to kfmclient from KDE version " << version;
0120             char wrapper[ 10 ];
0121             sprintf(wrapper, "kde%d", version);
0122             char **newargv = new char *[ argc + 2 ];
0123             newargv[ 0 ] = wrapper;
0124             for (int i = 0;
0125                     i < argc;
0126                     ++i) {
0127                 newargv[ i + 1 ] = argv[ i ];
0128             }
0129             newargv[ argc + 1 ] = nullptr;
0130 #ifdef WIN32
0131             _execvp(wrapper, newargv);
0132 #else
0133             execvp(wrapper, newargv);
0134 #endif
0135             // just continue if failed
0136         }
0137     }
0138 
0139     ClientApp client;
0140     return client.doIt(parser) ? 0 /*no error*/ : 1 /*error*/;
0141 }
0142 
0143 static bool s_dbus_initialized = false;
0144 static void needDBus()
0145 {
0146     if (!s_dbus_initialized) {
0147         extern void qDBusBindToApplication();
0148         qDBusBindToApplication();
0149         if (!QDBusConnection::sessionBus().isConnected()) {
0150             qFatal("Session bus not found");
0151         }
0152         s_dbus_initialized = true;
0153     }
0154 }
0155 
0156 static QUrl filteredUrl(const QString &url)
0157 {
0158     KUriFilterData data;
0159     data.setData(url);
0160     data.setAbsolutePath(QDir::currentPath());
0161     data.setCheckForExecutables(false);
0162 
0163     return data.uri();
0164 }
0165 
0166 ClientApp::ClientApp()
0167 {
0168 }
0169 
0170 ClientApp::BrowserApplicationParsingResult ClientApp::parseBrowserApplicationString(const QString& str)
0171 {
0172     // There is a configured browser application.
0173     // See whether it is a literal command (starting with '!')
0174     // or a service (no '!').
0175     BrowserApplicationParsingResult res;
0176     if (str.isEmpty()) {
0177         return res;
0178     }
0179     res.isCommand = str.startsWith('!');
0180     if (res.isCommand) {
0181         // A literal command.  Split the string up into a shell
0182         // command and arguments.
0183         res.args = KShell::splitArgs(str.mid(1), KShell::AbortOnMeta);
0184         res.isValid = !res.args.isEmpty();
0185         if (res.isValid) {
0186             res.commandOrService = res.args.takeFirst();
0187         }
0188         else {
0189             res.error = "Parsing browser command failed";
0190         }
0191     } else {
0192         res.commandOrService = str;
0193         res.isValid = true;
0194     }
0195     // Ensure that we are not calling ourselves recursively;
0196     // that is, the external command is not "kfmclient" or
0197     // any variation of it.
0198     if (res.commandOrService.startsWith("kfmclient")) {
0199         res.isValid = false;
0200         res.error = "Recursive external browser command or service detected";
0201     }
0202     return res;
0203 }
0204 
0205 bool ClientApp::launchExternalBrowser(const ClientApp::BrowserApplicationParsingResult& parseResult, const QUrl& url, bool tempFile)
0206 {
0207     KJob *job = nullptr;
0208     if (parseResult.isCommand) {
0209         QStringList args(parseResult.args);
0210         args << url.url();
0211         KStartupInfo::appStarted();
0212         job =  new KIO::CommandLauncherJob(parseResult.commandOrService, args);
0213     } else {
0214         KService::Ptr service = KService::serviceByStorageId(parseResult.commandOrService);
0215         if (!service) {
0216             qCWarning(KFMCLIENT_LOG) << "External browser service not known:" << parseResult.commandOrService;
0217             return false;
0218         }
0219         auto launcherJob = new KIO::ApplicationLauncherJob(service);
0220         launcherJob->setUrls({url});
0221         if (tempFile) {
0222             launcherJob->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0223         }
0224         job = launcherJob;
0225     }
0226     QObject::connect(job, &KJob::result, this, &ClientApp::slotResult);
0227     job->setUiDelegate(nullptr);
0228     job->start();
0229     return qApp->exec() == 0;
0230 }
0231 
0232 bool ClientApp::createNewWindow(const QUrl &url, bool newTab, bool tempFile, const QString &mimetype)
0233 {
0234     qCDebug(KFMCLIENT_LOG) << url << "mimetype=" << mimetype;
0235 
0236     bool launched = false;
0237 
0238     if (url.scheme().startsWith(QLatin1String("http"))) {
0239         KConfig config(QStringLiteral("kfmclientrc"));
0240         KConfigGroup generalGroup(&config, "General");
0241         const QString browserApp = generalGroup.readEntry("BrowserApplication");
0242         if (!browserApp.isEmpty()) {
0243             //Parse the BrowserApplication string and act accordingly
0244             BrowserApplicationParsingResult parseRes = parseBrowserApplicationString(browserApp);
0245             qCDebug(KFMCLIENT_LOG) << "Using external browser" << (parseRes.isCommand ? "command" : "service") << browserApp;
0246             if (parseRes.isValid) {
0247                 launched = launchExternalBrowser(parseRes, url, tempFile);
0248             } else {
0249                 qCWarning(KFMCLIENT_LOG) << parseRes.error;
0250             }
0251         }
0252     }
0253 
0254     if (!launched) {
0255         needDBus();
0256         // Launch Konqueror, or reuse an existing instance if possible.
0257         KonqClientRequest req;
0258         req.setUrl(url);
0259         req.setNewTab(newTab);
0260         req.setTempFile(tempFile);
0261         req.setMimeType(mimetype);
0262         launched = req.openUrl();
0263     }
0264 
0265     return launched;
0266 }
0267 
0268 bool ClientApp::openProfile(const QString &profileName, const QUrl &url, const QString &mimetype)
0269 {
0270     Q_UNUSED(profileName); // the concept disappeared
0271     return createNewWindow(url, false, false, mimetype);
0272 }
0273 
0274 void ClientApp::delayedQuit()
0275 {
0276     // Quit in 2 seconds. This leaves time for OpenUrlJob to pop up
0277     // "app not found" in KProcessRunner, if that was the case.
0278     QTimer::singleShot(2000, qApp, &QApplication::quit);
0279 }
0280 
0281 static void checkArgumentCount(int count, int min, int max)
0282 {
0283     if (count < min) {
0284         fprintf(stderr, "%s: %s",  appName, i18n("Syntax error, not enough arguments\n").toLocal8Bit().data());
0285         ::exit(1);
0286     }
0287     if (max && (count > max)) {
0288         fprintf(stderr, "%s: %s", appName, i18n("Syntax error, too many arguments\n").toLocal8Bit().data());
0289         ::exit(1);
0290     }
0291 }
0292 
0293 bool ClientApp::doIt(const QCommandLineParser &parser)
0294 {
0295     const QStringList args = parser.positionalArguments();
0296     int argc = args.count();
0297     checkArgumentCount(argc, 1, 0);
0298 
0299     if (!parser.isSet(QStringLiteral("noninteractive"))) {
0300         m_interactive = false;
0301     }
0302     QString command = args.at(0);
0303 
0304     if (command == QLatin1String("openURL") || command == QLatin1String("newTab")) {
0305         checkArgumentCount(argc, 1, 3);
0306         const bool tempFile = parser.isSet(QStringLiteral("tempfile"));
0307 
0308         QUrl url = argc > 1 ? filteredUrl(args.at(1)) : QUrl();
0309 
0310         //If the given URL is empty, show the start page
0311         if (url.isEmpty()) {
0312             KConfigGroup grp = KSharedConfig::openConfig(QStringLiteral("konquerorrc"))->group("UserSettings");
0313             url = QUrl(grp.readEntry("StartURL", QStringLiteral("konq:konqueror")));
0314         }
0315 
0316         QString mimetype = argc == 3 ? args.at(2) : QString();
0317         if (mimetype.isEmpty()) {
0318             mimetype = parser.value(QStringLiteral("mimetype"));
0319         }
0320 
0321         return createNewWindow(url, command == QLatin1String("newTab"), tempFile, mimetype);
0322     } else if (command == QLatin1String("openProfile")) { // deprecated command, kept for compat
0323         checkArgumentCount(argc, 2, 3);
0324         QUrl url;
0325         if (argc == 3) {
0326             url = QUrl::fromUserInput(args.at(2), QDir::currentPath());
0327         }
0328         return openProfile(args.at(1), url);
0329     } else if (command == QLatin1String("exec") && argc >= 2) {
0330         // compatibility with KDE 3 and xdg-open
0331         QStringList kioclientArgs;
0332         if (!m_interactive) {
0333             kioclientArgs << QStringLiteral("--noninteractive");
0334         }
0335         kioclientArgs << QStringLiteral("exec") << args.at(1);
0336         if (argc == 3) {
0337             kioclientArgs << args.at(2);
0338         }
0339 
0340         int ret = KProcess::execute(QStringLiteral("kioclient5"), kioclientArgs);
0341         return ret == 0;
0342     } else {
0343         fprintf(stderr, "%s: %s", appName, i18n("Syntax error, unknown command '%1'\n", command).toLocal8Bit().data());
0344         return false;
0345     }
0346     return true;
0347 }
0348 
0349 
0350 void ClientApp::slotResult(KJob *job)
0351 {
0352     if (job->error()) {
0353         qApp->exit(1);
0354     } else {
0355         delayedQuit();
0356     }
0357 }