From b6849053d25d74d1fa5bedc827587b1732f4b593 Mon Sep 17 00:00:00 2001 From: fanategorius <31000416+fanategorius@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:28:22 +0300 Subject: [PATCH] Update index.js --- js/index.js | 236 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 141 insertions(+), 95 deletions(-) diff --git a/js/index.js b/js/index.js index d9b7f1e..93814be 100644 --- a/js/index.js +++ b/js/index.js @@ -1,101 +1,147 @@ -db = $db; - $this->groupManager = $groupManager; - $this->userSession = $userSession; + function renderSqlResponse(term, response) { + if (!response) { + term.echo('[[;#ff5555;]Empty response from server]'); + return; + } + if (response.success === false) { + term.echo('[[;#ff5555;]Error: ]' + $.terminal.escape_formatting(response.error || 'unknown error')); + return; + } + var results = response.results || []; + if (!results.length) { + term.echo('[[;yellow;]No statements were executed]'); + return; + } + results.forEach(function (r) { + term.echo('[[;#009ae3;]> ]' + $.terminal.escape_formatting(r.query || '')); + if (r.type === 'error') { + term.echo('[[;#ff5555;] Error: ]' + $.terminal.escape_formatting(r.error || 'unknown error')); + } else if (r.type === 'select') { + term.echo('[[;gray;] ' + r.count + ' row(s)]'); + if (r.count > 0) { + term.echo($.terminal.escape_formatting(JSON.stringify(r.data, null, 2))); + } + } else if (r.type === 'set') { + term.echo('[[;green;] OK (session variable set)]'); + } else { + term.echo('[[;green;] OK, ' + r.affected_rows + ' row(s) affected]'); + } + }); } - /** - * @NoCSRFRequired - */ - public function query() - { - // Проверка прав администратора - $user = $this->userSession->getUser(); - if (!$user) { - return new JSONResponse(['error' => 'Not authenticated'], 401); - } - - if (!$this->groupManager->isAdmin($user->getUID())) { - return new JSONResponse(['error' => 'Admin privileges required'], 403); - } - - $sql = $this->request->getParam('sql', ''); - - if (empty(trim($sql))) { - return new JSONResponse(['success' => false, 'error' => 'Empty query']); - } - - // Разделяем запросы по точке с запятой. Все запросы выполняются - // последовательно на одном и том же соединении с БД (в рамках - // одного HTTP-запроса), поэтому SET сохраняет своё значение - // для current_setting() в последующих запросах пачки. - $queries = array_values(array_filter(array_map('trim', explode(';', $sql)), function ($q) { - return $q !== ''; - })); - - $results = []; - - foreach ($queries as $query) { - $isSelect = stripos($query, 'SELECT') === 0; - $isSet = stripos($query, 'SET ') === 0; - - try { - $stmt = $this->db->prepare($query); - $stmt->execute(); - - if ($isSelect) { - $rows = $stmt->fetchAll(); - $results[] = [ - 'query' => $query, - 'type' => 'select', - 'count' => count($rows), - 'data' => $rows - ]; - } else { - $affected = $stmt->rowCount(); - $results[] = [ - 'query' => $query, - 'type' => $isSet ? 'set' : 'write', - 'affected_rows' => $affected - ]; - } - } catch (\Exception $e) { - $results[] = [ - 'query' => $query, - 'type' => 'error', - 'error' => $e->getMessage() - ]; - // Останавливаемся на первой ошибке: не продолжаем выполнять - // оставшиеся запросы пачки (например, серию DELETE), - // если один из предыдущих шагов не выполнился. - break; - } - } - - return new JSONResponse(['success' => true, 'results' => $results]); + function enterSqlMode(term) { + mode = 'sql'; + term.set_prompt(SQL_PROMPT); + term.echo('[[;yellow;]Switched to SQL mode. Admin only — statements run directly against the database.]'); + term.echo('[[;gray;]Separate statements with ";". Shift+Enter for a new line, Enter to run. Type "occ" to go back.]'); } -} + + function exitSqlMode(term) { + mode = 'occ'; + term.set_prompt(OCC_PROMPT); + term.echo('[[;yellow;]Switched back to OCC mode.]'); + } + + $.get(baseUrl + '/cmd', function(response){ + $('#app-content').terminal(function(command, term) { + if (mode === 'sql') { + var trimmed = command.trim(); + if (trimmed === 'occ') { + exitSqlMode(term); + return; + } + if (trimmed === 'c') { + term.clear(); + return; + } + if (trimmed === 'exit') { + exitSqlMode(term); + term.reset(); + return; + } + if (!trimmed) { + return; + } + term.pause(); + $.ajax({ + url: baseUrl + '/db/query', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ sql: command }) + }).done(function (response) { + renderSqlResponse(term, response); + term.resume(); + }).fail(function (xhr) { + term.echo('[[;#ff5555;]Request failed: ]' + $.terminal.escape_formatting(xhr.status + ' ' + xhr.statusText)); + term.resume(); + }); + return; + } + + switch (command) { + case "c": + this.clear(); + break; + case "exit": + this.reset(); + break; + case "sql": + enterSqlMode(term); + break; + default: + var occCommand = { + command: command + }; + term.pause(); + $.ajax({ + url: baseUrl + '/cmd', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(occCommand) + }).done(function (response) { + term.echo('\n' + response).resume(); + }).fail(function (response, code) { + term.echo('\n' + response).resume(); + }); + } + }, { + greetings: function (callback) { + callback('[[;green;]' + new Date().toString().slice(0, 24) + "]\n\nPress [[;#ff5e99;]Enter] for more information on [[;#009ae3;]occ] commands.\nType [[;#ff5e99;]sql] to switch to SQL query mode.\n") + }, + name: 'occ', + prompt: OCC_PROMPT, + completion: response, + keydown: function (e) { + // Shift+Enter вставляет перевод строки вместо выполнения команды, + // это позволяет набирать многострочные SQL-скрипты в режиме sql. + if (e.shiftKey && e.key === 'Enter') { + this.insert('\n'); + return false; + } + }, + onResize: function(){ + scrollToBottom() + } + }); + }); + $('html').keypress(function(){ + scrollToBottom() + }) + }); +})(OC, window, jQuery);