diff --git a/src/widgets/krun.h b/src/widgets/krun.h --- a/src/widgets/krun.h +++ b/src/widgets/krun.h @@ -506,12 +506,6 @@ */ virtual void slotStatResult(KJob *); - /** - * This slot is called when 'chmod' job has finished. - * It is only used when user asks to make a file executable for him. - */ - void slotChmodFinished(KJob *); - protected: /** * All following protected methods are used by subclasses of KRun! diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -167,6 +167,136 @@ } } +// 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 + { + // 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); + updateGeometry(); + } + +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. + QSize screenSize = QApplication::desktop()->screen()->size(); + 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) +{ + QFile desktopFile(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 (!desktopFile.setPermissions(QFile::ExeUser | desktopFile.permissions())) { + qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << desktopFile.errorString(); + return false; + } + + return true; +} + #ifndef KIOWIDGETS_NO_DEPRECATED bool KRun::runUrl(const QUrl &url, const QString &mimetype, QWidget *window, bool tempFile, bool runExecutables, const QString &suggestedFileName, const QByteArray &asn) { @@ -182,6 +312,7 @@ // This is called by foundMimeType, since it knows the mimetype of the URL bool KRun::runUrl(const QUrl &u, const QString &_mimetype, QWidget *window, RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) { + const QMimeDatabase db; const bool runExecutables = flags.testFlag(KRun::RunExecutables); const bool tempFile = flags.testFlag(KRun::DeleteTemporaryFiles); bool noRun = false; @@ -194,26 +325,52 @@ if (u.isLocalFile() && runExecutables) { return KDesktopFileActions::runWithStartup(u, true, asn); } - } else if (isExecutableFile(u, _mimetype)) { + } else if (isExecutable(_mimetype)) { + // Check whether file is executable script + const QMimeType mime = db.mimeTypeForName(_mimetype); + bool isTextFile = mime.inherits(QStringLiteral("text/plain")); + // Only run local files if (u.isLocalFile() && runExecutables) { if (KAuthorized::authorize(QStringLiteral("shell_access"))) { - 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 + + bool canRun = true; + bool isFileExecutable = isExecutableFile(u, _mimetype); + + // For executables that aren't scripts and without execute bit, + // show prompt asking user if he wants to run the program. + if (!isFileExecutable && !isTextFile) { + canRun = false; + int result = showUntrustedProgramWarning(u.fileName(), window); + if (result == QDialog::Accepted) { + if (!setExecuteBit(u.toLocalFile())) { + KMessageBox::sorry( + window, + i18n("Unable to make file %1 executable!", u.toLocalFile()) + ); + } else { + canRun = true; + } + } + } else if (!isFileExecutable && isTextFile) { + // Don't try to run scripts without execute bit, instead + // open them with default application + canRun = false; + } + + if (canRun) { + 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 + } + } else { + // Show no permission warning noAuth = true; } - } else if (_mimetype == QLatin1String("application/x-executable")) { - noRun = true; - } - } else if (isExecutable(_mimetype)) { - if (!runExecutables) { + } else if (!isTextFile) { + // Show warning for executables that aren't scripts noRun = true; } - - if (!KAuthorized::authorize(QStringLiteral("shell_access"))) { - noAuth = true; - } } if (noRun) { @@ -515,65 +672,10 @@ return urls; } -// 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 - { - // 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); - updateGeometry(); - } - -private: - QPlainTextEdit *m_textEdit; -}; - // 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 makeFileExecutable(const QString &fileName) +static bool makeServiceFileExecutable(const QString &fileName) { // Open the file and read the first two characters, check if it's // #!. If not, create a new file, prepend appropriate lines, and copy @@ -632,15 +734,7 @@ } } // Add header - // 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 (!desktopFile.setPermissions(QFile::ExeUser | desktopFile.permissions())) { - qCWarning(KIO_WIDGETS) << "Unable to change permissions for" << fileName << desktopFile.errorString(); - return false; - } - - // whew - return true; + return setExecuteBit(fileName); } // Helper function to make a .desktop file executable if prompted by the user. @@ -654,67 +748,14 @@ return false; // Don't circumvent the Kiosk } - SecureMessageDialog *baseDialog = new SecureMessageDialog(window); - baseDialog->setWindowTitle(i18nc("Warning about executing unknown .desktop file", "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); - // 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(); } - QPlainTextEdit *textEdit = new QPlainTextEdit(baseWidget); - textEdit->setPlainText(program); - 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. - QSize screenSize = QApplication::desktop()->screen()->size(); - baseDialog->resize(screenSize.width() / 4, 50); - baseDialog->setMaximumHeight(screenSize.height() / 3); - baseDialog->setMaximumWidth(screenSize.width() / 10 * 8); - - int result = baseDialog->exec(); + int result = showUntrustedProgramWarning(program, window); if (result != QDialog::Accepted) { return false; } @@ -724,7 +765,7 @@ // .desktop file is. Now add a header to it if it doesn't already have one // and add the +x bit. - if (!::makeFileExecutable(service.entryPath())) { + if (!::makeServiceFileExecutable(service.entryPath())) { QString serviceName = service.name(); if (serviceName.isEmpty()) { serviceName = service.genericName(); @@ -1302,26 +1343,6 @@ } } -void KRun::slotChmodFinished(KJob *job) -{ - d->m_job = nullptr; - const int errCode = job->error(); - if (errCode) { - // ERR_NO_CONTENT is not an error, but an indication no further - // actions needs to be taken. - if (errCode != KIO::ERR_NO_CONTENT) { - qCWarning(KIO_WIDGETS) << this << "ERROR (stat):" << job->error() << ' ' << job->errorString(); - handleError(job); - - d->m_bFault = true; - } - } else { - //If job was succesfull, then restart KRun for the same file. - KRun *rerun = new KRun(d->m_strURL, d->m_window, d->m_bProgressInfo, d->m_asn); - } - setFinished(true); -} - void KRun::mimeTypeDetermined(const QString &mimeType) { // foundMimeType reimplementations might show a dialog box; @@ -1386,31 +1407,6 @@ KRun::RunFlags runFlags; if (d->m_runExecutables) { runFlags |= KRun::RunExecutables; - //If file is executable but doesn't have +x permission, ask user if he wants to set +x - if (!isExecutableFile(d->m_strURL, type) && isExecutable(type)) { - KMessageBox::ButtonCode makeExecutable = KMessageBox::questionYesNo( - d->m_window, - i18n("File %1 is not marked as executable. Do you wish to run it?
" - "Keep in mind that if you don't know the source of this file, it may be unsafe to run it!", - d->m_strURL.toLocalFile()), - i18n("Run executable file"), - KGuiItem(i18n("Make executable and run")), - KGuiItem(i18n("Open")) - ); - //If the answer was Yes, start KIO::chmod job - if (makeExecutable == KMessageBox::ButtonCode::Yes) { - KFileItem binaryFile(d->m_strURL); - mode_t newPermissions = binaryFile.permissions() | S_IXUSR; - KIO::Job* chmodJob = KIO::chmod(d->m_strURL, newPermissions); - connect(chmodJob, &KJob::result, - this, &KRun::slotChmodFinished); - chmodJob->start(); - d->m_job = chmodJob; - //Stop further execution, it will later be restarted or abandoned after the job finishes - return; - } - //If the answer was No, then execute normally - } } if (!KRun::runUrl(d->m_strURL, type, d->m_window, runFlags, d->m_suggestedFileName, d->m_asn)) {