/* Minetest Copyright (C) 2013 celeron55, Perttu Ahola This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "chat.h" #include "debug.h" #include "config.h" #include "util/strfnd.h" #include #include #include "util/string.h" #include "util/numeric.h" ChatBuffer::ChatBuffer(u32 scrollback): m_scrollback(scrollback), m_unformatted(), m_cols(0), m_rows(0), m_scroll(0), m_formatted(), m_empty_formatted_line() { if (m_scrollback == 0) m_scrollback = 1; m_empty_formatted_line.first = true; } ChatBuffer::~ChatBuffer() { } void ChatBuffer::addLine(std::wstring name, std::wstring text) { ChatLine line(name, text); m_unformatted.push_back(line); if (m_rows > 0) { // m_formatted is valid and must be kept valid bool scrolled_at_bottom = (m_scroll == getBottomScrollPos()); u32 num_added = formatChatLine(line, m_cols, m_formatted); if (scrolled_at_bottom) m_scroll += num_added; } // Limit number of lines by m_scrollback if (m_unformatted.size() > m_scrollback) { deleteOldest(m_unformatted.size() - m_scrollback); } } void ChatBuffer::clear() { m_unformatted.clear(); m_formatted.clear(); m_scroll = 0; } u32 ChatBuffer::getLineCount() const { return m_unformatted.size(); } u32 ChatBuffer::getScrollback() const { return m_scrollback; } const ChatLine& ChatBuffer::getLine(u32 index) const { assert(index < getLineCount()); // pre-condition return m_unformatted[index]; } void ChatBuffer::step(f32 dtime) { for (u32 i = 0; i < m_unformatted.size(); ++i) { m_unformatted[i].age += dtime; } } void ChatBuffer::deleteOldest(u32 count) { bool at_bottom = (m_scroll == getBottomScrollPos()); u32 del_unformatted = 0; u32 del_formatted = 0; while (count > 0 && del_unformatted < m_unformatted.size()) { ++del_unformatted; // keep m_formatted in sync if (del_formatted < m_formatted.size()) { sanity_check(m_formatted[del_formatted].first); ++del_formatted; while (del_formatted < m_formatted.size() && !m_formatted[del_formatted].first) ++del_formatted; } --count; } m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted); m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted); if (at_bottom) m_scroll = getBottomScrollPos(); else scrollAbsolute(m_scroll - del_formatted); } void ChatBuffer::deleteByAge(f32 maxAge) { u32 count = 0; while (count < m_unformatted.size() && m_unformatted[count].age > maxAge) ++count; deleteOldest(count); } u32 ChatBuffer::getColumns() const { return m_cols; } u32 ChatBuffer::getRows() const { return m_rows; } void ChatBuffer::reformat(u32 cols, u32 rows) { if (cols == 0 || rows == 0) { // Clear formatted buffer m_cols = 0; m_rows = 0; m_scroll = 0; m_formatted.clear(); } else if (cols != m_cols || rows != m_rows) { // TODO: Avoid reformatting ALL lines (even invisible ones) // each time the console size changes. // Find out the scroll position in *unformatted* lines u32 restore_scroll_unformatted = 0; u32 restore_scroll_formatted = 0; bool at_bottom = (m_scroll == getBottomScrollPos()); if (!at_bottom) { for (s32 i = 0; i < m_scroll; ++i) { if (m_formatted[i].first) ++restore_scroll_unformatted; } } // If number of columns change, reformat everything if (cols != m_cols) { m_formatted.clear(); for (u32 i = 0; i < m_unformatted.size(); ++i) { if (i == restore_scroll_unformatted) restore_scroll_formatted = m_formatted.size(); formatChatLine(m_unformatted[i], cols, m_formatted); } } // Update the console size m_cols = cols; m_rows = rows; // Restore the scroll position if (at_bottom) { scrollBottom(); } else { scrollAbsolute(restore_scroll_formatted); } } } const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const { s32 index = m_scroll + (s32) row; if (index >= 0 && index < (s32) m_formatted.size()) return m_formatted[index]; else return m_empty_formatted_line; } void ChatBuffer::scroll(s32 rows) { scrollAbsolute(m_scroll + rows); } void ChatBuffer::scrollAbsolute(s32 scroll) { s32 top = getTopScrollPos(); s32 bottom = getBottomScrollPos(); m_scroll = scroll; if (m_scroll < top) m_scroll = top; if (m_scroll > bottom) m_scroll = bottom; } void ChatBuffer::scrollBottom() { m_scroll = getBottomScrollPos(); } void ChatBuffer::scrollTop() { m_scroll = getTopScrollPos(); } u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols, std::vector& destination) const { u32 num_added = 0; std::vector next_frags; ChatFormattedLine next_line; ChatFormattedFragment temp_frag; u32 out_column = 0; u32 in_pos = 0; u32 hanging_indentation = 0; // Format the sender name and produce fragments if (!line.name.empty()) { temp_frag.text = L"<"; temp_frag.column = 0; //temp_frag.bold = 0; next_frags.push_back(te/* Minetest Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "cpp_api/s_base.h" #define CHECK_SECURE_PATH_INTERNAL(L, path, write_required, ptr) \ if (!ScriptApiSecurity::checkPath(L, path, write_required, ptr)) { \ throw LuaError(std::string("Mod security: Blocked attempted ") + \ (write_required ? "write to " : "read from ") + path); \ } #define CHECK_SECURE_PATH(L, path, write_required) \ if (ScriptApiSecurity::isSecure(L)) { \ CHECK_SECURE_PATH_INTERNAL(L, path, write_required, NULL); \ } #define CHECK_SECURE_PATH_POSSIBLE_WRITE(L, path, ptr) \ if (ScriptApiSecurity::isSecure(L)) { \ CHECK_SECURE_PATH_INTERNAL(L, path, false, ptr); \ } class ScriptApiSecurity : virtual public ScriptApiBase { public: int getThread(lua_State *L); // creates an empty Lua environment void createEmptyEnv(lua_State *L); // sets the enviroment to the table thats on top of the stack void setLuaEnv(lua_State *L, int thread); // Sets up security on the ScriptApi's Lua state void initializeSecurity(); void initializeSecurityClient(); // Checks if the Lua state has been secured static bool isSecure(lua_State *L); // Loads a string as Lua code safely (doesn't allow bytecode). static bool safeLoadString(lua_State *L, const std::string &code, const char *chunk_name); // Loads a file as Lua code safely (doesn't allow bytecode). static bool safeLoadFile(lua_State *L, const char *path, const char *display_name = NULL); // Checks if mods are allowed to read (and optionally write) to the path static bool checkPath(lua_State *L, const char *path, bool write_required, bool *write_allowed=NULL); private: // Syntax: "sl_" <Library name or 'g' (global)> '_' <Function name> // (sl stands for Secure Lua) static int sl_g_dofile(lua_State *L); static int sl_g_load(lua_State *L); static int sl_g_loadfile(lua_State *L); static int sl_g_loadstring(lua_State *L); static int sl_g_require(lua_State *L); static int sl_io_open(lua_State *L); static int sl_io_input(lua_State *L); static int sl_io_output(lua_State *L); static int sl_io_lines(lua_State *L); static int sl_os_rename(lua_State *L); static int sl_os_remove(lua_State *L); }; w_cursor])) new_cursor++; while (new_cursor < length && isspace(m_line[new_cursor])) new_cursor++; } else { // skip one word to the left while (new_cursor >= 1 && isspace(m_line[new_cursor - 1])) new_cursor--; while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1])) new_cursor--; } break; case CURSOROP_SCOPE_LINE: new_cursor += increment * length; break; case CURSOROP_SCOPE_SELECTION: break; } new_cursor = MYMAX(MYMIN(new_cursor, length), 0); switch (op) { case CURSOROP_MOVE: m_cursor = new_cursor; m_cursor_len = 0; break; case CURSOROP_DELETE: if (m_cursor_len > 0) { // Delete selected text first m_line.erase(m_cursor, m_cursor_len); } else { m_cursor = MYMIN(new_cursor, old_cursor); m_line.erase(m_cursor, abs(new_cursor - old_cursor)); } m_cursor_len = 0; break; case CURSOROP_SELECT: if (scope == CURSOROP_SCOPE_LINE) { m_cursor = 0; m_cursor_len = length; } else { m_cursor = MYMIN(new_cursor, old_cursor); m_cursor_len += abs(new_cursor - old_cursor); m_cursor_len = MYMIN(m_cursor_len, length - m_cursor); } break; } clampView(); m_nick_completion_start = 0; m_nick_completion_end = 0; } void ChatPrompt::clampView() { s32 length = m_line.size(); if (length + 1 <= m_cols) { m_view = 0; } else { m_view = MYMIN(m_view, length + 1 - m_cols); m_view = MYMIN(m_view, m_cursor); m_view = MYMAX(m_view, m_cursor - m_cols + 1); m_view = MYMAX(m_view, 0); } } ChatBackend::ChatBackend(): m_console_buffer(500), m_recent_buffer(6), m_prompt(L"]", 500) { } ChatBackend::~ChatBackend() { } void ChatBackend::addMessage(std::wstring name, std::wstring text) { // Note: A message may consist of multiple lines, for example the MOTD. WStrfnd fnd(text); while (!fnd.at_end()) { std::wstring line = fnd.next(L"\n"); m_console_buffer.addLine(name, line); m_recent_buffer.addLine(name, line); } } void ChatBackend::addUnparsedMessage(std::wstring message) { // TODO: Remove the need to parse chat messages client-side, by sending // separate name and text fields in TOCLIENT_CHAT_MESSAGE. if (message.size() >= 2 && message[0] == L'<') { std::size_t closing = message.find_first_of(L'>', 1); if (closing != std::wstring::npos && closing + 2 <= message.size() && message[closing+1] == L' ') { std::wstring name = message.substr(1, closing - 1); std::wstring text = message.substr(closing + 2); addMessage(name, text); return; } } // Unable to parse, probably a server message. addMessage(L"", message); } ChatBuffer& ChatBackend::getConsoleBuffer() { return m_console_buffer; } ChatBuffer& ChatBackend::getRecentBuffer() { return m_recent_buffer; } EnrichedString ChatBackend::getRecentChat() { EnrichedString result; for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) { const ChatLine& line = m_recent_buffer.getLine(i); if (i != 0) result += L"\n"; if (!line.name.empty()) { result += L"<"; result += line.name; result += L"> "; } result += line.text; } return result; } ChatPrompt& ChatBackend::getPrompt() { return m_prompt; } void ChatBackend::reformat(u32 cols, u32 rows) { m_console_buffer.reformat(cols, rows); // no need to reformat m_recent_buffer, its formatted lines // are not used m_prompt.reformat(cols); } void ChatBackend::clearRecentChat() { m_recent_buffer.clear(); } void ChatBackend::step(float dtime) { m_recent_buffer.step(dtime); m_recent_buffer.deleteByAge(60.0); // no need to age messages in anything but m_recent_buffer } void ChatBackend::scroll(s32 rows) { m_console_buffer.scroll(rows); } void ChatBackend::scrollPageDown() { m_console_buffer.scroll(m_console_buffer.getRows()); } void ChatBackend::scrollPageUp() { m_console_buffer.scroll(-(s32)m_console_buffer.getRows()); }