| 1 | /* |
| 2 | This file is part of the KDE project |
| 3 | SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org> |
| 4 | SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org> |
| 5 | SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org> |
| 6 | |
| 7 | SPDX-License-Identifier: GPL-2.0-or-later |
| 8 | */ |
| 9 | |
| 10 | #include "main.h" |
| 11 | #include "filecopyjob.h" |
| 12 | #include "kio_version.h" |
| 13 | #include "kioexecdebug.h" |
| 14 | #include "kioexecdinterface.h" |
| 15 | #include "statjob.h" |
| 16 | |
| 17 | #include <QDir> |
| 18 | #include <QFile> |
| 19 | |
| 20 | #include <KAboutData> |
| 21 | #include <KDBusService> |
| 22 | #include <KLocalizedString> |
| 23 | #include <KMessageBox> |
| 24 | #include <KService> |
| 25 | #include <QApplication> |
| 26 | #include <QDebug> |
| 27 | #include <copyjob.h> |
| 28 | #include <desktopexecparser.h> |
| 29 | #include <job.h> |
| 30 | |
| 31 | #include <QCommandLineOption> |
| 32 | #include <QCommandLineParser> |
| 33 | #include <QFileInfo> |
| 34 | #include <QStandardPaths> |
| 35 | #include <QThread> |
| 36 | |
| 37 | #include <config-kioexec.h> |
| 38 | |
| 39 | #if HAVE_X11 |
| 40 | #include <KStartupInfo> |
| 41 | #include <private/qtx11extras_p.h> |
| 42 | #endif |
| 43 | |
| 44 | KIOExec::KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName) |
| 45 | : mExited(false) |
| 46 | , mTempFiles(tempFiles) |
| 47 | , mUseDaemon(false) |
| 48 | , mSuggestedFileName(suggestedFileName) |
| 49 | , expectedCounter(0) |
| 50 | , command(args.first()) |
| 51 | , jobCounter(0) |
| 52 | { |
| 53 | qCDebug(KIOEXEC) << "command=" << command << "args=" << args; |
| 54 | |
| 55 | for (int i = 1; i < args.count(); i++) { |
| 56 | const QUrl urlArg = QUrl::fromUserInput(userInput: args.value(i)); |
| 57 | if (!urlArg.isValid()) { |
| 58 | KMessageBox::error(parent: nullptr, i18n("Invalid URL: %1" , args.value(i))); |
| 59 | exit(status: 1); |
| 60 | } |
| 61 | KIO::StatJob *mostlocal = KIO::mostLocalUrl(url: urlArg); |
| 62 | bool b = mostlocal->exec(); |
| 63 | if (!b) { |
| 64 | KMessageBox::error(parent: nullptr, i18n("File not found: %1" , urlArg.toDisplayString())); |
| 65 | exit(status: 1); |
| 66 | } |
| 67 | Q_ASSERT(b); |
| 68 | const QUrl url = mostlocal->mostLocalUrl(); |
| 69 | |
| 70 | // kDebug() << "url=" << url.url() << " filename=" << url.fileName(); |
| 71 | // A local file, not an URL ? |
| 72 | // => It is not encoded and not shell escaped, too. |
| 73 | if (url.isLocalFile()) { |
| 74 | FileInfo file; |
| 75 | file.path = url.toLocalFile(); |
| 76 | file.url = url; |
| 77 | fileList.append(t: file); |
| 78 | } else { |
| 79 | // It is an URL |
| 80 | if (!url.isValid()) { |
| 81 | KMessageBox::error(parent: nullptr, i18n("The URL %1\nis malformed" , url.url())); |
| 82 | } else if (mTempFiles) { |
| 83 | KMessageBox::error(parent: nullptr, i18n("Remote URL %1\nnot allowed with --tempfiles switch" , url.toDisplayString())); |
| 84 | } else { |
| 85 | // We must fetch the file |
| 86 | QString fileName = KIO::encodeFileName(str: url.fileName()); |
| 87 | if (!suggestedFileName.isEmpty()) { |
| 88 | fileName = suggestedFileName; |
| 89 | } |
| 90 | if (fileName.isEmpty()) { |
| 91 | fileName = QStringLiteral("unnamed" ); |
| 92 | } |
| 93 | // Build the destination filename, in ~/.cache/kioexec/krun/ |
| 94 | // Unlike KDE-1.1, we put the filename at the end so that the extension is kept |
| 95 | // (Some programs rely on it) |
| 96 | QString krun_writable = QStandardPaths::writableLocation(type: QStandardPaths::CacheLocation) |
| 97 | + QStringLiteral("/krun/%1_%2/" ).arg(a: QCoreApplication::applicationPid()).arg(a: jobCounter++); |
| 98 | QDir().mkpath(dirPath: krun_writable); // error handling will be done by the job |
| 99 | QString tmp = krun_writable + fileName; |
| 100 | FileInfo file; |
| 101 | file.path = tmp; |
| 102 | file.url = url; |
| 103 | fileList.append(t: file); |
| 104 | |
| 105 | expectedCounter++; |
| 106 | const QUrl dest = QUrl::fromLocalFile(localfile: tmp); |
| 107 | qCDebug(KIOEXEC) << "Copying" << url << "to" << dest; |
| 108 | KIO::Job *job = KIO::file_copy(src: url, dest); |
| 109 | jobList.append(t: job); |
| 110 | |
| 111 | connect(sender: job, signal: &KJob::result, context: this, slot: &KIOExec::slotResult); |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | if (mTempFiles) { |
| 117 | // delay call so QApplication::exit passes the exit code to exec() |
| 118 | QTimer::singleShot(interval: 0, receiver: this, slot: &KIOExec::slotRunApp); |
| 119 | return; |
| 120 | } |
| 121 | |
| 122 | counter = 0; |
| 123 | if (counter == expectedCounter) { |
| 124 | slotResult(nullptr); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | void KIOExec::slotResult(KJob *job) |
| 129 | { |
| 130 | if (job) { |
| 131 | KIO::FileCopyJob *copyJob = static_cast<KIO::FileCopyJob *>(job); |
| 132 | const QString path = copyJob->destUrl().path(); |
| 133 | |
| 134 | if (job->error()) { |
| 135 | // That error dialog would be queued, i.e. not immediate... |
| 136 | // job->showErrorDialog(); |
| 137 | if (job->error() != KIO::ERR_USER_CANCELED) { |
| 138 | KMessageBox::error(parent: nullptr, text: job->errorString()); |
| 139 | } |
| 140 | |
| 141 | auto it = std::find_if(first: fileList.begin(), last: fileList.end(), pred: [&path](const FileInfo &i) { |
| 142 | return i.path == path; |
| 143 | }); |
| 144 | if (it != fileList.end()) { |
| 145 | fileList.erase(pos: it); |
| 146 | } else { |
| 147 | qCDebug(KIOEXEC) << path << "not found in list" ; |
| 148 | } |
| 149 | } else { |
| 150 | // Tell kioexecd to watch the file for changes. |
| 151 | const QString dest = copyJob->srcUrl().toString(); |
| 152 | qCDebug(KIOEXEC) << "Telling kioexecd to watch path" << path << "dest" << dest; |
| 153 | OrgKdeKIOExecdInterface kioexecd(QStringLiteral("org.kde.kioexecd6" ), QStringLiteral("/modules/kioexecd" ), QDBusConnection::sessionBus()); |
| 154 | kioexecd.watch(path, destUrl: dest); |
| 155 | mUseDaemon = !kioexecd.lastError().isValid(); |
| 156 | if (!mUseDaemon) { |
| 157 | qCDebug(KIOEXEC) << "Not using kioexecd" ; |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | counter++; |
| 163 | |
| 164 | if (counter < expectedCounter) { |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | qCDebug(KIOEXEC) << "All files downloaded, will call slotRunApp shortly" ; |
| 169 | // We know we can run the app now - but let's finish the job properly first. |
| 170 | QTimer::singleShot(interval: 0, receiver: this, slot: &KIOExec::slotRunApp); |
| 171 | |
| 172 | jobList.clear(); |
| 173 | } |
| 174 | |
| 175 | void KIOExec::slotRunApp() |
| 176 | { |
| 177 | if (fileList.isEmpty()) { |
| 178 | qCDebug(KIOEXEC) << "No files downloaded -> exiting" ; |
| 179 | mExited = true; |
| 180 | QApplication::exit(retcode: 1); |
| 181 | return; |
| 182 | } |
| 183 | |
| 184 | KService service(QStringLiteral("dummy" ), command, QString()); |
| 185 | |
| 186 | QList<QUrl> list; |
| 187 | list.reserve(asize: fileList.size()); |
| 188 | // Store modification times |
| 189 | QList<FileInfo>::Iterator it = fileList.begin(); |
| 190 | for (; it != fileList.end(); ++it) { |
| 191 | QFileInfo info(it->path); |
| 192 | it->time = info.lastModified(); |
| 193 | QUrl url = QUrl::fromLocalFile(localfile: it->path); |
| 194 | list << url; |
| 195 | } |
| 196 | |
| 197 | KIO::DesktopExecParser execParser(service, list); |
| 198 | QStringList params = execParser.resultingArguments(); |
| 199 | if (params.isEmpty()) { |
| 200 | qWarning() << execParser.errorMessage(); |
| 201 | QApplication::exit(retcode: -1); |
| 202 | return; |
| 203 | } |
| 204 | |
| 205 | qCDebug(KIOEXEC) << "EXEC" << params.join(sep: QLatin1Char(' ')); |
| 206 | |
| 207 | #if HAVE_X11 |
| 208 | // propagate the startup identification to the started process |
| 209 | KStartupInfoId id; |
| 210 | QByteArray startupId; |
| 211 | if (QX11Info::isPlatformX11()) { |
| 212 | startupId = QX11Info::nextStartupId(); |
| 213 | } |
| 214 | id.initId(id: startupId); |
| 215 | id.setupStartupEnv(); |
| 216 | #endif |
| 217 | |
| 218 | QString exe(params.takeFirst()); |
| 219 | const int exit_code = QProcess::execute(program: exe, arguments: params); |
| 220 | |
| 221 | #if HAVE_X11 |
| 222 | KStartupInfo::resetStartupEnv(); |
| 223 | #endif |
| 224 | |
| 225 | qCDebug(KIOEXEC) << "EXEC done" ; |
| 226 | |
| 227 | QStringList tempFilesToRemove; |
| 228 | |
| 229 | // Test whether one of the files changed |
| 230 | for (it = fileList.begin(); it != fileList.end(); ++it) { |
| 231 | QString src = it->path; |
| 232 | const QUrl dest = it->url; |
| 233 | QFileInfo info(src); |
| 234 | const bool uploadChanges = !mUseDaemon && !dest.isLocalFile(); |
| 235 | if (info.exists() && (it->time != info.lastModified())) { |
| 236 | if (mTempFiles) { |
| 237 | const auto result = KMessageBox::questionTwoActions( |
| 238 | parent: nullptr, |
| 239 | i18n("The supposedly temporary file\n%1\nhas been modified.\nDo you still want to delete it?" , dest.toDisplayString(QUrl::PreferLocalFile)), |
| 240 | i18n("File Changed" ), |
| 241 | primaryAction: KStandardGuiItem::del(), |
| 242 | secondaryAction: KGuiItem(i18n("Do Not Delete" ))); |
| 243 | if (result != KMessageBox::PrimaryAction) { |
| 244 | continue; // don't delete the temp file |
| 245 | } |
| 246 | } else if (uploadChanges) { // no upload when it's already a local file or kioexecd already did it. |
| 247 | const auto result = |
| 248 | KMessageBox::questionTwoActions(parent: nullptr, |
| 249 | i18n("The file\n%1\nhas been modified.\nDo you want to upload the changes?" , dest.toDisplayString()), |
| 250 | i18n("File Changed" ), |
| 251 | primaryAction: KGuiItem(i18n("Upload" )), |
| 252 | secondaryAction: KGuiItem(i18n("Do Not Upload" ))); |
| 253 | if (result == KMessageBox::PrimaryAction) { |
| 254 | qCDebug(KIOEXEC) << "src=" << src << "dest=" << dest; |
| 255 | // Do it the synchronous way. |
| 256 | KIO::CopyJob *job = KIO::copy(src: QUrl::fromLocalFile(localfile: src), dest); |
| 257 | if (!job->exec()) { |
| 258 | KMessageBox::error(parent: nullptr, text: job->errorText()); |
| 259 | continue; // don't delete the temp file |
| 260 | } |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | if ((uploadChanges || mTempFiles) && exit_code == 0) { |
| 266 | // Note that a temp file needs to be removed later |
| 267 | tempFilesToRemove.append(t: src); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | if (!tempFilesToRemove.isEmpty()) { |
| 272 | // Wait for a reasonable time so that even if the application forks |
| 273 | // on startup (like OOo or amarok) it will have time to start up and |
| 274 | // read the file before it gets deleted. #130709. |
| 275 | const int sleepSecs = 180; |
| 276 | qCDebug(KIOEXEC) << "sleeping for" << sleepSecs << "seconds before deleting" << tempFilesToRemove.count() << "temp files..." ; |
| 277 | QThread::sleep(sleepSecs); |
| 278 | qCDebug(KIOEXEC) << sleepSecs << "seconds have passed, deleting temp files" ; |
| 279 | |
| 280 | for (const QString &src : std::as_const(t&: tempFilesToRemove)) { |
| 281 | QFileInfo info(src); |
| 282 | const QString parentDir = info.path(); |
| 283 | qCDebug(KIOEXEC) << "deleting" << info.filePath(); |
| 284 | QFile(src).remove(); |
| 285 | // NOTE: this is not necessarily a temporary directory. |
| 286 | if (QDir().rmdir(dirName: parentDir)) { |
| 287 | qCDebug(KIOEXEC) << "Removed empty parent directory" << parentDir; |
| 288 | } |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | mExited = true; |
| 293 | QApplication::exit(retcode: exit_code); |
| 294 | } |
| 295 | |
| 296 | int main(int argc, char **argv) |
| 297 | { |
| 298 | QApplication app(argc, argv); |
| 299 | KAboutData aboutData(QStringLiteral("kioexec" ), |
| 300 | i18n("KIOExec" ), |
| 301 | QStringLiteral(KIO_VERSION_STRING), |
| 302 | i18n("KIO Exec - Opens remote files, watches modifications, asks for upload" ), |
| 303 | KAboutLicense::GPL, |
| 304 | i18n("(c) 1998-2000,2003 The KFM/Konqueror Developers" )); |
| 305 | aboutData.addAuthor(i18n("David Faure" ), task: QString(), QStringLiteral("faure@kde.org" )); |
| 306 | aboutData.addAuthor(i18n("Stephan Kulow" ), task: QString(), QStringLiteral("coolo@kde.org" )); |
| 307 | aboutData.addAuthor(i18n("Bernhard Rosenkraenzer" ), task: QString(), QStringLiteral("bero@arklinux.org" )); |
| 308 | aboutData.addAuthor(i18n("Waldo Bastian" ), task: QString(), QStringLiteral("bastian@kde.org" )); |
| 309 | aboutData.addAuthor(i18n("Oswald Buddenhagen" ), task: QString(), QStringLiteral("ossi@kde.org" )); |
| 310 | KAboutData::setApplicationData(aboutData); |
| 311 | KDBusService service(KDBusService::Multiple); |
| 312 | |
| 313 | QCommandLineParser parser; |
| 314 | parser.addOption(commandLineOption: QCommandLineOption(QStringList{QStringLiteral("tempfiles" )}, i18n("Treat URLs as local files and delete them afterwards" ))); |
| 315 | parser.addOption( |
| 316 | commandLineOption: QCommandLineOption(QStringList{QStringLiteral("suggestedfilename" )}, i18n("Suggested file name for the downloaded file" ), QStringLiteral("filename" ))); |
| 317 | parser.addPositionalArgument(QStringLiteral("command" ), i18n("Command to execute" )); |
| 318 | parser.addPositionalArgument(QStringLiteral("urls" ), i18n("URL(s) or local file(s) used for 'command'" )); |
| 319 | |
| 320 | app.setQuitOnLastWindowClosed(false); |
| 321 | |
| 322 | aboutData.setupCommandLine(&parser); |
| 323 | parser.process(app); |
| 324 | aboutData.processCommandLine(parser: &parser); |
| 325 | |
| 326 | if (parser.positionalArguments().count() < 1) { |
| 327 | parser.showHelp(exitCode: -1); |
| 328 | return -1; |
| 329 | } |
| 330 | |
| 331 | const bool tempfiles = parser.isSet(QStringLiteral("tempfiles" )); |
| 332 | const QString suggestedfilename = parser.value(QStringLiteral("suggestedfilename" )); |
| 333 | KIOExec exec(parser.positionalArguments(), tempfiles, suggestedfilename); |
| 334 | |
| 335 | // Don't go into the event loop if we already want to exit (#172197) |
| 336 | if (exec.exited()) { |
| 337 | return 0; |
| 338 | } |
| 339 | |
| 340 | return app.exec(); |
| 341 | } |
| 342 | |
| 343 | #include "moc_main.cpp" |
| 344 | |