Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,721 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "openurljob.h"
#include "commandlauncherjob.h"
#include "desktopexecparser.h"
#include "global.h"
#include "job.h" // for buildErrorString
#include "jobuidelegatefactory.h"
#include "kiogui_debug.h"
#include "openorexecutefileinterface.h"
#include "openwithhandlerinterface.h"
#include "untrustedprogramhandlerinterface.h"
#include <KApplicationTrader>
#include <KAuthorized>
#include <KConfigGroup>
#include <KDesktopFile>
#include <KLocalizedString>
#include <KSandbox>
#include <KUrlAuthorized>
#include <QFileInfo>
#include <KProtocolManager>
#include <KSharedConfig>
#include <QDesktopServices>
#include <QHostInfo>
#include <QMimeDatabase>
#include <QOperatingSystemVersion>
#include <mimetypefinderjob.h>
// For unit test purposes, to test both code paths in externalBrowser()
KIOGUI_EXPORT bool openurljob_force_use_browserapp_kdeglobals = false;
class KIO::OpenUrlJobPrivate
{
public:
explicit OpenUrlJobPrivate(const QUrl &url, OpenUrlJob *qq)
: m_url(url)
, q(qq)
{
q->setCapabilities(KJob::Killable);
}
void emitAccessDenied();
void runUrlWithMimeType();
QString externalBrowser() const;
bool runExternalBrowser(const QString &exe);
void useSchemeHandler();
QUrl m_url;
KIO::OpenUrlJob *const q;
QString m_suggestedFileName;
QByteArray m_startupId;
QString m_mimeTypeName;
KService::Ptr m_preferredService;
bool m_deleteTemporaryFile = false;
bool m_runExecutables = false;
bool m_showOpenOrExecuteDialog = false;
bool m_externalBrowserEnabled = true;
bool m_followRedirections = true;
private:
void executeCommand();
void handleBinaries(const QMimeType &mimeType);
void handleBinariesHelper(const QString &localPath, bool isNativeBinary);
void handleDesktopFiles();
void handleScripts();
void openInPreferredApp();
void runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName);
void showOpenWithDialog();
void showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished);
void showUntrustedProgramWarningDialog(const QString &filePath);
void startService(const KService::Ptr &service, const QList<QUrl> &urls);
void startService(const KService::Ptr &service)
{
startService(service, {m_url});
}
};
KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, QObject *parent)
: KCompositeJob(parent)
, d(new OpenUrlJobPrivate(url, this))
{
}
KIO::OpenUrlJob::OpenUrlJob(const QUrl &url, const QString &mimeType, QObject *parent)
: KCompositeJob(parent)
, d(new OpenUrlJobPrivate(url, this))
{
d->m_mimeTypeName = mimeType;
}
KIO::OpenUrlJob::~OpenUrlJob()
{
}
void KIO::OpenUrlJob::setDeleteTemporaryFile(bool b)
{
d->m_deleteTemporaryFile = b;
}
void KIO::OpenUrlJob::setSuggestedFileName(const QString &suggestedFileName)
{
d->m_suggestedFileName = suggestedFileName;
}
void KIO::OpenUrlJob::setStartupId(const QByteArray &startupId)
{
d->m_startupId = startupId;
}
void KIO::OpenUrlJob::setRunExecutables(bool allow)
{
d->m_runExecutables = allow;
}
void KIO::OpenUrlJob::setShowOpenOrExecuteDialog(bool b)
{
d->m_showOpenOrExecuteDialog = b;
}
void KIO::OpenUrlJob::setEnableExternalBrowser(bool b)
{
d->m_externalBrowserEnabled = b;
}
void KIO::OpenUrlJob::setFollowRedirections(bool b)
{
d->m_followRedirections = b;
}
void KIO::OpenUrlJob::start()
{
if (!d->m_url.isValid() || d->m_url.scheme().isEmpty()) {
const QString error = !d->m_url.isValid() ? d->m_url.errorString() : d->m_url.toDisplayString();
setError(KIO::ERR_MALFORMED_URL);
setErrorText(i18n("Malformed URL\n%1", error));
emitResult();
return;
}
if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), d->m_url)) {
d->emitAccessDenied();
return;
}
auto qtOpenUrl = [this]() {
if (!QDesktopServices::openUrl(d->m_url)) {
// Is this an actual error, or USER_CANCELED?
setError(KJob::UserDefinedError);
setErrorText(i18n("Failed to open %1", d->m_url.toDisplayString()));
}
emitResult();
};
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
if (d->m_externalBrowserEnabled) {
// For Windows and MacOS, the mimetypes handling is different, so use QDesktopServices
qtOpenUrl();
return;
}
#endif
if (d->m_externalBrowserEnabled && KSandbox::isInside()) {
// Use the function from QDesktopServices as it handles portals correctly
// Note that it falls back to "normal way" if the portal service isn't running.
qtOpenUrl();
return;
}
// If we know the MIME type, proceed
if (!d->m_mimeTypeName.isEmpty()) {
d->runUrlWithMimeType();
return;
}
if (d->m_url.scheme().startsWith(QLatin1String("http"))) {
if (d->m_externalBrowserEnabled) {
const QString externalBrowser = d->externalBrowser();
if (!externalBrowser.isEmpty() && d->runExternalBrowser(externalBrowser)) {
return;
}
}
} else {
if (KIO::DesktopExecParser::hasSchemeHandler(d->m_url)) {
d->useSchemeHandler();
return;
}
}
auto *job = new KIO::MimeTypeFinderJob(d->m_url, this);
job->setFollowRedirections(d->m_followRedirections);
job->setSuggestedFileName(d->m_suggestedFileName);
connect(job, &KJob::result, this, [job, this]() {
const int errCode = job->error();
if (errCode) {
setError(errCode);
setErrorText(job->errorText());
emitResult();
} else {
d->m_suggestedFileName = job->suggestedFileName();
d->m_mimeTypeName = job->mimeType();
d->runUrlWithMimeType();
}
});
job->start();
}
bool KIO::OpenUrlJob::doKill()
{
return true;
}
QString KIO::OpenUrlJobPrivate::externalBrowser() const
{
if (!m_externalBrowserEnabled) {
return QString();
}
if (!openurljob_force_use_browserapp_kdeglobals) {
KService::Ptr externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/https"));
if (!externalBrowser) {
externalBrowser = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/http"));
}
if (externalBrowser) {
return externalBrowser->storageId();
}
}
const QString browserApp = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("General")).readEntry("BrowserApplication");
return browserApp;
}
bool KIO::OpenUrlJobPrivate::runExternalBrowser(const QString &exec)
{
if (exec.startsWith(QLatin1Char('!'))) {
// Literal command
const QString command = QStringView(exec).mid(1) + QLatin1String(" %u");
KService::Ptr service(new KService(QString(), command, QString()));
startService(service);
return true;
} else {
// Name of desktop file
KService::Ptr service = KService::serviceByStorageId(exec);
if (service) {
startService(service);
return true;
}
}
return false;
}
void KIO::OpenUrlJobPrivate::useSchemeHandler()
{
// look for an application associated with x-scheme-handler/<protocol>
const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + m_url.scheme());
if (service) {
startService(service);
return;
}
// fallback, look for associated helper protocol
Q_ASSERT(KProtocolInfo::isHelperProtocol(m_url.scheme()));
const auto exec = KProtocolInfo::exec(m_url.scheme());
if (exec.isEmpty()) {
// use default MIME type opener for file
m_mimeTypeName = KProtocolManager::defaultMimetype(m_url);
runUrlWithMimeType();
} else {
KService::Ptr servicePtr(new KService(QString(), exec, QString()));
startService(servicePtr);
}
}
void KIO::OpenUrlJobPrivate::startService(const KService::Ptr &service, const QList<QUrl> &urls)
{
KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service, q);
job->setUrls(urls);
job->setRunFlags(m_deleteTemporaryFile ? KIO::ApplicationLauncherJob::DeleteTemporaryFiles : KIO::ApplicationLauncherJob::RunFlags{});
job->setSuggestedFileName(m_suggestedFileName);
job->setStartupId(m_startupId);
q->addSubjob(job);
job->start();
}
void KIO::OpenUrlJobPrivate::runLink(const QString &filePath, const QString &urlStr, const QString &optionalServiceName)
{
if (urlStr.isEmpty()) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The desktop entry file\n%1\nis of type Link but has no URL=... entry.", filePath));
q->emitResult();
return;
}
m_url = QUrl::fromUserInput(urlStr);
m_mimeTypeName.clear();
// X-KDE-LastOpenedWith holds the service desktop entry name that
// should be preferred for opening this URL if possible.
// This is used by the Recent Documents menu for instance.
if (!optionalServiceName.isEmpty()) {
m_preferredService = KService::serviceByDesktopName(optionalServiceName);
}
// Restart from scratch with the target of the link
q->start();
}
void KIO::OpenUrlJobPrivate::emitAccessDenied()
{
q->setError(KIO::ERR_ACCESS_DENIED);
q->setErrorText(KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, m_url.toDisplayString()));
q->emitResult();
}
// was: KRun::isExecutable (minus application/x-desktop MIME type).
// Feel free to make public if needed.
static bool isBinary(const QMimeType &mimeType)
{
// - Binaries could be e.g.:
// - application/x-executable
// - application/x-sharedlib e.g. /usr/bin/ls, see
// https://gitlab.freedesktop.org/xdg/shared-mime-info/-/issues/11
//
// - MIME types that inherit application/x-executable _and_ text/plain are scripts, these are
// handled by handleScripts()
return (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-ms-dos-executable")));
}
// Helper function that returns whether a file is a text-based script
// e.g. ".sh", ".csh", ".py", ".js"
static bool isTextScript(const QMimeType &mimeType)
{
return (mimeType.inherits(QStringLiteral("application/x-executable")) && mimeType.inherits(QStringLiteral("text/plain")));
}
// Helper function that returns whether a file has the execute bit set or not.
static bool hasExecuteBit(const QString &fileName)
{
return QFileInfo(fileName).isExecutable();
}
bool KIO::OpenUrlJob::isExecutableFile(const QUrl &url, const QString &mimetypeString)
{
if (!url.isLocalFile()) {
return false;
}
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForName(mimetypeString);
return (isBinary(mimeType) || isTextScript(mimeType)) && hasExecuteBit(url.toLocalFile());
}
// Handle native binaries (.e.g. /usr/bin/*); and .exe files
void KIO::OpenUrlJobPrivate::handleBinaries(const QMimeType &mimeType)
{
if (!KAuthorized::authorize(KAuthorized::SHELL_ACCESS)) {
emitAccessDenied();
return;
}
const bool isLocal = m_url.isLocalFile();
// Don't run remote executables
if (!isLocal) {
q->setError(KJob::UserDefinedError);
q->setErrorText(
i18n("The executable file \"%1\" is located on a remote filesystem. "
"For safety reasons it will not be started.",
m_url.toDisplayString()));
q->emitResult();
return;
}
const QString localPath = m_url.toLocalFile();
bool isNativeBinary = true;
#ifndef Q_OS_WIN
isNativeBinary = !mimeType.inherits(QStringLiteral("application/x-ms-dos-executable"));
#endif
if (m_showOpenOrExecuteDialog) {
auto dialogFinished = [this, localPath, isNativeBinary](bool shouldExecute) {
// shouldExecute is always true if we get here, because for binaries the
// dialog only offers Execute/Cancel
Q_UNUSED(shouldExecute)
handleBinariesHelper(localPath, isNativeBinary);
};
// Ask the user for confirmation before executing this binary (for binaries
// the dialog will only show Execute/Cancel)
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
handleBinariesHelper(localPath, isNativeBinary);
}
void KIO::OpenUrlJobPrivate::handleBinariesHelper(const QString &localPath, bool isNativeBinary)
{
if (!m_runExecutables) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("For security reasons, launching executables is not allowed in this context."));
q->emitResult();
return;
}
// For local .exe files, open in the default app (e.g. WINE)
if (!isNativeBinary) {
openInPreferredApp();
return;
}
// Native binaries
if (!hasExecuteBit(localPath)) {
// Show untrustedProgram dialog for local, native executables without the execute bit
showUntrustedProgramWarningDialog(localPath);
return;
}
// Local executable with execute bit, proceed
executeCommand();
}
// For local, native executables (i.e. not shell scripts) without execute bit,
// show a prompt asking the user if he wants to run the program.
void KIO::OpenUrlJobPrivate::showUntrustedProgramWarningDialog(const QString &filePath)
{
auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(q);
if (!untrustedProgramHandler) {
// No way to ask the user to make it executable
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The program \"%1\" needs to have executable permission before it can be launched.", filePath));
q->emitResult();
return;
}
QObject::connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, q, [=, this](bool result) {
if (result) {
QString errorString;
if (untrustedProgramHandler->setExecuteBit(filePath, errorString)) {
executeCommand();
} else {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("Unable to make file \"%1\" executable.\n%2.", filePath, errorString));
q->emitResult();
}
} else {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
}
});
untrustedProgramHandler->showUntrustedProgramWarning(q, m_url.fileName());
}
void KIO::OpenUrlJobPrivate::executeCommand()
{
// Execute the URL as a command. This is how we start scripts and executables
KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(m_url.toLocalFile(), QStringList());
job->setStartupId(m_startupId);
job->setWorkingDirectory(m_url.adjusted(QUrl::RemoveFilename).toLocalFile());
q->addSubjob(job);
job->start();
// TODO implement deleting the file if tempFile==true
// CommandLauncherJob doesn't support that, unlike ApplicationLauncherJob
// We'd have to do it in KProcessRunner.
}
void KIO::OpenUrlJobPrivate::runUrlWithMimeType()
{
// Tell the app, in case it wants us to stop here
Q_EMIT q->mimeTypeFound(m_mimeTypeName);
if (q->error() == KJob::KilledJobError) {
q->emitResult();
return;
}
// Support for preferred service setting, see setPreferredService
if (m_preferredService && m_preferredService->hasMimeType(m_mimeTypeName)) {
startService(m_preferredService);
return;
}
// Scripts and executables
QMimeDatabase db;
const QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName);
// .desktop files
if (mimeType.inherits(QStringLiteral("application/x-desktop"))) {
handleDesktopFiles();
return;
}
// Scripts (e.g. .sh, .csh, .py, .js)
if (isTextScript(mimeType)) {
handleScripts();
return;
}
// Binaries (e.g. /usr/bin/{konsole,ls}) and .exe files
if (isBinary(mimeType)) {
handleBinaries(mimeType);
return;
}
// General case: look up associated application
openInPreferredApp();
}
void KIO::OpenUrlJobPrivate::handleDesktopFiles()
{
// Open remote .desktop files in the default (text editor) app
if (!m_url.isLocalFile()) {
openInPreferredApp();
return;
}
if (m_url.fileName() == QLatin1String(".directory") || m_mimeTypeName == QLatin1String("application/x-theme")) {
// We cannot execute these files, open in the default app
m_mimeTypeName = QStringLiteral("text/plain");
openInPreferredApp();
return;
}
const QString filePath = m_url.toLocalFile();
KDesktopFile cfg(filePath);
KConfigGroup cfgGroup = cfg.desktopGroup();
if (!cfgGroup.hasKey("Type")) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The desktop entry file %1 has no Type=... entry.", filePath));
q->emitResult();
openInPreferredApp();
return;
}
if (cfg.hasLinkType()) {
runLink(filePath, cfg.readUrl(), cfg.desktopGroup().readEntry("X-KDE-LastOpenedWith"));
return;
}
if ((cfg.hasApplicationType() || cfg.readType() == QLatin1String("Service"))) { // kio_settings lets users run Type=Service desktop files
KService::Ptr service(new KService(filePath));
if (!service->exec().isEmpty()) {
if (m_showOpenOrExecuteDialog) { // Show the openOrExecute dialog
auto dialogFinished = [this, filePath, service](bool shouldExecute) {
if (shouldExecute) { // Run the file
startService(service, {});
return;
}
// The user selected "open"
openInPreferredApp();
};
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
if (m_runExecutables) {
startService(service, {});
return;
}
} // exec is not empty
} // type Application or Service
// Fallback to opening in the default app
openInPreferredApp();
}
void KIO::OpenUrlJobPrivate::handleScripts()
{
// Executable scripts of any type can run arbitrary shell commands
if (!KAuthorized::authorize(KAuthorized::SHELL_ACCESS)) {
emitAccessDenied();
return;
}
const bool isLocal = m_url.isLocalFile();
const QString localPath = m_url.toLocalFile();
if (!isLocal || !hasExecuteBit(localPath)) {
// Open remote scripts or ones without the execute bit, with the default application
openInPreferredApp();
return;
}
if (m_showOpenOrExecuteDialog) {
auto dialogFinished = [this](bool shouldExecute) {
if (shouldExecute) {
executeCommand();
} else {
openInPreferredApp();
}
};
showOpenOrExecuteFileDialog(dialogFinished);
return;
}
if (m_runExecutables) { // Local executable script, proceed
executeCommand();
} else { // Open in the default (text editor) app
openInPreferredApp();
}
}
void KIO::OpenUrlJobPrivate::openInPreferredApp()
{
KService::Ptr service = KApplicationTrader::preferredService(m_mimeTypeName);
if (service) {
// If file mimetype is set to xdg-open or kde-open, the file will be opened in endless loop
// In these cases, showOpenWithDialog instead
const QStringList disallowedWrappers = {QStringLiteral("xdg-open"), QStringLiteral("kde-open")};
if (disallowedWrappers.contains(service.data()->exec())) {
showOpenWithDialog();
return;
}
startService(service);
} else {
// Avoid directly opening partial downloads and incomplete files
// This is done here in the off chance the user actually has a default handler for it
if (m_mimeTypeName == QLatin1String("application/x-partial-download")) {
q->setError(KJob::UserDefinedError);
q->setErrorText(
i18n("This file is incomplete and should not be opened.\n"
"Check your open applications and the notification area for any pending tasks or downloads."));
q->emitResult();
return;
}
showOpenWithDialog();
}
}
void KIO::OpenUrlJobPrivate::showOpenWithDialog()
{
if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("You are not authorized to select an application to open this file."));
q->emitResult();
return;
}
auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(q);
if (!openWithHandler || QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) {
// As KDE on windows doesn't know about the windows default applications, offers will be empty in nearly all cases.
// So we use QDesktopServices::openUrl to let windows decide how to open the file.
// It's also our fallback if there's no handler to show an open-with dialog.
if (!QDesktopServices::openUrl(m_url)) {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("Failed to open the file."));
}
q->emitResult();
return;
}
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
startService(service);
});
QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
q->emitResult();
});
openWithHandler->promptUserForApplication(q, {m_url}, m_mimeTypeName);
}
void KIO::OpenUrlJobPrivate::showOpenOrExecuteFileDialog(std::function<void(bool)> dialogFinished)
{
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForName(m_mimeTypeName);
auto *openOrExecuteFileHandler = KIO::delegateExtension<KIO::OpenOrExecuteFileInterface *>(q);
if (!openOrExecuteFileHandler) {
// No way to ask the user whether to execute or open
if (isTextScript(mimeType) || mimeType.inherits(QStringLiteral("application/x-desktop"))) { // Open text-based ones in the default app
openInPreferredApp();
} else {
q->setError(KJob::UserDefinedError);
q->setErrorText(i18n("The program \"%1\" could not be launched.", m_url.toDisplayString(QUrl::PreferLocalFile)));
q->emitResult();
}
return;
}
QObject::connect(openOrExecuteFileHandler, &KIO::OpenOrExecuteFileInterface::canceled, q, [this]() {
q->setError(KIO::ERR_USER_CANCELED);
q->emitResult();
});
QObject::connect(openOrExecuteFileHandler, &KIO::OpenOrExecuteFileInterface::executeFile, q, [this, dialogFinished](bool shouldExecute) {
m_runExecutables = shouldExecute;
dialogFinished(shouldExecute);
});
openOrExecuteFileHandler->promptUserOpenOrExecute(q, m_mimeTypeName);
}
void KIO::OpenUrlJob::slotResult(KJob *job)
{
// This is only used for the final application/launcher job, so we're done when it's done
const int errCode = job->error();
if (errCode) {
setError(errCode);
// We're a KJob, not a KIO::Job, so build the error string here
setErrorText(KIO::buildErrorString(errCode, job->errorText()));
}
emitResult();
}
#include "moc_openurljob.cpp"