diff --git a/autotests/src/katetextbuffertest.cpp b/autotests/src/katetextbuffertest.cpp index fc55e64f..bf252497 100644 --- a/autotests/src/katetextbuffertest.cpp +++ b/autotests/src/katetextbuffertest.cpp @@ -1,468 +1,502 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * Copyright (C) 2010 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) 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 Library 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 #include "katetextbuffertest.h" #include "katetextbuffer.h" #include "katetextcursor.h" #include "katetextfolding.h" QTEST_MAIN(KateTextBufferTest) KateTextBufferTest::KateTextBufferTest() : QObject() { KTextEditor::EditorPrivate::enableUnitTestMode(); } KateTextBufferTest::~KateTextBufferTest() { } void KateTextBufferTest::basicBufferTest() { // construct an empty text buffer Kate::TextBuffer buffer(nullptr, 1); // one line per default QVERIFY(buffer.lines() == 1); QVERIFY(buffer.text() == QString()); //FIXME: use QTestLib macros for checking the correct state // start editing buffer.startEditing(); // end editing buffer.finishEditing(); } void KateTextBufferTest::wrapLineTest() { // construct an empty text buffer Kate::TextBuffer buffer(nullptr, 1); // wrap first empty line -> we should have two empty lines buffer.startEditing(); buffer.wrapLine(KTextEditor::Cursor(0, 0)); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Two empty lines")); QVERIFY(buffer.text() == QLatin1String("\n")); // unwrap again -> only one empty line buffer.startEditing(); buffer.unwrapLine(1); buffer.finishEditing(); // print debug buffer.debugPrint(QLatin1String("Empty Buffer")); QVERIFY(buffer.text() == QString()); } void KateTextBufferTest::insertRemoveTextTest() { // construct an empty text buffer Kate::TextBuffer buffer(nullptr, 1); // wrap first line buffer.startEditing(); buffer.wrapLine(KTextEditor::Cursor(0, 0)); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Two empty lines")); QVERIFY(buffer.text() == QLatin1String("\n")); // remember second line Kate::TextLine second = buffer.line(1); // unwrap second line buffer.startEditing(); buffer.unwrapLine(1); buffer.finishEditing(); buffer.debugPrint(QLatin1String("One empty line")); QVERIFY(buffer.text() == QString()); // second text line should be still there //const QString &secondText = second->text (); //QVERIFY (secondText == "") // insert text buffer.startEditing(); buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("testremovetext")); buffer.finishEditing(); buffer.debugPrint(QLatin1String("One line")); QVERIFY(buffer.text() == QLatin1String("testremovetext")); // remove text buffer.startEditing(); buffer.removeText(KTextEditor::Range(KTextEditor::Cursor(0, 4), KTextEditor::Cursor(0, 10))); buffer.finishEditing(); buffer.debugPrint(QLatin1String("One line")); QVERIFY(buffer.text() == QLatin1String("testtext")); // wrap text buffer.startEditing(); buffer.wrapLine(KTextEditor::Cursor(0, 2)); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Two line")); QVERIFY(buffer.text() == QLatin1String("te\nsttext")); // unwrap text buffer.startEditing(); buffer.unwrapLine(1); buffer.finishEditing(); buffer.debugPrint(QLatin1String("One line")); QVERIFY(buffer.text() == QLatin1String("testtext")); } void KateTextBufferTest::cursorTest() { // last buffer content, for consistence checks QString lastBufferContent; // test with different block sizes for (int i = 1; i <= 4; ++i) { // construct an empty text buffer Kate::TextBuffer buffer(nullptr, i); // wrap first line buffer.startEditing(); buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("sfdfjdsklfjlsdfjlsdkfjskldfjklsdfjklsdjkfl")); buffer.wrapLine(KTextEditor::Cursor(0, 8)); buffer.wrapLine(KTextEditor::Cursor(1, 8)); buffer.wrapLine(KTextEditor::Cursor(2, 8)); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Cursor buffer")); // construct cursor Kate::TextCursor *cursor1 = new Kate::TextCursor(buffer, KTextEditor::Cursor(0, 0), Kate::TextCursor::MoveOnInsert); QVERIFY(cursor1->toCursor() == KTextEditor::Cursor(0, 0)); //printf ("cursor %d, %d\n", cursor1->line(), cursor1->column()); //Kate::TextCursor *cursor2 = new Kate::TextCursor (buffer, KTextEditor::Cursor (1, 8), Kate::TextCursor::MoveOnInsert); //printf ("cursor %d, %d\n", cursor2->line(), cursor2->column()); //Kate::TextCursor *cursor3 = new Kate::TextCursor (buffer, KTextEditor::Cursor (0, 123), Kate::TextCursor::MoveOnInsert); //printf ("cursor %d, %d\n", cursor3->line(), cursor3->column()); //Kate::TextCursor *cursor4 = new Kate::TextCursor (buffer, KTextEditor::Cursor (1323, 1), Kate::TextCursor::MoveOnInsert); //printf ("cursor %d, %d\n", cursor4->line(), cursor4->column()); // insert text buffer.startEditing(); buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("hallo")); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Cursor buffer")); //printf ("cursor %d, %d\n", cursor1->line(), cursor1->column()); QVERIFY(cursor1->toCursor() == KTextEditor::Cursor(0, 5)); // remove text buffer.startEditing(); buffer.removeText(KTextEditor::Range(KTextEditor::Cursor(0, 4), KTextEditor::Cursor(0, 10))); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Cursor buffer")); //printf ("cursor %d, %d\n", cursor1->line(), cursor1->column()); QVERIFY(cursor1->toCursor() == KTextEditor::Cursor(0, 4)); // wrap line buffer.startEditing(); buffer.wrapLine(KTextEditor::Cursor(0, 3)); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Cursor buffer")); //printf ("cursor %d, %d\n", cursor1->line(), cursor1->column()); QVERIFY(cursor1->toCursor() == KTextEditor::Cursor(1, 1)); // unwrap line buffer.startEditing(); buffer.unwrapLine(1); buffer.finishEditing(); buffer.debugPrint(QLatin1String("Cursor buffer")); //printf ("cursor %d, %d\n", cursor1->line(), cursor1->column()); QVERIFY(cursor1->toCursor() == KTextEditor::Cursor(0, 4)); // verify content if (i > 1) { QVERIFY(lastBufferContent == buffer.text()); } // remember content lastBufferContent = buffer.text(); } } void KateTextBufferTest::foldingTest() { // construct an empty text buffer & folding info Kate::TextBuffer buffer(nullptr, 1); Kate::TextFolding folding(buffer); // insert some text buffer.startEditing(); for (int i = 0; i < 100; ++i) { buffer.insertText(KTextEditor::Cursor(i, 0), QLatin1String("1234567890")); if (i < 99) { buffer.wrapLine(KTextEditor::Cursor(i, 10)); } } buffer.finishEditing(); QVERIFY(buffer.lines() == 100); // starting with empty folding! folding.debugPrint(QLatin1String("Empty Folding")); QVERIFY(folding.debugDump() == QLatin1String("tree - folded ")); // check visibility QVERIFY(folding.isLineVisible(0)); QVERIFY(folding.isLineVisible(99)); // all visible QVERIFY(folding.visibleLines() == 100); // we shall be able to insert new range QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(5, 0), KTextEditor::Cursor(10, 0))) == 0); // we shall have now exactly one range toplevel, that is not folded! folding.debugPrint(QLatin1String("One Toplevel Fold")); QVERIFY(folding.debugDump() == QLatin1String("tree [5:0 10:0] - folded ")); // fold the range! QVERIFY(folding.foldRange(0)); folding.debugPrint(QLatin1String("One Toplevel Fold - Folded")); QVERIFY(folding.debugDump() == QLatin1String("tree [5:0 f 10:0] - folded [5:0 f 10:0]")); // check visibility QVERIFY(folding.isLineVisible(5)); for (int i = 6; i <= 10; ++i) { QVERIFY(!folding.isLineVisible(i)); } QVERIFY(folding.isLineVisible(11)); // 5 lines are hidden QVERIFY(folding.visibleLines() == (100 - 5)); // check line mapping QVERIFY(folding.visibleLineToLine(5) == 5); for (int i = 6; i <= 50; ++i) { QVERIFY(folding.visibleLineToLine(i) == (i + 5)); } // there shall be one range starting at 5 QVector > forLine = folding.foldingRangesStartingOnLine(5); QVERIFY(forLine.size() == 1); QVERIFY(forLine[0].first == 0); QVERIFY(forLine[0].second & Kate::TextFolding::Folded); // we shall be able to insert new range QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(20, 0), KTextEditor::Cursor(30, 0)), Kate::TextFolding::Folded) == 1); // we shall have now exactly two range toplevel folding.debugPrint(QLatin1String("Two Toplevel Folds")); QVERIFY(folding.debugDump() == QLatin1String("tree [5:0 f 10:0] [20:0 f 30:0] - folded [5:0 f 10:0] [20:0 f 30:0]")); // check visibility QVERIFY(folding.isLineVisible(5)); for (int i = 6; i <= 10; ++i) { QVERIFY(!folding.isLineVisible(i)); } QVERIFY(folding.isLineVisible(11)); QVERIFY(folding.isLineVisible(20)); for (int i = 21; i <= 30; ++i) { QVERIFY(!folding.isLineVisible(i)); } QVERIFY(folding.isLineVisible(31)); // 15 lines are hidden QVERIFY(folding.visibleLines() == (100 - 5 - 10)); // check line mapping QVERIFY(folding.visibleLineToLine(5) == 5); for (int i = 6; i <= 15; ++i) { QVERIFY(folding.visibleLineToLine(i) == (i + 5)); } for (int i = 16; i <= 50; ++i) { QVERIFY(folding.visibleLineToLine(i) == (i + 15)); } // check line mapping QVERIFY(folding.lineToVisibleLine(5) == 5); for (int i = 11; i <= 20; ++i) { QVERIFY(folding.lineToVisibleLine(i) == (i - 5)); } for (int i = 31; i <= 40; ++i) { QVERIFY(folding.lineToVisibleLine(i) == (i - 15)); } // there shall be one range starting at 20 forLine = folding.foldingRangesStartingOnLine(20); QVERIFY(forLine.size() == 1); QVERIFY(forLine[0].first == 1); QVERIFY(forLine[0].second & Kate::TextFolding::Folded); // this shall fail to be inserted, as it badly overlaps with the first range! QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(6, 0), KTextEditor::Cursor(15, 0)), Kate::TextFolding::Folded) == -1); // this shall fail to be inserted, as it badly overlaps with the second range! QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(15, 0), KTextEditor::Cursor(25, 0)), Kate::TextFolding::Folded) == -1); // we shall still have now exactly two range toplevel folding.debugPrint(QLatin1String("Still Two Toplevel Folds")); QVERIFY(folding.debugDump() == QLatin1String("tree [5:0 f 10:0] [20:0 f 30:0] - folded [5:0 f 10:0] [20:0 f 30:0]")); // still 15 lines are hidden QVERIFY(folding.visibleLines() == (100 - 5 - 10)); // we shall be able to insert new range, should lead to nested folds! QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(15, 0), KTextEditor::Cursor(35, 0)), Kate::TextFolding::Folded) == 2); // we shall have now exactly two range toplevel and one embedded fold folding.debugPrint(QLatin1String("Two Toplevel Folds, One Nested Fold")); QVERIFY(folding.debugDump() == QLatin1String("tree [5:0 f 10:0] [15:0 f [20:0 f 30:0] 35:0] - folded [5:0 f 10:0] [15:0 f 35:0]")); // 25 lines are hidden QVERIFY(folding.visibleLines() == (100 - 5 - 20)); // check line mapping QVERIFY(folding.lineToVisibleLine(5) == 5); for (int i = 11; i <= 15; ++i) { QVERIFY(folding.lineToVisibleLine(i) == (i - 5)); } // special case: hidden lines, should fall ack to last visible one! for (int i = 16; i <= 35; ++i) { QVERIFY(folding.lineToVisibleLine(i) == 10); } for (int i = 36; i <= 40; ++i) { QVERIFY(folding.lineToVisibleLine(i) == (i - 25)); } // we shall be able to insert new range, should lead to nested folds! QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(0, 0), KTextEditor::Cursor(50, 0)), Kate::TextFolding::Folded) == 3); // we shall have now exactly one range toplevel and many embedded fold folding.debugPrint(QLatin1String("One Toplevel + Embedded Folds")); QVERIFY(folding.debugDump() == QLatin1String("tree [0:0 f [5:0 f 10:0] [15:0 f [20:0 f 30:0] 35:0] 50:0] - folded [0:0 f 50:0]")); // there shall still be one range starting at 20 forLine = folding.foldingRangesStartingOnLine(20); QVERIFY(forLine.size() == 1); QVERIFY(forLine[0].first == 1); QVERIFY(forLine[0].second & Kate::TextFolding::Folded); // add more regions starting at 20 QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(20, 5), KTextEditor::Cursor(24, 0)), Kate::TextFolding::Folded) == 4); QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(20, 3), KTextEditor::Cursor(25, 0)), Kate::TextFolding::Folded) == 5); folding.debugPrint(QLatin1String("More ranges at 20")); // there shall still be three ranges starting at 20 forLine = folding.foldingRangesStartingOnLine(20); QVERIFY(forLine.size() == 3); QVERIFY(forLine[0].first == 1); QVERIFY(forLine[0].second & Kate::TextFolding::Folded); QVERIFY(forLine[1].first == 5); QVERIFY(forLine[1].second & Kate::TextFolding::Folded); QVERIFY(forLine[2].first == 4); QVERIFY(forLine[2].second & Kate::TextFolding::Folded); // 50 lines are hidden QVERIFY(folding.visibleLines() == (100 - 50)); // save state QJsonDocument folds = folding.exportFoldingRanges(); QString textDump = folding.debugDump(); // clear folds folding.clear(); QVERIFY(folding.debugDump() == QLatin1String("tree - folded ")); // restore state folding.importFoldingRanges(folds); QVERIFY(folding.debugDump() == textDump); } void KateTextBufferTest::nestedFoldingTest() { // construct an empty text buffer & folding info Kate::TextBuffer buffer(nullptr, 1); Kate::TextFolding folding(buffer); // insert two nested folds in 5 lines buffer.startEditing(); for (int i = 0; i < 4; ++i) { buffer.wrapLine(KTextEditor::Cursor(0, 0)); } buffer.finishEditing(); QVERIFY(buffer.lines() == 5); // folding for line 1 QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(0, 0), KTextEditor::Cursor(3, 0)), Kate::TextFolding::Folded) == 0); QVERIFY(folding.newFoldingRange(KTextEditor::Range(KTextEditor::Cursor(1, 0), KTextEditor::Cursor(2, 0)), Kate::TextFolding::Folded) == 1); QVERIFY(folding.foldRange(1)); QVERIFY(folding.foldRange(0)); QVERIFY(folding.unfoldRange(0)); QVERIFY(folding.unfoldRange(1)); } void KateTextBufferTest::saveFileInUnwritableFolder() { // create temp dir and get file name inside QTemporaryDir dir; QVERIFY(dir.isValid()); const QString folder_name = dir.path(); const QString file_path = folder_name + QLatin1String("/foo"); QFile f(file_path); QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); f.write("1234567890"); QVERIFY(f.flush()); f.close(); QFile::setPermissions(folder_name, QFile::ExeOwner); Kate::TextBuffer buffer(nullptr, 1); buffer.setTextCodec(QTextCodec::codecForName("UTF-8")); buffer.setFallbackTextCodec(QTextCodec::codecForName("UTF-8")); bool a, b; int c; buffer.load(file_path, a, b, c, true); buffer.clear(); buffer.startEditing(); buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("ABC")); buffer.finishEditing(); qDebug() << buffer.text(); buffer.save(file_path); f.open(QIODevice::ReadOnly); QCOMPARE(f.readAll(), QByteArray("ABC")); f.close(); QFile::setPermissions(folder_name, QFile::WriteOwner | QFile::ExeOwner); QVERIFY(f.remove()); QVERIFY(dir.remove()); } + +void KateTextBufferTest::saveFileWithElevatedPrivileges() +{ + // create temp dir and get file name inside + QTemporaryDir dir; + QVERIFY(dir.isValid()); + const QString file_path = dir.path() + QLatin1String("/foo"); + + QFile f(file_path); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); + f.write("1234567890"); + QVERIFY(f.flush()); + f.close(); + + Kate::TextBuffer buffer(nullptr, 1, true); + buffer.setTextCodec(QTextCodec::codecForName("UTF-8")); + buffer.setFallbackTextCodec(QTextCodec::codecForName("UTF-8")); + bool a, b; + int c; + buffer.load(file_path, a, b, c, true); + buffer.clear(); + buffer.startEditing(); + buffer.insertText(KTextEditor::Cursor(0, 0), QLatin1String("ABC")); + buffer.finishEditing(); + qDebug() << buffer.text(); + buffer.save(file_path); + + f.open(QIODevice::ReadOnly); + QCOMPARE(f.readAll(), QByteArray("ABC")); + f.close(); + + QVERIFY(f.remove()); + QVERIFY(dir.remove()); +} diff --git a/autotests/src/katetextbuffertest.h b/autotests/src/katetextbuffertest.h index 0ddcff08..7a3df3c1 100644 --- a/autotests/src/katetextbuffertest.h +++ b/autotests/src/katetextbuffertest.h @@ -1,45 +1,46 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * Copyright (C) 2010 Dominik Haumann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) 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 Library 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 KATEBUFFERTEST_H #define KATEBUFFERTEST_H #include #include class KateTextBufferTest : public QObject { Q_OBJECT public: KateTextBufferTest(); virtual ~KateTextBufferTest(); private Q_SLOTS: void basicBufferTest(); void wrapLineTest(); void insertRemoveTextTest(); void cursorTest(); void foldingTest(); void nestedFoldingTest(); void saveFileInUnwritableFolder(); + void saveFileWithElevatedPrivileges(); }; #endif // KATEBUFFERTEST_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8f414b07..b7adedca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,338 +1,348 @@ # handle data files, .desktop & .cmake add_subdirectory(data) # jscripts for the part add_subdirectory( script/data ) # set right defines for libgit2 usage if(LIBGIT2_FOUND) add_definitions(-DLIBGIT2_FOUND) SET (CMAKE_REQUIRED_LIBRARIES LibGit2::LibGit2) set (KTEXTEDITOR_OPTIONAL_LIBS ${KTEXTEDITOR_OPTIONAL_LIBS} LibGit2::LibGit2) endif() if(EditorConfig_FOUND) add_definitions(-DEDITORCONFIG_FOUND) SET (CMAKE_REQUIRED_LIBRARIES editorconfig) set (KTEXTEDITOR_OPTIONAL_LIBS ${KTEXTEDITOR_OPTIONAL_LIBS} editorconfig) endif() # handle include files, both normal ones and generated ones add_subdirectory(include) # includes include_directories( # for config.h ${CMAKE_BINARY_DIR} # for generated ktexteditor headers ${CMAKE_CURRENT_BINARY_DIR}/include # for normal sources ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include/ktexteditor ${CMAKE_CURRENT_SOURCE_DIR}/buffer ${CMAKE_CURRENT_SOURCE_DIR}/completion ${CMAKE_CURRENT_SOURCE_DIR}/dialogs ${CMAKE_CURRENT_SOURCE_DIR}/document ${CMAKE_CURRENT_SOURCE_DIR}/script ${CMAKE_CURRENT_SOURCE_DIR}/mode ${CMAKE_CURRENT_SOURCE_DIR}/render ${CMAKE_CURRENT_SOURCE_DIR}/search ${CMAKE_CURRENT_SOURCE_DIR}/syntax ${CMAKE_CURRENT_SOURCE_DIR}/schema ${CMAKE_CURRENT_SOURCE_DIR}/undo ${CMAKE_CURRENT_SOURCE_DIR}/utils ${CMAKE_CURRENT_SOURCE_DIR}/inputmode ${CMAKE_CURRENT_SOURCE_DIR}/view ${CMAKE_CURRENT_SOURCE_DIR}/swapfile ${CMAKE_CURRENT_SOURCE_DIR}/variableeditor) # KTextEditor interface sources set(ktexteditor_LIB_SRCS # text buffer & buffer helpers +buffer/katesecuretextbuffer.cpp buffer/katetextbuffer.cpp buffer/katetextblock.cpp buffer/katetextline.cpp buffer/katetextcursor.cpp buffer/katetextrange.cpp buffer/katetexthistory.cpp buffer/katetextfolding.cpp # completion (widget, model, delegate, ...) completion/katecompletionwidget.cpp completion/katecompletionmodel.cpp completion/katecompletiontree.cpp completion/katecompletionconfig.cpp completion/kateargumenthinttree.cpp completion/kateargumenthintmodel.cpp completion/katecompletiondelegate.cpp completion/expandingtree/expandingwidgetmodel.cpp completion/expandingtree/expandingdelegate.cpp completion/expandingtree/expandingtree.cpp # simple internal word completion completion/katewordcompletion.cpp # internal syntax-file based keyword completion completion/katekeywordcompletion.cpp # dialogs dialogs/kateconfigpage.cpp dialogs/katedialogs.cpp dialogs/kateconfigpage.cpp # document (THE document, buffer, lines/cursors/..., CORE STUFF) document/katedocument.cpp document/katebuffer.cpp # undo undo/kateundo.cpp undo/katemodifiedundo.cpp undo/kateundomanager.cpp # scripting script/katescript.cpp script/kateindentscript.cpp script/katecommandlinescript.cpp script/katescriptmanager.cpp script/katescriptaction.cpp # scripting wrappers script/katescriptdocument.cpp script/katescriptview.cpp script/katescripthelpers.cpp # mode (modemanager and co) mode/katemodemanager.cpp mode/katemodeconfigpage.cpp mode/katemodemenu.cpp mode/katewildcardmatcher.cpp # modeline variable editor variableeditor/variablelineedit.cpp variableeditor/variablelistview.cpp variableeditor/variableeditor.cpp variableeditor/variableitem.cpp variableeditor/katehelpbutton.cpp # printing classes printing/kateprinter.cpp printing/printpainter.cpp printing/printconfigwidgets.cpp # rendering stuff (katerenderer and helpers) render/katerenderer.cpp render/katerenderrange.cpp render/katelayoutcache.cpp render/katetextlayout.cpp render/katelinelayout.cpp # search stuff search/kateregexp.cpp search/kateplaintextsearch.cpp search/kateregexpsearch.cpp search/katematch.cpp search/katesearchbar.cpp # syntax related stuff (highlighting, xml file parsing, ...) syntax/katesyntaxmanager.cpp syntax/katehighlight.cpp syntax/katehighlighthelpers.cpp syntax/katehighlightmenu.cpp syntax/katesyntaxdocument.cpp syntax/katehighlightingcmds.cpp # view stuff (THE view and its helpers) view/kateview.cpp view/kateviewinternal.cpp view/kateviewhelpers.cpp view/katemessagewidget.cpp view/katefadeeffect.cpp view/kateanimation.cpp view/katetextanimation.cpp view/katetextpreview.cpp view/katestatusbar.cpp view/wordcounter.cpp # spell checking spellcheck/prefixstore.h spellcheck/prefixstore.cpp spellcheck/ontheflycheck.h spellcheck/ontheflycheck.cpp spellcheck/spellcheck.h spellcheck/spellcheck.cpp spellcheck/spellcheckdialog.h spellcheck/spellcheckdialog.cpp spellcheck/spellcheckbar.cpp spellcheck/spellingmenu.h spellcheck/spellingmenu.cpp # generic stuff, unsorted... utils/katecmds.cpp utils/kateconfig.cpp utils/katebookmarks.cpp utils/kateautoindent.cpp utils/katetemplatehandler.cpp utils/kateglobal.cpp utils/katecmd.cpp utils/ktexteditor.cpp utils/document.cpp utils/range.cpp utils/documentcursor.cpp utils/attribute.cpp utils/codecompletioninterface.cpp utils/codecompletionmodel.cpp utils/codecompletionmodelcontrollerinterface.cpp utils/configinterface.cpp utils/movinginterface.cpp utils/movingcursor.cpp utils/movingrange.cpp utils/movingrangefeedback.cpp utils/messageinterface.cpp utils/application.cpp utils/mainwindow.cpp utils/katedefaultcolors.cpp utils/katecommandrangeexpressionparser.cpp utils/katesedcmd.cpp # schema schema/kateschema.cpp schema/kateschemaconfig.cpp schema/katestyletreewidget.cpp schema/katecolortreewidget.cpp schema/katecategorydrawer.cpp # swapfile swapfile/kateswapdiffcreator.cpp swapfile/kateswapfile.cpp # export as HTML export/exporter.cpp export/htmlexporter.cpp # input modes inputmode/kateabstractinputmode.cpp inputmode/kateabstractinputmodefactory.cpp inputmode/katenormalinputmode.cpp inputmode/katenormalinputmodefactory.cpp ) # optionally compile with EditorConfig support if(EditorConfig_FOUND) set(ktexteditor_LIB_SRCS ${ktexteditor_LIB_SRCS} document/editorconfig.cpp) endif() ki18n_wrap_ui(ktexteditor_LIB_SRCS dialogs/textareaappearanceconfigwidget.ui dialogs/bordersappearanceconfigwidget.ui dialogs/commandmenuconfigwidget.ui dialogs/commandmenueditwidget.ui dialogs/completionconfigtab.ui dialogs/navigationconfigwidget.ui dialogs/editconfigwidget.ui dialogs/filetypeconfigwidget.ui dialogs/indentationconfigwidget.ui dialogs/opensaveconfigwidget.ui dialogs/opensaveconfigadvwidget.ui dialogs/completionconfigwidget.ui search/searchbarincremental.ui search/searchbarpower.ui spellcheck/spellcheckbar.ui dialogs/spellcheckconfigwidget.ui schema/howtoimportschema.ui ) # add the resource files, the one with mascot + ui file and the generated ones qt5_add_resources( ktexteditor_LIB_SRCS data/ktexteditor.qrc "${CMAKE_CURRENT_BINARY_DIR}/script/data/script.qrc") if (BUILD_VIMODE) ki18n_wrap_ui(ktexteditor_LIB_SRCS vimode/config/configwidget.ui) set(ktexteditor_LIB_SRCS ${ktexteditor_LIB_SRCS} inputmode/kateviinputmode.cpp inputmode/kateviinputmodefactory.cpp # vi input mode vimode/config/configtab.cpp vimode/modes/insertvimode.cpp vimode/modes/modebase.cpp vimode/modes/normalvimode.cpp vimode/modes/replacevimode.cpp vimode/modes/visualvimode.cpp vimode/appcommands.cpp vimode/cmds.cpp vimode/inputmodemanager.cpp vimode/command.cpp vimode/motion.cpp vimode/range.cpp vimode/keyparser.cpp vimode/globalstate.cpp vimode/emulatedcommandbar/emulatedcommandbar.cpp vimode/emulatedcommandbar/matchhighlighter.cpp vimode/emulatedcommandbar/completer.cpp vimode/emulatedcommandbar/activemode.cpp vimode/emulatedcommandbar/interactivesedreplacemode.cpp vimode/emulatedcommandbar/searchmode.cpp vimode/emulatedcommandbar/commandmode.cpp vimode/commandrangeexpressionparser.cpp vimode/keymapper.cpp vimode/marks.cpp vimode/jumps.cpp vimode/history.cpp vimode/macros.cpp vimode/mappings.cpp vimode/registers.cpp vimode/searcher.cpp vimode/completion.cpp vimode/completionrecorder.cpp vimode/completionreplayer.cpp vimode/macrorecorder.cpp vimode/lastchangerecorder.cpp ) endif() add_library(KF5TextEditor ${ktexteditor_LIB_SRCS} ${KTEXTEDITOR_PUBLIC_HEADERS}) generate_export_header(KF5TextEditor BASE_NAME KTextEditor) add_library(KF5::TextEditor ALIAS KF5TextEditor) target_include_directories(KF5TextEditor INTERFACE "$") # API is more or less KParts++, other stuff is used only internally target_link_libraries(KF5TextEditor PUBLIC KF5::Parts PRIVATE Qt5::Script Qt5::PrintSupport KF5::I18n KF5::Archive KF5::GuiAddons KF5::IconThemes KF5::ItemViews KF5::SonnetCore KF5::SyntaxHighlighting ${KTEXTEDITOR_OPTIONAL_LIBS} ) set_target_properties(KF5TextEditor PROPERTIES VERSION ${KTEXTEDITOR_VERSION_STRING} SOVERSION ${KTEXTEDITOR_SOVERSION} EXPORT_NAME "TextEditor" ) install(TARGETS KF5TextEditor EXPORT KF5TextEditorTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ktexteditor_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextEditor COMPONENT Devel ) include(ECMGeneratePriFile) 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(kauth_ktexteditor_helper buffer/katesecuretextbuffer.cpp) +target_link_libraries(kauth_ktexteditor_helper + KF5::Auth +) +install(TARGETS kauth_ktexteditor_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR} ) +kauth_install_helper_files(kauth_ktexteditor_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.cpp b/src/buffer/katesecuretextbuffer.cpp new file mode 100644 index 00000000..979bb62f --- /dev/null +++ b/src/buffer/katesecuretextbuffer.cpp @@ -0,0 +1,156 @@ +/* This file is part of the KTextEditor project. + * + * Copyright (C) 2017 KDE Developers + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) 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 Library 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 "katesecuretextbuffer_p.h" + +#ifndef Q_OS_WIN +#include +#include +#endif + +#include +#include +#include +#include + +KAUTH_HELPER_MAIN("org.kde.ktexteditor.katetextbuffer", SecureTextBuffer) + +ActionReply SecureTextBuffer::savefile(const QVariantMap &args) +{ + const ActionMode actionMode = static_cast(args[QLatin1String("actionMode")].toInt()); + const QString targetFile = args[QLatin1String("targetFile")].toString(); + const uint ownerId = (uint) args[QLatin1String("ownerId")].toInt(); + + if (actionMode == ActionMode::Prepare) { + + const QString temporaryFile = prepareTempFileInternal(targetFile, ownerId); + + if (temporaryFile.isEmpty()) { + return ActionReply::HelperErrorReply(); + } + + ActionReply reply; + reply.addData(QLatin1String("temporaryFile"), temporaryFile); + + return reply; + + } + + if (actionMode == ActionMode::Move) { + + const QString sourceFile = args[QLatin1String("sourceFile")].toString(); + const uint groupId = (uint) args[QLatin1String("groupId")].toInt(); + + if (moveFileInternal(sourceFile, targetFile, ownerId, groupId)) { + return ActionReply::SuccessReply(); + } + } + + return ActionReply::HelperErrorReply(); +} + +bool SecureTextBuffer::moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId) +{ + const bool newFile = !QFile::exists(targetFile); + bool atomicRenameSucceeded = false; + + /** + * There is no atomic rename operation publicly exposed by Qt. + * + * We use std::rename for UNIX and for now no-op for windows (triggers fallback). + * + * As fallback we are copying source file to destination with the help of QSaveFile + * to ensure targetFile is overwritten safely. + */ +#ifndef Q_OS_WIN + const int result = std::rename(QFile::encodeName(sourceFile).constData(), QFile::encodeName(targetFile).constData()); + if (result == 0) { + syncToDisk(QFile(targetFile).handle()); + atomicRenameSucceeded = true; + } +#else + atomicRenameSucceeded = false; +#endif + + if (!atomicRenameSucceeded) { + // as fallback copy the temporary file to the target with help of QSaveFile + QFile readFile(sourceFile); + QSaveFile saveFile(targetFile); + if (!readFile.open(QIODevice::ReadOnly) || !saveFile.open(QIODevice::WriteOnly)) { + return false; + } + char buffer[bufferLength]; + qint64 read = -1; + while ((read = readFile.read(buffer, bufferLength)) > 0) { + if (saveFile.write(buffer, read) == -1) { + return false; + } + } + if (read == -1 || !saveFile.commit()) { + return false; + } + } + + if (!newFile) { + // ensure file has the same owner and group as before + setOwner(targetFile, ownerId, groupId); + } + + return true; +} + +QString SecureTextBuffer::prepareTempFileInternal(const QString &targetFile, const uint ownerId) +{ + QTemporaryFile tempFile(targetFile); + if (!tempFile.open()) { + return QString(); + } + tempFile.setAutoRemove(false); + setOwner(tempFile.fileName(), ownerId, -1); + return tempFile.fileName(); +} + +void SecureTextBuffer::setOwner(const QString &filename, const uint ownerId, const uint groupId) +{ +#ifndef Q_OS_WIN + if (ownerId != (uint)-2 && groupId != (uint)-2) { + const int result = chown(QFile::encodeName(filename).constData(), ownerId, groupId); + // set at least correct group if owner cannot be changed + if (result != 0 && errno == EPERM) { + chown(QFile::encodeName(filename).constData(), getuid(), groupId); + } + } +#else + // no-op for windows +#endif +} + +void SecureTextBuffer::syncToDisk(const int fd) +{ +#ifndef Q_OS_WIN +#ifdef HAVE_FDATASYNC + fdatasync(fd); +#else + fsync(fd); +#endif +#else + // no-op for windows +#endif +} \ No newline at end of file diff --git a/src/buffer/katesecuretextbuffer_p.h b/src/buffer/katesecuretextbuffer_p.h new file mode 100644 index 00000000..b931aa27 --- /dev/null +++ b/src/buffer/katesecuretextbuffer_p.h @@ -0,0 +1,89 @@ +/* This file is part of the KTextEditor project. + * + * Copyright (C) 2017 KDE Developers + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) 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 Library 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 KATE_SECURE_TEXTBUFFER_P_H +#define KATE_SECURE_TEXTBUFFER_P_H + +#include +#include + +#include + +using namespace KAuth; + +/** + * Class used as KAuth helper binary. + * It is supposed to be called through KAuth action. + * + * It also contains couple of common methods intended to be used + * directly by TextBuffer as well as from helper binary. + * + * This class should only be used by TextBuffer. + */ +class SecureTextBuffer : public QObject +{ + Q_OBJECT + +public: + + /** + * We support Prepare action for temporary file creation + * and Move action for moving final file to its destination + */ + enum ActionMode { + Prepare = 1, + Move = 2 + }; + + SecureTextBuffer() {} + + ~SecureTextBuffer() {} + + /** + * Common helper methods + */ + static void setOwner(const QString &filename, const uint ownerId, const uint groupId); + static void syncToDisk(const int fd); + +private: + static const qint64 bufferLength = 4096; + + /** + * Creates temporary file based on given target file path. + * Temporary file is set to not be deleted on object destroy + * so KTextEditor can save contents in it. + */ + static QString prepareTempFileInternal(const QString &targetFile, const uint ownerId); + + /** + * Move file to its given destination and set owner. + */ + static bool moveFileInternal(const QString &sourceFile, const QString &targetFile, const uint ownerId, const uint groupId); + +public Q_SLOTS: + /** + * KAuth action to perform both prepare or move work based on given parameters. + * We keep this code in one method to prevent multiple KAuth user queries during one save action. + */ + static ActionReply savefile(const QVariantMap &args); + +}; + +#endif diff --git a/src/buffer/katetextbuffer.cpp b/src/buffer/katetextbuffer.cpp index 91474dff..95603fa7 100644 --- a/src/buffer/katetextbuffer.cpp +++ b/src/buffer/katetextbuffer.cpp @@ -1,962 +1,1061 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) 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 Library 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 "config.h" +#include "kateglobal.h" #include "katetextbuffer.h" +#include "katesecuretextbuffer_p.h" #include "katetextloader.h" // this is unfortunate, but needed for performance #include "katedocument.h" #include "kateview.h" #include "katepartdebug.h" #ifndef Q_OS_WIN #include - -// needed for umask application -#include -#include #endif #include +#include +#include +#include #if 0 #define BUFFER_DEBUG qCDebug(LOG_KTE) #else #define BUFFER_DEBUG if (0) qCDebug(LOG_KTE) #endif namespace Kate { -TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize) +TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize, bool alwaysUseKAuth) : QObject(parent) , m_document(parent) , m_history(*this) , m_blockSize(blockSize) , m_lines(0) , m_lastUsedBlock(0) , m_revision(0) , m_editingTransactions(0) , m_editingLastRevision(0) , m_editingLastLines(0) , m_editingMinimalLineChanged(-1) , m_editingMaximalLineChanged(-1) , m_encodingProberType(KEncodingProber::Universal) , m_fallbackTextCodec(nullptr) , m_textCodec(nullptr) , m_generateByteOrderMark(false) , m_endOfLineMode(eolUnix) , m_newLineAtEof(false) , m_lineLengthLimit(4096) + , m_alwaysUseKAuthForSave(alwaysUseKAuth) { // minimal block size must be > 0 Q_ASSERT(m_blockSize > 0); // create initial state clear(); } TextBuffer::~TextBuffer() { // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect m_document = nullptr; // not allowed during editing Q_ASSERT(m_editingTransactions == 0); // kill all ranges, work on copy, they will remove themself from the hash QSet copyRanges = m_ranges; qDeleteAll(copyRanges); Q_ASSERT(m_ranges.empty()); // clean out all cursors and lines, only cursors belonging to range will survive foreach (TextBlock *block, m_blocks) { block->deleteBlockContent(); } // delete all blocks, now that all cursors are really deleted // else asserts in destructor of blocks will fail! qDeleteAll(m_blocks); m_blocks.clear(); // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks QSet copyCursors = m_invalidCursors; qDeleteAll(copyCursors); Q_ASSERT(m_invalidCursors.empty()); } void TextBuffer::invalidateRanges() { // invalidate all ranges, work on copy, they might delete themself... QSet copyRanges = m_ranges; foreach (TextRange *range, copyRanges) { range->setRange(KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); } } void TextBuffer::clear() { // not allowed during editing Q_ASSERT(m_editingTransactions == 0); invalidateRanges(); // new block for empty buffer TextBlock *newBlock = new TextBlock(this, 0); newBlock->appendLine(QString()); // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range foreach (TextBlock *block, m_blocks) { block->clearBlockContent(newBlock); } // kill all buffer blocks qDeleteAll(m_blocks); m_blocks.clear(); // insert one block with one empty line m_blocks.append(newBlock); // reset lines and last used block m_lines = 1; m_lastUsedBlock = 0; // reset revision m_revision = 0; // reset bom detection m_generateByteOrderMark = false; // reset the filter device m_mimeTypeForFilterDev = QStringLiteral("text/plain"); // clear edit history m_history.clear(); // we got cleared emit cleared(); } TextLine TextBuffer::line(int line) const { // get block, this will assert on invalid line int blockIndex = blockForLine(line); // get line return m_blocks.at(blockIndex)->line(line); } QString TextBuffer::text() const { QString text; // combine all blocks foreach (TextBlock *block, m_blocks) { block->text(text); } // return generated string return text; } bool TextBuffer::startEditing() { // increment transaction counter ++m_editingTransactions; // if not first running transaction, do nothing if (m_editingTransactions > 1) { return false; } // reset information about edit... m_editingLastRevision = m_revision; m_editingLastLines = m_lines; m_editingMinimalLineChanged = -1; m_editingMaximalLineChanged = -1; // transaction has started emit editingStarted(); if (m_document) emit m_document->KTextEditor::Document::editingStarted(m_document); // first transaction started return true; } bool TextBuffer::finishEditing() { // only allowed if still transactions running Q_ASSERT(m_editingTransactions > 0); // decrement counter --m_editingTransactions; // if not last running transaction, do nothing if (m_editingTransactions > 0) { return false; } // assert that if buffer changed, the line ranges are set and valid! Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); // transaction has finished emit editingFinished(); if (m_document) emit m_document->KTextEditor::Document::editingFinished(m_document); // last transaction finished return true; } void TextBuffer::wrapLine(const KTextEditor::Cursor &position) { // debug output for REAL low-level debugging BUFFER_DEBUG << "wrapLine" << position; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); /** * let the block handle the wrapLine * this can only lead to one more line in this block * no other blocks will change * this call will trigger fixStartLines */ ++m_lines; // first alter the line counter, as functions called will need the valid one m_blocks.at(blockIndex)->wrapLine(position, blockIndex); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() <= m_editingMaximalLineChanged) { ++m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = position.line() + 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineWrapped(position); if (m_document) emit m_document->KTextEditor::Document::lineWrapped(m_document, position); } void TextBuffer::unwrapLine(int line) { // debug output for REAL low-level debugging BUFFER_DEBUG << "unwrapLine" << line; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // line 0 can't be unwrapped Q_ASSERT(line > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(line); // is this the first line in the block? bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); /** * let the block handle the unwrapLine * this can either lead to one line less in this block or the previous one * the previous one could even end up with zero lines * this call will trigger fixStartLines */ m_blocks.at(blockIndex)->unwrapLine(line, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex); --m_lines; // decrement index for later fixup, if we modified the block in front of the found one if (firstLineInBlock) { --blockIndex; } // remember changes ++m_revision; // update changed line interval if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = line - 1; } if (line <= m_editingMaximalLineChanged) { --m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = line - 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineUnwrapped(line); if (m_document) emit m_document->KTextEditor::Document::lineUnwrapped(m_document, line); } void TextBuffer::insertText(const KTextEditor::Cursor &position, const QString &text) { // debug output for REAL low-level debugging BUFFER_DEBUG << "insertText" << position << text; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // skip work, if no text to insert if (text.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); // let the block handle the insertText m_blocks.at(blockIndex)->insertText(position, text); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = position.line(); } // emit signal about done change emit textInserted(position, text); if (m_document) emit m_document->KTextEditor::Document::textInserted(m_document, position, text); } void TextBuffer::removeText(const KTextEditor::Range &range) { // debug output for REAL low-level debugging BUFFER_DEBUG << "removeText" << range; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // only ranges on one line are supported Q_ASSERT(range.start().line() == range.end().line()); // start column <= end column and >= 0 Q_ASSERT(range.start().column() <= range.end().column()); Q_ASSERT(range.start().column() >= 0); // skip work, if no text to remove if (range.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(range.start().line()); // let the block handle the removeText, retrieve removed text QString text; m_blocks.at(blockIndex)->removeText(range, text); // remember changes ++m_revision; // update changed line interval if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = range.start().line(); } if (range.start().line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = range.start().line(); } // emit signal about done change emit textRemoved(range, text); if (m_document) emit m_document->KTextEditor::Document::textRemoved(m_document, range, text); } int TextBuffer::blockForLine(int line) const { // only allow valid lines if ((line < 0) || (line >= lines())) { qFatal("out of range line requested in text buffer (%d out of [0, %d[)", line, lines()); } // we need blocks and last used block should not be negative Q_ASSERT(!m_blocks.isEmpty()); Q_ASSERT(m_lastUsedBlock >= 0); /** * shortcut: try last block first */ if (m_lastUsedBlock < m_blocks.size()) { /** * check if block matches * if yes, just return again this block */ TextBlock *block = m_blocks[m_lastUsedBlock]; const int start = block->startLine(); const int lines = block->lines(); if (start <= line && line < (start + lines)) { return m_lastUsedBlock; } } /** * search for right block * use binary search * if we leave this loop not by returning the found element we have an error */ int blockStart = 0; int blockEnd = m_blocks.size() - 1; while (blockEnd >= blockStart) { // get middle and ensure it is OK int middle = blockStart + ((blockEnd - blockStart) / 2); Q_ASSERT(middle >= 0); Q_ASSERT(middle < m_blocks.size()); // facts bout this block TextBlock *block = m_blocks[middle]; const int start = block->startLine(); const int lines = block->lines(); // right block found, remember it and return it if (start <= line && line < (start + lines)) { m_lastUsedBlock = middle; return middle; } // half our stuff ;) if (line < start) { blockEnd = middle - 1; } else { blockStart = middle + 1; } } // we should always find a block qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines()); return -1; } void TextBuffer::fixStartLines(int startBlock) { // only allow valid start block Q_ASSERT(startBlock >= 0); Q_ASSERT(startBlock < m_blocks.size()); // new start line for next block TextBlock *block = m_blocks.at(startBlock); int newStartLine = block->startLine() + block->lines(); // fixup block for (int index = startBlock + 1; index < m_blocks.size(); ++index) { // set new start line block = m_blocks.at(index); block->setStartLine(newStartLine); // calculate next start line newStartLine += block->lines(); } } void TextBuffer::balanceBlock(int index) { /** * two cases, too big or too small block */ TextBlock *blockToBalance = m_blocks.at(index); // first case, too big one, split it if (blockToBalance->lines() >= 2 * m_blockSize) { // half the block int halfSize = blockToBalance->lines() / 2; // create and insert new block behind current one, already set right start line TextBlock *newBlock = blockToBalance->splitBlock(halfSize); Q_ASSERT(newBlock); m_blocks.insert(m_blocks.begin() + index + 1, newBlock); // split is done return; } // second case: possibly too small block // if only one block, no chance to unite // same if this is first block, we always append to previous one if (index == 0) { return; } // block still large enough, do nothing if (2 * blockToBalance->lines() > m_blockSize) { return; } // unite small block with predecessor TextBlock *targetBlock = m_blocks.at(index - 1); // merge block blockToBalance->mergeBlock(targetBlock); // delete old block delete blockToBalance; m_blocks.erase(m_blocks.begin() + index); } void TextBuffer::debugPrint(const QString &title) const { // print header with title printf("%s (lines: %d bs: %d)\n", qPrintable(title), m_lines, m_blockSize); // print all blocks for (int i = 0; i < m_blocks.size(); ++i) { m_blocks.at(i)->debugPrint(i); } } bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec) { // fallback codec must exist Q_ASSERT(m_fallbackTextCodec); // codec must be set! Q_ASSERT(m_textCodec); /** * first: clear buffer in any case! */ clear(); /** * construct the file loader for the given file, with correct prober type */ Kate::TextLoader file(filename, m_encodingProberType); /** * triple play, maximal three loading rounds * 0) use the given encoding, be done, if no encoding errors happen * 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done * 2) use fallback encoding, be done, if no encoding errors happen * 3) use again given encoding, be done in any case */ for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { /** * kill all blocks beside first one */ for (int b = 1; b < m_blocks.size(); ++b) { TextBlock *block = m_blocks.at(b); block->clearLines(); delete block; } m_blocks.resize(1); /** * remove lines in first block */ m_blocks.last()->clearLines(); m_lines = 0; /** * try to open file, with given encoding * in round 0 + 3 use the given encoding from user * in round 1 use 0, to trigger detection * in round 2 use fallback */ QTextCodec *codec = m_textCodec; if (i == 1) { codec = nullptr; } else if (i == 2) { codec = m_fallbackTextCodec; } if (!file.open(codec)) { // create one dummy textline, in any case m_blocks.last()->appendLine(QString()); m_lines++; return false; } // read in all lines... encodingErrors = false; while (!file.eof()) { // read line int offset = 0, length = 0; bool currentError = !file.readLine(offset, length); encodingErrors = encodingErrors || currentError; // bail out on encoding error, if not last round! if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << (file.textCodec() ? file.textCodec()->name() : "(null)"); break; } // get Unicode data for this line const QChar *unicodeData = file.unicode() + offset; if (longestLineLoaded < length) longestLineLoaded=length; /** * split lines, if too large */ do { /** * calculate line length */ int lineLength = length; if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { /** * search for place to wrap */ int spacePosition = m_lineLengthLimit - 1; for (int testPosition = m_lineLengthLimit - 1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit / 10))); --testPosition) { /** * wrap place found? */ if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { spacePosition = testPosition; break; } } /** * wrap the line */ lineLength = spacePosition + 1; length -= lineLength; tooLongLinesWrapped = true; } else { /** * be done after this round */ length = 0; } /** * construct new text line with content from file * move data pointer */ QString textLine(unicodeData, lineLength); unicodeData += lineLength; /** * ensure blocks aren't too large */ if (m_blocks.last()->lines() >= m_blockSize) { m_blocks.append(new TextBlock(this, m_blocks.last()->startLine() + m_blocks.last()->lines())); } /** * append line to last block */ m_blocks.last()->appendLine(textLine); ++m_lines; } while (length > 0); } // if no encoding error, break out of reading loop if (!encodingErrors) { // remember used codec, might change bom setting setTextCodec(file.textCodec()); break; } } // save checksum of file on disk setDigest(file.digest()); // remember if BOM was found if (file.byteOrderMarkFound()) { setGenerateByteOrderMark(true); } // remember eol mode, if any found in file if (file.eol() != eolUnknown) { setEndOfLineMode(file.eol()); } // remember mime type for filter device m_mimeTypeForFilterDev = file.mimeTypeForFilterDev(); // assert that one line is there! Q_ASSERT(m_lines > 0); // report CODEC + ERRORS BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec->name() << (encodingErrors ? "with" : "without") << "encoding errors"; // report BOM BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark"; // report filter device mime-type BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev; // emit success emit loaded(filename, encodingErrors); // file loading worked, modulo encoding problems return true; } const QByteArray &TextBuffer::digest() const { return m_digest; } void TextBuffer::setDigest(const QByteArray &checksum) { m_digest = checksum; } void TextBuffer::setTextCodec(QTextCodec *codec) { m_textCodec = codec; // enforce bom for some encodings int mib = m_textCodec->mibEnum(); if (mib == 1013 || mib == 1014 || mib == 1015) { // utf16 setGenerateByteOrderMark(true); } if (mib == 1017 || mib == 1018 || mib == 1019) { // utf32 setGenerateByteOrderMark(true); } } bool TextBuffer::save(const QString &filename) { // codec must be set! Q_ASSERT(m_textCodec); -#ifndef Q_OS_WIN const bool newFile = !QFile::exists(filename); -#endif + + /** + * 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(); + } /** * use QSaveFile for save write + rename */ - QSaveFile saveFile(filename); - saveFile.setDirectWriteFallback(true); + QScopedPointer saveFile(new QSaveFile(filename)); + static_cast(saveFile.data())->setDirectWriteFallback(true); - if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - return false; + bool usingTemporaryFile = false; + QScopedPointer kAuthActionArgs; + QScopedPointer kAuthSaveAction; + + // open QSaveFile for write + if (m_alwaysUseKAuthForSave || !saveFile->open(QIODevice::WriteOnly)) { + + // if that fails we need more privileges to save this file + // -> we write to temporary file and then move it to target location + + usingTemporaryFile = true; + + QString targetFilename(filename); + + kAuthActionArgs.reset(new QVariantMap()); + kAuthActionArgs->insert(QLatin1String("actionMode"), SecureTextBuffer::ActionMode::Prepare); + kAuthActionArgs->insert(QLatin1String("targetFile"), targetFilename); + kAuthActionArgs->insert(QLatin1String("ownerId"), getuid()); + + // call save with elevated privileges + if (KTextEditor::EditorPrivate::unitTestMode()) { + + // unit testing purposes only + ActionReply reply = SecureTextBuffer::savefile(*kAuthActionArgs); + if (!reply.succeeded()) { + return false; + } + targetFilename = reply.data()[QLatin1String("temporaryFile")].toString(); + + } else { + + // call action + kAuthSaveAction.reset(new KAuth::Action(QLatin1String("org.kde.ktexteditor.katetextbuffer.savefile"))); + kAuthSaveAction->setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); + kAuthSaveAction->setArguments(*kAuthActionArgs); + KAuth::ExecuteJob *job = kAuthSaveAction->execute(); + if (!job->exec()) { + return false; + } + + // get temporary file path from the reply + targetFilename = job->data()[QLatin1String("temporaryFile")].toString(); + + } + + if (targetFilename.isEmpty()) { + return false; + } + + // we are now saving to a prepared temporary file + saveFile.reset(new QFile(targetFilename)); + + // open QTemporaryFile for write + if (!saveFile->open(QIODevice::WriteOnly)) { + return false; + } + + if (!newFile) { + // set original file's permissions to temporary file (QSaveFile does this automatically) + saveFile->setPermissions(QFile(filename).permissions()); + } } /** * construct correct filter device and try to open */ KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); - KCompressionDevice file(&saveFile, false, type); + KCompressionDevice file(saveFile.data(), 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 ;) 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; } } 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(); // close and delete file file.close(); // flush file - if (!saveFile.flush()) { + 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 + if (usingTemporaryFile) { + // ensure that the file is written to disk + // just for temporary file (QSaveFile does this automatically in commit()) + SecureTextBuffer::syncToDisk(saveFile->handle()); + } // did save work? // only finalize if stream status == OK - bool ok = (stream.status() == QTextStream::Ok) && saveFile.commit(); + bool ok = (stream.status() == QTextStream::Ok); - // remember this revision as last saved if we had success! + // commit changes 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); + if (usingTemporaryFile) { + + // temporary file was used to save the file + // -> moving this file to original location with KAuth action + + kAuthActionArgs->insert(QLatin1String("actionMode"), SecureTextBuffer::ActionMode::Move); + kAuthActionArgs->insert(QLatin1String("sourceFile"), saveFile->fileName()); + kAuthActionArgs->insert(QLatin1String("targetFile"), filename); + kAuthActionArgs->insert(QLatin1String("ownerId"), ownerId); + kAuthActionArgs->insert(QLatin1String("groupId"), groupId); + + // call save with elevated privileges + if (KTextEditor::EditorPrivate::unitTestMode()) { + + // unit testing purposes only + ok = SecureTextBuffer::savefile(*kAuthActionArgs).succeeded(); + + } else { + + kAuthSaveAction->setArguments(*kAuthActionArgs); + KAuth::ExecuteJob *job = kAuthSaveAction->execute(); + ok = job->exec(); - const mode_t fileMode = 0666 & ~mask; - chmod(QFile::encodeName(filename).constData(), fileMode); + } + + } else { + + // standard save without elevated privileges + + ok = static_cast(saveFile.data())->commit(); + + if (ok && !newFile) { + // ensure correct owner + SecureTextBuffer::setOwner(filename, ownerId, groupId); + } + + } + } + + // remember this revision as last saved if we had success! + if (ok) { + m_history.setLastSavedRevision(); } -#endif // report CODEC + ERRORS BUFFER_DEBUG << "Saved file " << filename << "with codec" << m_textCodec->name() << (ok ? "without" : "with") << "errors"; if (ok) { markModifiedLinesAsSaved(); } // emit signal on success if (ok) { emit saved(filename); } // return success or not return ok; } void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute) { /** * ignore calls if no document is around */ if (!m_document) { return; } /** * update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really * just create 20k ranges in a go and you wait seconds on a decent machine */ const QList &views = m_document->views(); foreach (KTextEditor::View *curView, views) { // filter wrong views if (view && view != curView) { continue; } // notify view, it is really a kate view static_cast(curView)->notifyAboutRangeChange(startLine, endLine, rangeWithAttribute); } } void TextBuffer::markModifiedLinesAsSaved() { foreach (TextBlock *block, m_blocks) { block->markModifiedLinesAsSaved(); } } QList TextBuffer::rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const { // get block, this will assert on invalid line const int blockIndex = blockForLine(line); // get the ranges of the right block QList rightRanges; foreach (const QSet &ranges, m_blocks.at(blockIndex)->rangesForLine(line)) { foreach (TextRange *const range, ranges) { /** * we want only ranges with attributes, but this one has none */ if (rangesWithAttributeOnly && !range->hasAttribute()) { continue; } /** * we want ranges for no view, but this one's attribute is only valid for views */ if (!view && range->attributeOnlyForViews()) { continue; } /** * the range's attribute is not valid for this view */ if (range->view() && range->view() != view) { continue; } /** * if line is in the range, ok */ if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) { rightRanges.append(range); } } } // return right ranges return rightRanges; } } diff --git a/src/buffer/katetextbuffer.h b/src/buffer/katetextbuffer.h index 92fddce7..8ec356fe 100644 --- a/src/buffer/katetextbuffer.h +++ b/src/buffer/katetextbuffer.h @@ -1,651 +1,656 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) 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 Library 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 KATE_TEXTBUFFER_H #define KATE_TEXTBUFFER_H #include #include #include #include #include #include #include "katedocument.h" #include #include "katetextblock.h" #include "katetextcursor.h" #include "katetextrange.h" #include "katetexthistory.h" // encoding prober #include namespace Kate { /** * Class representing a text buffer. * The interface is line based, internally the text will be stored in blocks of text lines. */ class KTEXTEDITOR_EXPORT TextBuffer : public QObject { friend class TextCursor; friend class TextRange; friend class TextBlock; Q_OBJECT public: /** * End of line mode */ enum EndOfLineMode { eolUnknown = -1 , eolUnix = 0 , eolDos = 1 , eolMac = 2 }; /** * Construct an empty text buffer. * Empty means one empty line in one block. * @param parent parent qobject * @param blockSize block size in lines the buffer should try to hold, default 64 lines */ - TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize = 64); + TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize = 64, bool alwaysUseKAuth = false); /** * Destruct the text buffer * Virtual, we allow inheritance */ virtual ~TextBuffer(); /** * Clears the buffer, reverts to initial empty state. * Empty means one empty line in one block. * Virtual, can be overwritten. */ virtual void clear(); /** * Set encoding prober type for this buffer to use for load. * @param proberType prober type to use for encoding */ void setEncodingProberType(KEncodingProber::ProberType proberType) { m_encodingProberType = proberType; } /** * Get encoding prober type for this buffer * @return currently in use prober type of this buffer */ KEncodingProber::ProberType encodingProberType() const { return m_encodingProberType; } /** * Set fallback codec for this buffer to use for load. * @param codec fallback QTextCodec to use for encoding */ void setFallbackTextCodec(QTextCodec *codec) { m_fallbackTextCodec = codec; } /** * Get fallback codec for this buffer * @return currently in use fallback codec of this buffer */ QTextCodec *fallbackTextCodec() const { return m_fallbackTextCodec; } /** * Set codec for this buffer to use for load/save. * Loading might overwrite this, if it encounters problems and finds a better codec. * Might change BOM setting. * @param codec QTextCodec to use for encoding */ void setTextCodec(QTextCodec *codec); /** * Get codec for this buffer * @return currently in use codec of this buffer */ QTextCodec *textCodec() const { return m_textCodec; } /** * Generate byte order mark on save. * Loading might overwrite this setting, if there is a BOM found inside the file. * @param generateByteOrderMark should BOM be generated? */ void setGenerateByteOrderMark(bool generateByteOrderMark) { m_generateByteOrderMark = generateByteOrderMark; } /** * Generate byte order mark on save? * @return should BOM be generated? */ bool generateByteOrderMark() const { return m_generateByteOrderMark; } /** * Set end of line mode for this buffer, not allowed to be set to unknown. * Loading might overwrite this setting, if there is a eol found inside the file. * @param endOfLineMode new eol mode */ void setEndOfLineMode(EndOfLineMode endOfLineMode) { Q_ASSERT(endOfLineMode != eolUnknown); m_endOfLineMode = endOfLineMode; } /** * Get end of line mode * @return end of line mode */ EndOfLineMode endOfLineMode() const { return m_endOfLineMode; } /** * Set whether to insert a newline character on save at the end of the file * @param newlineAtEof should newline be added if non-existing */ void setNewLineAtEof(bool newlineAtEof) { m_newLineAtEof = newlineAtEof; } /** * Set line length limit * @param lineLengthLimit new line length limit */ void setLineLengthLimit(int lineLengthLimit) { m_lineLengthLimit = lineLengthLimit; } /** * Load the given file. This will first clear the buffer and then load the file. * Even on error during loading the buffer will still be cleared. * Before calling this, setTextCodec must have been used to set codec! * @param filename file to open * @param encodingErrors were there problems occurred while decoding the file? * @param tooLongLinesWrapped were too long lines found and wrapped? * @param longestLineLoaded the longest line in the file (before wrapping) * @param enforceTextCodec enforce to use only the set text codec * @return success, the file got loaded, perhaps with encoding errors * Virtual, can be overwritten. */ virtual bool load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec); /** * Save the current buffer content to the given file. * Before calling this, setTextCodec and setFallbackTextCodec must have been used to set codec! * @param filename file to save * @return success * Virtual, can be overwritten. */ virtual bool save(const QString &filename); /** * Lines currently stored in this buffer. * This is never 0, even clear will let one empty line remain. */ int lines() const { Q_ASSERT(m_lines > 0); return m_lines; } /** * Revision of this buffer. Is set to 0 on construction, clear() (load will trigger clear()). * Is incremented on each change to the buffer. * @return current revision */ qint64 revision() const { return m_revision; } /** * Retrieve a text line. * @param line wanted line number * @return text line */ TextLine line(int line) const; /** * Retrieve text of complete buffer. * @return text for this buffer, lines separated by '\n' */ QString text() const; /** * Start an editing transaction, the wrapLine/unwrapLine/insertText and removeText functions * are only allowed to be called inside a editing transaction. * Editing transactions can stack. The number of startEdit and endEdit calls must match. * @return returns true, if no transaction was already running * Virtual, can be overwritten. */ virtual bool startEditing(); /** * Finish an editing transaction. Only allowed to be called if editing transaction is started. * @return returns true, if this finished last running transaction * Virtual, can be overwritten. */ virtual bool finishEditing(); /** * Query the number of editing transactions running atm. * @return number of running transactions */ int editingTransactions() const { return m_editingTransactions; } /** * Query the revsion of this buffer before the ongoing editing transactions. * @return revision of buffer before current editing transaction altered it */ qint64 editingLastRevision() const { return m_editingLastRevision; } /** * Query the number of lines of this buffer before the ongoing editing transactions. * @return number of lines of buffer before current editing transaction altered it */ int editingLastLines() const { return m_editingLastLines; } /** * Query information from the last editing transaction: was the content of the buffer changed? * This is checked by comparing the editingLastRevision() with the current revision(). * @return content of buffer was changed in last transaction? */ bool editingChangedBuffer() const { return editingLastRevision() != revision(); } /** * Query information from the last editing transaction: was the number of lines of the buffer changed? * This is checked by comparing the editingLastLines() with the current lines(). * @return content of buffer was changed in last transaction? */ bool editingChangedNumberOfLines() const { return editingLastLines() != lines(); } /** * Get minimal line number changed by last editing transaction * @return maximal line number changed by last editing transaction, or -1, if none changed */ int editingMinimalLineChanged() const { return m_editingMinimalLineChanged; } /** * Get maximal line number changed by last editing transaction * @return maximal line number changed by last editing transaction, or -1, if none changed */ int editingMaximalLineChanged() const { return m_editingMaximalLineChanged; } /** * Wrap line at given cursor position. * @param position line/column as cursor where to wrap * Virtual, can be overwritten. */ virtual void wrapLine(const KTextEditor::Cursor &position); /** * Unwrap given line. * @param line line to unwrap * Virtual, can be overwritten. */ virtual void unwrapLine(int line); /** * Insert text at given cursor position. Does nothing if text is empty, beside some consistency checks. * @param position position where to insert text * @param text text to insert * Virtual, can be overwritten. */ virtual void insertText(const KTextEditor::Cursor &position, const QString &text); /** * Remove text at given range. Does nothing if range is empty, beside some consistency checks. * @param range range of text to remove, must be on one line only. * Virtual, can be overwritten. */ virtual void removeText(const KTextEditor::Range &range); /** * TextHistory of this buffer * @return text history for this buffer */ TextHistory &history() { return m_history; } Q_SIGNALS: /** * Buffer got cleared. This is emitted when constructor or load have called clear() internally, * or when the user of the buffer has called clear() itself. */ void cleared(); /** * Buffer loaded successfully a file * @param filename file which was loaded * @param encodingErrors were there problems occurred while decoding the file? */ void loaded(const QString &filename, bool encodingErrors); /** * Buffer saved successfully a file * @param filename file which was saved */ void saved(const QString &filename); /** * Editing transaction has started. */ void editingStarted(); /** * Editing transaction has finished. */ void editingFinished(); /** * A line got wrapped. * @param position position where the wrap occurred */ void lineWrapped(const KTextEditor::Cursor &position); /** * A line got unwrapped. * @param line line where the unwrap occurred */ void lineUnwrapped(int line); /** * Text got inserted. * @param position position where the insertion occurred * @param text inserted text */ void textInserted(const KTextEditor::Cursor &position, const QString &text); /** * Text got removed. * @param range range where the removal occurred * @param text removed text */ void textRemoved(const KTextEditor::Range &range, const QString &text); private: /** * Find block containing given line. * @param line we want to find block for this line * @return index of found block */ int blockForLine(int line) const; /** * Fix start lines of all blocks after the given one * @param startBlock index of block from which we start to fix */ void fixStartLines(int startBlock); /** * Balance the given block. Look if it is too small or too large. * @param index block to balance */ void balanceBlock(int index); /** * Block for given index in block list. * @param index block index * @return block matching this index */ TextBlock *blockForIndex(int index) { return m_blocks[index]; } /** * A range changed, notify the views, in case of attributes or feedback. * @param view which view is affected? 0 for all views * @param startLine start line of change * @param endLine end line of change * @param rangeWithAttribute attribute changed or is active, this will perhaps lead to repaints */ void notifyAboutRangeChange(KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute); /** * Mark all modified lines as lines saved on disk (modified line system). */ void markModifiedLinesAsSaved(); public: /** * Gets the document to which this buffer is bound. * \return a pointer to the document */ KTextEditor::DocumentPrivate *document() const { return m_document; } /** * Debug output, print whole buffer content with line numbers and line length * @param title title for this output */ void debugPrint(const QString &title) const; /** * Return the ranges which affect the given line. * @param line line to look at * @param view only return ranges associated with given view * @param rangesWithAttributeOnly only return ranges which have a attribute set * @return list of ranges affecting this line */ QList rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const; /** * Check if the given range pointer is still valid. * @return range pointer still belongs to range for this buffer */ bool rangePointerValid(TextRange *range) const { return m_ranges.contains(range); } /** * Invalidate all ranges in this buffer. */ void invalidateRanges(); // // checksum handling // public: /** * Checksum of the document on disk, set either through file loading * in openFile() or in KTextEditor::DocumentPrivate::saveFile() * @return git compatible sha1 checksum for this document */ const QByteArray &digest() const; /** * Set the checksum of this buffer. Make sure this checksum is up-to-date * when reading digest(). * @param checksum git compatible sha1 digest for the document on disk */ void setDigest(const QByteArray &checksum); private: QByteArray m_digest; private: /** * parent document */ KTextEditor::DocumentPrivate *m_document; /** * text history */ TextHistory m_history; /** * block size in lines the buffer will try to hold */ const int m_blockSize; /** * List of blocks which contain the lines of this buffer */ QVector m_blocks; /** * Number of lines in buffer */ int m_lines; /** * Last used block in the buffer. Is used for speeding up blockForLine. * May contain invalid index, must be checked before using. */ mutable int m_lastUsedBlock; /** * Revision of the buffer. */ qint64 m_revision; /** * Current number of running edit transactions */ int m_editingTransactions; /** * Revision remembered at start of current editing transaction */ qint64 m_editingLastRevision; /** * Number of lines remembered at start of current editing transaction */ int m_editingLastLines; /** * minimal line number changed by last editing transaction */ int m_editingMinimalLineChanged; /** * maximal line number changed by last editing transaction */ int m_editingMaximalLineChanged; /** * Set of invalid cursors for this whole buffer. * Valid cursors are inside the block the belong to. */ QSet m_invalidCursors; /** * Set of ranges of this whole buffer. */ QSet m_ranges; /** * Encoding prober type to use */ KEncodingProber::ProberType m_encodingProberType; /** * Fallback text codec to use */ QTextCodec *m_fallbackTextCodec; /** * Text codec to use */ QTextCodec *m_textCodec; /** * Mime-Type used for transparent compression/decompression support * Set by load(), reset by clear() */ QString m_mimeTypeForFilterDev; /** * Should byte order mark be created? */ bool m_generateByteOrderMark; /** * End of line mode, default is Unix */ EndOfLineMode m_endOfLineMode; /** * Insert newline character at the end of the file? */ bool m_newLineAtEof; /** * Limit for line length, longer lines will be wrapped on load */ int m_lineLengthLimit; + + /** + * For unit-testing purposes only. + */ + bool m_alwaysUseKAuthForSave; }; } #endif diff --git a/src/buffer/org.kde.ktexteditor.katetextbuffer.actions b/src/buffer/org.kde.ktexteditor.katetextbuffer.actions new file mode 100644 index 00000000..223933fc --- /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.savefile] +Name=Save Document +Description=Root privileges are needed to save this document +Policy=auth_admin +Persistence=session