Skip to content

Commit 73cf8fb

Browse files
committed
CLI commands to manage sites
Implement `sites:list` Reference: gh-47
1 parent 95b13b5 commit 73cf8fb

File tree

6 files changed

+376
-57
lines changed

6 files changed

+376
-57
lines changed

cli/panopticon.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Symfony\Component\Console\Application;
1111

1212
const AKEEBA = 1;
13+
const AKEEBA_CLI = 1;
1314

1415
// Make sure we're running under the PHP CLI SAPI
1516
if (php_sapi_name() !== 'cli')

src/Application.php

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,14 @@ public static function getUserMenuTitle(): string
147147

148148
$avatar = $user->getAvatar(64);
149149

150-
return "<img src=\"$avatar\" alt=\"\" class=\"me-1\" style=\"width: 1.25em; border-radius: 0.625em \" >" .
151-
$user->getUsername();
150+
return "<img src=\"$avatar\" alt=\"\" class=\"me-1\" style=\"width: 1.25em; border-radius: 0.625em \" >"
151+
. $user->getUsername();
152152
}
153153

154154
public static function getUserNameTitle(): string
155155
{
156156
return sprintf(
157-
'<span class="small text-muted">%s</span>',
158-
Factory::getContainer()->userManager->getUser()->getName()
157+
'<span class="small text-muted">%s</span>', Factory::getContainer()->userManager->getUser()->getName()
159158
);
160159
}
161160

@@ -278,6 +277,35 @@ public function createOrUpdateSessionPath(string $path, bool $silent = true): vo
278277
}
279278
}
280279

