diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,6 +52,7 @@ # KTextEditor interface sources set(ktexteditor_LIB_SRCS # text buffer & buffer helpers +buffer/katesecuretextbuffer.cpp buffer/katetextbuffer.cpp buffer/katetextblock.cpp buffer/katetextline.cpp @@ -334,5 +335,17 @@ ecm_generate_pri_file(BASE_NAME KTextEditor LIB_NAME KF5TextEditor DEPS "KParts" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextEditor) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + +add_executable(katetextbuffer_helper + buffer/katesecuretextbuffer.cpp + buffer/katetextline.cpp) +target_link_libraries(katetextbuffer_helper + KF5::Archive + KF5::Auth +) +install(TARGETS katetextbuffer_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR} ) +kauth_install_helper_files(katetextbuffer_helper org.kde.ktexteditor.katetextbuffer root) +kauth_install_actions(org.kde.ktexteditor.katetextbuffer buffer/org.kde.ktexteditor.katetextbuffer.actions) + # add part add_subdirectory(part) diff --git a/src/buffer/katesecuretextbuffer.h b/src/buffer/katesecuretextbuffer.h new file mode 100644 --- /dev/null +++ b/src/buffer/katesecuretextbuffer.h @@ -0,0 +1,30 @@ +#ifndef KATE_SECURE_TEXTBUFFER_H +#define KATE_SECURE_TEXTBUFFER_H + +#include +#include +#include + +#include + +using namespace KAuth; + +class SecureTextBuffer : public QObject +{ + Q_OBJECT + +public: + + SecureTextBuffer() {} + + ~SecureTextBuffer() {} + + bool saveInternal(const QString &filename, const QString &mimeTypeForFilterDev, const QString &textCodec, const bool generateByteOrderMark, + const int endOfLineMode, const bool newLineAtEof, const QList &dataToSave); + +public Q_SLOTS: + ActionReply save(const QVariantMap &args); + +}; + +#endif diff --git a/src/buffer/katesecuretextbuffer.cpp b/src/buffer/katesecuretextbuffer.cpp new file mode 100644 --- /dev/null +++ b/src/buffer/katesecuretextbuffer.cpp @@ -0,0 +1,168 @@ +#include "katesecuretextbuffer.h" +#include "katetextline.h" + +#ifndef Q_OS_WIN +#include +#include + +// needed for umask application +#include +#include +#endif + +//#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +KAUTH_HELPER_MAIN("org.kde.ktexteditor.katetextbuffer", SecureTextBuffer) + +ActionReply SecureTextBuffer::save(const QVariantMap& args) +{ + const QString filename = args[QLatin1String("filename")].toString(); + const QString mimeTypeForFilterDev = args[QLatin1String("mimeTypeForFilterDev")].toString(); + const QString textCodec = args[QLatin1String("textCodec")].toString(); + const bool generateByteOrderMark = args[QLatin1String("generateByteOrderMark")].toBool(); + const int endOfLineMode = args[QLatin1String("endOfLineMode")].toInt(); + const bool newLineAtEof = args[QLatin1String("newLineAtEof")].toBool(); + const QList dataToSave = args[QLatin1String("dataToSave")].toList(); + + const bool ok = saveInternal(filename, mimeTypeForFilterDev, textCodec, generateByteOrderMark, endOfLineMode, newLineAtEof, dataToSave); + + return ok ? ActionReply::SuccessReply() : ActionReply::HelperErrorReply(); +} + +bool SecureTextBuffer::saveInternal(const QString &filename, const QString &mimeTypeForFilterDev, const QString &textCodec, + const bool generateByteOrderMark, const int endOfLineMode, const bool newLineAtEof, const QList &dataToSave) +{ +#ifndef Q_OS_WIN + const bool newFile = !QFile::exists(filename); + + /** + * Memorize owner and group. Due to design of QSaveFile we will have to re-set them after save is complete. + */ + uint ownerId = -2; + uint groupId = -2; + if (!newFile) { + QFileInfo fileInfo(filename); + ownerId = fileInfo.ownerId(); + groupId = fileInfo.groupId(); + } +#endif + + /** + * use QSaveFile for save write + rename + */ + QSaveFile saveFile(filename); + saveFile.setDirectWriteFallback(true); + + if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + + /** + * construct correct filter device and try to open + */ + KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(mimeTypeForFilterDev); + KCompressionDevice file(&saveFile, false, type); + + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + /** + * construct stream + disable Unicode headers + */ + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-16")); + + // set the correct codec + stream.setCodec(QTextCodec::codecForName(textCodec.toLatin1())); + + // generate byte order mark? + stream.setGenerateByteOrderMark(generateByteOrderMark); + + // our loved eol string ;) + QString eol = QStringLiteral("\n"); //m_doc->config()->eolString (); + if (endOfLineMode == 1) { + eol = QStringLiteral("\r\n"); + } else if (endOfLineMode == 2) { + eol = QStringLiteral("\r"); + } + + // just dump the lines out ;) + int lineCount = dataToSave.count(); + for (int i = 0; i < lineCount; ++i) { + // get line to save + Kate::TextLine textline(new Kate::TextLineData(dataToSave[i].toString())); + + stream << textline->text(); + + // append correct end of line string + if ((i + 1) < lineCount) { + stream << eol; + } + } + + if (newLineAtEof) { + Q_ASSERT(lineCount > 0); // see .h file + Kate::TextLine lastLine(new Kate::TextLineData(dataToSave[lineCount - 1].toString())); + const int firstChar = lastLine->firstChar(); + if (firstChar > -1 || lastLine->length() > 0) { + stream << eol; + } + } + + // flush stream + stream.flush(); + + // close and delete file + file.close(); + + // flush file + if (!saveFile.flush()) { + return false; + } + +#ifndef Q_OS_WIN + // ensure that the file is written to disk +#ifdef HAVE_FDATASYNC + fdatasync(saveFile.handle()); +#else + fsync(saveFile.handle()); +#endif +#endif + + // did save work? + // only finalize if stream status == OK + bool ok = (stream.status() == QTextStream::Ok) && saveFile.commit(); + +#ifndef Q_OS_WIN + if (ok) { + // QTemporaryFile sets permissions to 0600, so fixing this + if (newFile) { + const mode_t mask = umask(0); + umask(mask); + + const mode_t fileMode = 0666 & ~mask; + chmod(QFile::encodeName(filename).constData(), fileMode); + } + // ensure file has the same owner and group as before + if (!newFile && ownerId != (uint)-2 && groupId != (uint)-2) { + int chownResult = chown(QFile::encodeName(filename).constData(), ownerId, groupId); + // set at least correct group if owner cannot be changed + if (chownResult != 0 && errno == EPERM) { + chown(QFile::encodeName(filename).constData(), getuid(), groupId); + } + } + } +#endif + + return ok; +} diff --git a/src/buffer/katetextbuffer.cpp b/src/buffer/katetextbuffer.cpp --- a/src/buffer/katetextbuffer.cpp +++ b/src/buffer/katetextbuffer.cpp @@ -21,6 +21,7 @@ #include "config.h" #include "katetextbuffer.h" +#include "katesecuretextbuffer.h" #include "katetextloader.h" // this is unfortunate, but needed for performance @@ -763,111 +764,45 @@ // codec must be set! Q_ASSERT(m_textCodec); -#ifndef Q_OS_WIN - const bool newFile = !QFile::exists(filename); -#endif - - /** - * use QSaveFile for save write + rename - */ - QSaveFile saveFile(filename); - saveFile.setDirectWriteFallback(true); - - if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - return false; - } - - /** - * construct correct filter device and try to open - */ - KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); - KCompressionDevice file(&saveFile, false, type); - - if (!file.open(QIODevice::WriteOnly)) { - return false; - } - - /** - * construct stream + disable Unicode headers - */ - QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-16")); - - // set the correct codec - stream.setCodec(m_textCodec); - - // generate byte order mark? - stream.setGenerateByteOrderMark(generateByteOrderMark()); - - // our loved eol string ;) - QString eol = QStringLiteral("\n"); //m_doc->config()->eolString (); - if (endOfLineMode() == eolDos) { - eol = QStringLiteral("\r\n"); - } else if (endOfLineMode() == eolMac) { - eol = QStringLiteral("\r"); - } - - // just dump the lines out ;) + // prepare parameters for calling helper method + const QString textCodec = QString::fromLatin1(m_textCodec->name()); + const int eolMode = static_cast(endOfLineMode()); + QList dataToSave; for (int i = 0; i < m_lines; ++i) { - // get line to save - Kate::TextLine textline = line(i); - - stream << textline->text(); - - // append correct end of line string - if ((i + 1) < m_lines) { - stream << eol; - } + const Kate::TextLine textline = line(i); + const QString line = textline->string(); + dataToSave.push_back(QVariant(line)); } - if (m_newLineAtEof) { - Q_ASSERT(m_lines > 0); // see .h file - const Kate::TextLine lastLine = line(m_lines - 1); - const int firstChar = lastLine->firstChar(); - if (firstChar > -1 || lastLine->length() > 0) { - stream << eol; - } - } - - // flush stream - stream.flush(); + // trying regular save + bool ok = SecureTextBuffer().saveInternal(filename, m_mimeTypeForFilterDev, textCodec, generateByteOrderMark(), eolMode, m_newLineAtEof, dataToSave); - // close and delete file - file.close(); + // save was not successful, trying with elevated privileges + if (!ok) { - // flush file - if (!saveFile.flush()) { - return false; - } + // prepare parameter map for helper + QVariantMap args; + args[QLatin1String("filename")] = filename; + args[QLatin1String("mimeTypeForFilterDev")] = m_mimeTypeForFilterDev; + args[QLatin1String("textCodec")] = textCodec; + args[QLatin1String("generateByteOrderMark")] = generateByteOrderMark(); + args[QLatin1String("endOfLineMode")] = eolMode; + args[QLatin1String("newLineAtEof")] = m_newLineAtEof; + args[QLatin1String("dataToSave")] = dataToSave; -#ifndef Q_OS_WIN - // ensure that the file is written to disk -#ifdef HAVE_FDATASYNC - fdatasync(saveFile.handle()); -#else - fsync(saveFile.handle()); -#endif -#endif + // call save with elevated privileges + KAuth::Action saveAction(QLatin1String("org.kde.ktexteditor.katetextbuffer.save")); + saveAction.setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); + saveAction.setArguments(args); + KAuth::ExecuteJob *job = saveAction.execute(); + ok = job->exec(); - // did save work? - // only finalize if stream status == OK - bool ok = (stream.status() == QTextStream::Ok) && saveFile.commit(); + } - // remember this revision as last saved if we had success! if (ok) { m_history.setLastSavedRevision(); } -#ifndef Q_OS_WIN - if (ok && newFile) { // QTemporaryFile sets permissions to 0600, so fixing this - const mode_t mask = umask(0); - umask(mask); - - const mode_t fileMode = 0666 & ~mask; - chmod(QFile::encodeName(filename).constData(), fileMode); - } -#endif - // report CODEC + ERRORS BUFFER_DEBUG << "Saved file " << filename << "with codec" << m_textCodec->name() << (ok ? "without" : "with") << "errors"; diff --git a/src/buffer/org.kde.ktexteditor.katetextbuffer.actions b/src/buffer/org.kde.ktexteditor.katetextbuffer.actions new file mode 100644 --- /dev/null +++ b/src/buffer/org.kde.ktexteditor.katetextbuffer.actions @@ -0,0 +1,10 @@ +[Domain] +Name=Document Actions +Policy=auth_admin +Persistence=session + +[org.kde.ktexteditor.katetextbuffer.save] +Name=Save Document +Description=Root privileges are needed to save this document +Policy=auth_admin +Persistence=session