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();