From 7d05643ee637f133e8c5ab302824a5eb0d344491 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Sat, 26 Sep 2026 00:13:03 +0200 Subject: [PATCH] Translate Russian comments and user-facing strings to English Covers OccOutput.php, DbController.php and js/index.js: docblocks, inline comments, the forbidden-construct labels shown to admins in error responses, and the SQL template descriptions shown in the terminal (delete-user, list-user, rename-user). --- js/index.js | 64 +++++++------- lib/Controller/DbController.php | 147 +++++++++++++++++--------------- lib/Controller/OccOutput.php | 8 +- 3 files changed, 113 insertions(+), 106 deletions(-) diff --git a/js/index.js b/js/index.js index 23035db..f1d802c 100644 --- a/js/index.js +++ b/js/index.js @@ -7,17 +7,17 @@ } var baseUrl = OC.generateUrl('/apps/occweb'); - // Текущий режим терминала: 'occ' (обычные occ-команды) или 'sql' - // (выполнение произвольных SQL-запросов через /db/query). + // Current terminal mode: 'occ' (regular occ commands) or 'sql' + // (running arbitrary SQL queries via /db/query). var mode = 'occ'; var OCC_PROMPT = 'occ $ '; var SQL_PROMPT = '[[;#ff5555;]sql]# '; - // Разбивает пачку запросов по ";" с учётом одинарных И двойных кавычек - // (в т.ч. '' / "" как экранированной кавычки внутри строки/идентификатора), - // чтобы ";" в строковом литерале или "квотированном идентификаторе" не - // ломал разбиение. Зеркалит splitStatements() на бэкенде. + // Splits a batch of queries on ";" while respecting single AND double + // quotes (incl. '' / "" as an escaped quote inside a string/identifier), + // so that a ";" inside a string literal or "quoted identifier" doesn't + // break the split. Mirrors splitStatements() on the backend. function splitStatements(sql) { var statements = []; var current = ''; @@ -54,9 +54,9 @@ return statements.filter(function (s) { return s !== ''; }); } - // Быстрая клиентская проверка на DELETE/UPDATE — только для UX (чтобы не - // делать лишний запрос к серверу). Итоговое решение всё равно - // принимает бэкенд (requiresConfirmation), это лишь подсказка. + // Quick client-side check for DELETE/UPDATE — purely for UX (to avoid + // an extra round trip to the server). The backend always makes the + // final call (requiresConfirmation); this is just a hint. function scriptNeedsConfirmation(sql) { return splitStatements(sql).some(function (part) { var normalized = part.replace(/^(\s*--[^\n]*\n)*\s*/, ''); @@ -68,18 +68,18 @@ return String(value).replace(/'/g, "''"); } - // Готовые шаблоны SQL-скриптов. build() возвращает текст скрипта, - // который вставляется в командную строку через term.set_command() — - // ничего не выполняется автоматически, пользователь сам проверяет - // и жмёт Enter (после чего срабатывает обычное подтверждение DELETE). + // Ready-made SQL script templates. build() returns the script text, + // which is inserted into the command line via term.set_command() — + // nothing runs automatically, the user reviews it themselves and + // presses Enter (which then triggers the normal DELETE confirmation). var TEMPLATES = { 'delete-user': { args: ['uid'], - description: 'Полностью удалить локального пользователя (oc_preferences, oc_group_user, oc_ldap_user_mapping, oc_users)', + description: 'Completely delete a local user (oc_preferences, oc_group_user, oc_ldap_user_mapping, oc_users)', build: function (uid) { - // Значение подставляется напрямую в каждый запрос (а не через - // SET+current_setting) — так скрипт остаётся рабочим, даже если - // пользователь скопирует/выполнит только часть строк по отдельности. + // The value is substituted directly into each query (rather than + // via SET+current_setting) — this keeps the script working even + // if the user copies/runs only some of the lines individually. var v = escapeSqlString(uid); return [ "SELECT * FROM oc_users WHERE uid = '" + v + "'", @@ -93,7 +93,7 @@ }, 'list-user': { args: ['uid'], - description: 'Только посмотреть данные пользователя, без удаления (oc_users, oc_preferences, oc_group_user, oc_ldap_user_mapping)', + description: "View a user's data only, without deleting anything (oc_users, oc_preferences, oc_group_user, oc_ldap_user_mapping)", build: function (uid) { var v = escapeSqlString(uid); return [ @@ -106,14 +106,15 @@ }, 'rename-user': { args: ['old_uid', 'new_uid'], - description: 'ВНИМАНИЕ: не официальная операция Nextcloud. Переименовывает uid только в основных таблицах ядра — покрывает не всё, требует ручных доп. шагов (см. предупреждение в самом скрипте)', + description: "WARNING: not an official Nextcloud operation. Renames the uid only in the core tables — doesn't cover everything, requires manual extra steps (see the warning in the script itself)", build: function (oldUid, newUid) { var o = escapeSqlString(oldUid); var n = escapeSqlString(newUid); - // Предупреждение — только однострочные "--"-комментарии без ";" внутри, - // поэтому splitStatements() (парный на бэкенде и здесь) не разобьёт их - // как отдельные запросы, а stripLeadingComments() на бэкенде уберёт - // этот блок перед определением типа самого первого запроса (SELECT). + // The warning consists only of single-line "--" comments with no + // ";" inside them, so splitStatements() (mirrored on the backend + // and here) won't split them into separate statements, and + // stripLeadingComments() on the backend will strip this block + // before determining the type of the very first query (SELECT). var warning = "-- WARNING rename is NOT an officially supported Nextcloud operation\n" + "-- This only updates core tables below - it does NOT cover app-specific\n" + @@ -169,7 +170,7 @@ term.echo('[[;yellow;]Template inserted into the command line — review it, then press Enter to run.]'); } - // Табличный вывод результатов SELECT в стиле psql. + // Tabular rendering of SELECT results, psql-style. function renderTable(term, rows) { if (!rows || !rows.length) { return; @@ -178,8 +179,9 @@ function formatCell(v) { if (v === null || v === undefined) { - // Явная метка, а не пустая строка — иначе NULL неотличим от - // настоящей пустой строки '' в выводе таблицы. + // An explicit label rather than an empty string — otherwise NULL + // would be indistinguishable from an actual empty string '' in + // the table output. return '[NULL]'; } if (typeof v === 'object') { @@ -273,9 +275,9 @@ term.echo('[[;yellow;]Switched back to OCC mode.]'); } - // Таймаут для больших/долгих пачек (например, DELETE по большой таблице - // без индекса): без него зависший запрос молча оставит терминал - // заблокированным (term.pause()) навсегда, если сервер не ответит. + // Timeout for large/long-running batches (e.g. a DELETE on a big table + // without an index): without it, a hung query would silently leave the + // terminal blocked (term.pause()) forever if the server never responds. var SQL_REQUEST_TIMEOUT_MS = 120000; function sendSqlQuery(term, sql, confirmed) { @@ -388,8 +390,8 @@ prompt: OCC_PROMPT, completion: response, keydown: function (e) { - // Shift+Enter вставляет перевод строки вместо выполнения команды, - // это позволяет набирать многострочные SQL-скрипты в режиме sql. + // Shift+Enter inserts a newline instead of running the command, + // letting you type multi-line SQL scripts in sql mode. if (e.shiftKey && e.key === 'Enter') { this.insert('\n'); return false; diff --git a/lib/Controller/DbController.php b/lib/Controller/DbController.php index b2d7afd..29c32e6 100644 --- a/lib/Controller/DbController.php +++ b/lib/Controller/DbController.php @@ -13,28 +13,28 @@ use Psr\Log\LoggerInterface; class DbController extends Controller { - /** Максимум строк, возвращаемых на один SELECT (защита от OOM/DoS). */ + /** Maximum rows returned per SELECT (protection against OOM/DoS). */ private const MAX_ROWS = 1000; - /** Максимальный размер присланного SQL-текста в байтах (защита от OOM/DoS). */ + /** Maximum size of submitted SQL text in bytes (protection against OOM/DoS). */ private const MAX_SQL_BYTES = 1048576; // 1 MB /** - * Конструкции, дающие доступ к файловой системе сервера или запуску - * внешних программ через SQL. Блокируются полностью, без возможности - * подтверждения — в отличие от DELETE, это не про потерю данных, - * а про потенциальный захват сервера. + * Constructs that give access to the server's filesystem or allow + * launching external programs via SQL. These are blocked entirely, + * with no way to confirm past them — unlike DELETE, this isn't + * about data loss, it's about potential server takeover. */ private const FORBIDDEN_PATTERNS = [ - '/\bCOPY\b[\s\S]*\bPROGRAM\b/i' => 'COPY ... PROGRAM (запуск внешних команд)', - '/\bCOPY\b[\s\S]*\b(FROM|TO)\b\s*\'/i' => "COPY ... FROM/TO 'file' (доступ к файловой системе сервера)", + '/\bCOPY\b[\s\S]*\bPROGRAM\b/i' => 'COPY ... PROGRAM (runs external commands)', + '/\bCOPY\b[\s\S]*\b(FROM|TO)\b\s*\'/i' => "COPY ... FROM/TO 'file' (access to the server's filesystem)", '/\bpg_read_binary_file\s*\(/i' => 'pg_read_binary_file()', '/\bpg_read_file\s*\(/i' => 'pg_read_file()', '/\bpg_ls_dir\s*\(/i' => 'pg_ls_dir()', '/\bpg_stat_file\s*\(/i' => 'pg_stat_file()', '/\blo_import\s*\(/i' => 'lo_import()', '/\blo_export\s*\(/i' => 'lo_export()', - '/\bdblink(_connect)?\s*\(/i' => 'dblink() (подключение к произвольным БД)', + '/\bdblink(_connect)?\s*\(/i' => 'dblink() (connects to arbitrary databases)', '/\bLOAD_FILE\s*\(/i' => 'LOAD_FILE()', '/\bINTO\s+(OUTFILE|DUMPFILE)\b/i' => 'INTO OUTFILE/DUMPFILE', ]; @@ -55,15 +55,15 @@ class DbController extends Controller $this->db = $db; $this->groupManager = $groupManager; $this->userSession = $userSession; - // Через OC::$server, как и в OccController — не добавляем LoggerInterface - // в конструктор, чтобы не менять сигнатуру, резолвящуюся DI-контейнером. + // Via OC::$server, same as in OccController — we don't add LoggerInterface + // to the constructor so as not to change the signature resolved by the DI container. $this->logger = OC::$server->get(LoggerInterface::class); } /** - * Убирает ведущие однострочные комментарии ("-- ...") перед запросом. - * После разбиения пачки такой комментарий может "приклеиться" - * к следующему запросу и помешать определить его тип (SELECT/SET/DELETE). + * Strips leading single-line comments ("-- ...") before a query. + * After splitting a batch, such a comment can end up stuck to the + * next query and interfere with detecting its type (SELECT/SET/DELETE). */ private function stripLeadingComments($query) { @@ -75,12 +75,12 @@ class DbController extends Controller } /** - * Убирает ВСЕ однострочные (-- ...) и блочные C-style комментарии - * из запроса, включая те, что стоят внутри выражения (например, между - * именем функции и открывающей скобкой — иначе так можно спрятать - * запрещённую конструкцию от findForbiddenConstruct). Используется - * только для проверки на запрещённые конструкции — сам запрос на - * выполнение идёт без изменений. + * Strips ALL single-line (-- ...) and block-style C comments from a + * query, including ones sitting inside an expression (e.g. between a + * function name and its opening parenthesis — otherwise a forbidden + * construct could be hidden from findForbiddenConstruct that way). + * Used only for checking against forbidden constructs — the query + * that actually gets executed is left unmodified. */ private function removeAllComments($query) { @@ -90,10 +90,10 @@ class DbController extends Controller } /** - * Разбивает пачку запросов по ";" с учётом одинарных и двойных - * кавычек (строки и экранированные идентификаторы Postgres), чтобы - * точка с запятой внутри 'строки' или "идентификатора" (в том числе - * с '' / "" как экранированной кавычкой) не ломала разбиение. + * Splits a batch of queries on ";" while respecting single and double + * quotes (strings and Postgres-escaped identifiers), so that a + * semicolon inside a 'string' or "identifier" (including '' / "" as + * an escaped quote) doesn't break up the split. */ private function splitStatements($sql) { @@ -143,10 +143,11 @@ class DbController extends Controller } /** - * Возвращает описание найденной запрещённой конструкции (доступ к ФС, - * запуск программ) или null, если запрос безопасен в этом плане. - * Проверяется версия запроса без комментариев — иначе конструкцию - * можно спрятать, вставив комментарий между именем функции и "(". + * Returns a description of the forbidden construct found (filesystem + * access, running programs), or null if the query is safe in that + * regard. The comment-stripped version of the query is checked — + * otherwise the construct could be hidden by inserting a comment + * between the function name and "(". */ private function findForbiddenConstruct($query) { @@ -164,7 +165,7 @@ class DbController extends Controller */ public function query() { - // Проверка прав администратора + // Check admin privileges $user = $this->userSession->getUser(); if (!$user) { return new JSONResponse(['error' => 'Not authenticated'], 401); @@ -188,25 +189,24 @@ class DbController extends Controller ], 413); } - // Аудит: логируем сам факт попытки выполнения ДО всех проверок, - // чтобы в логе остались и заблокированные/отклонённые запросы, - // а не только успешно выполненные. + // Audit: log the fact that execution was attempted BEFORE any checks, + // so blocked/rejected queries are recorded in the log too, not just + // the ones that succeeded. $this->logger->warning('[occweb] SQL submitted by {user}: {sql}', [ 'app' => 'occweb', 'user' => $user->getUID(), 'sql' => $sql, ]); - // Разбиваем пачку по ";" с учётом кавычек (см. splitStatements). - // Все запросы выполняются последовательно на одном и том же - // соединении с БД (в рамках одного HTTP-запроса), поэтому SET - // сохраняет своё значение для current_setting() в следующих - // запросах этой же пачки. + // Split the batch on ";" respecting quotes (see splitStatements). + // All queries run sequentially on the same DB connection (within a + // single HTTP request), so SET retains its value for + // current_setting() in the following queries of the same batch. $queries = $this->splitStatements($sql); - // Доступ к файловой системе сервера / запуск программ через SQL - // блокируется полностью — это не про потерю данных (как DELETE), - // а про потенциальный захват сервера, подтверждением не обходится. + // Access to the server's filesystem / running programs via SQL is + // blocked entirely — this isn't about data loss (like DELETE), it's + // about potential server takeover, and confirmation doesn't bypass it. foreach ($queries as $query) { $forbidden = $this->findForbiddenConstruct($query); if ($forbidden !== null) { @@ -223,9 +223,9 @@ class DbController extends Controller } } - // DELETE и UPDATE необратимы (или трудно обратимы), поэтому - // требуем явное подтверждение с клиента (confirm=true), прежде - // чем выполнять хоть один запрос из пачки. + // DELETE and UPDATE are irreversible (or hard to reverse), so we + // require explicit confirmation from the client (confirm=true) before + // executing a single query from the batch. $deleteCount = 0; $updateCount = 0; foreach ($queries as $query) { @@ -254,21 +254,23 @@ class DbController extends Controller ]); } - // Пачка выполняется в одной транзакции: если один из запросов - // упадёт (например, DELETE на середине серии из-за FK), все уже - // выполненные в этой же пачке изменения откатываются, а не - // остаются частично применёнными. SET (без LOCAL) не транзакционен - // в PostgreSQL, поэтому откат не затрагивает current_setting(). + // The batch runs inside a single transaction: if one of the queries + // fails (e.g. a DELETE partway through a series due to an FK), all + // changes already made within this same batch are rolled back instead + // of staying partially applied. SET (without LOCAL) isn't + // transactional in PostgreSQL, so the rollback doesn't affect + // current_setting(). // - // Про DDL: в PostgreSQL (наша БД) DDL — CREATE/ALTER/DROP TABLE и - // т.п. — полностью транзакционен и откатывается вместе с остальными - // изменениями пачки, поэтому здесь для Postgres проблемы нет. Но - // Nextcloud работает и с MySQL/MariaDB через тот же IDBConnection, а - // там DDL делает неявный COMMIT — если эта пачка выполнится на - // MySQL-инстансе, commit()/rollBack() после такого DDL получат - // исключение "no active transaction". Оборачиваем begin/commit/ - // rollBack в try/catch, чтобы это не превращалось в необработанное - // исключение и HTTP 500 вместо аккуратного JSON-ответа. + // About DDL: in PostgreSQL (our database) DDL — CREATE/ALTER/DROP + // TABLE etc. — is fully transactional and rolls back together with + // the rest of the batch's changes, so there's no problem here for + // Postgres. But Nextcloud also works with MySQL/MariaDB through the + // same IDBConnection, and there DDL causes an implicit COMMIT — if + // this batch runs on a MySQL instance, commit()/rollBack() after such + // a DDL statement will get a "no active transaction" exception. We + // wrap begin/commit/rollBack in try/catch so this doesn't turn into + // an unhandled exception and an HTTP 500 instead of a clean JSON + // response. $results = []; $rolledBack = false; $rollbackFailed = false; @@ -296,9 +298,10 @@ class DbController extends Controller $stmt->execute(); if ($isSelect) { - // Читаем построчно и останавливаемся на MAX_ROWS, а не - // fetchAll() + array_slice — иначе SELECT без LIMIT на - // огромной таблице всё равно утащит всё в память PHP. + // Read row by row and stop at MAX_ROWS, instead of + // fetchAll() + array_slice — otherwise a SELECT without + // LIMIT on a huge table would still pull everything into + // PHP memory. $rows = []; $truncated = false; while (($row = $stmt->fetch()) !== false) { @@ -338,19 +341,20 @@ class DbController extends Controller 'type' => 'error', 'error' => $e->getMessage() ]; - // Откатываем всю пачку и останавливаемся: не продолжаем - // выполнять оставшиеся запросы (например, серию DELETE), - // если один из предыдущих шагов не выполнился. + // Roll back the whole batch and stop: don't keep executing + // the remaining queries (e.g. a series of DELETEs) if one of + // the preceding steps failed. if ($transactionStarted) { try { $this->db->rollBack(); $rolledBack = true; } catch (\Exception $rollbackError) { - // Транзакция уже закрыта не нами — например, неявным - // COMMIT-ом от DDL-запроса на MySQL/MariaDB. Всё, что - // выполнилось ДО этой точки в пачке, могло остаться - // применённым навсегда — намеренно НЕ ставим - // rolledBack в true, это было бы неправдой. + // The transaction was already closed by something + // other than us — e.g. an implicit COMMIT from a DDL + // statement on MySQL/MariaDB. Anything that executed + // BEFORE this point in the batch may remain + // permanently applied — we deliberately do NOT set + // rolledBack to true, since that would be untrue. $rollbackFailed = true; $this->logger->warning('[occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [ 'app' => 'occweb', @@ -366,9 +370,10 @@ class DbController extends Controller try { $this->db->commit(); } catch (\Exception $e) { - // Транзакция уже закоммичена неявно (например, DDL-запросом - // на MySQL/MariaDB) — эффекты уже сохранены, это не ошибка - // выполнения самой пачки. + // The transaction was already committed implicitly (e.g. by + // a DDL statement on MySQL/MariaDB) — the effects are + // already saved, this isn't an error in executing the batch + // itself. $this->logger->info('[occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [ 'app' => 'occweb', 'error' => $e->getMessage(), diff --git a/lib/Controller/OccOutput.php b/lib/Controller/OccOutput.php index 4afe826..d654f53 100644 --- a/lib/Controller/OccOutput.php +++ b/lib/Controller/OccOutput.php @@ -37,17 +37,17 @@ class OccOutput extends BufferedOutput implements ConsoleOutputInterface public function fetch(): string { - // Получаем основной буфер + // Get the main buffer $content = parent::fetch(); - - // Если есть stream от секций, читаем его тоже + + // If there's a stream from sections, read that too if ($this->stream !== null) { rewind($this->stream); $streamContent = stream_get_contents($this->stream); if ($streamContent !== false) { $content .= $streamContent; } - // Очищаем stream для следующего вызова + // Clear the stream for the next call ftruncate($this->stream, 0); rewind($this->stream); }