diff --git a/cli/panopticon.php b/cli/panopticon.php index da0e9a86..19916a5e 100755 --- a/cli/panopticon.php +++ b/cli/panopticon.php @@ -10,6 +10,7 @@ use Symfony\Component\Console\Application; const AKEEBA = 1; +const AKEEBA_CLI = 1; // Make sure we're running under the PHP CLI SAPI if (php_sapi_name() !== 'cli') diff --git a/src/Application.php b/src/Application.php index 4243ca8b..39b9f6b3 100644 --- a/src/Application.php +++ b/src/Application.php @@ -147,15 +147,14 @@ public static function getUserMenuTitle(): string $avatar = $user->getAvatar(64); - return "\"\"" . - $user->getUsername(); + return "\"\"" + . $user->getUsername(); } public static function getUserNameTitle(): string { return sprintf( - '%s', - Factory::getContainer()->userManager->getUser()->getName() + '%s', Factory::getContainer()->userManager->getUser()->getName() ); } @@ -278,6 +277,35 @@ public function createOrUpdateSessionPath(string $path, bool $silent = true): vo } } + public function loadLanguages(): void + { + try + { + $defaultLanguage = $this->container->appConfig->get('language', 'en-GB'); + } + catch (Exception $e) + { + $defaultLanguage = 'en-GB'; + } + + $detectedLanguage = Text::detectLanguage($this->container, '.ini', $this->container->languagePath); + + // Always load the English (Great Britain) language. It contains all the strings. + Text::loadLanguage('en-GB', $this->container, '.ini', true, $this->container->languagePath); + + // Load the site's default language, if it's different from en-GB. + if ($defaultLanguage != 'en-GB') + { + Text::loadLanguage($defaultLanguage, $this->container, '.ini', true, $this->container->languagePath); + } + + // Load the auto-detected preferred language (per browser settings), as long as it's not one we already loaded. + if (!in_array($detectedLanguage, [$defaultLanguage, 'en-GB'])) + { + Text::loadLanguage($detectedLanguage, $this->container, '.ini', true, $this->container->languagePath); + } + } + private function initialiseMenu(array $items = self::MAIN_MENU, ?Item $parent = null): void { $menu = $this->getDocument()->getMenu(); @@ -288,8 +316,7 @@ private function initialiseMenu(array $items = self::MAIN_MENU, ?Item $parent = { $allowed = array_reduce( $params['permissions'] ?? [], - fn(bool $carry, string $permission) => $carry && $user->getPrivilege($permission), - true + fn(bool $carry, string $permission) => $carry && $user->getPrivilege($permission), true ); if (!$allowed) @@ -463,35 +490,6 @@ private function discoverSessionSavePath(): void } } - private function loadLanguages(): void - { - try - { - $defaultLanguage = $this->container->appConfig->get('language', 'en-GB'); - } - catch (Exception $e) - { - $defaultLanguage = 'en-GB'; - } - - $detectedLanguage = Text::detectLanguage($this->container, '.ini', $this->container->languagePath); - - // Always load the English (Great Britain) language. It contains all the strings. - Text::loadLanguage('en-GB', $this->container, '.ini', true, $this->container->languagePath); - - // Load the site's default language, if it's different from en-GB. - if ($defaultLanguage != 'en-GB') - { - Text::loadLanguage($defaultLanguage, $this->container, '.ini', true, $this->container->languagePath); - } - - // Load the auto-detected preferred language (per browser settings), as long as it's not one we already loaded. - if (!in_array($detectedLanguage, [$defaultLanguage, 'en-GB'])) - { - Text::loadLanguage($detectedLanguage, $this->container, '.ini', true, $this->container->languagePath); - } - } - private function applyTimezonePreference(): void { if (!function_exists('date_default_timezone_get') || !function_exists('date_default_timezone_set')) @@ -615,13 +613,10 @@ private function redirectToSetup(): bool { $configPath = $this->container->appConfig->getDefaultPath(); - if ( - @file_exists($configPath) - || in_array( - $this->getContainer()->input->getCmd('view', ''), - self::NO_LOGIN_VIEWS - ) - ) + if (@file_exists($configPath) + || in_array( + $this->getContainer()->input->getCmd('view', ''), self::NO_LOGIN_VIEWS + )) { return false; } diff --git a/src/CliCommand/AbstractCommand.php b/src/CliCommand/AbstractCommand.php index c7234bd0..0cd3c7d0 100644 --- a/src/CliCommand/AbstractCommand.php +++ b/src/CliCommand/AbstractCommand.php @@ -10,6 +10,7 @@ use Akeeba\Panopticon\CliCommand\Attribute\AppHeader; use Akeeba\Panopticon\CliCommand\Attribute\ConfigAssertion; use Akeeba\Panopticon\Factory; +use Awf\Text\Text; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; @@ -37,11 +38,28 @@ protected function initialize(InputInterface $input, OutputInterface $output) parent::initialize($input, $output); - $this->header(); + // Load the application language + Factory::getApplication()->loadLanguages(); + + // Conditionally emit header + $this->header($input); } - protected function header() + protected function header(InputInterface $input) { + // No header in quiet mode + if ($this->ioStyle->isQuiet()) + { + return; + } + + // No header when using a special output format + if ($input->hasOption('format') && !in_array($input->getOption('format'), [null, 'table', 'txt', 'text', 'human'])) + { + return; + } + + // Check the command class' attributes $showHeader = true; $cliApp = $this->getApplication(); @@ -53,21 +71,25 @@ protected function header() $showHeader = $attributes[0]->getArguments()[0]; } - if ($showHeader && !$this->ioStyle->isQuiet()) + // Forced to never emit a header? Go away. + if (!$showHeader) { - $this->ioStyle->writeln($cliApp->getName() . ' ' . $cliApp->getVersion() . ''); - - $year = gmdate('Y'); - $this->ioStyle->writeln([ - "Copyright (c) 2023-$year Akeeba Ltd", - "", - "Distributed under the terms of the GNU General Public License as published", - "by the Free Software Foundation, either version 3 of the License, or (at your", - "option) any later version. See LICENSE.txt.", - ]); - - $this->ioStyle->title($this->getDescription()); + return; } + + // If I am still here I need to emit the header. + $this->ioStyle->writeln($cliApp->getName() . ' ' . $cliApp->getVersion() . ''); + + $year = gmdate('Y'); + $this->ioStyle->writeln([ + "Copyright (c) 2023-$year Akeeba Ltd", + "", + "Distributed under the terms of the GNU General Public License as published", + "by the Free Software Foundation, either version 3 of the License, or (at your", + "option) any later version. See LICENSE.txt.", + ]); + + $this->ioStyle->title($this->getDescription()); } protected function configureSymfonyIO(InputInterface $input, OutputInterface $output) diff --git a/src/CliCommand/SitesList.php b/src/CliCommand/SitesList.php new file mode 100644 index 00000000..8e0b757c --- /dev/null +++ b/src/CliCommand/SitesList.php @@ -0,0 +1,132 @@ +mvcFactory->makeTempModel('Sites'); + + // Apply filters + $enabled = $input->getOption('enabled'); + + if ($enabled !== null) + { + $model->setState('enabled', $enabled ? '1' : '0'); + } + + $search = $input->getOption('search'); + + if ($search !== null) + { + $model->setState('search', $search); + } + + $coreUpdate = $input->getOption('core-update'); + + if ($coreUpdate !== null) + { + $model->setState('coreUpdates', $coreUpdate ? '1' : '0'); + } + + $extensionUpdate = $input->getOption('extension-update'); + + if ($extensionUpdate !== null) + { + $model->setState('extUpdates', $extensionUpdate ? '1' : '0'); + } + + $cmsFamily = $input->getOption('cms-family'); + + if ($cmsFamily !== null) + { + $model->setState('cmsFamily', $cmsFamily); + } + + $phpFamily = $input->getOption('php-family'); + + if ($phpFamily !== null) + { + $model->setState('phpFamily', $phpFamily); + } + + // Get the items, removing the configuration parameters + $items = $model + ->get(true) + ->map( + fn(Sites $x) => [ + 'id' => $x->id, + 'name' => $x->name, + 'url' => $x->getBaseUrl(), + 'enabled' => $x->enabled, + 'created_by' => $x->created_by, + 'created_on' => $x->created_on, + 'modified_by' => $x->modified_by, + 'modified_on' => $x->modified_on, + ] + ); + + $this->printFormattedAndReturn( + $items->toArray(), + $input->getOption('format') ?: 'table' + ); + + return Command::SUCCESS; + } + + protected function configure(): void + { + $this + ->addOption( + 'format', 'f', InputOption::VALUE_OPTIONAL, 'Output format (table, json, yaml, csv, count)', 'mysqli' + ) + ->addOption( + 'enabled', 'e', InputOption::VALUE_NEGATABLE, 'Only show enabled sites' + ) + ->addOption( + 'search', 's', InputOption::VALUE_OPTIONAL, 'Search among titles and URLs' + ) + ->addOption( + 'cms-family', null, InputOption::VALUE_OPTIONAL, 'Only show sites with this CMS family (e.g. 1.2)' + ) + ->addOption( + 'php-family', null, InputOption::VALUE_OPTIONAL, 'Only show sites with this PHP family (e.g. 1.2)' + ) + ->addOption( + 'core-update', null, InputOption::VALUE_NEGATABLE, 'Only show sites with available core updates' + ) + ->addOption( + 'extension-update', null, InputOption::VALUE_NEGATABLE, 'Only show sites with available extension updates' + ) + ; + } + +} \ No newline at end of file diff --git a/src/CliCommand/Trait/PrintFormattedArrayTrait.php b/src/CliCommand/Trait/PrintFormattedArrayTrait.php new file mode 100644 index 00000000..e1038ab2 --- /dev/null +++ b/src/CliCommand/Trait/PrintFormattedArrayTrait.php @@ -0,0 +1,164 @@ +ioStyle->table($headers, $data); + break; + + case 'json': + $this->ioStyle->writeln(json_encode($data, JSON_PRETTY_PRINT)); + break; + + case 'yaml': + if (!function_exists('yaml_emit')) + { + $line1 = Text::_('COM_ADMINTOOLS_CLI_ERR_CANNOT_GENERATE_YAML'); + $line2 = Text::_('COM_ADMINTOOLS_CLI_ERR_YAML_EXTENSION_NOT_FOUND'); + + $this->ioStyle->error(<<< ERROR +$line1 + +$line2 + +ERROR + + ); + + return 1; + } + + $this->ioStyle->writeln(yaml_emit($data)); + break; + + case 'csv': + $this->ioStyle->writeln($this->toCsv($data)); + break; + + case 'count': + $this->ioStyle->writeln(count($data)); + break; + } + + return 0; + } + + /** + * Converts an array to its CSV representation + * + * @param array $data The array data to convert to CSV + * @param bool $csvHeader Should I print a CSV header row? + * + * @return string + * @since 1.0.0 + */ + private function toCsv(array $data, bool $csvHeader = true): string + { + $output = ''; + $item = array_pop($data); + $data[] = $item; + $keys = array_keys($item); + + if ($csvHeader) + { + $csv = []; + + foreach ($keys as $k) + { + $k = str_replace('"', '""', $k); + $k = str_replace("\r", '\\r', $k); + $k = str_replace("\n", '\\n', $k); + $k = '"' . $k . '"'; + + $csv[] = $k; + } + + $output .= implode(",", $csv) . "\r\n"; + } + + foreach ($data as $item) + { + $csv = []; + + foreach ($keys as $k) + { + $v = $item[$k]; + + if (is_array($v)) + { + $v = 'Array'; + } + elseif (is_object($v)) + { + $v = 'Object'; + } + + $v = str_replace('"', '""', $v); + $v = str_replace("\r", '\\r', $v); + $v = str_replace("\n", '\\n', $v); + $v = '"' . $v . '"'; + + $csv[] = $v; + } + + $output .= implode(",", $csv) . "\r\n"; + } + + return $output; + } +} \ No newline at end of file diff --git a/src/Model/Site.php b/src/Model/Site.php index 00ab589f..4ca8eb6d 100644 --- a/src/Model/Site.php +++ b/src/Model/Site.php @@ -818,6 +818,11 @@ protected function isSiteSpecificTaskScheduled(string $type): bool private function applyUserGroupsToQuery(Query $query): void { + if (defined('AKEEBA_CLI')) + { + return; + } + // Get the user, so we can apply per group privilege checks $user = $this->container->userManager->getUser();