1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org> |
4 | SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
7 | */ |
8 | |
9 | #include "openurljob.h" |
10 | #include "commandlauncherjob.h" |
11 | #include "desktopexecparser.h" |
12 | #include "global.h" |
13 | #include "job.h" // for buildErrorString |
14 | #include "jobuidelegatefactory.h" |
15 | #include "kiogui_debug.h" |
16 | #include "openorexecutefileinterface.h" |
17 | #include "openwithhandlerinterface.h" |
18 | #include "untrustedprogramhandlerinterface.h" |
19 | |
20 | #include <KApplicationTrader> |
21 | #include <KAuthorized> |
22 | #include <KConfigGroup> |
23 | #include <KDesktopFile> |
24 | #include <KLocalizedString> |
25 | #include <KSandbox> |
26 | #include <KUrlAuthorized> |
27 | #include <QFileInfo> |
28 | |
29 | #include <KProtocolManager> |
30 | #include <KSharedConfig> |
31 | #include <QDesktopServices> |
32 | #include <QHostInfo> |
33 | #include <QMimeDatabase> |
34 | #include <QOperatingSystemVersion> |
35 | #include <mimetypefinderjob.h> |
36 | |
37 | // For unit test purposes, to test both code paths in externalBrowser() |
38 | KIOGUI_EXPORT bool openurljob_force_use_browserapp_kdeglobals = false; |
39 | |
40 | class KIO::OpenUrlJobPrivate |
41 | { |
42 | public: |
43 | explicit OpenUrlJobPrivate(const QUrl &url, OpenUrlJob *qq) |
44 | : m_url(url) |
45 | , q(qq) |
46 | { |
47 | q->setCapabilities(KJob::Killable); |
48 | } |
49 | |
50 | void emitAccessDenied(); |
51 | void runUrlWithMimeType(); |
52 | QString externalBrowser() const; |
53 | bool runExternalBrowser(const QString &exe); |
54 | void useSchemeHandler(); |
55 | |
56 | QUrl m_url; |
57 | KIO::OpenUrlJob *const q; |
58 | QString m_suggestedFileName; |
59 | QByteArray m_startupId; |
60 | QString m_mimeTypeName; |
61 | KService::Ptr m_preferredService; |
62 | bool m_deleteTemporaryFile = false; |
63 | bool m_runExecutables = false; |
64 | bool m_showOpenOrExecuteDialog = false; |
65 | bool m_externalBrowserEnabled = true; |
66 | bool m_followRedirections = true; |
67 | |
68 | private: |
69 | void executeCommand(); |
70 | void handleBinaries(const QMimeType &mimeType); |
71 | void handleBinariesHelper(const QString &localPath, bool isNativeBinary); |
72 | void handleDesktopFiles(); |
73 | void handleScripts(); |
74 | void openInPreferredApp(); |
75 | void runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName); |
76 | |
77 | void showOpenWithDialog(); |
78 | void showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished); |
79 | void showUntrustedProgramWarningDialog(const QString &filePath); |
80 | |
81 | void startService(const KService::Ptr &service, const QList<QUrl> &urls); |
82 | void startService(const KService::Ptr &service) |
83 | { |
84 | startService(service, urls: {m_url}); |
85 | } |
86 | }; |
87 | |
88 | KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, QObject *parent) |
89 | : KCompositeJob(parent) |
90 | , d(new OpenUrlJobPrivate(url, this)) |
91 | { |
92 | } |
93 | |
94 | KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent) |
95 | : KCompositeJob(parent) |
96 | , d(new OpenUrlJobPrivate(url, this)) |
97 | { |
98 | d->m_mimeTypeName = mimeType; |
99 | } |
100 | |
101 | KIO::OpenUrlJob::~OpenUrlJob() |
102 | { |
103 | } |
104 | |
105 | QUrl KIO::OpenUrlJob::url() const |
106 | { |
107 | return d->m_url; |
108 | } |
109 | |
110 | void KIO::OpenUrlJob::setDeleteTemporaryFile(bool b) |
111 | { |
112 | d->m_deleteTemporaryFile = b; |
113 | } |
114 | |
115 | void KIO::OpenUrlJob::setSuggestedFileName(const QString &suggestedFileName) |
116 | { |
117 | d->m_suggestedFileName = suggestedFileName; |
118 | } |
119 | |
120 | void KIO::OpenUrlJob::setStartupId(const QByteArray &startupId) |
121 | { |
122 | d->m_startupId = startupId; |
123 | } |
124 | |
125 | void KIO::OpenUrlJob::setRunExecutables(bool allow) |
126 | { |
127 | d->m_runExecutables = allow; |
128 | } |
129 | |
130 | void KIO::OpenUrlJob::setShowOpenOrExecuteDialog(bool b) |
131 | { |
132 | d->m_showOpenOrExecuteDialog = b; |
133 | } |
134 | |
135 | void KIO::OpenUrlJob::setEnableExternalBrowser(bool b) |
136 | { |
137 | d->m_externalBrowserEnabled = b; |
138 | } |
139 | |
140 | void KIO::OpenUrlJob::setFollowRedirections(bool b) |
141 | { |
142 | d->m_followRedirections = b; |
143 | } |
144 | |
145 | void KIO::OpenUrlJob::start() |
146 | { |
147 | if (!d->m_url.isValid() || d->m_url.scheme().isEmpty()) { |
148 | const QString error = !d->m_url.isValid() ? d->m_url.errorString() : d->m_url.toDisplayString(); |
149 | setError(KIO::ERR_MALFORMED_URL); |
150 | setErrorText(i18n("Malformed URL\n%1" , error)); |
151 | emitResult(); |
152 | return; |
153 | } |
154 | if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open" ), baseUrl: QUrl(), destUrl: d->m_url)) { |
155 | d->emitAccessDenied(); |
156 | return; |
157 | } |
158 | |
159 | auto qtOpenUrl = [this]() { |
160 | if (!QDesktopServices::openUrl(url: d->m_url)) { |
161 | // Is this an actual error, or USER_CANCELED? |
162 | setError(KJob::UserDefinedError); |
163 | setErrorText(i18n("Failed to open %1" , d->m_url.toDisplayString())); |
164 | } |
165 | emitResult(); |
166 | }; |
167 | |
168 | #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) |
169 | if (d->m_externalBrowserEnabled) { |
170 | // For Windows and MacOS, the mimetypes handling is different, so use QDesktopServices |
171 | qtOpenUrl(); |
172 | return; |
173 | } |
174 | #endif |
175 | |
176 | if (d->m_externalBrowserEnabled && KSandbox::isInside()) { |
177 | // Use the function from QDesktopServices as it handles portals correctly |
178 | // Note that it falls back to "normal way" if the portal service isn't running. |
179 | qtOpenUrl(); |
180 | return; |
181 | } |
182 | |
183 | // If we know the MIME type, proceed |
184 | if (!d->m_mimeTypeName.isEmpty()) { |
185 | d->runUrlWithMimeType(); |
186 | return; |
187 | } |
188 | |
189 | if (d->m_url.scheme().startsWith(s: QLatin1String("http" ))) { |
190 | if (d->m_externalBrowserEnabled) { |
191 | const QString externalBrowser = d->externalBrowser(); |
192 | if (!externalBrowser.isEmpty() && d->runExternalBrowser(exe: externalBrowser)) { |
193 | return; |
194 | } |
195 | } |
196 | } else { |
197 | if (KIO::DesktopExecParser::hasSchemeHandler(url: d->m_url)) { |
198 | d->useSchemeHandler(); |
199 | return; |
200 | } |
201 | } |
202 | |
203 | auto *job = new KIO::MimeTypeFinderJob(d->m_url, this); |
204 | job->setFollowRedirections(d->m_followRedirections); |
205 | job->setSuggestedFileName(d->m_suggestedFileName); |
206 | connect(sender: job, signal: &KJob::result, context: this, slot: [job, this]() { |
207 | const int errCode = job->error(); |
208 | if (errCode) { |
209 | setError(errCode); |
210 | setErrorText(job->errorText()); |
211 | emitResult(); |
212 | } else { |
213 | d->m_suggestedFileName = job->suggestedFileName(); |
214 | d->m_mimeTypeName = job->mimeType(); |
215 | d->runUrlWithMimeType(); |
216 | } |
217 | }); |
218 | job->start(); |
219 | } |
220 | |
221 | bool KIO::OpenUrlJob::doKill() |
222 | { |
223 | return true; |
224 | } |
225 | |
226 | QString KIO::OpenUrlJobPrivate::externalBrowser() const |
227 | { |
228 | if (!m_externalBrowserEnabled) { |
229 | return QString(); |
230 | } |
231 | |
232 | if (!openurljob_force_use_browserapp_kdeglobals) { |
233 | KService::Ptr externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/https" )); |
234 | if (!externalBrowser) { |
235 | externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/http" )); |
236 | } |
237 | if (externalBrowser) { |
238 | return externalBrowser->storageId(); |
239 | } |
240 | } |
241 | |
242 | const QString browserApp = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("General" )).readEntry(key: "BrowserApplication" ); |
243 | return browserApp; |
244 | } |
245 | |
246 | bool KIO::OpenUrlJobPrivate::runExternalBrowser(const QString &exec) |
247 | { |
248 | if (exec.startsWith(c: QLatin1Char('!'))) { |
249 | // Literal command |
250 | const QString command = QStringView(exec).mid(pos: 1) + QLatin1String(" %u" ); |
251 | KService::Ptr service(new KService(QString(), command, QString())); |
252 | startService(service); |
253 | return true; |
254 | } else { |
255 | // Name of desktop file |
256 | KService::Ptr service = KService::serviceByStorageId(storageId: exec); |
257 | if (service) { |
258 | startService(service); |
259 | return true; |
260 | } |
261 | } |
262 | return false; |
263 | } |
264 | |
265 | void KIO::OpenUrlJobPrivate::useSchemeHandler() |
266 | { |
267 | // look for an application associated with x-scheme-handler/<protocol> |
268 | const KService::Ptr service = KApplicationTrader::preferredService(mimeType: QLatin1String("x-scheme-handler/" ) + m_url.scheme()); |
269 | if (service) { |
270 | startService(service); |
271 | return; |
272 | } |
273 | // fallback, look for associated helper protocol |
274 | Q_ASSERT(KProtocolInfo::isHelperProtocol(m_url.scheme())); |
275 | const auto exec = KProtocolInfo::exec(protocol: m_url.scheme()); |
276 | if (exec.isEmpty()) { |
277 | // use default MIME type opener for file |
278 | m_mimeTypeName = KProtocolManager::defaultMimetype(url: m_url); |
279 | runUrlWithMimeType(); |
280 | } else { |
281 | KService::Ptr servicePtr(new KService(QString(), exec, QString())); |
282 | startService(service: servicePtr); |
283 | } |
284 | } |
285 | |
286 | void KIO::OpenUrlJobPrivate::startService(const KService::Ptr &service, const QList<QUrl> &urls) |
287 | { |
288 | KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q); |
289 | job->setUrls(urls); |
290 | job->setRunFlags(m_deleteTemporaryFile ? KIO::ApplicationLauncherJob::DeleteTemporaryFiles : KIO::ApplicationLauncherJob::RunFlags{}); |
291 | job->setSuggestedFileName(m_suggestedFileName); |
292 | job->setStartupId(m_startupId); |
293 | q->addSubjob(job); |
294 | job->start(); |
295 | } |
296 | |
297 | void KIO::OpenUrlJobPrivate::runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName) |
298 | { |
299 | if (urlStr.isEmpty()) { |
300 | q->setError(KJob::UserDefinedError); |
301 | q->setErrorText(i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry." , filePath)); |
302 | q->emitResult(); |
303 | return; |
304 | } |
305 | |
306 | m_url = QUrl::fromUserInput(userInput: urlStr); |
307 | m_mimeTypeName.clear(); |
308 | |
309 | // X-KDE-LastOpenedWith holds the service desktop entry name that |
310 | // should be preferred for opening this URL if possible. |
311 | // This is used by the Recent Documents menu for instance. |
312 | if (!optionalServiceName.isEmpty()) { |
313 | m_preferredService = KService::serviceByDesktopName(name: optionalServiceName); |
314 | } |
315 | |
316 | // Restart from scratch with the target of the link |
317 | q->start(); |
318 | } |
319 | |
320 | void KIO::OpenUrlJobPrivate::emitAccessDenied() |
321 | { |
322 | q->setError(KIO::ERR_ACCESS_DENIED); |
323 | q->setErrorText(KIO::buildErrorString(errorCode: KIO::ERR_ACCESS_DENIED, errorText: m_url.toDisplayString())); |
324 | q->emitResult(); |
325 | } |
326 | |
327 | // was: KRun::isExecutable (minus application/x-desktop MIME type). |
328 | // Feel free to make public if needed. |
329 | static bool isBinary(const QMimeType &mimeType) |
330 | { |
331 | // - Binaries could be e.g.: |
332 | // - application/x-executable |
333 | // - application/x-sharedlib e.g. /usr/bin/ls, see |
334 | // https://gitlab.freedesktop.org/xdg/shared-mime-info/-/issues/11 |
335 | // |
336 | // - MIME types that inherit application/x-executable _and_ text/plain are scripts, these are |
337 | // handled by handleScripts() |
338 | |
339 | return (mimeType.inherits(QStringLiteral("application/x-executable" )) || mimeType.inherits(QStringLiteral("application/x-ms-dos-executable" ))); |
340 | } |
341 | |
342 | // Helper function that returns whether a file is a text-based script |
343 | // e.g. ".sh", ".csh", ".py", ".js" |
344 | static bool isTextScript(const QMimeType &mimeType) |
345 | { |
346 | return (mimeType.inherits(QStringLiteral("application/x-executable" )) && mimeType.inherits(QStringLiteral("text/plain" ))); |
347 | } |
348 | |
349 | // Helper function that returns whether a file has the execute bit set or not. |
350 | static bool hasExecuteBit(const QString &fileName) |
351 | { |
352 | return QFileInfo(fileName).isExecutable(); |
353 | } |
354 | |
355 | bool KIO::OpenUrlJob::isExecutableFile(const QUrl &url, const QString &mimetypeString) |
356 | { |
357 | if (!url.isLocalFile()) { |
358 | return false; |
359 | } |
360 | |
361 | QMimeDatabase db; |
362 | QMimeType mimeType = db.mimeTypeForName(nameOrAlias: mimetypeString); |
363 | return (isBinary(mimeType) || isTextScript(mimeType)) && hasExecuteBit(fileName: url.toLocalFile()); |
364 | } |
365 | |
366 | // Handle native binaries (.e.g. /usr/bin/*); and .exe files |
367 | void KIO::OpenUrlJobPrivate::handleBinaries(const QMimeType &mimeType) |
368 | { |
369 | if (!KAuthorized::authorize(action: KAuthorized::SHELL_ACCESS)) { |
370 | emitAccessDenied(); |
371 | return; |
372 | } |
373 | |
374 | const bool isLocal = m_url.isLocalFile(); |
375 | // Don't run remote executables |
376 | if (!isLocal) { |
377 | q->setError(KJob::UserDefinedError); |
378 | q->setErrorText( |
379 | i18n("The executable file \"%1\" is located on a remote filesystem. " |
380 | "For safety reasons it will not be started." , |
381 | m_url.toDisplayString())); |
382 | q->emitResult(); |
383 | return; |
384 | } |
385 | |
386 | const QString localPath = m_url.toLocalFile(); |
387 | |
388 | bool isNativeBinary = true; |
389 | #ifndef Q_OS_WIN |
390 | isNativeBinary = !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable" )); |
391 | #endif |
392 | |
393 | if (m_showOpenOrExecuteDialog) { |
394 | auto dialogFinished = [this, localPath, isNativeBinary](bool shouldExecute) { |
395 | // shouldExecute is always true if we get here, because for binaries the |
396 | // dialog only offers Execute/Cancel |
397 | Q_UNUSED(shouldExecute) |
398 | |
399 | handleBinariesHelper(localPath, isNativeBinary); |
400 | }; |
401 | |
402 | // Ask the user for confirmation before executing this binary (for binaries |
403 | // the dialog will only show Execute/Cancel) |
404 | showOpenOrExecuteFileDialog(dialogFinished); |
405 | return; |
406 | } |
407 | |
408 | handleBinariesHelper(localPath, isNativeBinary); |
409 | } |
410 | |
411 | void KIO::OpenUrlJobPrivate::handleBinariesHelper(const QString &localPath, bool isNativeBinary) |
412 | { |
413 | if (!m_runExecutables) { |
414 | q->setError(KJob::UserDefinedError); |
415 | q->setErrorText(i18n("For security reasons, launching executables is not allowed in this context." )); |
416 | q->emitResult(); |
417 | return; |
418 | } |
419 | |
420 | // For local .exe files, open in the default app (e.g. WINE) |
421 | if (!isNativeBinary) { |
422 | openInPreferredApp(); |
423 | return; |
424 | } |
425 | |
426 | // Native binaries |
427 | if (!hasExecuteBit(fileName: localPath)) { |
428 | // Show untrustedProgram dialog for local, native executables without the execute bit |
429 | showUntrustedProgramWarningDialog(filePath: localPath); |
430 | return; |
431 | } |
432 | |
433 | // Local executable with execute bit, proceed |
434 | executeCommand(); |
435 | } |
436 | |
437 | // For local, native executables (i.e. not shell scripts) without execute bit, |
438 | // show a prompt asking the user if he wants to run the program. |
439 | void KIO::OpenUrlJobPrivate::showUntrustedProgramWarningDialog(const QString &filePath) |
440 | { |
441 | auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(job: q); |
442 | if (!untrustedProgramHandler) { |
443 | // No way to ask the user to make it executable |
444 | q->setError(KJob::UserDefinedError); |
445 | q->setErrorText(i18n("The program \"%1\" needs to have executable permission before it can be launched." , filePath)); |
446 | q->emitResult(); |
447 | return; |
448 | } |
449 | QObject::connect(sender: untrustedProgramHandler, signal: &KIO::UntrustedProgramHandlerInterface::result, context: q, slot: [=, this](bool result) { |
450 | if (result) { |
451 | QString errorString; |
452 | if (untrustedProgramHandler->setExecuteBit(fileName: filePath, errorString)) { |
453 | executeCommand(); |
454 | } else { |
455 | q->setError(KJob::UserDefinedError); |
456 | q->setErrorText(i18n("Unable to make file \"%1\" executable.\n%2." , filePath, errorString)); |
457 | q->emitResult(); |
458 | } |
459 | } else { |
460 | q->setError(KIO::ERR_USER_CANCELED); |
461 | q->emitResult(); |
462 | } |
463 | }); |
464 | untrustedProgramHandler->showUntrustedProgramWarning(job: q, programName: m_url.fileName()); |
465 | } |
466 | |
467 | void KIO::OpenUrlJobPrivate::executeCommand() |
468 | { |
469 | // Execute the URL as a command. This is how we start scripts and executables |
470 | KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(m_url.toLocalFile(), QStringList()); |
471 | job->setStartupId(m_startupId); |
472 | job->setWorkingDirectory(m_url.adjusted(options: QUrl::RemoveFilename).toLocalFile()); |
473 | q->addSubjob(job); |
474 | job->start(); |
475 | |
476 | // TODO implement deleting the file if tempFile==true |
477 | // CommandLauncherJob doesn't support that, unlike ApplicationLauncherJob |
478 | // We'd have to do it in KProcessRunner. |
479 | } |
480 | |
481 | void KIO::OpenUrlJobPrivate::runUrlWithMimeType() |
482 | { |
483 | // Tell the app, in case it wants us to stop here |
484 | Q_EMIT q->mimeTypeFound(mimeType: m_mimeTypeName); |
485 | if (q->error() == KJob::KilledJobError) { |
486 | q->emitResult(); |
487 | return; |
488 | } |
489 | |
490 | // Support for preferred service setting, see setPreferredService |
491 | if (m_preferredService && m_preferredService->hasMimeType(mimeType: m_mimeTypeName)) { |
492 | startService(service: m_preferredService); |
493 | return; |
494 | } |
495 | |
496 | // Scripts and executables |
497 | QMimeDatabase db; |
498 | const QMimeType mimeType = db.mimeTypeForName(nameOrAlias: m_mimeTypeName); |
499 | |
500 | // .desktop files |
501 | if (mimeType.inherits(QStringLiteral("application/x-desktop" ))) { |
502 | handleDesktopFiles(); |
503 | return; |
504 | } |
505 | |
506 | // Scripts (e.g. .sh, .csh, .py, .js) |
507 | if (isTextScript(mimeType)) { |
508 | handleScripts(); |
509 | return; |
510 | } |
511 | |
512 | // Binaries (e.g. /usr/bin/{konsole,ls}) and .exe files |
513 | if (isBinary(mimeType)) { |
514 | handleBinaries(mimeType); |
515 | return; |
516 | } |
517 | |
518 | // General case: look up associated application |
519 | openInPreferredApp(); |
520 | } |
521 | |
522 | void KIO::OpenUrlJobPrivate::handleDesktopFiles() |
523 | { |
524 | // Open remote .desktop files in the default (text editor) app |
525 | if (!m_url.isLocalFile()) { |
526 | openInPreferredApp(); |
527 | return; |
528 | } |
529 | |
530 | if (m_url.fileName() == QLatin1String(".directory" ) || m_mimeTypeName == QLatin1String("application/x-theme" )) { |
531 | // We cannot execute these files, open in the default app |
532 | m_mimeTypeName = QStringLiteral("text/plain" ); |
533 | openInPreferredApp(); |
534 | return; |
535 | } |
536 | |
537 | const QString filePath = m_url.toLocalFile(); |
538 | KDesktopFile cfg(filePath); |
539 | KConfigGroup cfgGroup = cfg.desktopGroup(); |
540 | if (!cfgGroup.hasKey(key: "Type" )) { |
541 | q->setError(KJob::UserDefinedError); |
542 | q->setErrorText(i18n("The desktop entry file %1 has no Type=... entry." , filePath)); |
543 | q->emitResult(); |
544 | openInPreferredApp(); |
545 | return; |
546 | } |
547 | |
548 | if (cfg.hasLinkType()) { |
549 | runLink(filePath, urlStr: cfg.readUrl(), optionalServiceName: cfg.desktopGroup().readEntry(key: "X-KDE-LastOpenedWith" )); |
550 | return; |
551 | } |
552 | |
553 | if ((cfg.hasApplicationType() || cfg.readType() == QLatin1String("Service" ))) { // kio_settings lets users run Type=Service desktop files |
554 | KService::Ptr service(new KService(filePath)); |
555 | if (!service->exec().isEmpty()) { |
556 | if (m_showOpenOrExecuteDialog) { // Show the openOrExecute dialog |
557 | auto dialogFinished = [this, filePath, service](bool shouldExecute) { |
558 | if (shouldExecute) { // Run the file |
559 | startService(service, urls: {}); |
560 | return; |
561 | } |
562 | // The user selected "open" |
563 | openInPreferredApp(); |
564 | }; |
565 | |
566 | showOpenOrExecuteFileDialog(dialogFinished); |
567 | return; |
568 | } |
569 | |
570 | if (m_runExecutables) { |
571 | startService(service, urls: {}); |
572 | return; |
573 | } |
574 | } // exec is not empty |
575 | } // type Application or Service |
576 | |
577 | // Fallback to opening in the default app |
578 | openInPreferredApp(); |
579 | } |
580 | |
581 | void KIO::OpenUrlJobPrivate::handleScripts() |
582 | { |
583 | // Executable scripts of any type can run arbitrary shell commands |
584 | if (!KAuthorized::authorize(action: KAuthorized::SHELL_ACCESS)) { |
585 | emitAccessDenied(); |
586 | return; |
587 | } |
588 | |
589 | const bool isLocal = m_url.isLocalFile(); |
590 | const QString localPath = m_url.toLocalFile(); |
591 | if (!isLocal || !hasExecuteBit(fileName: localPath)) { |
592 | // Open remote scripts or ones without the execute bit, with the default application |
593 | openInPreferredApp(); |
594 | return; |
595 | } |
596 | |
597 | if (m_showOpenOrExecuteDialog) { |
598 | auto dialogFinished = [this](bool shouldExecute) { |
599 | if (shouldExecute) { |
600 | executeCommand(); |
601 | } else { |
602 | openInPreferredApp(); |
603 | } |
604 | }; |
605 | |
606 | showOpenOrExecuteFileDialog(dialogFinished); |
607 | return; |
608 | } |
609 | |
610 | if (m_runExecutables) { // Local executable script, proceed |
611 | executeCommand(); |
612 | } else { // Open in the default (text editor) app |
613 | openInPreferredApp(); |
614 | } |
615 | } |
616 | |
617 | void KIO::OpenUrlJobPrivate::openInPreferredApp() |
618 | { |
619 | KService::Ptr service = KApplicationTrader::preferredService(mimeType: m_mimeTypeName); |
620 | if (service) { |
621 | // If file mimetype is set to xdg-open or kde-open, the file will be opened in endless loop |
622 | // In these cases, showOpenWithDialog instead |
623 | const QStringList disallowedWrappers = {QStringLiteral("xdg-open" ), QStringLiteral("kde-open" )}; |
624 | if (disallowedWrappers.contains(str: service.data()->exec())) { |
625 | showOpenWithDialog(); |
626 | return; |
627 | } |
628 | startService(service); |
629 | } else { |
630 | // Avoid directly opening partial downloads and incomplete files |
631 | // This is done here in the off chance the user actually has a default handler for it |
632 | if (m_mimeTypeName == QLatin1String("application/x-partial-download" )) { |
633 | q->setError(KJob::UserDefinedError); |
634 | q->setErrorText( |
635 | i18n("This file is incomplete and should not be opened.\n" |
636 | "Check your open applications and the notification area for any pending tasks or downloads." )); |
637 | q->emitResult(); |
638 | return; |
639 | } |
640 | |
641 | showOpenWithDialog(); |
642 | } |
643 | } |
644 | |
645 | void KIO::OpenUrlJobPrivate::showOpenWithDialog() |
646 | { |
647 | if (!KAuthorized::authorizeAction(QStringLiteral("openwith" ))) { |
648 | q->setError(KJob::UserDefinedError); |
649 | q->setErrorText(i18n("You are not authorized to select an application to open this file." )); |
650 | q->emitResult(); |
651 | return; |
652 | } |
653 | |
654 | auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(job: q); |
655 | if (!openWithHandler || QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) { |
656 | // As KDE on windows doesn't know about the windows default applications, offers will be empty in nearly all cases. |
657 | // So we use QDesktopServices::openUrl to let windows decide how to open the file. |
658 | // It's also our fallback if there's no handler to show an open-with dialog. |
659 | if (!QDesktopServices::openUrl(url: m_url)) { |
660 | q->setError(KJob::UserDefinedError); |
661 | q->setErrorText(i18n("Failed to open the file." )); |
662 | } |
663 | q->emitResult(); |
664 | return; |
665 | } |
666 | |
667 | QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::canceled, context: q, slot: [this]() { |
668 | q->setError(KIO::ERR_USER_CANCELED); |
669 | q->emitResult(); |
670 | }); |
671 | |
672 | QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::serviceSelected, context: q, slot: [this](const KService::Ptr &service) { |
673 | startService(service); |
674 | }); |
675 | |
676 | QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::handled, context: q, slot: [this]() { |
677 | q->emitResult(); |
678 | }); |
679 | |
680 | openWithHandler->promptUserForApplication(job: q, urls: {m_url}, mimeType: m_mimeTypeName); |
681 | } |
682 | |
683 | void KIO::OpenUrlJobPrivate::showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished) |
684 | { |
685 | QMimeDatabase db; |
686 | QMimeType mimeType = db.mimeTypeForName(nameOrAlias: m_mimeTypeName); |
687 | |
688 | auto *openOrExecuteFileHandler = KIO::delegateExtension<KIO::OpenOrExecuteFileInterface *>(job: q); |
689 | if (!openOrExecuteFileHandler) { |
690 | // No way to ask the user whether to execute or open |
691 | if (isTextScript(mimeType) || mimeType.inherits(QStringLiteral("application/x-desktop" ))) { // Open text-based ones in the default app |
692 | openInPreferredApp(); |
693 | } else { |
694 | q->setError(KJob::UserDefinedError); |
695 | q->setErrorText(i18n("The program \"%1\" could not be launched." , m_url.toDisplayString(QUrl::PreferLocalFile))); |
696 | q->emitResult(); |
697 | } |
698 | return; |
699 | } |
700 | |
701 | QObject::connect(sender: openOrExecuteFileHandler, signal: &KIO::OpenOrExecuteFileInterface::canceled, context: q, slot: [this]() { |
702 | q->setError(KIO::ERR_USER_CANCELED); |
703 | q->emitResult(); |
704 | }); |
705 | |
706 | QObject::connect(sender: openOrExecuteFileHandler, signal: &KIO::OpenOrExecuteFileInterface::executeFile, context: q, slot: [this, dialogFinished](bool shouldExecute) { |
707 | m_runExecutables = shouldExecute; |
708 | dialogFinished(shouldExecute); |
709 | }); |
710 | |
711 | openOrExecuteFileHandler->promptUserOpenOrExecute(job: q, mimetype: m_mimeTypeName); |
712 | } |
713 | |
714 | void KIO::OpenUrlJob::slotResult(KJob *job) |
715 | { |
716 | // This is only used for the final application/launcher job, so we're done when it's done |
717 | const int errCode = job->error(); |
718 | if (errCode) { |
719 | setError(errCode); |
720 | // We're a KJob, not a KIO::Job, so build the error string here |
721 | setErrorText(KIO::buildErrorString(errorCode: errCode, errorText: job->errorText())); |
722 | } |
723 | emitResult(); |
724 | } |
725 | |
726 | #include "moc_openurljob.cpp" |
727 | |