diff --git a/autotests/applicationlauncherjobtest.h b/autotests/applicationlauncherjobtest.h --- a/autotests/applicationlauncherjobtest.h +++ b/autotests/applicationlauncherjobtest.h @@ -36,6 +36,7 @@ void startProcess_data(); void startProcess(); + void shouldFailOnNonExecutableDesktopFile_data(); void shouldFailOnNonExecutableDesktopFile(); void shouldFailOnNonExistingExecutable_data(); diff --git a/autotests/applicationlauncherjobtest.cpp b/autotests/applicationlauncherjobtest.cpp --- a/autotests/applicationlauncherjobtest.cpp +++ b/autotests/applicationlauncherjobtest.cpp @@ -21,6 +21,8 @@ #include "applicationlauncherjobtest.h" #include "applicationlauncherjob.h" +#include +#include #include "kiotesthelper.h" // createTestFile etc. @@ -35,10 +37,29 @@ #include #include #include -#include QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) +namespace KIO { +KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); +} +class TestUntrustedProgramHandler : public KIO::UntrustedProgramHandlerInterface +{ +public: + bool showUntrustedProgramWarning(KJob *job, const QString &programName) override { + Q_UNUSED(job) + m_calls << programName; + return m_retVal; + } + + void setRetVal(bool b) { m_retVal = b; } + + QStringList m_calls; + bool m_retVal = false; +}; + +static TestUntrustedProgramHandler s_handler; + void ApplicationLauncherJobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); @@ -133,8 +154,21 @@ QTRY_COMPARE(KProcessRunner::instanceCount(), 0); } +void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile_data() +{ + QTest::addColumn("withHandler"); + QTest::addColumn("handlerRetVal"); + + QTest::newRow("no_handler") << false << false; + QTest::newRow("handler_false") << true << false; + QTest::newRow("handler_true") << true << true; +} + void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() { + QFETCH(bool, withHandler); + QFETCH(bool, handlerRetVal); + // Given a .desktop file in a temporary directory (outside the trusted paths) QTemporaryDir tempDir; const QString srcDir = tempDir.path(); @@ -146,11 +180,30 @@ createSrcFile(srcFile); const QList urls{QUrl::fromLocalFile(srcFile)}; KService::Ptr servicePtr(new KService(desktopFilePath)); + + s_handler.m_calls.clear(); + s_handler.setRetVal(handlerRetVal); + KIO::setDefaultUntrustedProgramHandler(withHandler ? &s_handler : nullptr); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); job->setUrls(urls); - QVERIFY(!job->exec()); - QCOMPARE(job->error(), KJob::UserDefinedError); - QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); + if (withHandler && handlerRetVal) { + QVERIFY(job->exec()); + // The actual shell process will race against the deletion of the QTemporaryDir, + // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory + QTest::qWait(50); // this helps a bit + } else { + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); + } + + if (withHandler) { + // check that the handler was called + QCOMPARE(s_handler.m_calls.count(), 1); + QCOMPARE(s_handler.m_calls.at(0), QStringLiteral("KRunUnittestService")); + } + } void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -84,6 +84,7 @@ kmountpoint.cpp kcoredirlister.cpp faviconscache.cpp + untrustedprogramhandlerinterface.cpp ksslcertificatemanager.cpp ksslerroruidata.cpp diff --git a/src/core/jobuidelegateextension.h b/src/core/jobuidelegateextension.h --- a/src/core/jobuidelegateextension.h +++ b/src/core/jobuidelegateextension.h @@ -269,6 +269,8 @@ */ virtual void updateUrlInClipboard(const QUrl &src, const QUrl &dest); + // TODO KF6: add virtual_hook + private: class Private; Private *const d; diff --git a/src/core/untrustedprogramhandlerinterface.h b/src/core/untrustedprogramhandlerinterface.h new file mode 100644 --- /dev/null +++ b/src/core/untrustedprogramhandlerinterface.h @@ -0,0 +1,96 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef UNTRUSTEDPROGRAMHANDLERINTERFACE_H +#define UNTRUSTEDPROGRAMHANDLERINTERFACE_H + +#include +class KJob; +class QString; + +namespace KIO { + +/** + * @brief The UntrustedProgramHandlerInterface class allows ApplicationLauncherJob to + * prompt the user about an untrusted executable or desktop file. + * This extension mechanism for jobs is similar to KIO::JobUiDelegateExtension. + * + * The class also provides helper methods to set the execute bit so that the program + * can be started. + * @since 5.70 + */ +class KIOCORE_EXPORT UntrustedProgramHandlerInterface +{ +protected: + /** + * Constructor + */ + UntrustedProgramHandlerInterface(); + + /** + * Destructor + */ + virtual ~UntrustedProgramHandlerInterface(); + +public: + /** + * Show a warning to the user about the program not being trusted for execution. + * This could be an executable which is not a script and without the execute bit. + * Or it could be a desktop file outside the standard locations, without the execute bit. + * @param job the job calling this. Useful to get the associated window. + * @param programName the full path to the executable or desktop file + * @return true if the user confirmed executing the program + * + * If this function returns true, the caller should then call + * either setExecuteBit or makeServiceFileExecutable; those helper methods + * are provided by this class. + * + * The default implementation in this base class simply returns false. + * Any application linking to KIOWidgets will benefit from an automatically registered + * subclass which implements this method using QtWidgets. + */ + virtual bool showUntrustedProgramWarning(KJob *job, const QString &programName); + + /** + * Helper function that attempts to make a desktop file executable. + * In addition to the execute bit, this includes fixing its first line to ensure that + * it says #!/usr/bin/env xdg-open. + * @param fileName the full path to the file + * @param errorString output parameter so the method can return an error message + * @return true on success, false on error + */ + bool makeServiceFileExecutable(const QString &fileName, QString &errorString); + + /** + * Helper function that attempts to set execute bit for given file. + * @param fileName the full path to the file + * @param errorString output parameter so the method can return an error message + * @return true on success, false on error + */ + bool setExecuteBit(const QString &fileName, QString &errorString); + +private: + class Private; + Private *const d; +}; + +} + +#endif // UNTRUSTEDPROGRAMHANDLERINTERFACE_H diff --git a/src/core/untrustedprogramhandlerinterface.cpp b/src/core/untrustedprogramhandlerinterface.cpp new file mode 100644 --- /dev/null +++ b/src/core/untrustedprogramhandlerinterface.cpp @@ -0,0 +1,128 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "untrustedprogramhandlerinterface.h" + +#include +#include +#include "kiocoredebug.h" + +using namespace KIO; + +UntrustedProgramHandlerInterface::UntrustedProgramHandlerInterface() + : d(nullptr) +{ +} + +UntrustedProgramHandlerInterface::~UntrustedProgramHandlerInterface() +{ +} + +bool UntrustedProgramHandlerInterface::showUntrustedProgramWarning(KJob *job, const QString &programName) +{ + Q_UNUSED(job) + Q_UNUSED(programName) + return false; +} + +bool UntrustedProgramHandlerInterface::makeServiceFileExecutable(const QString &fileName, QString &errorString) +{ + // Open the file and read the first two characters, check if it's + // #!. If not, create a new file, prepend appropriate lines, and copy + // over. + QFile desktopFile(fileName); + if (!desktopFile.open(QFile::ReadOnly)) { + errorString = desktopFile.errorString(); + qCWarning(KIO_CORE) << "Error opening service" << fileName << errorString; + return false; + } + + QByteArray header = desktopFile.peek(2); // First two chars of file + if (header.size() == 0) { + errorString = desktopFile.errorString(); + qCWarning(KIO_CORE) << "Error inspecting service" << fileName << errorString; + return false; // Some kind of error + } + + if (header != "#!") { + // Add header + QSaveFile saveFile; + saveFile.setFileName(fileName); + if (!saveFile.open(QIODevice::WriteOnly)) { + errorString = saveFile.errorString(); + qCWarning(KIO_CORE) << "Unable to open replacement file for" << fileName << errorString; + return false; + } + + QByteArray shebang("#!/usr/bin/env xdg-open\n"); + if (saveFile.write(shebang) != shebang.size()) { + errorString = saveFile.errorString(); + qCWarning(KIO_CORE) << "Error occurred adding header for" << fileName << errorString; + saveFile.cancelWriting(); + return false; + } + + // Now copy the one into the other and then close and reopen desktopFile + QByteArray desktopData(desktopFile.readAll()); + if (desktopData.isEmpty()) { + errorString = desktopFile.errorString(); + qCWarning(KIO_CORE) << "Unable to read service" << fileName << errorString; + saveFile.cancelWriting(); + return false; + } + + if (saveFile.write(desktopData) != desktopData.size()) { + errorString = saveFile.errorString(); + qCWarning(KIO_CORE) << "Error copying service" << fileName << errorString; + saveFile.cancelWriting(); + return false; + } + + desktopFile.close(); + if (!saveFile.commit()) { // Figures.... + errorString = saveFile.errorString(); + qCWarning(KIO_CORE) << "Error committing changes to service" << fileName << errorString; + return false; + } + + if (!desktopFile.open(QFile::ReadOnly)) { + errorString = desktopFile.errorString(); + qCWarning(KIO_CORE) << "Error re-opening service" << fileName << errorString; + return false; + } + } // Add header + + return setExecuteBit(fileName, errorString); +} + +bool UntrustedProgramHandlerInterface::setExecuteBit(const QString &fileName, QString &errorString) +{ + QFile file(fileName); + + // corresponds to owner on unix, which will have to do since if the user + // isn't the owner we can't change perms anyways. + if (!file.setPermissions(QFile::ExeUser | file.permissions())) { + errorString = file.errorString(); + qCWarning(KIO_CORE) << "Unable to change permissions for" << fileName << errorString; + return false; + } + + return true; +} diff --git a/src/gui/applicationlauncherjob.h b/src/gui/applicationlauncherjob.h --- a/src/gui/applicationlauncherjob.h +++ b/src/gui/applicationlauncherjob.h @@ -27,7 +27,7 @@ #include #include -class KRunPrivate; // KF6 REMOVE +class KRun; // KF6 REMOVE class ApplicationLauncherJobTest; // KF6 REMOVE namespace KIO { @@ -42,10 +42,6 @@ * It creates a startup notification and finishes it on success or on error (for the taskbar). * It also emits an error message if necessary (e.g. "program not found"). * - * Note that this class doesn't support warning the user if a desktop file or a binary - * does not have the executable bit set and offering to make it so. Therefore file managers - * should use KRun::runApplication rather than using ApplicationLauncherJob directly. - * * When passing multiple URLs to an application that doesn't support opening * multiple files, the application will be launched once for each URL. * @@ -55,8 +51,11 @@ * For error handling, either connect to the result() signal, or for a simple messagebox on error, * you can do * @code - * job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); + * job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); * @endcode + * Using JobUiDelegate (which is widgets based) also enables the feature of asking the user + * in case the executable or desktop file isn't marked as executable. Otherwise the job will + * just refuse executing those files. * * @since 5.69 */ @@ -150,12 +149,14 @@ QVector pids() const; private: - friend class ::KRunPrivate; // KF6 REMOVE + friend class ::KRun; // KF6 REMOVE friend class ::ApplicationLauncherJobTest; // KF6 REMOVE /** * Blocks until the process has started. Only exists for KRun, will disappear in KF6. */ bool waitForStarted(); + void emitUnauthorizedError(); + bool checkAuthorizedExecution(); friend class ApplicationLauncherJobPrivate; QScopedPointer d; diff --git a/src/gui/applicationlauncherjob.cpp b/src/gui/applicationlauncherjob.cpp --- a/src/gui/applicationlauncherjob.cpp +++ b/src/gui/applicationlauncherjob.cpp @@ -21,7 +21,20 @@ #include "applicationlauncherjob.h" #include "kprocessrunner_p.h" +#include "untrustedprogramhandlerinterface.h" #include "kiogui_debug.h" +#include "../core/global.h" + +#include +#include +#include +#include + +static KIO::UntrustedProgramHandlerInterface *s_untrustedProgramHandler = nullptr; +namespace KIO { +// Hidden API because in KF6 we'll just check if the job's uiDelegate implements UntrustedProgramHandlerInterface. +KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface) { s_untrustedProgramHandler = iface; } +} class KIO::ApplicationLauncherJobPrivate { @@ -84,8 +97,63 @@ d->m_startupId = startupId; } +void KIO::ApplicationLauncherJob::emitUnauthorizedError() +{ + setError(KJob::UserDefinedError); + setErrorText(i18n("You are not authorized to execute this file.")); +} + +bool KIO::ApplicationLauncherJob::checkAuthorizedExecution() +{ + if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { + // KIOSK restriction, cannot be circumvented + emitUnauthorizedError(); + return false; + } + if (!d->m_service->entryPath().isEmpty() && + !KDesktopFile::isAuthorizedDesktopFile(d->m_service->entryPath())) { + // We can use QStandardPaths::findExecutable to resolve relative pathnames + // but that gets rid of the command line arguments. + QString program = QFileInfo(d->m_service->exec()).canonicalFilePath(); + if (program.isEmpty()) { // e.g. due to command line arguments + program = d->m_service->exec(); + } + if (!s_untrustedProgramHandler) { + emitUnauthorizedError(); + return false; + } + if (!s_untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name())) { + setError(KIO::ERR_USER_CANCELED); + return false; + } + + // Assume that service is an absolute path since we're being called (relative paths + // would have been allowed unless Kiosk said no, therefore we already know where the + // .desktop file is. Now add a header to it if it doesn't already have one + // and add the +x bit. + + QString errorString; + if (!s_untrustedProgramHandler->makeServiceFileExecutable(d->m_service->entryPath(), errorString)) { + QString serviceName = d->m_service->name(); + if (serviceName.isEmpty()) { + serviceName = d->m_service->genericName(); + } + setError(KJob::UserDefinedError); + setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", + serviceName, errorString)); + return false; + } + } + return true; +} + void KIO::ApplicationLauncherJob::start() { + if (!checkAuthorizedExecution()) { + emitResult(); + return; + } + if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) { // We need to launch the application N times. // We ignore the result for application 2 to N. diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -62,6 +62,7 @@ dndpopupmenuplugin.cpp kurifiltersearchprovideractions.cpp renamefiledialog.cpp + widgetsuntrustedprogramhandler.cpp ) if (WIN32) list(APPEND kiowidgets_SRCS diff --git a/src/widgets/jobuidelegate.cpp b/src/widgets/jobuidelegate.cpp --- a/src/widgets/jobuidelegate.cpp +++ b/src/widgets/jobuidelegate.cpp @@ -23,6 +23,8 @@ #include "jobuidelegate.h" #include #include "kio_widgets_debug.h" +#include "kiogui_export.h" +#include "widgetsuntrustedprogramhandler.h" #include #include @@ -48,9 +50,16 @@ public: }; +namespace KIO { +KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface); +} + KIO::JobUiDelegate::JobUiDelegate() : d(new Private()) { + // KF6 TODO: remove, inherit from WidgetsUntrustedProgramHandler instead + static WidgetsUntrustedProgramHandler s_handler; + KIO::setDefaultUntrustedProgramHandler(&s_handler); } KIO::JobUiDelegate::~JobUiDelegate() diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -32,18 +32,12 @@ #include #include #include -#include -#include -#include -#include -#include #include #include #include #include #include #include -#include #include #include @@ -57,6 +51,8 @@ #include #include "kprocessrunner_p.h" // for KIOGuiPrivate::checkStartupNotify #include "applicationlauncherjob.h" +#include "jobuidelegate.h" +#include "widgetsuntrustedprogramhandler.h" #include #include @@ -74,7 +70,6 @@ #include #include #include -#include #if HAVE_X11 #include @@ -107,20 +102,6 @@ qEnvironmentVariableIsSet("SNAP"); } -qint64 KRunPrivate::runApplicationLauncherJob(KIO::ApplicationLauncherJob *job, QWidget *widget) -{ - QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); - QObject::connect(job, &KJob::result, receiver, [widget](KJob *job) { - if (job->error()) { - QEventLoopLocker locker; - KMessageBox::sorry(widget, job->errorString()); - } - }); - job->start(); - job->waitForStarted(); - return job->error() ? 0 : job->pid(); -} - qint64 KRunPrivate::runCommandLauncherJob(KIO::CommandLauncherJob *job, QWidget *widget) { QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); @@ -186,144 +167,6 @@ } } -// Simple QDialog that resizes the given text edit after being shown to more -// or less fit the enclosed text. -class SecureMessageDialog : public QDialog -{ - Q_OBJECT -public: - SecureMessageDialog(QWidget *parent) : QDialog(parent), m_textEdit(nullptr) - { - } - - void setTextEdit(QPlainTextEdit *textEdit) - { - m_textEdit = textEdit; - } - -protected: - void showEvent(QShowEvent *e) override - { - if (e->spontaneous()) { - return; - } - - // Now that we're shown, use our width to calculate a good - // bounding box for the text, and resize m_textEdit appropriately. - QDialog::showEvent(e); - - if (!m_textEdit) { - return; - } - - QSize fudge(20, 24); // About what it sounds like :-/ - - // Form rect with a lot of height for bounding. Use no more than - // 5 lines. - QRect curRect(m_textEdit->rect()); - QFontMetrics metrics(fontMetrics()); - curRect.setHeight(5 * metrics.lineSpacing()); - curRect.setWidth(qMax(curRect.width(), 300)); // At least 300 pixels ok? - - QString text(m_textEdit->toPlainText()); - curRect = metrics.boundingRect(curRect, Qt::TextWordWrap | Qt::TextSingleLine, text); - - // Scroll bars interfere. If we don't think there's enough room, enable - // the vertical scrollbar however. - m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - if (curRect.height() < m_textEdit->height()) { // then we've got room - m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_textEdit->setMaximumHeight(curRect.height() + fudge.height()); - } - - m_textEdit->setMinimumSize(curRect.size() + fudge); - m_textEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); - } - -private: - QPlainTextEdit *m_textEdit; -}; - -// Shows confirmation dialog whether an untrusted program should be run -// or not, since it may be potentially dangerous. -static int showUntrustedProgramWarning(const QString &programName, QWidget *window) -{ - SecureMessageDialog *baseDialog = new SecureMessageDialog(window); - baseDialog->setWindowTitle(i18nc("Warning about executing unknown program", "Warning")); - - QVBoxLayout *topLayout = new QVBoxLayout; - baseDialog->setLayout(topLayout); - - // Dialog will have explanatory text with a disabled lineedit with the - // Exec= to make it visually distinct. - QWidget *baseWidget = new QWidget(baseDialog); - QHBoxLayout *mainLayout = new QHBoxLayout(baseWidget); - - QLabel *iconLabel = new QLabel(baseWidget); - QPixmap warningIcon(KIconLoader::global()->loadIcon(QStringLiteral("dialog-warning"), KIconLoader::NoGroup, KIconLoader::SizeHuge)); - mainLayout->addWidget(iconLabel); - iconLabel->setPixmap(warningIcon); - - QVBoxLayout *contentLayout = new QVBoxLayout; - QString warningMessage = i18nc("program name follows in a line edit below", - "This will start the program:"); - - QLabel *message = new QLabel(warningMessage, baseWidget); - contentLayout->addWidget(message); - - QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); - textEdit->setPlainText(programName); - textEdit->setReadOnly(true); - contentLayout->addWidget(textEdit); - - QLabel *footerLabel = new QLabel(i18n("If you do not trust this program, click Cancel")); - contentLayout->addWidget(footerLabel); - contentLayout->addStretch(0); // Don't allow the text edit to expand - - mainLayout->addLayout(contentLayout); - - topLayout->addWidget(baseWidget); - baseDialog->setTextEdit(textEdit); - - QDialogButtonBox *buttonBox = new QDialogButtonBox(baseDialog); - buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::cont()); - buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); - buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, baseDialog, &QDialog::accept); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, baseDialog, &QDialog::reject); - topLayout->addWidget(buttonBox); - - // Constrain maximum size. Minimum size set in - // the dialog's show event. -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - const QSize screenSize = QApplication::screens().at(0)->size(); -#else - const QSize screenSize = baseDialog->screen()->size(); -#endif - baseDialog->resize(screenSize.width() / 4, 50); - baseDialog->setMaximumHeight(screenSize.height() / 3); - baseDialog->setMaximumWidth(screenSize.width() / 10 * 8); - - return baseDialog->exec(); -} - -// Helper function that attempts to set execute bit for given file. -static bool setExecuteBit(const QString &fileName, QString &errorString) -{ - QFile file(fileName); - - // corresponds to owner on unix, which will have to do since if the user - // isn't the owner we can't change perms anyways. - if (!file.setPermissions(QFile::ExeUser | file.permissions())) { - errorString = file.errorString(); - qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << errorString; - return false; - } - - return true; -} - bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { RunFlags flags = tempFile ? KRun::DeleteTemporaryFiles : RunFlags(); @@ -369,15 +212,15 @@ // show prompt asking user if he wants to run the program. if (!isFileExecutable && isNativeBinary) { canRun = false; - int result = showUntrustedProgramWarning(u.fileName(), window); - if (result == QDialog::Accepted) { + KIO::WidgetsUntrustedProgramHandler handler; + if (handler.showUntrustedProgramWarning(nullptr /*no job yet...*/, u.fileName())) { QString errorString; - if (!setExecuteBit(u.toLocalFile(), errorString)) { + if (!handler.setExecuteBit(u.toLocalFile(), errorString)) { KMessageBox::sorry( - window, - i18n("Unable to make file %1 executable.\n%2.", - u.toLocalFile(), errorString) - ); + window, + i18n("Unable to make file %1 executable.\n%2.", + u.toLocalFile(), errorString) + ); } else { canRun = true; } @@ -389,6 +232,7 @@ } if (canRun) { + qDebug() << "Execute the URL as a command"; return (KRun::runCommand(KShell::quoteArg(u.toLocalFile()), QString(), QString(), window, asn, u.adjusted(QUrl::RemoveFilename).toLocalFile())); // just execute the url as a command // ## TODO implement deleting the file if tempFile==true @@ -498,126 +342,6 @@ return KIOGuiPrivate::checkStartupNotify(service, silent_arg, wmclass_arg); } -// Helper function to make the given .desktop file executable by ensuring -// that a #!/usr/bin/env xdg-open line is added if necessary and the file has -// the +x bit set for the user. Returns false if either fails. -static bool makeServiceFileExecutable(const QString &fileName, QString &errorString) -{ - // Open the file and read the first two characters, check if it's - // #!. If not, create a new file, prepend appropriate lines, and copy - // over. - QFile desktopFile(fileName); - if (!desktopFile.open(QFile::ReadOnly)) { - errorString = desktopFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error opening service" << fileName << errorString; - return false; - } - - QByteArray header = desktopFile.peek(2); // First two chars of file - if (header.size() == 0) { - errorString = desktopFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error inspecting service" << fileName << errorString; - return false; // Some kind of error - } - - if (header != "#!") { - // Add header - QSaveFile saveFile; - saveFile.setFileName(fileName); - if (!saveFile.open(QIODevice::WriteOnly)) { - errorString = saveFile.errorString(); - qCWarning(KIO_WIDGETS) << "Unable to open replacement file for" << fileName << errorString; - return false; - } - - QByteArray shebang("#!/usr/bin/env xdg-open\n"); - if (saveFile.write(shebang) != shebang.size()) { - errorString = saveFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error occurred adding header for" << fileName << errorString; - saveFile.cancelWriting(); - return false; - } - - // Now copy the one into the other and then close and reopen desktopFile - QByteArray desktopData(desktopFile.readAll()); - if (desktopData.isEmpty()) { - errorString = desktopFile.errorString(); - qCWarning(KIO_WIDGETS) << "Unable to read service" << fileName << errorString; - saveFile.cancelWriting(); - return false; - } - - if (saveFile.write(desktopData) != desktopData.size()) { - errorString = saveFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error copying service" << fileName << errorString; - saveFile.cancelWriting(); - return false; - } - - desktopFile.close(); - if (!saveFile.commit()) { // Figures.... - errorString = saveFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error committing changes to service" << fileName << errorString; - return false; - } - - if (!desktopFile.open(QFile::ReadOnly)) { - errorString = desktopFile.errorString(); - qCWarning(KIO_WIDGETS) << "Error re-opening service" << fileName << errorString; - return false; - } - } // Add header - - return setExecuteBit(fileName, errorString); -} - -// Helper function to make a .desktop file executable if prompted by the user. -// returns true if KRun::run() should continue with execution, false if user declined -// to make the file executable or we failed to make it executable. -static bool makeServiceExecutable(const KService &service, QWidget *window) -{ - if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) { - qCWarning(KIO_WIDGETS) << "No authorization to execute " << service.entryPath(); - KMessageBox::sorry(window, i18n("You are not authorized to execute this service.")); - return false; // Don't circumvent the Kiosk - } - - // We can use KStandardDirs::findExe to resolve relative pathnames - // but that gets rid of the command line arguments. - QString program = QFileInfo(service.exec()).canonicalFilePath(); - if (program.isEmpty()) { // e.g. due to command line arguments - program = service.exec(); - } - - int result = showUntrustedProgramWarning(program, window); - if (result != QDialog::Accepted) { - return false; - } - - // Assume that service is an absolute path since we're being called (relative paths - // would have been allowed unless Kiosk said no, therefore we already know where the - // .desktop file is. Now add a header to it if it doesn't already have one - // and add the +x bit. - - QString errorString; - if (!::makeServiceFileExecutable(service.entryPath(), errorString)) { - QString serviceName = service.name(); - if (serviceName.isEmpty()) { - serviceName = service.genericName(); - } - - KMessageBox::sorry( - window, - i18n("Unable to make the service %1 executable, aborting execution.\n%2.", - serviceName, errorString) - ); - - return false; - } - - return true; -} - bool KRun::run(const KService &_service, const QList &_urls, QWidget *window, bool tempFiles, const QString &suggestedFileName, const QByteArray &asn) { @@ -629,12 +353,6 @@ RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { - if (!service.entryPath().isEmpty() && - !KDesktopFile::isAuthorizedDesktopFile(service.entryPath()) && - !::makeServiceExecutable(service, window)) { - return 0; - } - KService::Ptr servicePtr(new KService(service)); // clone // QTBUG-59017 Calling winId() on an embedded widget will break interaction // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using @@ -650,7 +368,10 @@ } job->setSuggestedFileName(suggestedFileName); job->setStartupId(asn); - return KRunPrivate::runApplicationLauncherJob(job, window); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, window)); + job->start(); + job->waitForStarted(); + return job->error() ? 0 : job->pid(); } qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, @@ -1418,4 +1139,3 @@ #include "moc_krun.cpp" #include "moc_krun_p.cpp" -#include "krun.moc" diff --git a/src/widgets/krun_p.h b/src/widgets/krun_p.h --- a/src/widgets/krun_p.h +++ b/src/widgets/krun_p.h @@ -58,7 +58,6 @@ const QString &suggestedFileName, const QByteArray &asn); #endif bool runExternalBrowser(const QString &_exec); - static qint64 runApplicationLauncherJob(KIO::ApplicationLauncherJob *job, QWidget *widget); static qint64 runCommandLauncherJob(KIO::CommandLauncherJob *job, QWidget *widget); void showPrompt(); diff --git a/src/widgets/widgetsuntrustedprogramhandler.h b/src/widgets/widgetsuntrustedprogramhandler.h new file mode 100644 --- /dev/null +++ b/src/widgets/widgetsuntrustedprogramhandler.h @@ -0,0 +1,50 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef WIDGETSUNTRUSTEDPROGRAMHANDLER_H +#define WIDGETSUNTRUSTEDPROGRAMHANDLER_H + +#include "untrustedprogramhandlerinterface.h" + +namespace KIO { + +// TODO KF6: Make KIO::JobUiDelegate inherit from WidgetsUntrustedProgramHandler +// (or even merge the two classes) +// so that setDelegate(new KIO::JobUiDelegate) provides both dialog boxes on error +// and the messagebox for handling untrusted programs. +// Then port those users of ApplicationLauncherJob which were setting a KDialogJobUiDelegate +// to set a KIO::JobUiDelegate instead. + +class WidgetsUntrustedProgramHandler : public UntrustedProgramHandlerInterface +{ +public: + WidgetsUntrustedProgramHandler(); + ~WidgetsUntrustedProgramHandler() override; + + bool showUntrustedProgramWarning(KJob *job, const QString &programName) override; + +private: + class Private; + Private *const d; +}; + +} + +#endif // WIDGETSUNTRUSTEDPROGRAMHANDLER_H diff --git a/src/widgets/widgetsuntrustedprogramhandler.cpp b/src/widgets/widgetsuntrustedprogramhandler.cpp new file mode 100644 --- /dev/null +++ b/src/widgets/widgetsuntrustedprogramhandler.cpp @@ -0,0 +1,171 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "widgetsuntrustedprogramhandler.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +KIO::WidgetsUntrustedProgramHandler::WidgetsUntrustedProgramHandler() + : KIO::UntrustedProgramHandlerInterface(), d(nullptr) +{ +} + +KIO::WidgetsUntrustedProgramHandler::~WidgetsUntrustedProgramHandler() +{ +} + +// Simple QDialog that resizes the given text edit after being shown to more +// or less fit the enclosed text. +class SecureMessageDialog : public QDialog +{ + Q_OBJECT +public: + SecureMessageDialog(QWidget *parent) : QDialog(parent), m_textEdit(nullptr) + { + } + + void setTextEdit(QPlainTextEdit *textEdit) + { + m_textEdit = textEdit; + } + +protected: + void showEvent(QShowEvent *e) override + { + if (e->spontaneous()) { + return; + } + + // Now that we're shown, use our width to calculate a good + // bounding box for the text, and resize m_textEdit appropriately. + QDialog::showEvent(e); + + if (!m_textEdit) { + return; + } + + QSize fudge(20, 24); // About what it sounds like :-/ + + // Form rect with a lot of height for bounding. Use no more than + // 5 lines. + QRect curRect(m_textEdit->rect()); + QFontMetrics metrics(fontMetrics()); + curRect.setHeight(5 * metrics.lineSpacing()); + curRect.setWidth(qMax(curRect.width(), 300)); // At least 300 pixels ok? + + QString text(m_textEdit->toPlainText()); + curRect = metrics.boundingRect(curRect, Qt::TextWordWrap | Qt::TextSingleLine, text); + + // Scroll bars interfere. If we don't think there's enough room, enable + // the vertical scrollbar however. + m_textEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + if (curRect.height() < m_textEdit->height()) { // then we've got room + m_textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_textEdit->setMaximumHeight(curRect.height() + fudge.height()); + } + + m_textEdit->setMinimumSize(curRect.size() + fudge); + m_textEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + } + +private: + QPlainTextEdit *m_textEdit; +}; + + +bool KIO::WidgetsUntrustedProgramHandler::showUntrustedProgramWarning(KJob *job, const QString &programName) +{ + QWidget *parentWidget = job ? KJobWidgets::window(job) : qApp->activeWindow(); + SecureMessageDialog *baseDialog = new SecureMessageDialog(parentWidget); + baseDialog->setWindowTitle(i18nc("Warning about executing unknown program", "Warning")); + + QVBoxLayout *topLayout = new QVBoxLayout; + baseDialog->setLayout(topLayout); + + // Dialog will have explanatory text with a disabled lineedit with the + // Exec= to make it visually distinct. + QWidget *baseWidget = new QWidget(baseDialog); + QHBoxLayout *mainLayout = new QHBoxLayout(baseWidget); + + QLabel *iconLabel = new QLabel(baseWidget); + const QIcon icon = baseDialog->style()->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, baseDialog); + const QPixmap warningIcon(icon.pixmap(KIconLoader::SizeHuge)); + mainLayout->addWidget(iconLabel); + iconLabel->setPixmap(warningIcon); + + QVBoxLayout *contentLayout = new QVBoxLayout; + QString warningMessage = i18nc("program name follows in a line edit below", + "This will start the program:"); + + QLabel *message = new QLabel(warningMessage, baseWidget); + contentLayout->addWidget(message); + + QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); + textEdit->setPlainText(programName); + textEdit->setReadOnly(true); + contentLayout->addWidget(textEdit); + + QLabel *footerLabel = new QLabel(i18n("If you do not trust this program, click Cancel")); + contentLayout->addWidget(footerLabel); + contentLayout->addStretch(0); // Don't allow the text edit to expand + + mainLayout->addLayout(contentLayout); + + topLayout->addWidget(baseWidget); + baseDialog->setTextEdit(textEdit); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(baseDialog); + buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::cont()); + buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); + buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, baseDialog, &QDialog::accept); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, baseDialog, &QDialog::reject); + topLayout->addWidget(buttonBox); + + // Constrain maximum size. Minimum size set in + // the dialog's show event. +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + const QSize screenSize = QApplication::screens().at(0)->size(); +#else + const QSize screenSize = baseDialog->screen()->size(); +#endif + baseDialog->resize(screenSize.width() / 4, 50); + baseDialog->setMaximumHeight(screenSize.height() / 3); + baseDialog->setMaximumWidth(screenSize.width() / 10 * 8); + + return baseDialog->exec() == QDialog::Accepted; +} + +#include "widgetsuntrustedprogramhandler.moc" diff --git a/tests/kruntest.cpp b/tests/kruntest.cpp --- a/tests/kruntest.cpp +++ b/tests/kruntest.cpp @@ -20,7 +20,7 @@ #include "kruntest.h" #include -#include +#include #include #include @@ -57,6 +57,7 @@ { "run(doesnotexit, no url)", "should show error message", "doesnotexist", nullptr }, { "run(doesnotexit, file url)", "should show error message", "doesnotexist", testFile }, { "run(doesnotexit, remote url)", "should use kioexec and show error message", "doesnotexist", "http://www.kde.org" }, + { "run(not-executable-desktopfile)", "should ask for confirmation", "nonexec", nullptr }, { "run(missing lib, no url)", "should show error message (remove libqca-qt5.so.2 for this, e.g. by editing LD_LIBRARY_PATH if qca is in its own prefix)", "qcatool-qt5", nullptr }, { "run(missing lib, file url)", "should show error message (remove libqca-qt5.so.2 for this, e.g. by editing LD_LIBRARY_PATH if qca is in its own prefix)", "qcatool-qt5", testFile }, { "run(missing lib, remote url)", "should show error message (remove libqca-qt5.so.2 for this, e.g. by editing LD_LIBRARY_PATH if qca is in its own prefix)", "qcatool-qt5", "http://www.kde.org" }, @@ -111,10 +112,25 @@ } urls << QUrl::fromUserInput(urlStr); } - KService::Ptr service(new KService("Some Name", s_tests[testNumber].exec, QString())); + KService::Ptr service; + if (QByteArray(s_tests[testNumber].exec) == "nonexec") { + const QString desktopFile = QFINDTESTDATA("../src/ioslaves/trash/kcmtrash.desktop"); + if (desktopFile.isEmpty()) { + qWarning() << "kcmtrash.desktop not found!"; + } + const QString dest = QStringLiteral("kcmtrash.desktop"); + QFile::remove(dest); + bool ok = QFile::copy(desktopFile, dest); + if (!ok) { + qWarning() << "Failed to copy" << desktopFile << "to" << dest; + } + service = KService::Ptr(new KService(QDir::currentPath() + QLatin1Char('/') + dest)); + } else { + service = KService::Ptr(new KService("Some Name", s_tests[testNumber].exec, QString())); + } auto *job = new KIO::ApplicationLauncherJob(service, this); job->setUrls(urls); - job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); job->start(); }