diff --git a/appinfo/info.xml b/appinfo/info.xml index 0c39953..45c1f76 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ OCC Web OCC Commands in a web terminal - 0.2.3 + 0.3.0 agpl Adphi OCCWeb @@ -15,7 +15,7 @@ https://github.com/adphi/occweb https://github.com/adphi/occweb/raw/main/appinfo/screenshot.png - + diff --git a/css/style.css b/css/style.css index 6141904..b4fda40 100644 --- a/css/style.css +++ b/css/style.css @@ -773,3 +773,15 @@ terminal .terminal-output > div { .prompt span, .cursor-line span span, .command div span{ color: #009ae3 !important; } + +/* Keep terminal content left-aligned even when Nextcloud runs in RTL mode */ +html[dir="rtl"] #app-content.terminal, +html[dir="rtl"] #app-content .terminal, +html[dir="rtl"] #app-content .terminal-output, +html[dir="rtl"] #app-content .terminal-output > div > div, +html[dir="rtl"] #app-content .cmd, +html[dir="rtl"] #app-content .cmd div, +html[dir="rtl"] #app-content .cmd .prompt { + direction: ltr; + text-align: left; +} diff --git a/js/index.js b/js/index.js index 4641951..df715dd 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,42 @@ (function (OC, window, $, undefined) { 'use strict'; $(function() { + var longRunningCommandPatterns = [ + /^files:scan(?:\s|$)/, + /^files:transfer-ownership(?:\s|$)/, + /^encryption:/, + /^fulltextsearch:/, + /^preview:generate-all(?:\s|$)/, + /^versions:cleanup(?:\s|$)/, + /^trashbin:cleanup(?:\s|$)/, + /^db:add-missing-indices(?:\s|$)/, + /^db:add-missing-columns(?:\s|$)/, + /^db:add-missing-primary-keys(?:\s|$)/, + /^maintenance:repair(?:\s|$)/ + ]; + + function isPotentiallyLongRunning(command) { + var normalizedCommand = (command || '').trim().toLowerCase(); + if (!normalizedCommand) { + return false; + } + return longRunningCommandPatterns.some(function(pattern) { + return pattern.test(normalizedCommand); + }); + } + + function confirmLongRunningCommand(command) { + if (!isPotentiallyLongRunning(command)) { + return true; + } + + return window.confirm( + 'Warning: "' + command + '" can take a long time on large instances and may timeout in the browser.\n\n' + + 'Recommendation: Run this command via CLI/SSH for better reliability.\n\n' + + 'Do you want to run it anyway?' + ); + } + function scrollToBottom(){ var html = $('html'); html.scrollTop(html.prop('scrollHeight')); @@ -16,6 +52,10 @@ this.reset(); break; default: + if (!confirmLongRunningCommand(command)) { + term.echo('\nCommand canceled.'); + break; + } var occCommand = { command: command }; diff --git a/lib/Controller/OccController.php b/lib/Controller/OccController.php index 3ead8c1..bfae7ef 100644 --- a/lib/Controller/OccController.php +++ b/lib/Controller/OccController.php @@ -20,7 +20,6 @@ class OccController extends Controller private $userId; private $application; - private $symphonyApplication; private $output; public function __construct($AppName, IRequest $request, $userId) @@ -41,14 +40,12 @@ public function __construct($AppName, IRequest $request, $userId) ); $this->application->setAutoExit(false); $this->output = new OccOutput(OutputInterface::VERBOSITY_NORMAL, true); - $this->application->loadCommands(new StringInput(""), $this->output); - $reflectionProperty = new \ReflectionProperty(Application::class, 'application'); - $reflectionProperty->setAccessible(true); - $this->symphonyApplication = $reflectionProperty->getValue($this->application); + $this->application->loadCommands(new StringInput(""), $this->output); } /** * @NoCSRFRequired + * @AdminRequired */ public function index() { @@ -73,23 +70,51 @@ private function run($input) /** * @param string $command * @return DataResponse + * @AdminRequired */ public function cmd($command) { - $this->logger->debug($command); - $input = new StringInput($command); + $startedAt = microtime(true); + $rawCommand = trim((string)$command); + $commandName = strtok($rawCommand, ' ') ?: 'unknown'; + + $this->logger->info('occweb command started', [ + 'user' => $this->userId, + 'command' => $commandName, + ]); + + $input = new StringInput($rawCommand); $response = $this->run($input); - $this->logger->debug($response); + + $this->logger->info('occweb command finished', [ + 'user' => $this->userId, + 'command' => $commandName, + 'duration_ms' => (int)round((microtime(true) - $startedAt) * 1000), + ]); + return new DataResponse($response); } + /** + * @AdminRequired + */ public function list() { - $defs = $this->symphonyApplication->all(); - $cmds = array(); - foreach ($defs as $d) { - array_push($cmds, $d->getName()); + $output = $this->run(new StringInput('list --raw')); + $lines = preg_split('/\r\n|\r|\n/', (string)$output); + $cmds = []; + + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + + $parts = preg_split('/\s+/', $line, 2); + if (!empty($parts[0])) { + $cmds[] = $parts[0]; + } } - return new DataResponse($cmds); + + return new DataResponse(array_values(array_unique($cmds))); } } - diff --git a/lib/Controller/OccOutput.php b/lib/Controller/OccOutput.php index 5f0c0d0..74891b2 100644 --- a/lib/Controller/OccOutput.php +++ b/lib/Controller/OccOutput.php @@ -12,6 +12,7 @@ class OccOutput extends BufferedOutput implements ConsoleOutputInterface { private $consoleSectionOutputs = []; + private $errorOutput; private $stream; /** @@ -21,13 +22,16 @@ class OccOutput extends BufferedOutput implements ConsoleOutputInterface */ public function getErrorOutput(): OutputInterface { - // TODO: Implement getErrorOutput() method. - return $this; + if ($this->errorOutput === null) { + // Keep compatibility when no explicit stderr output is configured. + $this->errorOutput = $this; + } + return $this->errorOutput; } public function setErrorOutput(OutputInterface $error) { - + $this->errorOutput = $error; } /**