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