File indexing completed on 2024-04-28 11:41:39

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
0004     SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "main.h"
0011 #include "kio_version.h"
0012 #include "kioexecdinterface.h"
0013 
0014 #include <QDir>
0015 #include <QFile>
0016 
0017 #include <KAboutData>
0018 #include <KDBusService>
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 #include <KService>
0022 #include <QApplication>
0023 #include <QDebug>
0024 #include <copyjob.h>
0025 #include <desktopexecparser.h>
0026 #include <job.h>
0027 
0028 #include <QCommandLineOption>
0029 #include <QCommandLineParser>
0030 #include <QFileInfo>
0031 #include <QStandardPaths>
0032 #include <QThread>
0033 
0034 #include <KStartupInfo>
0035 #include <config-kioexec.h>
0036 
0037 #if HAVE_X11
0038 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0039 #include <private/qtx11extras_p.h>
0040 #else
0041 #include <QX11Info>
0042 #endif
0043 #endif
0044 
0045 KIOExec::KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName)
0046     : mExited(false)
0047     , mTempFiles(tempFiles)
0048     , mUseDaemon(false)
0049     , mSuggestedFileName(suggestedFileName)
0050     , expectedCounter(0)
0051     , command(args.first())
0052     , jobCounter(0)
0053 {
0054     qDebug() << "command=" << command << "args=" << args;
0055 
0056     for (int i = 1; i < args.count(); i++) {
0057         const QUrl urlArg = QUrl::fromUserInput(args.value(i));
0058         if (!urlArg.isValid()) {
0059             KMessageBox::error(nullptr, i18n("Invalid URL: %1", args.value(i)));
0060             exit(1);
0061         }
0062         KIO::StatJob *mostlocal = KIO::mostLocalUrl(urlArg);
0063         bool b = mostlocal->exec();
0064         if (!b) {
0065             KMessageBox::error(nullptr, i18n("File not found: %1", urlArg.toDisplayString()));
0066             exit(1);
0067         }
0068         Q_ASSERT(b);
0069         const QUrl url = mostlocal->mostLocalUrl();
0070 
0071         // kDebug() << "url=" << url.url() << " filename=" << url.fileName();
0072         // A local file, not an URL ?
0073         // => It is not encoded and not shell escaped, too.
0074         if (url.isLocalFile()) {
0075             FileInfo file;
0076             file.path = url.toLocalFile();
0077             file.url = url;
0078             fileList.append(file);
0079         } else {
0080             // It is an URL
0081             if (!url.isValid()) {
0082                 KMessageBox::error(nullptr, i18n("The URL %1\nis malformed", url.url()));
0083             } else if (mTempFiles) {
0084                 KMessageBox::error(nullptr, i18n("Remote URL %1\nnot allowed with --tempfiles switch", url.toDisplayString()));
0085             } else {
0086                 // We must fetch the file
0087                 QString fileName = KIO::encodeFileName(url.fileName());
0088                 if (!suggestedFileName.isEmpty()) {
0089                     fileName = suggestedFileName;
0090                 }
0091                 if (fileName.isEmpty()) {
0092                     fileName = QStringLiteral("unnamed");
0093                 }
0094                 // Build the destination filename, in ~/.cache/kioexec/krun/
0095                 // Unlike KDE-1.1, we put the filename at the end so that the extension is kept
0096                 // (Some programs rely on it)
0097                 QString krun_writable = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
0098                     + QStringLiteral("/krun/%1_%2/").arg(QCoreApplication::applicationPid()).arg(jobCounter++);
0099                 QDir().mkpath(krun_writable); // error handling will be done by the job
0100                 QString tmp = krun_writable + fileName;
0101                 FileInfo file;
0102                 file.path = tmp;
0103                 file.url = url;
0104                 fileList.append(file);
0105 
0106                 expectedCounter++;
0107                 const QUrl dest = QUrl::fromLocalFile(tmp);
0108                 qDebug() << "Copying" << url << " to" << dest;
0109                 KIO::Job *job = KIO::file_copy(url, dest);
0110                 jobList.append(job);
0111 
0112                 connect(job, &KJob::result, this, &KIOExec::slotResult);
0113             }
0114         }
0115     }
0116 
0117     if (mTempFiles) {
0118         // delay call so QApplication::exit passes the exit code to exec()
0119         QTimer::singleShot(0, this, &KIOExec::slotRunApp);
0120         return;
0121     }
0122 
0123     counter = 0;
0124     if (counter == expectedCounter) {
0125         slotResult(nullptr);
0126     }
0127 }
0128 
0129 void KIOExec::slotResult(KJob *job)
0130 {
0131     if (job) {
0132         KIO::FileCopyJob *copyJob = static_cast<KIO::FileCopyJob *>(job);
0133         const QString path = copyJob->destUrl().path();
0134 
0135         if (job->error()) {
0136             // That error dialog would be queued, i.e. not immediate...
0137             // job->showErrorDialog();
0138             if (job->error() != KIO::ERR_USER_CANCELED) {
0139                 KMessageBox::error(nullptr, job->errorString());
0140             }
0141 
0142             auto it = std::find_if(fileList.begin(), fileList.end(), [&path](const FileInfo &i) {
0143                 return i.path == path;
0144             });
0145             if (it != fileList.end()) {
0146                 fileList.erase(it);
0147             } else {
0148                 qDebug() << path << " not found in list";
0149             }
0150         } else {
0151             // Tell kioexecd to watch the file for changes.
0152             const QString dest = copyJob->srcUrl().toString();
0153             qDebug() << "Telling kioexecd to watch path" << path << "dest" << dest;
0154             OrgKdeKIOExecdInterface kioexecd(QStringLiteral("org.kde.kioexecd"), QStringLiteral("/modules/kioexecd"), QDBusConnection::sessionBus());
0155             kioexecd.watch(path, dest);
0156             mUseDaemon = !kioexecd.lastError().isValid();
0157             if (!mUseDaemon) {
0158                 qDebug() << "Not using kioexecd";
0159             }
0160         }
0161     }
0162 
0163     counter++;
0164 
0165     if (counter < expectedCounter) {
0166         return;
0167     }
0168 
0169     qDebug() << "All files downloaded, will call slotRunApp shortly";
0170     // We know we can run the app now - but let's finish the job properly first.
0171     QTimer::singleShot(0, this, &KIOExec::slotRunApp);
0172 
0173     jobList.clear();
0174 }
0175 
0176 void KIOExec::slotRunApp()
0177 {
0178     if (fileList.isEmpty()) {
0179         qDebug() << "No files downloaded -> exiting";
0180         mExited = true;
0181         QApplication::exit(1);
0182         return;
0183     }
0184 
0185     KService service(QStringLiteral("dummy"), command, QString());
0186 
0187     QList<QUrl> list;
0188     list.reserve(fileList.size());
0189     // Store modification times
0190     QList<FileInfo>::Iterator it = fileList.begin();
0191     for (; it != fileList.end(); ++it) {
0192         QFileInfo info(it->path);
0193         it->time = info.lastModified();
0194         QUrl url = QUrl::fromLocalFile(it->path);
0195         list << url;
0196     }
0197 
0198     KIO::DesktopExecParser execParser(service, list);
0199     QStringList params = execParser.resultingArguments();
0200     if (params.isEmpty()) {
0201         qWarning() << execParser.errorMessage();
0202         QApplication::exit(-1);
0203         return;
0204     }
0205 
0206     qDebug() << "EXEC" << params.join(QLatin1Char(' '));
0207 
0208     // propagate the startup identification to the started process
0209     KStartupInfoId id;
0210     QByteArray startupId;
0211 #if HAVE_X11
0212     if (QX11Info::isPlatformX11()) {
0213         startupId = QX11Info::nextStartupId();
0214     }
0215 #endif
0216     id.initId(startupId);
0217     id.setupStartupEnv();
0218 
0219     QString exe(params.takeFirst());
0220     const int exit_code = QProcess::execute(exe, params);
0221 
0222     KStartupInfo::resetStartupEnv();
0223 
0224     qDebug() << "EXEC done";
0225 
0226     // Test whether one of the files changed
0227     for (it = fileList.begin(); it != fileList.end(); ++it) {
0228         QString src = it->path;
0229         const QUrl dest = it->url;
0230         QFileInfo info(src);
0231         const bool uploadChanges = !mUseDaemon && !dest.isLocalFile();
0232         if (info.exists() && (it->time != info.lastModified())) {
0233             if (mTempFiles) {
0234                 const auto result = KMessageBox::questionTwoActions(
0235                     nullptr,
0236                     i18n("The supposedly temporary file\n%1\nhas been modified.\nDo you still want to delete it?", dest.toDisplayString(QUrl::PreferLocalFile)),
0237                     i18n("File Changed"),
0238                     KStandardGuiItem::del(),
0239                     KGuiItem(i18n("Do Not Delete")));
0240                 if (result != KMessageBox::PrimaryAction) {
0241                     continue; // don't delete the temp file
0242                 }
0243             } else if (uploadChanges) { // no upload when it's already a local file or kioexecd already did it.
0244                 const auto result =
0245                     KMessageBox::questionTwoActions(nullptr,
0246                                                     i18n("The file\n%1\nhas been modified.\nDo you want to upload the changes?", dest.toDisplayString()),
0247                                                     i18n("File Changed"),
0248                                                     KGuiItem(i18n("Upload")),
0249                                                     KGuiItem(i18n("Do Not Upload")));
0250                 if (result == KMessageBox::PrimaryAction) {
0251                     qDebug() << "src='" << src << "'  dest='" << dest << "'";
0252                     // Do it the synchronous way.
0253                     KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(src), dest);
0254                     if (!job->exec()) {
0255                         KMessageBox::error(nullptr, job->errorText());
0256                         continue; // don't delete the temp file
0257                     }
0258                 }
0259             }
0260         }
0261 
0262         if ((uploadChanges || mTempFiles) && exit_code == 0) {
0263             // Wait for a reasonable time so that even if the application forks on startup (like OOo or amarok)
0264             // it will have time to start up and read the file before it gets deleted. #130709.
0265             const int sleepSecs = 180;
0266             qDebug() << "sleeping for" << sleepSecs << "seconds before deleting file...";
0267             QThread::sleep(sleepSecs);
0268             const QString parentDir = info.path();
0269             qDebug() << sleepSecs << "seconds have passed, deleting" << info.filePath();
0270             QFile(src).remove();
0271             // NOTE: this is not necessarily a temporary directory.
0272             if (QDir().rmdir(parentDir)) {
0273                 qDebug() << "Removed empty parent directory" << parentDir;
0274             }
0275         }
0276     }
0277 
0278     mExited = true;
0279     QApplication::exit(exit_code);
0280 }
0281 
0282 int main(int argc, char **argv)
0283 {
0284     QApplication app(argc, argv);
0285     KAboutData aboutData(QStringLiteral("kioexec"),
0286                          i18n("KIOExec"),
0287                          QStringLiteral(KIO_VERSION_STRING),
0288                          i18n("KIO Exec - Opens remote files, watches modifications, asks for upload"),
0289                          KAboutLicense::GPL,
0290                          i18n("(c) 1998-2000,2003 The KFM/Konqueror Developers"));
0291     aboutData.addAuthor(i18n("David Faure"), QString(), QStringLiteral("faure@kde.org"));
0292     aboutData.addAuthor(i18n("Stephan Kulow"), QString(), QStringLiteral("coolo@kde.org"));
0293     aboutData.addAuthor(i18n("Bernhard Rosenkraenzer"), QString(), QStringLiteral("bero@arklinux.org"));
0294     aboutData.addAuthor(i18n("Waldo Bastian"), QString(), QStringLiteral("bastian@kde.org"));
0295     aboutData.addAuthor(i18n("Oswald Buddenhagen"), QString(), QStringLiteral("ossi@kde.org"));
0296     KAboutData::setApplicationData(aboutData);
0297     KDBusService service(KDBusService::Multiple);
0298 
0299     QCommandLineParser parser;
0300     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("tempfiles")}, i18n("Treat URLs as local files and delete them afterwards")));
0301     parser.addOption(
0302         QCommandLineOption(QStringList{QStringLiteral("suggestedfilename")}, i18n("Suggested file name for the downloaded file"), QStringLiteral("filename")));
0303     parser.addPositionalArgument(QStringLiteral("command"), i18n("Command to execute"));
0304     parser.addPositionalArgument(QStringLiteral("urls"), i18n("URL(s) or local file(s) used for 'command'"));
0305 
0306     app.setQuitOnLastWindowClosed(false);
0307 
0308     aboutData.setupCommandLine(&parser);
0309     parser.process(app);
0310     aboutData.processCommandLine(&parser);
0311 
0312     if (parser.positionalArguments().count() < 1) {
0313         parser.showHelp(-1);
0314         return -1;
0315     }
0316 
0317     const bool tempfiles = parser.isSet(QStringLiteral("tempfiles"));
0318     const QString suggestedfilename = parser.value(QStringLiteral("suggestedfilename"));
0319     KIOExec exec(parser.positionalArguments(), tempfiles, suggestedfilename);
0320 
0321     // Don't go into the event loop if we already want to exit (#172197)
0322     if (exec.exited()) {
0323         return 0;
0324     }
0325 
0326     return app.exec();
0327 }
0328 
0329 #include "moc_main.cpp"