280+
public function loadLanguages(): void
281+
{
282+
try
283+
{
284+
$defaultLanguage = $this->container->appConfig->get('language', 'en-GB');
285+
}
286+
catch (Exception $e)
287+
{
288+
$defaultLanguage = 'en-GB';
289+
}
290+
291+
$detectedLanguage = Text::detectLanguage($this->container, '.ini', $this->container->languagePath);
292+
293+
// Always load the English (Great Britain) language. It contains all the strings.
294+
Text::loadLanguage('en-GB', $this->container, '.ini', true, $this->container->languagePath);
295+
296+
// Load the site's default language, if it's different from en-GB.
297+
if ($defaultLanguage != 'en-GB')
298+
{
299+
Text::loadLanguage($defaultLanguage, $this->container, '.ini', true, $this->container->languagePath);
300+
}
301+
302+
// Load the auto-detected preferred language (per browser settings), as long as it's not one we already loaded.
303+
if (!in_array($detectedLanguage, [$defaultLanguage, 'en-GB']))
304+
{
305+
Text::loadLanguage($detectedLanguage, $this->container, '.ini', true, $this->container->languagePath);
306+
}
307+
}
308+
281309
private function initialiseMenu(array $items = self::MAIN_MENU, ?Item $parent = null): void
282310
{
283311
$menu = $this->getDocument()->getMenu();
@@ -288,8 +316,7 @@ private function initialiseMenu(array $items = self::MAIN_MENU, ?Item $parent =
288316
{
289317
$allowed = array_reduce(
290318
$params['permissions'] ?? [],
291-
fn(bool $carry, string $permission) => $carry && $user->getPrivilege($permission),
292-
true
319+
fn(bool $carry, string $permission) => $carry && $user->getPrivilege($permission), true
293320
);
294321

295322
if (!$allowed)
@@ -463,35 +490,6 @@ private function discoverSessionSavePath(): void
463490
}
464491
}
465492

466-
private function loadLanguages(): void
467-
{
468-
try
469-
{
470-
$defaultLanguage = $this->container->appConfig->get('language', 'en-GB');
471-
}
472-
catch (Exception $e)
473-
{
474-
$defaultLanguage = 'en-GB';
475-
}
476-
477-
$detectedLanguage = Text::detectLanguage($this->container, '.ini', $this->container->languagePath);
478-
479-
// Always load the English (Great Britain) language. It contains all the strings.
480-
Text::loadLanguage('en-GB', $this->container, '.ini', true, $this->container->languagePath);
481-
482-
// Load the site's default language, if it's different from en-GB.
483-
if ($defaultLanguage != 'en-GB')
484-
{
485-
Text::loadLanguage($defaultLanguage, $this->container, '.ini', true, $this->container->languagePath);
486-
}
487-
488-
// Load the auto-detected preferred language (per browser settings), as long as it's not one we already loaded.
489-
if (!in_array($detectedLanguage, [$defaultLanguage, 'en-GB']))
490-
{
491-
Text::loadLanguage($detectedLanguage, $this->container, '.ini', true, $this->container->languagePath);
492-
}
493-
}
494-
495493
private function applyTimezonePreference(): void
496494
{
497495
if (!function_exists('date_default_timezone_get') || !function_exists('date_default_timezone_set'))
@@ -615,13 +613,10 @@ private function redirectToSetup(): bool
615613
{
616614
$configPath = $this->container->appConfig->getDefaultPath();
617615

618-
if (
619-
@file_exists($configPath)
620-
|| in_array(
621-
$this->getContainer()->input->getCmd('view', ''),
622-
self::NO_LOGIN_VIEWS
623-
)
624-
)
616+
if (@file_exists($configPath)
617+
|| in_array(
618+
$this->getContainer()->input->getCmd('view', ''), self::NO_LOGIN_VIEWS
619+
))
625620
{
626621
return false;
627622
}

src/CliCommand/AbstractCommand.php

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Akeeba\Panopticon\CliCommand\Attribute\AppHeader;
1111
use Akeeba\Panopticon\CliCommand\Attribute\ConfigAssertion;
1212
use Akeeba\Panopticon\Factory;
13+
use Awf\Text\Text;
1314
use Symfony\Component\Console\Command\Command;
1415
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
1516
use Symfony\Component\Console\Input\InputInterface;
@@ -37,11 +38,28 @@ protected function initialize(InputInterface $input, OutputInterface $output)
3738

3839
parent::initialize($input, $output);
3940

40-
$this->header();
41+
// Load the application language
42+
Factory::getApplication()->loadLanguages();
43+
44+
// Conditionally emit header
45+
$this->header($input);
4146
}
4247

43-
protected function header()
48+
protected function header(InputInterface $input)
4449
{
50+
// No header in quiet mode
51+
if ($this->ioStyle->isQuiet())
52+
{
53+
return;
54+
}
55+
56+
// No header when using a special output format
57+
if ($input->hasOption('format') && !in_array($input->getOption('format'), [null, 'table', 'txt', 'text', 'human']))
58+
{
59+
return;
60+
}
61+
62+
// Check the command class' attributes
4563
$showHeader = true;
4664
$cliApp = $this->getApplication();
4765

@@ -53,21 +71,25 @@ protected function header()
5371
$showHeader = $attributes[0]->getArguments()[0];
5472
}
5573

56-
if ($showHeader && !$this->ioStyle->isQuiet())
74+
// Forced to never emit a header? Go away.
75+
if (!$showHeader)
5776
{
58-
$this->ioStyle->writeln($cliApp->getName() . ' <info>' . $cliApp->getVersion() . '</info>');
59-
60-
$year = gmdate('Y');
61-
$this->ioStyle->writeln([
62-
"Copyright (c) 2023-$year Akeeba Ltd",
63-
"",
64-
"<debug>Distributed under the terms of the GNU General Public License as published",
65-
"by the Free Software Foundation, either version 3 of the License, or (at your",
66-
"option) any later version. See LICENSE.txt.</debug>",
67-
]);
68-
69-
$this->ioStyle->title($this->getDescription());
77+
return;
7078
}
79+
80+
// If I am still here I need to emit the header.
81+
$this->ioStyle->writeln($cliApp->getName() . ' <info>' . $cliApp->getVersion() . '</info>');
82+
83+
$year = gmdate('Y');
84+
$this->ioStyle->writeln([
85+
"Copyright (c) 2023-$year Akeeba Ltd",
86+
"",
87+
"<debug>Distributed under the terms of the GNU General Public License as published",
88+
"by the Free Software Foundation, either version 3 of the License, or (at your",
89+
"option) any later version. See LICENSE.txt.</debug>",
90+
]);
91+
92+
$this->ioStyle->title($this->getDescription());
7193
}
7294

7395
protected function configureSymfonyIO(InputInterface $input, OutputInterface $output)

src/CliCommand/SitesList.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
/**
3+
* @package panopticon
4+
* @copyright Copyright (c)2023-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
5+
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
6+
*/
7+
8+
namespace Akeeba\Panopticon\CliCommand;
9+
10+
defined('AKEEBA') || die;
11+
12+
use Akeeba\Panopticon\CliCommand\Attribute\ConfigAssertion;
13+
use Akeeba\Panopticon\CliCommand\Trait\PrintFormattedArrayTrait;
14+
use Akeeba\Panopticon\Factory;
15+
use Akeeba\Panopticon\Model\Sites;
16+
use Symfony\Component\Console\Attribute\AsCommand;
17+
use Symfony\Component\Console\Command\Command;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
#[AsCommand(
23+
name: 'sites:list',
24+
description: 'List configured sites',
25+
hidden: false,
26+
)]
27+
#[ConfigAssertion(true)]
28+
class SitesList extends AbstractCommand
29+
{
30+
use PrintFormattedArrayTrait;
31+
32+
protected function execute(InputInterface $input, OutputInterface $output): int
33+
{
34+
$container = Factory::getContainer();
35+
/** @var Sites $model */
36+
$model = $container->mvcFactory->makeTempModel('Sites');
37+
38+
// Apply filters
39+
$enabled = $input->getOption('enabled');
40+
41+
if ($enabled !== null)
42+
{
43+
$model->setState('enabled', $enabled ? '1' : '0');
44+
}
45+
46+
$search = $input->getOption('search');
47+
48+
if ($search !== null)
49+
{
50+
$model->setState('search', $search);
51+
}
52+
53+
$coreUpdate = $input->getOption('core-update');
54+
55+
if ($coreUpdate !== null)
56+
{
57+
$model->setState('coreUpdates', $coreUpdate ? '1' : '0');
58+
}
59+
60+
$extensionUpdate = $input->getOption('extension-update');
61+
62+
if ($extensionUpdate !== null)
63+
{
64+
$model->setState('extUpdates', $extensionUpdate ? '1' : '0');
65+
}
66+
67+
$cmsFamily = $input->getOption('cms-family');
68+
69+
if ($cmsFamily !== null)
70+
{
71+
$model->setState('cmsFamily', $cmsFamily);
72+
}
73+
74+
$phpFamily = $input->getOption('php-family');
75+
76+
if ($phpFamily !== null)
77+
{
78+
$model->setState('phpFamily', $phpFamily);
79+
}
80+
81+
// Get the items, removing the configuration parameters
82+
$items = $model
83+
->get(true)
84+
->map(
85+
fn(Sites $x) => [
86+
'id' => $x->id,
87+
'name' => $x->name,
88+
'url' => $x->getBaseUrl(),
89+
'enabled' => $x->enabled,
90+
'created_by' => $x->created_by,
91+
'created_on' => $x->created_on,
92+
'modified_by' => $x->modified_by,
93+
'modified_on' => $x->modified_on,
94+
]
95+
);
96+
97+
$this->printFormattedAndReturn(
98+
$items->toArray(),
99+
$input->getOption('format') ?: 'table'
100+
);
101+
102+
return Command::SUCCESS;
103+
}
104+
105+
protected function configure(): void
106+
{
107+
$this
108+
->addOption(
109+
'format', 'f', InputOption::VALUE_OPTIONAL, 'Output format (table, json, yaml, csv, count)', 'mysqli'
110+
)
111+
->addOption(
112+
'enabled', 'e', InputOption::VALUE_NEGATABLE, 'Only show enabled sites'
113+
)
114+
->addOption(
115+
'search', 's', InputOption::VALUE_OPTIONAL, 'Search among titles and URLs'
116+
)
117+
->addOption(
118+
'cms-family', null, InputOption::VALUE_OPTIONAL, 'Only show sites with this CMS family (e.g. 1.2)'
119+
)
120+
->addOption(
121+
'php-family', null, InputOption::VALUE_OPTIONAL, 'Only show sites with this PHP family (e.g. 1.2)'
122+
)
123+
->addOption(
124+
'core-update', null, InputOption::VALUE_NEGATABLE, 'Only show sites with available core updates'
125+
)
126+
->addOption(
127+
'extension-update', null, InputOption::VALUE_NEGATABLE, 'Only show sites with available extension updates'
128+
)
129+
;
130+
}
131+
132+
}

0 commit comments

Comments
 (0)