first commi

This commit is contained in:
2021-03-05 09:30:59 +01:00
commit bfa5e61d5e
853 changed files with 120974 additions and 0 deletions
@@ -0,0 +1,106 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Common\Grav;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class Application extends \Symfony\Component\Console\Application
{
/** @var string|null */
protected $environment;
/** @var string|null */
protected $language;
/** @var bool */
protected $initialized = false;
/**
* @param InputInterface $input
* @return string|null
*/
public function getCommandName(InputInterface $input): ?string
{
$this->environment = $input->getOption('env');
$this->language = $input->getOption('lang') ?? $this->language;
$this->init();
return parent::getCommandName($input);
}
/**
* @return void
*/
protected function init(): void
{
if ($this->initialized) {
return;
}
$this->initialized = true;
$grav = Grav::instance();
$grav->setup($this->environment);
}
/**
* Add global a --env option.
*
* @return InputDefinition
*/
protected function getDefaultInputDefinition(): InputDefinition
{
$inputDefinition = parent::getDefaultInputDefinition();
$inputDefinition->addOption(
new InputOption(
'env',
null,
InputOption::VALUE_OPTIONAL,
'Use environment configuration (defaults to localhost)'
)
);
$inputDefinition->addOption(
new InputOption(
'lang',
null,
InputOption::VALUE_OPTIONAL,
'Language to be used (defaults to en)'
)
);
return $inputDefinition;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return void
*/
protected function configureIO(InputInterface $input, OutputInterface $output)
{
$formatter = $output->getFormatter();
$formatter->setStyle('normal', new OutputFormatterStyle('white'));
$formatter->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold']));
$formatter->setStyle('red', new OutputFormatterStyle('red', null, ['bold']));
$formatter->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold']));
$formatter->setStyle('green', new OutputFormatterStyle('green', null, ['bold']));
$formatter->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold']));
$formatter->setStyle('white', new OutputFormatterStyle('white', null, ['bold']));
parent::configureIO($input, $output);
}
}
@@ -0,0 +1,95 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application\CommandLoader;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class PluginCommandLoader implements CommandLoaderInterface
{
/** @var array */
private $commands;
/**
* PluginCommandLoader constructor.
*
* @param string $name
*/
public function __construct(string $name)
{
$this->commands = [];
try {
$path = "plugins://{$name}/cli";
$pattern = '([A-Z]\w+Command\.php)';
$commands = is_dir($path) ? Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm', 'levels' => 1]) : [];
} catch (RuntimeException $e) {
throw new RuntimeException("Failed to load console commands for plugin {$name}");
}
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
foreach ($commands as $command_path) {
$full_path = $locator->findResource("plugins://{$name}/cli/{$command_path}");
require_once $full_path;
$command_class = 'Grav\Plugin\Console\\' . preg_replace('/.php$/', '', $command_path);
if (class_exists($command_class)) {
$command = new $command_class();
if ($command instanceof Command) {
$this->commands[$command->getName()] = $command;
}
}
}
}
/**
* @param string $name
* @return Command
*/
public function get($name): Command
{
$command = $this->commands[$name] ?? null;
if (null === $command) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
return $command;
}
/**
* @param string $name
* @return bool
*/
public function has($name): bool
{
return isset($this->commands[$name]);
}
/**
* @return string[]
*/
public function getNames(): array
{
return array_keys($this->commands);
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Console\Gpm\DirectInstallCommand;
use Grav\Console\Gpm\IndexCommand;
use Grav\Console\Gpm\InfoCommand;
use Grav\Console\Gpm\InstallCommand;
use Grav\Console\Gpm\SelfupgradeCommand;
use Grav\Console\Gpm\UninstallCommand;
use Grav\Console\Gpm\UpdateCommand;
use Grav\Console\Gpm\VersionCommand;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class GpmApplication extends Application
{
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new IndexCommand(),
new VersionCommand(),
new InfoCommand(),
new InstallCommand(),
new UninstallCommand(),
new UpdateCommand(),
new SelfupgradeCommand(),
new DirectInstallCommand(),
]);
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Console\Cli\BackupCommand;
use Grav\Console\Cli\CleanCommand;
use Grav\Console\Cli\ClearCacheCommand;
use Grav\Console\Cli\ComposerCommand;
use Grav\Console\Cli\InstallCommand;
use Grav\Console\Cli\LogViewerCommand;
use Grav\Console\Cli\NewProjectCommand;
use Grav\Console\Cli\PageSystemValidatorCommand;
use Grav\Console\Cli\SandboxCommand;
use Grav\Console\Cli\SchedulerCommand;
use Grav\Console\Cli\SecurityCommand;
use Grav\Console\Cli\ServerCommand;
use Grav\Console\Cli\YamlLinterCommand;
/**
* Class GravApplication
* @package Grav\Console\Application
*/
class GravApplication extends Application
{
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new InstallCommand(),
new ComposerCommand(),
new SandboxCommand(),
new CleanCommand(),
new ClearCacheCommand(),
new BackupCommand(),
new NewProjectCommand(),
new SchedulerCommand(),
new SecurityCommand(),
new LogViewerCommand(),
new YamlLinterCommand(),
new ServerCommand(),
new PageSystemValidatorCommand(),
]);
}
}
@@ -0,0 +1,116 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Common\Grav;
use Grav\Common\Plugins;
use Grav\Console\Application\CommandLoader\PluginCommandLoader;
use Grav\Console\Plugin\PluginListCommand;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Class PluginApplication
* @package Grav\Console\Application
*/
class PluginApplication extends Application
{
/** @var string|null */
protected $pluginName;
/**
* PluginApplication constructor.
* @param string $name
* @param string $version
*/
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new PluginListCommand(),
]);
}
/**
* @param string $pluginName
* @return void
*/
public function setPluginName(string $pluginName): void
{
$this->pluginName = $pluginName;
}
/**
* @return string
*/
public function getPluginName(): string
{
return $this->pluginName;
}
/**
* @param InputInterface|null $input
* @param OutputInterface|null $output
* @return int
* @throws Throwable
*/
public function run(InputInterface $input = null, OutputInterface $output = null): int
{
if (null === $input) {
$argv = $_SERVER['argv'] ?? [];
$bin = array_shift($argv);
$this->pluginName = array_shift($argv);
$argv = array_merge([$bin], $argv);
$input = new ArgvInput($argv);
}
return parent::run($input, $output);
}
/**
* @return void
*/
protected function init(): void
{
if ($this->initialized) {
return;
}
parent::init();
if (null === $this->pluginName) {
$this->setDefaultCommand('plugins:list');
return;
}
$grav = Grav::instance();
$grav->initializeCli();
/** @var Plugins $plugins */
$plugins = $grav['plugins'];
$plugin = $this->pluginName ? $plugins::get($this->pluginName) : null;
if (null === $plugin) {
throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not installed.");
}
if (!$plugin->enabled) {
throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not enabled.");
}
$this->setCommandLoader(new PluginCommandLoader($this->pluginName));
}
}
@@ -0,0 +1,138 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Backup\Backups;
use Grav\Common\Grav;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Question\ChoiceQuestion;
use ZipArchive;
use function count;
/**
* Class BackupCommand
* @package Grav\Console\Cli
*/
class BackupCommand extends GravCommand
{
/** @var string $source */
protected $source;
/** @var ProgressBar $progress */
protected $progress;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('backup')
->addArgument(
'id',
InputArgument::OPTIONAL,
'The ID of the backup profile to perform without prompting'
)
->setDescription('Creates a backup of the Grav instance')
->setHelp('The <info>backup</info> creates a zipped backup.');
$this->source = getcwd();
}
/**
* @return int
*/
protected function serve(): int
{
$this->initializeGrav();
$input = $this->getInput();
$io = $this->getIO();
$io->title('Grav Backup');
if (!class_exists(ZipArchive::class)) {
$io->error('php-zip extension needs to be enabled!');
return 1;
}
ProgressBar::setFormatDefinition('zip', 'Archiving <cyan>%current%</cyan> files [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s% <yellow>%message%</yellow>');
$this->progress = new ProgressBar($this->output, 100);
$this->progress->setFormat('zip');
/** @var Backups $backups */
$backups = Grav::instance()['backups'];
$backups_list = $backups::getBackupProfiles();
$backups_names = $backups->getBackupNames();
$id = null;
$inline_id = $input->getArgument('id');
if (null !== $inline_id && is_numeric($inline_id)) {
$id = $inline_id;
}
if (null === $id) {
if (count($backups_list) > 1) {
$question = new ChoiceQuestion(
'Choose a backup?',
$backups_names,
0
);
$question->setErrorMessage('Option %s is invalid.');
$backup_name = $io->askQuestion($question);
$id = array_search($backup_name, $backups_names, true);
$io->newLine();
$io->note('Selected backup: ' . $backup_name);
} else {
$id = 0;
}
}
$backup = $backups::backup($id, function($args) { $this->outputProgress($args); });
$io->newline(2);
$io->success('Backup Successfully Created: ' . $backup);
return 0;
}
/**
* @param array $args
* @return void
*/
public function outputProgress(array $args): void
{
switch ($args['type']) {
case 'count':
$steps = $args['steps'];
$freq = (int)($steps > 100 ? round($steps / 100) : $steps);
$this->progress->setMaxSteps($steps);
$this->progress->setRedrawFrequency($freq);
$this->progress->setMessage('Adding files...');
break;
case 'message':
$this->progress->setMessage($args['message']);
$this->progress->display();
break;
case 'progress':
if (isset($args['complete']) && $args['complete']) {
$this->progress->finish();
} else {
$this->progress->advance();
}
break;
}
}
}
@@ -0,0 +1,411 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Filesystem\Folder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Class CleanCommand
* @package Grav\Console\Cli
*/
class CleanCommand extends Command
{
/** @var InputInterface */
protected $input;
/** @var SymfonyStyle */
protected $io;
/** @var array */
protected $paths_to_remove = [
'codeception.yml',
'tests/',
'user/plugins/admin/vendor/bacon/bacon-qr-code/tests',
'user/plugins/admin/vendor/bacon/bacon-qr-code/.gitignore',
'user/plugins/admin/vendor/bacon/bacon-qr-code/.travis.yml',
'user/plugins/admin/vendor/bacon/bacon-qr-code/composer.json',
'user/plugins/admin/vendor/robthree/twofactorauth/demo',
'user/plugins/admin/vendor/robthree/twofactorauth/.vs',
'user/plugins/admin/vendor/robthree/twofactorauth/tests',
'user/plugins/admin/vendor/robthree/twofactorauth/.gitignore',
'user/plugins/admin/vendor/robthree/twofactorauth/.travis.yml',
'user/plugins/admin/vendor/robthree/twofactorauth/composer.json',
'user/plugins/admin/vendor/robthree/twofactorauth/composer.lock',
'user/plugins/admin/vendor/robthree/twofactorauth/logo.png',
'user/plugins/admin/vendor/robthree/twofactorauth/multifactorauthforeveryone.png',
'user/plugins/admin/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj',
'user/plugins/admin/vendor/robthree/twofactorauth/TwoFactorAuth.sin',
'user/plugins/admin/vendor/zendframework/zendxml/tests',
'user/plugins/admin/vendor/zendframework/zendxml/.gitignore',
'user/plugins/admin/vendor/zendframework/zendxml/.travis.yml',
'user/plugins/admin/vendor/zendframework/zendxml/composer.json',
'user/plugins/email/vendor/swiftmailer/swiftmailer/.travis.yml',
'user/plugins/email/vendor/swiftmailer/swiftmailer/build.xml',
'user/plugins/email/vendor/swiftmailer/swiftmailer/composer.json',
'user/plugins/email/vendor/swiftmailer/swiftmailer/create_pear_package.php',
'user/plugins/email/vendor/swiftmailer/swiftmailer/package.xml.tpl',
'user/plugins/email/vendor/swiftmailer/swiftmailer/.gitattributes',
'user/plugins/email/vendor/swiftmailer/swiftmailer/.gitignore',
'user/plugins/email/vendor/swiftmailer/swiftmailer/README.git',
'user/plugins/email/vendor/swiftmailer/swiftmailer/tests',
'user/plugins/email/vendor/swiftmailer/swiftmailer/test-suite',
'user/plugins/email/vendor/swiftmailer/swiftmailer/notes',
'user/plugins/email/vendor/swiftmailer/swiftmailer/doc',
'user/themes/antimatter/.sass-cache',
'vendor/antoligy/dom-string-iterators/composer.json',
'vendor/composer/ca-bundle/composer.json',
'vendor/composer/ca-bundle/phpstan.neon.dist',
'vendor/composer/semver/CHANGELOG.md',
'vendor/composer/semver/composer.json',
'vendor/composer/semver/phpstan.neon.dist',
'vendor/doctrine/cache/.travis.yml',
'vendor/doctrine/cache/build.properties',
'vendor/doctrine/cache/build.xml',
'vendor/doctrine/cache/composer.json',
'vendor/doctrine/cache/phpunit.xml.dist',
'vendor/doctrine/cache/.coveralls.yml',
'vendor/doctrine/cache/.gitignore',
'vendor/doctrine/cache/.git',
'vendor/doctrine/cache/tests',
'vendor/doctrine/cache/UPGRADE.md',
'vendor/doctrine/collections/docs',
'vendor/doctrine/collections/.doctrine-project.json',
'vendor/doctrine/collections/CONTRIBUTING.md',
'vendor/doctrine/collections/psalm.xml.dist',
'vendor/doctrine/collections/composer.json',
'vendor/doctrine/collections/phpunit.xml.dist',
'vendor/doctrine/collections/tests',
'vendor/donatj/phpuseragentparser/.git',
'vendor/donatj/phpuseragentparser/.github',
'vendor/donatj/phpuseragentparser/.gitignore',
'vendor/donatj/phpuseragentparser/.editorconfig',
'vendor/donatj/phpuseragentparser/.travis.yml',
'vendor/donatj/phpuseragentparser/composer.json',
'vendor/donatj/phpuseragentparser/phpunit.xml.dist',
'vendor/donatj/phpuseragentparser/tests',
'vendor/donatj/phpuseragentparser/Tools',
'vendor/donatj/phpuseragentparser/CONTRIBUTING.md',
'vendor/donatj/phpuseragentparser/Makefile',
'vendor/donatj/phpuseragentparser/.mddoc.xml',
'vendor/dragonmantank/cron-expression/.editorconfig',
'vendor/dragonmantank/cron-expression/composer.json',
'vendor/dragonmantank/cron-expression/tests',
'vendor/dragonmantank/cron-expression/CHANGELOG.md',
'vendor/enshrined/svg-sanitize/tests',
'vendor/enshrined/svg-sanitize/.gitignore',
'vendor/enshrined/svg-sanitize/.travis.yml',
'vendor/enshrined/svg-sanitize/composer.json',
'vendor/enshrined/svg-sanitize/phpunit.xml',
'vendor/erusev/parsedown/composer.json',
'vendor/erusev/parsedown/phpunit.xml.dist',
'vendor/erusev/parsedown/.travis.yml',
'vendor/erusev/parsedown/.git',
'vendor/erusev/parsedown/test',
'vendor/erusev/parsedown-extra/composer.json',
'vendor/erusev/parsedown-extra/phpunit.xml.dist',
'vendor/erusev/parsedown-extra/.travis.yml',
'vendor/erusev/parsedown-extra/.git',
'vendor/erusev/parsedown-extra/test',
'vendor/filp/whoops/composer.json',
'vendor/filp/whoops/docs',
'vendor/filp/whoops/examples',
'vendor/filp/whoops/tests',
'vendor/filp/whoops/.git',
'vendor/filp/whoops/.github',
'vendor/filp/whoops/.gitignore',
'vendor/filp/whoops/.scrutinizer.yml',
'vendor/filp/whoops/.travis.yml',
'vendor/filp/whoops/phpunit.xml.dist',
'vendor/filp/whoops/CHANGELOG.md',
'vendor/gregwar/image/Gregwar/Image/composer.json',
'vendor/gregwar/image/Gregwar/Image/phpunit.xml',
'vendor/gregwar/image/Gregwar/Image/phpunit.xml.dist',
'vendor/gregwar/image/Gregwar/Image/Makefile',
'vendor/gregwar/image/Gregwar/Image/.editorconfig',
'vendor/gregwar/image/Gregwar/Image/.php_cs',
'vendor/gregwar/image/Gregwar/Image/.styleci.yml',
'vendor/gregwar/image/Gregwar/Image/.travis.yml',
'vendor/gregwar/image/Gregwar/Image/.gitignore',
'vendor/gregwar/image/Gregwar/Image/.git',
'vendor/gregwar/image/Gregwar/Image/doc',
'vendor/gregwar/image/Gregwar/Image/demo',
'vendor/gregwar/image/Gregwar/Image/tests',
'vendor/gregwar/cache/Gregwar/Cache/composer.json',
'vendor/gregwar/cache/Gregwar/Cache/phpunit.xml',
'vendor/gregwar/cache/Gregwar/Cache/.travis.yml',
'vendor/gregwar/cache/Gregwar/Cache/.gitignore',
'vendor/gregwar/cache/Gregwar/Cache/.git',
'vendor/gregwar/cache/Gregwar/Cache/demo',
'vendor/gregwar/cache/Gregwar/Cache/tests',
'vendor/guzzlehttp/psr7/composer.json',
'vendor/guzzlehttp/psr7/.editorconfig',
'vendor/guzzlehttp/psr7/CHANGELOG.md',
'vendor/itsgoingd/clockwork/.gitattributes',
'vendor/itsgoingd/clockwork/CHANGELOG.md',
'vendor/itsgoingd/clockwork/composer.json',
'vendor/kodus/psr7-server/composer.json',
'vendor/kodus/psr7-server/CHANGELOG.md',
'vendor/league/climate/composer.json',
'vendor/league/climate/CHANGELOG.md',
'vendor/league/climate/CONTRIBUTING.md',
'vendor/league/climate/Dockerfile',
'vendor/league/climate/CODE_OF_CONDUCT.md',
'vendor/matthiasmullie/minify/.github',
'vendor/matthiasmullie/minify/bin',
'vendor/matthiasmullie/minify/composer.json',
'vendor/matthiasmullie/minify/docker-compose.yml',
'vendor/matthiasmullie/minify/Dockerfile',
'vendor/matthiasmullie/minify/CONTRIBUTING.md',
'vendor/matthiasmullie/path-converter/composer.json',
'vendor/maximebf/debugbar/.github',
'vendor/maximebf/debugbar/bower.json',
'vendor/maximebf/debugbar/composer.json',
'vendor/maximebf/debugbar/.bowerrc',
'vendor/maximebf/debugbar/src/DebugBar/Resources/vendor',
'vendor/maximebf/debugbar/demo',
'vendor/maximebf/debugbar/docs',
'vendor/maximebf/debugbar/tests',
'vendor/maximebf/debugbar/phpunit.xml.dist',
'vendor/miljar/php-exif/.coveralls.yml',
'vendor/miljar/php-exif/.gitignore',
'vendor/miljar/php-exif/.travis.yml',
'vendor/miljar/php-exif/composer.json',
'vendor/miljar/php-exif/composer.lock',
'vendor/miljar/php-exif/phpunit.xml.dist',
'vendor/miljar/php-exif/Resources',
'vendor/miljar/php-exif/tests',
'vendor/miljar/php-exif/CHANGELOG.rst',
'vendor/monolog/monolog/composer.json',
'vendor/monolog/monolog/doc',
'vendor/monolog/monolog/phpunit.xml.dist',
'vendor/monolog/monolog/.php_cs',
'vendor/monolog/monolog/tests',
'vendor/monolog/monolog/CHANGELOG.md',
'vendor/monolog/monolog/phpstan.neon.dist',
'vendor/nyholm/psr7/composer.json',
'vendor/nyholm/psr7/phpstan.neon.dist',
'vendor/nyholm/psr7/CHANGELOG.md',
'vendor/nyholm/psr7/psalm.xml',
'vendor/phive/twig-extensions-deferred/.gitignore',
'vendor/phive/twig-extensions-deferred/.travis.yml',
'vendor/phive/twig-extensions-deferred/composer.json',
'vendor/phive/twig-extensions-deferred/phpunit.xml.dist',
'vendor/phive/twig-extensions-deferred/tests',
'vendor/php-http/message-factory/composer.json',
'vendor/php-http/message-factory/puli.json',
'vendor/php-http/message-factory/CHANGELOG.md',
'vendor/pimple/pimple/.gitignore',
'vendor/pimple/pimple/.travis.yml',
'vendor/pimple/pimple/composer.json',
'vendor/pimple/pimple/ext',
'vendor/pimple/pimple/phpunit.xml.dist',
'vendor/pimple/pimple/src/Pimple/Tests',
'vendor/pimple/pimple/.php_cs.dist',
'vendor/pimple/pimple/CHANGELOG',
'vendor/psr/cache/CHANGELOG.md',
'vendor/psr/cache/composer.json',
'vendor/psr/container/composer.json',
'vendor/psr/container/.gitignore',
'vendor/psr/http-factory/.gitignore',
'vendor/psr/http-factory/.pullapprove.yml',
'vendor/psr/http-factory/composer.json',
'vendor/psr/http-message/composer.json',
'vendor/psr/http-message/CHANGELOG.md',
'vendor/psr/http-server-handler/composer.json',
'vendor/psr/http-server-middleware/composer.json',
'vendor/psr/simple-cache/.editorconfig',
'vendor/psr/simple-cache/composer.json',
'vendor/psr/log/composer.json',
'vendor/psr/log/.gitignore',
'vendor/ralouphie/getallheaders/.gitignore',
'vendor/ralouphie/getallheaders/.travis.yml',
'vendor/ralouphie/getallheaders/composer.json',
'vendor/ralouphie/getallheaders/phpunit.xml',
'vendor/ralouphie/getallheaders/tests/',
'vendor/rockettheme/toolbox/.git',
'vendor/rockettheme/toolbox/.gitignore',
'vendor/rockettheme/toolbox/.scrutinizer.yml',
'vendor/rockettheme/toolbox/.travis.yml',
'vendor/rockettheme/toolbox/composer.json',
'vendor/rockettheme/toolbox/phpunit.xml',
'vendor/rockettheme/toolbox/CHANGELOG.md',
'vendor/rockettheme/toolbox/Blueprints/tests',
'vendor/rockettheme/toolbox/ResourceLocator/tests',
'vendor/rockettheme/toolbox/Session/tests',
'vendor/rockettheme/toolbox/tests',
'vendor/seld/cli-prompt/composer.json',
'vendor/seld/cli-prompt/.gitignore',
'vendor/seld/cli-prompt/.github',
'vendor/seld/cli-prompt/phpstan.neon.dist',
'vendor/symfony/console/composer.json',
'vendor/symfony/console/phpunit.xml.dist',
'vendor/symfony/console/.gitignore',
'vendor/symfony/console/.git',
'vendor/symfony/console/Tester',
'vendor/symfony/console/Tests',
'vendor/symfony/console/CHANGELOG.md',
'vendor/symfony/contracts/Cache/.gitignore',
'vendor/symfony/contracts/Cache/composer.json',
'vendor/symfony/contracts/EventDispatcher/.gitignore',
'vendor/symfony/contracts/EventDispatcher/composer.json',
'vendor/symfony/contracts/HttpClient/.gitignore',
'vendor/symfony/contracts/HttpClient/composer.json',
'vendor/symfony/contracts/HttpClient/Test',
'vendor/symfony/contracts/Service/.gitignore',
'vendor/symfony/contracts/Service/composer.json',
'vendor/symfony/contracts/Service/Test',
'vendor/symfony/contracts/Tests',
'vendor/symfony/contracts/Translation/.gitignore',
'vendor/symfony/contracts/Translation/composer.json',
'vendor/symfony/contracts/Translation/Test',
'vendor/symfony/contracts/.gitignore',
'vendor/symfony/contracts/composer.json',
'vendor/symfony/contracts/phpunit.xml.dist',
'vendor/symfony/event-dispatcher/.git',
'vendor/symfony/event-dispatcher/.gitignore',
'vendor/symfony/event-dispatcher/composer.json',
'vendor/symfony/event-dispatcher/phpunit.xml.dist',
'vendor/symfony/event-dispatcher/Tests',
'vendor/symfony/event-dispatcher/CHANGELOG.md',
'vendor/symfony/http-client/CHANGELOG.md',
'vendor/symfony/http-client/composer.json',
'vendor/symfony/polyfill-ctype/composer.json',
'vendor/symfony/polyfill-iconv/.git',
'vendor/symfony/polyfill-iconv/.gitignore',
'vendor/symfony/polyfill-iconv/composer.json',
'vendor/symfony/polyfill-mbstring/.git',
'vendor/symfony/polyfill-mbstring/.gitignore',
'vendor/symfony/polyfill-mbstring/composer.json',
'vendor/symfony/polyfill-php72/composer.json',
'vendor/symfony/polyfill-php73/composer.json',
'vendor/symfony/process/.gitignore',
'vendor/symfony/process/composer.json',
'vendor/symfony/process/phpunit.xml.dist',
'vendor/symfony/process/Tests',
'vendor/symfony/process/CHANGELOG.md',
'vendor/symfony/var-dumper/.git',
'vendor/symfony/var-dumper/.gitignore',
'vendor/symfony/var-dumper/composer.json',
'vendor/symfony/var-dumper/phpunit.xml.dist',
'vendor/symfony/var-dumper/Test',
'vendor/symfony/var-dumper/Tests',
'vendor/symfony/var-dumper/CHANGELOG.md',
'vendor/symfony/yaml/composer.json',
'vendor/symfony/yaml/phpunit.xml.dist',
'vendor/symfony/yaml/.gitignore',
'vendor/symfony/yaml/.git',
'vendor/symfony/yaml/Tests',
'vendor/symfony/yaml/CHANGELOG.md',
'vendor/twig/twig/.editorconfig',
'vendor/twig/twig/.php_cs.dist',
'vendor/twig/twig/.travis.yml',
'vendor/twig/twig/.gitignore',
'vendor/twig/twig/.git',
'vendor/twig/twig/.github',
'vendor/twig/twig/composer.json',
'vendor/twig/twig/phpunit.xml.dist',
'vendor/twig/twig/doc',
'vendor/twig/twig/ext',
'vendor/twig/twig/test',
'vendor/twig/twig/.gitattributes',
'vendor/twig/twig/CHANGELOG',
'vendor/twig/twig/drupal_test.sh',
'vendor/willdurand/negotiation/.gitignore',
'vendor/willdurand/negotiation/.travis.yml',
'vendor/willdurand/negotiation/appveyor.yml',
'vendor/willdurand/negotiation/composer.json',
'vendor/willdurand/negotiation/phpunit.xml.dist',
'vendor/willdurand/negotiation/tests',
'vendor/willdurand/negotiation/CONTRIBUTING.md',
'user/config/security.yaml',
'cache/compiled/',
];
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('clean')
->setDescription('Handles cleaning chores for Grav distribution')
->setHelp('The <info>clean</info> clean extraneous folders and data');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->setupConsole($input, $output);
return $this->cleanPaths() ? 0 : 1;
}
/**
* @return bool
*/
private function cleanPaths(): bool
{
$success = true;
$this->io->writeln('');
$this->io->writeln('<red>DELETING</red>');
$anything = false;
foreach ($this->paths_to_remove as $path) {
$path = GRAV_ROOT . DS . $path;
try {
if (is_dir($path) && Folder::delete($path)) {
$anything = true;
$this->io->writeln('<red>dir: </red>' . $path);
} elseif (is_file($path) && @unlink($path)) {
$anything = true;
$this->io->writeln('<red>file: </red>' . $path);
}
} catch (\Exception $e) {
$success = false;
$this->io->error(sprintf('Failed to delete %s: %s', $path, $e->getMessage()));
}
}
if (!$anything) {
$this->io->writeln('');
$this->io->writeln('<green>Nothing to clean...</green>');
}
return $success;
}
/**
* Set colors style definition for the formatter.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return void
*/
public function setupConsole(InputInterface $input, OutputInterface $output): void
{
$this->input = $input;
$this->io = new SymfonyStyle($input, $output);
$this->io->getFormatter()->setStyle('normal', new OutputFormatterStyle('white'));
$this->io->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold']));
$this->io->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, ['bold']));
$this->io->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold']));
$this->io->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, ['bold']));
$this->io->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold']));
$this->io->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, ['bold']));
}
}
@@ -0,0 +1,104 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Cache;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ClearCacheCommand
* @package Grav\Console\Cli
*/
class ClearCacheCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('cache')
->setAliases(['clearcache', 'cache-clear'])
->setDescription('Clears Grav cache')
->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files')
->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches')
->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches')
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
->addOption('images-only', null, InputOption::VALUE_NONE, 'If set will remove only images/*')
->addOption('cache-only', null, InputOption::VALUE_NONE, 'If set will remove only cache/*')
->addOption('tmp-only', null, InputOption::VALUE_NONE, 'If set will remove only tmp/*')
->setHelp('The <info>cache</info> command allows you to interact with Grav cache');
}
/**
* @return int
*/
protected function serve(): int
{
// Old versions of Grav called this command after grav upgrade.
// We need make this command to work with older GravCommand instance:
if (!method_exists($this, 'initializePlugins')) {
Cache::clearCache('all');
return 0;
}
$this->initializePlugins();
$this->cleanPaths();
return 0;
}
/**
* loops over the array of paths and deletes the files/folders
*
* @return void
*/
private function cleanPaths(): void
{
$input = $this->getInput();
$io = $this->getIO();
$io->newLine();
if ($input->getOption('purge')) {
$io->writeln('<magenta>Purging old cache</magenta>');
$io->newLine();
$msg = Cache::purgeJob();
$io->writeln($msg);
} else {
$io->writeln('<magenta>Clearing cache</magenta>');
$io->newLine();
if ($input->getOption('all')) {
$remove = 'all';
} elseif ($input->getOption('assets-only')) {
$remove = 'assets-only';
} elseif ($input->getOption('images-only')) {
$remove = 'images-only';
} elseif ($input->getOption('cache-only')) {
$remove = 'cache-only';
} elseif ($input->getOption('tmp-only')) {
$remove = 'tmp-only';
} elseif ($input->getOption('invalidate')) {
$remove = 'invalidate';
} else {
$remove = 'standard';
}
foreach (Cache::clearCache($remove) as $result) {
$io->writeln($result);
}
}
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ComposerCommand
* @package Grav\Console\Cli
*/
class ComposerCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('composer')
->addOption(
'install',
'i',
InputOption::VALUE_NONE,
'install the dependencies'
)
->addOption(
'update',
'u',
InputOption::VALUE_NONE,
'update the dependencies'
)
->setDescription('Updates the composer vendor dependencies needed by Grav.')
->setHelp('The <info>composer</info> command updates the composer vendor dependencies needed by Grav');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$action = $input->getOption('install') ? 'install' : ($input->getOption('update') ? 'update' : 'install');
if ($input->getOption('install')) {
$action = 'install';
}
// Updates composer first
$io->writeln("\nInstalling vendor dependencies");
$io->writeln($this->composerUpdate(GRAV_ROOT, $action));
return 0;
}
}
@@ -0,0 +1,302 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Console\GravCommand;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\JsonFile;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function is_array;
/**
* Class InstallCommand
* @package Grav\Console\Cli
*/
class InstallCommand extends GravCommand
{
/** @var array */
protected $config;
/** @var string */
protected $destination;
/** @var string */
protected $user_path;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('install')
->addOption(
'symlink',
's',
InputOption::VALUE_NONE,
'Symlink the required bits'
)
->addOption(
'plugin',
'p',
InputOption::VALUE_REQUIRED,
'Install plugin (symlink)'
)
->addOption(
'theme',
't',
InputOption::VALUE_REQUIRED,
'Install theme (symlink)'
)
->addArgument(
'destination',
InputArgument::OPTIONAL,
'Where to install the required bits (default to current project)'
)
->setDescription('Installs the dependencies needed by Grav. Optionally can create symbolic links')
->setHelp('The <info>install</info> command installs the dependencies needed by Grav. Optionally can create symbolic links');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$dependencies_file = '.dependencies';
$this->destination = $input->getArgument('destination') ?: GRAV_ROOT;
// fix trailing slash
$this->destination = rtrim($this->destination, DS) . DS;
$this->user_path = $this->destination . USER_PATH;
if ($local_config_file = $this->loadLocalConfig()) {
$io->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
}
// Look for dependencies file in ROOT and USER dir
if (file_exists($this->user_path . $dependencies_file)) {
$file = YamlFile::instance($this->user_path . $dependencies_file);
} elseif (file_exists($this->destination . $dependencies_file)) {
$file = YamlFile::instance($this->destination . $dependencies_file);
} else {
$io->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
if ($input->getArgument('destination')) {
$io->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
} else {
$io->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
}
return 1;
}
$this->config = $file->content();
$file->free();
// If no config, fail.
if (!$this->config) {
$io->writeln('<red>ERROR</red> invalid YAML in ' . $dependencies_file);
return 1;
}
$plugin = $input->getOption('plugin');
$theme = $input->getOption('theme');
$name = $plugin ?? $theme;
$symlink = $name || $input->getOption('symlink');
if (!$symlink) {
// Updates composer first
$io->writeln("\nInstalling vendor dependencies");
$io->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
$error = $this->gitclone();
} else {
$type = $name ? ($plugin ? 'plugin' : 'theme') : null;
$error = $this->symlink($name, $type);
}
return $error;
}
/**
* Clones from Git
*
* @return int
*/
private function gitclone(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<green>Cloning Bits</green>');
$io->writeln('============');
$io->newLine();
$error = 0;
$this->destination = rtrim($this->destination, DS);
foreach ($this->config['git'] as $repo => $data) {
$path = $this->destination . DS . $data['path'];
if (!file_exists($path)) {
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return);
if (!$return) {
$io->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
} else {
$io->writeln('<red>ERROR</red> cloning <magenta>' . $data['url']);
$error = 1;
}
$io->newLine();
} else {
$io->writeln('<yellow>' . $path . ' already exists, skipping...</yellow>');
$io->newLine();
}
}
return $error;
}
/**
* Symlinks
*
* @param string|null $name
* @param string|null $type
* @return int
*/
private function symlink(string $name = null, string $type = null): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<green>Symlinking Bits</green>');
$io->writeln('===============');
$io->newLine();
if (!$this->local_config) {
$io->writeln('<red>No local configuration available, aborting...</red>');
$io->newLine();
return 1;
}
$error = 0;
$this->destination = rtrim($this->destination, DS);
if ($name) {
$src = "grav-{$type}-{$name}";
$links = [
$name => [
'scm' => 'github', // TODO: make configurable
'src' => $src,
'path' => "user/{$type}s/{$name}"
]
];
} else {
$links = $this->config['links'];
}
foreach ($links as $name => $data) {
$scm = $data['scm'] ?? null;
$src = $data['src'] ?? null;
$path = $data['path'] ?? null;
if (!isset($scm, $src, $path)) {
$io->writeln("<red>Dependency '$name' has broken configuration, skipping...</red>");
$io->newLine();
$error = 1;
continue;
}
$locations = (array) $this->local_config["{$scm}_repos"];
$to = $this->destination . DS . $path;
$from = null;
foreach ($locations as $location) {
$test = rtrim($location, '\\/') . DS . $src;
if (file_exists($test)) {
$from = $test;
continue;
}
}
if (is_link($to) && !realpath($to)) {
$io->writeln('<yellow>Removed broken symlink '. $path .'</yellow>');
unlink($to);
}
if (null === $from) {
$io->writeln('<red>source for ' . $src . ' does not exists, skipping...</red>');
$io->newLine();
$error = 1;
} elseif (!file_exists($to)) {
$error = $this->addSymlinks($from, $to, ['name' => $name, 'src' => $src, 'path' => $path]);
$io->newLine();
} else {
$io->writeln('<yellow>destination: ' . $path . ' already exists, skipping...</yellow>');
$io->newLine();
}
}
return $error;
}
private function addSymlinks(string $from, string $to, array $options): int
{
$io = $this->getIO();
$hebe = $this->readHebe($from);
if (null === $hebe) {
symlink($from, $to);
$io->writeln('<green>SUCCESS</green> symlinked <magenta>' . $options['src'] . '</magenta> -> <cyan>' . $options['path'] . '</cyan>');
} else {
$to = GRAV_ROOT;
$name = $options['name'];
$io->writeln("Processing <magenta>{$name}</magenta>");
foreach ($hebe as $section => $symlinks) {
foreach ($symlinks as $symlink) {
$src = trim($symlink['source'], '/');
$dst = trim($symlink['destination'], '/');
$s = "{$from}/{$src}";
$d = "{$to}/{$dst}";
if (is_link($d) && !realpath($d)) {
unlink($d);
$io->writeln(' <yellow>Removed broken symlink '. $dst .'</yellow>');
}
if (!file_exists($d)) {
symlink($s, $d);
$io->writeln(' symlinked <magenta>' . $src . '</magenta> -> <cyan>' . $dst . '</cyan>');
}
}
}
$io->writeln('<green>SUCCESS</green>');
}
return 0;
}
private function readHebe(string $folder): ?array
{
$filename = "{$folder}/hebe.json";
if (!is_file($filename)) {
return null;
}
$formatter = new JsonFormatter();
$file = new JsonFile($filename, $formatter);
$hebe = $file->load();
$paths = $hebe['platforms']['grav']['nodes'] ?? null;
return is_array($paths) ? $paths : null;
}
}
@@ -0,0 +1,96 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use DateTime;
use Grav\Common\Grav;
use Grav\Common\Helpers\LogViewer;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Class LogViewerCommand
* @package Grav\Console\Cli
*/
class LogViewerCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('logviewer')
->addOption(
'file',
'f',
InputOption::VALUE_OPTIONAL,
'custom log file location (default = grav.log)'
)
->addOption(
'lines',
'l',
InputOption::VALUE_OPTIONAL,
'number of lines (default = 10)'
)
->setDescription('Display the last few entries of Grav log')
->setHelp('Display the last few entries of Grav log');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$file = $input->getOption('file') ?? 'grav.log';
$lines = $input->getOption('lines') ?? 20;
$verbose = $input->getOption('verbose') ?? false;
$io->title('Log Viewer');
$io->writeln(sprintf('viewing last %s entries in <white>%s</white>', $lines, $file));
$io->newLine();
$viewer = new LogViewer();
$grav = Grav::instance();
$logfile = $grav['locator']->findResource('log://' . $file);
if (!$logfile) {
$io->error('cannot find the log file: logs/' . $file);
return 1;
}
$rows = $viewer->objectTail($logfile, $lines, true);
foreach ($rows as $log) {
$date = $log['date'];
$level_color = LogViewer::levelColor($log['level']);
if ($date instanceof DateTime) {
$output = "<yellow>{$log['date']->format('Y-m-d h:i:s')}</yellow> [<{$level_color}>{$log['level']}</{$level_color}>]";
if ($log['trace'] && $verbose) {
$output .= " <white>{$log['message']}</white>\n";
foreach ((array) $log['trace'] as $index => $tracerow) {
$output .= "<white>{$index}</white>${tracerow}\n";
}
} else {
$output .= " {$log['message']}";
}
$io->writeln($output);
}
}
return 0;
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
/**
* Class NewProjectCommand
* @package Grav\Console\Cli
*/
class NewProjectCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('new-project')
->setAliases(['newproject'])
->addArgument(
'destination',
InputArgument::REQUIRED,
'The destination directory of your new Grav project'
)
->addOption(
'symlink',
's',
InputOption::VALUE_NONE,
'Symlink the required bits'
)
->setDescription('Creates a new Grav project with all the dependencies installed')
->setHelp("The <info>new-project</info> command is a combination of the `setup` and `install` commands.\nCreates a new Grav instance and performs the installation of all the required dependencies.");
}
/**
* @return int
*/
protected function serve(): int
{
$io = $this->getIO();
$sandboxCommand = $this->getApplication()->find('sandbox');
$installCommand = $this->getApplication()->find('install');
$sandboxArguments = new ArrayInput([
'command' => 'sandbox',
'destination' => $this->input->getArgument('destination'),
'-s' => $this->input->getOption('symlink')
]);
$installArguments = new ArrayInput([
'command' => 'install',
'destination' => $this->input->getArgument('destination'),
'-s' => $this->input->getOption('symlink')
]);
$error = $sandboxCommand->run($sandboxArguments, $io);
if ($error === 0) {
$error = $installCommand->run($installArguments, $io);
}
return $error;
}
}
@@ -0,0 +1,299 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Config\Config;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Console\GravCommand;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Input\InputOption;
use function in_array;
use function is_object;
/**
* Class PageSystemValidatorCommand
* @package Grav\Console\Cli
*/
class PageSystemValidatorCommand extends GravCommand
{
/** @var array */
protected $tests = [
// Content
'header' => [[]],
'summary' => [[], [200], [200, true]],
'content' => [[]],
'getRawContent' => [[]],
'rawMarkdown' => [[]],
'value' => [['content'], ['route'], ['order'], ['ordering'], ['folder'], ['slug'], ['name'], /*['frontmatter'],*/ ['header.menu'], ['header.slug']],
'title' => [[]],
'menu' => [[]],
'visible' => [[]],
'published' => [[]],
'publishDate' => [[]],
'unpublishDate' => [[]],
'process' => [[]],
'slug' => [[]],
'order' => [[]],
//'id' => [[]],
'modified' => [[]],
'lastModified' => [[]],
'folder' => [[]],
'date' => [[]],
'dateformat' => [[]],
'taxonomy' => [[]],
'shouldProcess' => [['twig'], ['markdown']],
'isPage' => [[]],
'isDir' => [[]],
'exists' => [[]],
// Forms
'forms' => [[]],
// Routing
'urlExtension' => [[]],
'routable' => [[]],
'link' => [[], [false], [true]],
'permalink' => [[]],
'canonical' => [[], [false], [true]],
'url' => [[], [true], [true, true], [true, true, false], [false, false, true, false]],
'route' => [[]],
'rawRoute' => [[]],
'routeAliases' => [[]],
'routeCanonical' => [[]],
'redirect' => [[]],
'relativePagePath' => [[]],
'path' => [[]],
//'folder' => [[]],
'parent' => [[]],
'topParent' => [[]],
'currentPosition' => [[]],
'active' => [[]],
'activeChild' => [[]],
'home' => [[]],
'root' => [[]],
// Translations
'translatedLanguages' => [[], [false], [true]],
'untranslatedLanguages' => [[], [false], [true]],
'language' => [[]],
// Legacy
'raw' => [[]],
'frontmatter' => [[]],
'httpResponseCode' => [[]],
'httpHeaders' => [[]],
'blueprintName' => [[]],
'name' => [[]],
'childType' => [[]],
'template' => [[]],
'templateFormat' => [[]],
'extension' => [[]],
'expires' => [[]],
'cacheControl' => [[]],
'ssl' => [[]],
'metadata' => [[]],
'eTag' => [[]],
'filePath' => [[]],
'filePathClean' => [[]],
'orderDir' => [[]],
'orderBy' => [[]],
'orderManual' => [[]],
'maxCount' => [[]],
'modular' => [[]],
'modularTwig' => [[]],
//'children' => [[]],
'isFirst' => [[]],
'isLast' => [[]],
'prevSibling' => [[]],
'nextSibling' => [[]],
'adjacentSibling' => [[]],
'ancestor' => [[]],
//'inherited' => [[]],
//'inheritedField' => [[]],
'find' => [['/']],
//'collection' => [[]],
//'evaluate' => [[]],
'folderExists' => [[]],
//'getOriginal' => [[]],
//'getAction' => [[]],
];
/** @var Grav */
protected $grav;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('page-system-validator')
->setDescription('Page validator can be used to compare site before/after update and when migrating to Flex Pages.')
->addOption('record', 'r', InputOption::VALUE_NONE, 'Record results')
->addOption('check', 'c', InputOption::VALUE_NONE, 'Compare site against previously recorded results')
->setHelp('The <info>page-system-validator</info> command can be used to test the pages before and after upgrade');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$this->setLanguage('en');
$this->initializePages();
$io->newLine();
$this->grav = $grav = Grav::instance();
$grav->fireEvent('onPageInitialized', new Event(['page' => $grav['page']]));
/** @var Config $config */
$config = $grav['config'];
if ($input->getOption('record')) {
$io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$io->writeln('<magenta>Record tests</magenta>');
$io->newLine();
$results = $this->record();
$file = $this->getFile('pages-old');
$file->save($results);
$io->writeln('Recorded tests to ' . $file->filename());
} elseif ($input->getOption('check')) {
$io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$io->writeln('<magenta>Run tests</magenta>');
$io->newLine();
$new = $this->record();
$file = $this->getFile('pages-new');
$file->save($new);
$io->writeln('Recorded tests to ' . $file->filename());
$file = $this->getFile('pages-old');
$old = $file->content();
$results = $this->check($old, $new);
$file = $this->getFile('diff');
$file->save($results);
$io->writeln('Recorded results to ' . $file->filename());
} else {
$io->writeln('<green>page-system-validator [-r|--record] [-c|--check]</green>');
}
$io->newLine();
return 0;
}
/**
* @return array
*/
private function record(): array
{
$io = $this->getIO();
/** @var Pages $pages */
$pages = $this->grav['pages'];
$all = $pages->all();
$results = [];
$results[''] = $this->recordRow($pages->root());
foreach ($all as $path => $page) {
if (null === $page) {
$io->writeln('<red>Error on page ' . $path . '</red>');
continue;
}
$results[$page->rawRoute()] = $this->recordRow($page);
}
return json_decode(json_encode($results), true);
}
/**
* @param PageInterface $page
* @return array
*/
private function recordRow(PageInterface $page): array
{
$results = [];
foreach ($this->tests as $method => $params) {
$params = $params ?: [[]];
foreach ($params as $p) {
$result = $page->$method(...$p);
if (in_array($method, ['summary', 'content', 'getRawContent'], true)) {
$result = preg_replace('/name="(form-nonce|__unique_form_id__)" value="[^"]+"/',
'name="\\1" value="DYNAMIC"', $result);
$result = preg_replace('`src=("|\'|&quot;)/images/./././././[^"]+\\1`',
'src="\\1images/GENERATED\\1', $result);
$result = preg_replace('/\?\d{10}/', '?1234567890', $result);
} elseif ($method === 'httpHeaders' && isset($result['Expires'])) {
$result['Expires'] = 'Thu, 19 Sep 2019 13:10:24 GMT (REPLACED AS DYNAMIC)';
} elseif ($result instanceof PageInterface) {
$result = $result->rawRoute();
} elseif (is_object($result)) {
$result = json_decode(json_encode($result), true);
}
$ps = [];
foreach ($p as $val) {
$ps[] = (string)var_export($val, true);
}
$pstr = implode(', ', $ps);
$call = "->{$method}({$pstr})";
$results[$call] = $result;
}
}
return $results;
}
/**
* @param array $old
* @param array $new
* @return array
*/
private function check(array $old, array $new): array
{
$errors = [];
foreach ($old as $path => $page) {
if (!isset($new[$path])) {
$errors[$path] = 'PAGE REMOVED';
continue;
}
foreach ($page as $method => $test) {
if (($new[$path][$method] ?? null) !== $test) {
$errors[$path][$method] = ['old' => $test, 'new' => $new[$path][$method]];
}
}
}
return $errors;
}
/**
* @param string $name
* @return CompiledYamlFile
*/
private function getFile(string $name): CompiledYamlFile
{
return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml');
}
}
@@ -0,0 +1,342 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Filesystem\Folder;
use Grav\Console\GravCommand;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function count;
/**
* Class SandboxCommand
* @package Grav\Console\Cli
*/
class SandboxCommand extends GravCommand
{
/** @var array */
protected $directories = [
'/assets',
'/backup',
'/cache',
'/images',
'/logs',
'/tmp',
'/user/accounts',
'/user/config',
'/user/data',
'/user/pages',
'/user/plugins',
'/user/themes',
];
/** @var array */
protected $files = [
'/.dependencies',
'/.htaccess',
'/user/config/site.yaml',
'/user/config/system.yaml',
];
/** @var array */
protected $mappings = [
'/.gitignore' => '/.gitignore',
'/.editorconfig' => '/.editorconfig',
'/CHANGELOG.md' => '/CHANGELOG.md',
'/LICENSE.txt' => '/LICENSE.txt',
'/README.md' => '/README.md',
'/CONTRIBUTING.md' => '/CONTRIBUTING.md',
'/index.php' => '/index.php',
'/composer.json' => '/composer.json',
'/bin' => '/bin',
'/system' => '/system',
'/vendor' => '/vendor',
'/webserver-configs' => '/webserver-configs',
];
/** @var string */
protected $source;
/** @var string */
protected $destination;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('sandbox')
->setDescription('Setup of a base Grav system in your webroot, good for development, playing around or starting fresh')
->addArgument(
'destination',
InputArgument::REQUIRED,
'The destination directory to symlink into'
)
->addOption(
'symlink',
's',
InputOption::VALUE_NONE,
'Symlink the base grav system'
)
->setHelp("The <info>sandbox</info> command help create a development environment that can optionally use symbolic links to link the core of grav to the git cloned repository.\nGood for development, playing around or starting fresh");
$source = getcwd();
if ($source === false) {
throw new RuntimeException('Internal Error');
}
$this->source = $source;
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$this->destination = $input->getArgument('destination');
// Create Some core stuff if it doesn't exist
$error = $this->createDirectories();
if ($error) {
return $error;
}
// Copy files or create symlinks
$error = $input->getOption('symlink') ? $this->symlink() : $this->copy();
if ($error) {
return $error;
}
$error = $this->pages();
if ($error) {
return $error;
}
$error = $this->initFiles();
if ($error) {
return $error;
}
$error = $this->perms();
if ($error) {
return $error;
}
return 0;
}
/**
* @return int
*/
private function createDirectories(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Creating Directories</comment>');
$dirs_created = false;
if (!file_exists($this->destination)) {
Folder::create($this->destination);
}
foreach ($this->directories as $dir) {
if (!file_exists($this->destination . $dir)) {
$dirs_created = true;
$io->writeln(' <cyan>' . $dir . '</cyan>');
Folder::create($this->destination . $dir);
}
}
if (!$dirs_created) {
$io->writeln(' <red>Directories already exist</red>');
}
return 0;
}
/**
* @return int
*/
private function copy(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Copying Files</comment>');
foreach ($this->mappings as $source => $target) {
if ((string)(int)$source === (string)$source) {
$source = $target;
}
$from = $this->source . $source;
$to = $this->destination . $target;
$io->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
@Folder::rcopy($from, $to);
}
return 0;
}
/**
* @return int
*/
private function symlink(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Resetting Symbolic Links</comment>');
foreach ($this->mappings as $source => $target) {
if ((string)(int)$source === (string)$source) {
$source = $target;
}
$from = $this->source . $source;
$to = $this->destination . $target;
$io->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
if (is_dir($to)) {
@Folder::delete($to);
} else {
@unlink($to);
}
symlink($from, $to);
}
return 0;
}
/**
* @return int
*/
private function pages(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Pages Initializing</comment>');
// get pages files and initialize if no pages exist
$pages_dir = $this->destination . '/user/pages';
$pages_files = array_diff(scandir($pages_dir), ['..', '.']);
if (count($pages_files) === 0) {
$destination = $this->source . '/user/pages';
Folder::rcopy($destination, $pages_dir);
$io->writeln(' <cyan>' . $destination . '</cyan> <comment>-></comment> Created');
}
return 0;
}
/**
* @return int
*/
private function initFiles(): int
{
if (!$this->check()) {
return 1;
}
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>File Initializing</comment>');
$files_init = false;
// Copy files if they do not exist
foreach ($this->files as $source => $target) {
if ((string)(int)$source === (string)$source) {
$source = $target;
}
$from = $this->source . $source;
$to = $this->destination . $target;
if (!file_exists($to)) {
$files_init = true;
copy($from, $to);
$io->writeln(' <cyan>' . $target . '</cyan> <comment>-></comment> Created');
}
}
if (!$files_init) {
$io->writeln(' <red>Files already exist</red>');
}
return 0;
}
/**
* @return int
*/
private function perms(): int
{
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Permissions Initializing</comment>');
$dir_perms = 0755;
$binaries = glob($this->destination . DS . 'bin' . DS . '*');
foreach ($binaries as $bin) {
chmod($bin, $dir_perms);
$io->writeln(' <cyan>bin/' . basename($bin) . '</cyan> permissions reset to ' . decoct($dir_perms));
}
$io->newLine();
return 0;
}
/**
* @return bool
*/
private function check(): bool
{
$success = true;
$io = $this->getIO();
if (!file_exists($this->destination)) {
$io->writeln(' file: <red>' . $this->destination . '</red> does not exist!');
$success = false;
}
foreach ($this->directories as $dir) {
if (!file_exists($this->destination . $dir)) {
$io->writeln(' directory: <red>' . $dir . '</red> does not exist!');
$success = false;
}
}
foreach ($this->mappings as $target => $link) {
if (!file_exists($this->destination . $target)) {
$io->writeln(' mappings: <red>' . $target . '</red> does not exist!');
$success = false;
}
}
if (!$success) {
$io->newLine();
$io->writeln('<comment>install should be run with --symlink|--s to symlink first</comment>');
}
return $success;
}
}
@@ -0,0 +1,225 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Cron\CronExpression;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Common\Scheduler\Scheduler;
use Grav\Console\GravCommand;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputOption;
use function is_null;
/**
* Class SchedulerCommand
* @package Grav\Console\Cli
*/
class SchedulerCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('scheduler')
->addOption(
'install',
'i',
InputOption::VALUE_NONE,
'Show Install Command'
)
->addOption(
'jobs',
'j',
InputOption::VALUE_NONE,
'Show Jobs Summary'
)
->addOption(
'details',
'd',
InputOption::VALUE_NONE,
'Show Job Details'
)
->addOption(
'run',
'r',
InputOption::VALUE_OPTIONAL,
'Force run all jobs or a specific job if you specify a specific Job ID',
false
)
->setDescription('Run the Grav Scheduler. Best when integrated with system cron')
->setHelp("Running without any options will force the Scheduler to run through it's jobs and process them");
}
/**
* @return int
*/
protected function serve(): int
{
$this->initializePlugins();
$grav = Grav::instance();
$grav['backups']->init();
$this->initializePages();
$this->initializeThemes();
/** @var Scheduler $scheduler */
$scheduler = $grav['scheduler'];
$grav->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler]));
$this->setHelp('foo');
$input = $this->getInput();
$io = $this->getIO();
$error = 0;
$run = $input->getOption('run');
if ($input->getOption('jobs')) {
// Show jobs list
$jobs = $scheduler->getAllJobs();
$job_states = (array)$scheduler->getJobStates()->content();
$rows = [];
$table = new Table($io);
$table->setStyle('box');
$headers = ['Job ID', 'Command', 'Run At', 'Status', 'Last Run', 'State'];
$io->title('Scheduler Jobs Listing');
foreach ($jobs as $job) {
$job_status = ucfirst($job_states[$job->getId()]['state'] ?? 'ready');
$last_run = $job_states[$job->getId()]['last-run'] ?? 0;
$status = $job_status === 'Failure' ? "<red>{$job_status}</red>" : "<green>{$job_status}</green>";
$state = $job->getEnabled() ? '<cyan>Enabled</cyan>' : '<red>Disabled</red>';
$row = [
$job->getId(),
"<white>{$job->getCommand()}</white>",
"<magenta>{$job->getAt()}</magenta>",
$status,
'<yellow>' . ($last_run === 0 ? 'Never' : date('Y-m-d H:i', $last_run)) . '</yellow>',
$state,
];
$rows[] = $row;
}
if (!empty($rows)) {
$table->setHeaders($headers);
$table->setRows($rows);
$table->render();
} else {
$io->text('no jobs found...');
}
$io->newLine();
$io->note('For error details run "bin/grav scheduler -d"');
$io->newLine();
} elseif ($input->getOption('details')) {
$jobs = $scheduler->getAllJobs();
$job_states = (array)$scheduler->getJobStates()->content();
$io->title('Job Details');
$table = new Table($io);
$table->setStyle('box');
$table->setHeaders(['Job ID', 'Last Run', 'Next Run', 'Errors']);
$rows = [];
foreach ($jobs as $job) {
$job_state = $job_states[$job->getId()];
$error = isset($job_state['error']) ? trim($job_state['error']) : false;
/** @var CronExpression $expression */
$expression = $job->getCronExpression();
$next_run = $expression->getNextRunDate();
$row = [];
$row[] = $job->getId();
if (!is_null($job_state['last-run'])) {
$row[] = '<yellow>' . date('Y-m-d H:i', $job_state['last-run']) . '</yellow>';
} else {
$row[] = '<yellow>Never</yellow>';
}
$row[] = '<yellow>' . $next_run->format('Y-m-d H:i') . '</yellow>';
if ($error) {
$row[] = "<error>{$error}</error>";
} else {
$row[] = '<green>None</green>';
}
$rows[] = $row;
}
$table->setRows($rows);
$table->render();
} elseif ($run !== false && $run !== null) {
$io->title('Force Run Job: ' . $run);
$job = $scheduler->getJob($run);
if ($job) {
$job->inForeground()->run();
if ($job->isSuccessful()) {
$io->success('Job ran successfully...');
} else {
$error = 1;
$io->error('Job failed to run successfully...');
}
$output = $job->getOutput();
if ($output) {
$io->write($output);
}
} else {
$error = 1;
$io->error('Could not find a job with id: ' . $run);
}
} elseif ($input->getOption('install')) {
$io->title('Install Scheduler');
$verb = 'install';
if ($scheduler->isCrontabSetup()) {
$io->success('All Ready! You have already set up Grav\'s Scheduler in your crontab. You can validate this by running "crontab -l" to list your current crontab entries.');
$verb = 'reinstall';
} else {
$user = $scheduler->whoami();
$error = 1;
$io->error('Can\'t find a crontab for ' . $user . '. You need to set up Grav\'s Scheduler in your crontab');
}
if (!Utils::isWindows()) {
$io->note("To $verb, run the following command from your terminal:");
$io->newLine();
$io->text(trim($scheduler->getCronCommand()));
} else {
$io->note("To $verb, create a scheduled task in Windows.");
$io->text('Learn more at https://learn.getgrav.org/advanced/scheduler');
}
} else {
// Run scheduler
$force = $run === null;
$scheduler->run(null, $force);
if ($input->getOption('verbose')) {
$io->title('Running Scheduled Jobs');
$io->text($scheduler->getVerboseOutput());
}
}
return $error;
}
}
@@ -0,0 +1,102 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Grav;
use Grav\Common\Security;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use function count;
/**
* Class SecurityCommand
* @package Grav\Console\Cli
*/
class SecurityCommand extends GravCommand
{
/** @var ProgressBar $progress */
protected $progress;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('security')
->setDescription('Capable of running various Security checks')
->setHelp('The <info>security</info> runs various security checks on your Grav site');
}
/**
* @return int
*/
protected function serve(): int
{
$this->initializePages();
$io = $this->getIO();
/** @var Grav $grav */
$grav = Grav::instance();
$this->progress = new ProgressBar($this->output, count($grav['pages']->routes()) - 1);
$this->progress->setFormat('Scanning <cyan>%current%</cyan> pages [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s%');
$this->progress->setBarWidth(100);
$io->title('Grav Security Check');
$io->newline(2);
$output = Security::detectXssFromPages($grav['pages'], false, [$this, 'outputProgress']);
$error = 0;
if (!empty($output)) {
$counter = 1;
foreach ($output as $route => $results) {
$results_parts = array_map(static function ($value, $key) {
return $key.': \''.$value . '\'';
}, array_values($results), array_keys($results));
$io->writeln($counter++ .' - <cyan>' . $route . '</cyan> → <red>' . implode(', ', $results_parts) . '</red>');
}
$error = 1;
$io->error('Security Scan complete: ' . count($output) . ' potential XSS issues found...');
} else {
$io->success('Security Scan complete: No issues found...');
}
$io->newline(1);
return $error;
}
/**
* @param array $args
* @return void
*/
public function outputProgress(array $args): void
{
switch ($args['type']) {
case 'count':
$steps = $args['steps'];
$freq = (int)($steps > 100 ? round($steps / 100) : $steps);
$this->progress->setMaxSteps($steps);
$this->progress->setRedrawFrequency($freq);
break;
case 'progress':
if (isset($args['complete']) && $args['complete']) {
$this->progress->finish();
} else {
$this->progress->advance();
}
break;
}
}
}
@@ -0,0 +1,154 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Utils;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
/**
* Class ServerCommand
* @package Grav\Console\Cli
*/
class ServerCommand extends GravCommand
{
const SYMFONY_SERVER = 'Symfony Server';
const PHP_SERVER = 'Built-in PHP Server';
/** @var string */
protected $ip;
/** @var int */
protected $port;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('server')
->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Preferred HTTP port rather than auto-find (default is 8000-9000')
->addOption('symfony', null, InputOption::VALUE_NONE, 'Force using Symfony server')
->addOption('php', null, InputOption::VALUE_NONE, 'Force using built-in PHP server')
->setDescription("Runs built-in web-server, Symfony first, then tries PHP's")
->setHelp("Runs built-in web-server, Symfony first, then tries PHP's");
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$io->title('Grav Web Server');
// Ensure CLI colors are on
ini_set('cli_server.color', 'on');
// Options
$force_symfony = $input->getOption('symfony');
$force_php = $input->getOption('php');
// Find PHP
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find(false);
$this->ip = '127.0.0.1';
$this->port = (int)($input->getOption('port') ?? 8000);
// Get an open port
while (!$this->portAvailable($this->ip, $this->port)) {
$this->port++;
}
// Setup the commands
$symfony_cmd = ['symfony', 'server:start', '--ansi', '--port=' . $this->port];
$php_cmd = [$php, '-S', $this->ip.':'.$this->port, 'system/router.php'];
$commands = [
self::SYMFONY_SERVER => $symfony_cmd,
self::PHP_SERVER => $php_cmd
];
if ($force_symfony) {
unset($commands[self::PHP_SERVER]);
} elseif ($force_php) {
unset($commands[self::SYMFONY_SERVER]);
}
$error = 0;
foreach ($commands as $name => $command) {
$process = $this->runProcess($name, $command);
if (!$process) {
$io->note('Starting ' . $name . '...');
}
// Should only get here if there's an error running
if (!$process->isRunning() && (($name === self::SYMFONY_SERVER && $force_symfony) || ($name === self::PHP_SERVER))) {
$error = 1;
$io->error('Could not start ' . $name);
}
}
return $error;
}
/**
* @param string $name
* @param array $cmd
* @return Process
*/
protected function runProcess(string $name, array $cmd): Process
{
$io = $this->getIO();
$process = new Process($cmd);
$process->setTimeout(0);
$process->start();
if ($name === self::SYMFONY_SERVER && Utils::contains($process->getErrorOutput(), 'symfony: not found')) {
$io->error('The symfony binary could not be found, please install the CLI tools: https://symfony.com/download');
$io->warning('Falling back to PHP web server...');
}
if ($name === self::PHP_SERVER) {
$io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . PHP_VERSION . ')');
}
$process->wait(function ($type, $buffer) {
$this->getIO()->write($buffer);
});
return $process;
}
/**
* Simple function test the port
*
* @param string $ip
* @param int $port
* @return bool
*/
protected function portAvailable(string $ip, int $port): bool
{
$fp = @fsockopen($ip, $port, $errno, $errstr, 0.1);
if (!$fp) {
return true;
}
fclose($fp);
return false;
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Helpers\YamlLinter;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Class YamlLinterCommand
* @package Grav\Console\Cli
*/
class YamlLinterCommand extends GravCommand
{
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('yamllinter')
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Go through the whole Grav installation'
)
->addOption(
'folder',
'f',
InputOption::VALUE_OPTIONAL,
'Go through specific folder'
)
->setDescription('Checks various files for YAML errors')
->setHelp('Checks various files for YAML errors');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$io->title('Yaml Linter');
$error = 0;
if ($input->getOption('all')) {
$io->section('All');
$errors = YamlLinter::lint('');
if (empty($errors)) {
$io->success('No YAML Linting issues found');
} else {
$error = 1;
$this->displayErrors($errors, $io);
}
} elseif ($folder = $input->getOption('folder')) {
$io->section($folder);
$errors = YamlLinter::lint($folder);
if (empty($errors)) {
$io->success('No YAML Linting issues found');
} else {
$error = 1;
$this->displayErrors($errors, $io);
}
} else {
$io->section('User Configuration');
$errors = YamlLinter::lintConfig();
if (empty($errors)) {
$io->success('No YAML Linting issues with configuration');
} else {
$error = 1;
$this->displayErrors($errors, $io);
}
$io->section('Pages Frontmatter');
$errors = YamlLinter::lintPages();
if (empty($errors)) {
$io->success('No YAML Linting issues with pages');
} else {
$error = 1;
$this->displayErrors($errors, $io);
}
$io->section('Page Blueprints');
$errors = YamlLinter::lintBlueprints();
if (empty($errors)) {
$io->success('No YAML Linting issues with blueprints');
} else {
$error = 1;
$this->displayErrors($errors, $io);
}
}
return $error;
}
/**
* @param array $errors
* @param SymfonyStyle $io
* @return void
*/
protected function displayErrors(array $errors, SymfonyStyle $io): void
{
$io->error('YAML Linting issues found...');
foreach ($errors as $path => $error) {
$io->writeln("<yellow>{$path}</yellow> - {$error}");
}
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ConsoleCommand
* @package Grav\Console
*/
class ConsoleCommand extends Command
{
use ConsoleTrait;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setupConsole($input, $output);
return $this->serve();
}
/**
* Override with your implementation.
*
* @return int
*/
protected function serve()
{
// Return error.
return 1;
}
}
+338
View File
@@ -0,0 +1,338 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console;
use Exception;
use Grav\Common\Cache;
use Grav\Common\Grav;
use Grav\Common\Composer;
use Grav\Common\Language\Language;
use Grav\Common\Processors\InitializeProcessor;
use Grav\Console\Cli\ClearCacheCommand;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Trait ConsoleTrait
* @package Grav\Console
*/
trait ConsoleTrait
{
/** @var string */
protected $argv;
/** @var InputInterface */
protected $input;
/** @var SymfonyStyle */
protected $output;
/** @var array */
protected $local_config;
/** @var bool */
private $plugins_initialized = false;
/** @var bool */
private $themes_initialized = false;
/** @var bool */
private $pages_initialized = false;
/**
* Set colors style definition for the formatter.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return void
*/
public function setupConsole(InputInterface $input, OutputInterface $output)
{
$this->argv = $_SERVER['argv'][0];
$this->input = $input;
$this->output = new SymfonyStyle($input, $output);
$this->setupGrav();
}
public function getInput(): InputInterface
{
return $this->input;
}
/**
* @return SymfonyStyle
*/
public function getIO(): SymfonyStyle
{
return $this->output;
}
/**
* Adds an option.
*
* @param string $name The option name
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
* @param string $description A description text
* @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE)
* @return $this
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
{
if ($name !== 'env' && $name !== 'lang') {
parent::addOption($name, $shortcut, $mode, $description, $default);
}
return $this;
}
/**
* @return void
*/
final protected function setupGrav(): void
{
try {
$language = $this->input->getOption('lang');
if ($language) {
// Set used language.
$this->setLanguage($language);
}
} catch (InvalidArgumentException $e) {}
// Initialize cache with CLI compatibility
$grav = Grav::instance();
$grav['config']->set('system.cache.cli_compatibility', true);
}
/**
* Initialize Grav.
*
* - Load configuration
* - Initialize logger
* - Disable debugger
* - Set timezone, locale
* - Load plugins (call PluginsLoadedEvent)
* - Set Pages and Users type to be used in the site
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeGrav()
{
InitializeProcessor::initializeCli(Grav::instance());
return $this;
}
/**
* Set language to be used in CLI.
*
* @param string|null $code
* @return $this
*/
final protected function setLanguage(string $code = null)
{
$this->initializeGrav();
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
if ($language->enabled()) {
if ($code && $language->validate($code)) {
$language->setActive($code);
} else {
$language->setActive($language->getDefault());
}
}
return $this;
}
/**
* Properly initialize plugins.
*
* - call $this->initializeGrav()
* - call onPluginsInitialized event
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePlugins()
{
if (!$this->plugins_initialized) {
$this->plugins_initialized = true;
$this->initializeGrav();
// Initialize plugins.
$grav = Grav::instance();
$grav['plugins']->init();
$grav->fireEvent('onPluginsInitialized');
}
return $this;
}
/**
* Properly initialize themes.
*
* - call $this->initializePlugins()
* - initialize theme (call onThemeInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeThemes()
{
if (!$this->themes_initialized) {
$this->themes_initialized = true;
$this->initializePlugins();
// Initialize themes.
$grav = Grav::instance();
$grav['themes']->init();
}
return $this;
}
/**
* Properly initialize pages.
*
* - call $this->initializeThemes()
* - initialize assets (call onAssetsInitialized event)
* - initialize twig (calls the twig events)
* - initialize pages (calls onPagesInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePages()
{
if (!$this->pages_initialized) {
$this->pages_initialized = true;
$this->initializeThemes();
$grav = Grav::instance();
// Initialize assets.
$grav['assets']->init();
$grav->fireEvent('onAssetsInitialized');
// Initialize twig.
$grav['twig']->init();
// Initialize pages.
$pages = $grav['pages'];
$pages->init();
$grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages]));
}
return $this;
}
/**
* @param string $path
* @return void
*/
public function isGravInstance($path)
{
$io = $this->getIO();
if (!file_exists($path)) {
$io->writeln('');
$io->writeln("<red>ERROR</red>: Destination doesn't exist:");
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
if (!is_dir($path)) {
$io->writeln('');
$io->writeln("<red>ERROR</red>: Destination chosen to install is not a directory:");
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
if (!file_exists($path . DS . 'index.php') || !file_exists($path . DS . '.dependencies') || !file_exists($path . DS . 'system' . DS . 'config' . DS . 'system.yaml')) {
$io->writeln('');
$io->writeln('<red>ERROR</red>: Destination chosen to install does not appear to be a Grav instance:');
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
}
/**
* @param string $path
* @param string $action
* @return string|false
*/
public function composerUpdate($path, $action = 'install')
{
$composer = Composer::getComposerExecutor();
return system($composer . ' --working-dir="'.$path.'" --no-interaction --no-dev --prefer-dist -o '. $action);
}
/**
* @param array $all
* @return int
* @throws Exception
*/
public function clearCache($all = [])
{
if ($all) {
$all = ['--all' => true];
}
$command = new ClearCacheCommand();
$input = new ArrayInput($all);
return $command->run($input, $this->output);
}
/**
* @return void
*/
public function invalidateCache()
{
Cache::invalidateCache();
}
/**
* Load the local config file
*
* @return string|false The local config file name. false if local config does not exist
*/
public function loadLocalConfig()
{
$home_folder = getenv('HOME') ?: getenv('HOMEDRIVE') . getenv('HOMEPATH');
$local_config_file = $home_folder . '/.grav/config';
if (file_exists($local_config_file)) {
$file = YamlFile::instance($local_config_file);
$this->local_config = $file->content();
$file->free();
return $local_config_file;
}
return false;
}
}
@@ -0,0 +1,322 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Exception;
use Grav\Common\Cache;
use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Console\GpmCommand;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZipArchive;
use function is_array;
use function is_callable;
/**
* Class DirectInstallCommand
* @package Grav\Console\Gpm
*/
class DirectInstallCommand extends GpmCommand
{
/** @var string */
protected $all_yes;
/** @var string */
protected $destination;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('direct-install')
->setAliases(['directinstall'])
->addArgument(
'package-file',
InputArgument::REQUIRED,
'Installable package local <path> or remote <URL>. Can install specific version'
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'destination',
'd',
InputOption::VALUE_OPTIONAL,
'The destination where the package should be installed at. By default this would be where the grav instance has been launched from',
GRAV_ROOT
)
->setDescription('Installs Grav, plugin, or theme directly from a file or a URL')
->setHelp('The <info>direct-install</info> command installs Grav, plugin, or theme directly from a file or a URL');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('Direct Install');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
// Making sure the destination is usable
$this->destination = realpath($input->getOption('destination'));
if (!Installer::isGravInstance($this->destination) ||
!Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
) {
$io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
return 1;
}
$this->all_yes = $input->getOption('all-yes');
$package_file = $input->getArgument('package-file');
$question = new ConfirmationQuestion("Are you sure you want to direct-install <cyan>{$package_file}</cyan> [y|N] ", false);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$io->writeln('exiting...');
$io->newLine();
return 1;
}
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp_zip = $tmp_dir . uniqid('/Grav-', false);
$io->newLine();
$io->writeln("Preparing to install <cyan>{$package_file}</cyan>");
$zip = null;
if (Response::isRemote($package_file)) {
$io->write(' |- Downloading package... 0%');
try {
$zip = GPM::downloadPackage($package_file, $tmp_zip);
} catch (RuntimeException $e) {
$io->newLine();
$io->writeln(" `- <red>ERROR: {$e->getMessage()}</red>");
$io->newLine();
return 1;
}
if ($zip) {
$io->write("\x0D");
$io->write(' |- Downloading package... 100%');
$io->newLine();
}
} elseif (is_file($package_file)) {
$io->write(' |- Copying package... 0%');
$zip = GPM::copyPackage($package_file, $tmp_zip);
if ($zip) {
$io->write("\x0D");
$io->write(' |- Copying package... 100%');
$io->newLine();
}
}
if ($zip && file_exists($zip)) {
$tmp_source = $tmp_dir . uniqid('/Grav-', false);
$io->write(' |- Extracting package... ');
$extracted = Installer::unZip($zip, $tmp_source);
if (!$extracted) {
$io->write("\x0D");
$io->writeln(' |- Extracting package... <red>failed</red>');
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$io->write("\x0D");
$io->writeln(' |- Extracting package... <green>ok</green>');
$type = GPM::getPackageType($extracted);
if (!$type) {
$io->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$blueprint = GPM::getBlueprints($extracted);
if ($blueprint) {
if (isset($blueprint['dependencies'])) {
$dependencies = [];
foreach ($blueprint['dependencies'] as $dependency) {
if (is_array($dependency)) {
if (isset($dependency['name'])) {
$dependencies[] = $dependency['name'];
}
if (isset($dependency['github'])) {
$dependencies[] = $dependency['github'];
}
} else {
$dependencies[] = $dependency;
}
}
$io->writeln(' |- Dependencies found... <cyan>[' . implode(',', $dependencies) . ']</cyan>');
$question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$io->writeln('exiting...');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
}
}
if ($type === 'grav') {
$io->write(' |- Checking destination... ');
Installer::isValidDestination(GRAV_ROOT . '/system');
if (Installer::IS_LINK === Installer::lastErrorCode()) {
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$io->writeln(" '- <red>ERROR: symlinks found...</red> <yellow>" . GRAV_ROOT . '</yellow>');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
$io->write(' |- Installing package... ');
$this->upgradeGrav($zip, $extracted);
} else {
$name = GPM::getPackageName($extracted);
if (!$name) {
$io->writeln('<red>ERROR: Name could not be determined.</red> Please specify with --name|-n');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$install_path = GPM::getInstallPath($type, $name);
$is_update = file_exists($install_path);
$io->write(' |- Checking destination... ');
Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
if (Installer::lastErrorCode() === Installer::IS_LINK) {
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$io->writeln(" '- <red>ERROR: symlink found...</red> <yellow>" . GRAV_ROOT . DS . $install_path . '</yellow>');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
$io->write(' |- Installing package... ');
Installer::install(
$zip,
$this->destination,
$options = [
'install_path' => $install_path,
'theme' => (($type === 'theme')),
'is_update' => $is_update
],
$extracted
);
// clear cache after successful upgrade
$this->clearCache();
}
Folder::delete($tmp_source);
$io->write("\x0D");
if (Installer::lastErrorCode()) {
$io->writeln(" '- <red>" . Installer::lastErrorMsg() . '</red>');
$io->newLine();
} else {
$io->writeln(' |- Installing package... <green>ok</green>');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
} else {
$io->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
Folder::delete($tmp_zip);
return 1;
}
Folder::delete($tmp_zip);
return 0;
}
/**
* @param string $zip
* @param string $folder
* @return void
*/
private function upgradeGrav(string $zip, string $folder): void
{
if (!is_dir($folder)) {
Installer::setError('Invalid source folder');
}
try {
$script = $folder . '/system/install.php';
/** Install $installer */
if ((file_exists($script) && $install = include $script) && is_callable($install)) {
$install($zip);
} else {
throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
}
} catch (Exception $e) {
Installer::setError($e->getMessage());
}
}
}
@@ -0,0 +1,337 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\GPM\Remote\AbstractPackageCollection;
use Grav\Common\GPM\Remote\Package;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Remote\Packages;
use Grav\Common\GPM\Remote\Plugins;
use Grav\Common\GPM\Remote\Themes;
use Grav\Common\Utils;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputOption;
use function count;
/**
* Class IndexCommand
* @package Grav\Console\Gpm
*/
class IndexCommand extends GpmCommand
{
/** @var Packages */
protected $data;
/** @var GPM */
protected $gpm;
/** @var array */
protected $options;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('index')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addOption(
'filter',
'F',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Allows to limit the results based on one or multiple filters input. This can be either portion of a name/slug or a regex'
)
->addOption(
'themes-only',
'T',
InputOption::VALUE_NONE,
'Filters the results to only Themes'
)
->addOption(
'plugins-only',
'P',
InputOption::VALUE_NONE,
'Filters the results to only Plugins'
)
->addOption(
'updates-only',
'U',
InputOption::VALUE_NONE,
'Filters the results to Updatable Themes and Plugins only'
)
->addOption(
'installed-only',
'I',
InputOption::VALUE_NONE,
'Filters the results to only the Themes and Plugins you have installed'
)
->addOption(
'sort',
's',
InputOption::VALUE_REQUIRED,
'Allows to sort (ASC) the results. SORT can be either "name", "slug", "author", "date"',
'date'
)
->addOption(
'desc',
'D',
InputOption::VALUE_NONE,
'Reverses the order of the output.'
)
->addOption(
'enabled',
'e',
InputOption::VALUE_NONE,
'Filters the results to only enabled Themes and Plugins.'
)
->addOption(
'disabled',
'd',
InputOption::VALUE_NONE,
'Filters the results to only disabled Themes and Plugins.'
)
->setDescription('Lists the plugins and themes available for installation')
->setHelp('The <info>index</info> command lists the plugins and themes available for installation')
;
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$this->options = $input->getOptions();
$this->gpm = new GPM($this->options['force']);
$this->displayGPMRelease();
$this->data = $this->gpm->getRepository();
$data = $this->filter($this->data);
$io = $this->getIO();
if (count($data) === 0) {
$io->writeln('No data was found in the GPM repository stored locally.');
$io->writeln('Please try clearing cache and running the <green>bin/gpm index -f</green> command again');
$io->writeln('If this doesn\'t work try tweaking your GPM system settings.');
$io->newLine();
$io->writeln('For more help go to:');
$io->writeln(' -> <yellow>https://learn.getgrav.org/troubleshooting/common-problems#cannot-connect-to-the-gpm</yellow>');
return 1;
}
foreach ($data as $type => $packages) {
$io->writeln('<green>' . strtoupper($type) . '</green> [ ' . count($packages) . ' ]');
$packages = $this->sort($packages);
if (!empty($packages)) {
$io->section('Packages table');
$table = new Table($io);
$table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed', 'Enabled']);
$index = 0;
foreach ($packages as $slug => $package) {
$row = [
'Count' => $index++ + 1,
'Name' => '<cyan>' . Utils::truncate($package->name, 20, false, ' ', '...') . '</cyan> ',
'Slug' => $slug,
'Version'=> $this->version($package),
'Installed' => $this->installed($package),
'Enabled' => $this->enabled($package),
];
$table->addRow($row);
}
$table->render();
}
$io->newLine();
}
$io->writeln('You can either get more informations about a package by typing:');
$io->writeln(" <green>{$this->argv} info <cyan><package></cyan></green>");
$io->newLine();
$io->writeln('Or you can install a package by typing:');
$io->writeln(" <green>{$this->argv} install <cyan><package></cyan></green>");
$io->newLine();
return 0;
}
/**
* @param Package $package
* @return string
*/
private function version(Package $package): string
{
$list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}();
$package = $list[$package->slug] ?? $package;
$type = ucfirst(preg_replace('/s$/', '', $package->package_type));
$updatable = $this->gpm->{'is' . $type . 'Updatable'}($package->slug);
$installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug);
$local = $this->gpm->{'getInstalled' . $type}($package->slug);
if (!$installed || !$updatable) {
$version = $installed ? $local->version : $package->version;
return "v<green>{$version}</green>";
}
return "v<red>{$package->version}</red> <cyan>-></cyan> v<green>{$package->available}</green>";
}
/**
* @param Package $package
* @return string
*/
private function installed(Package $package): string
{
$package = $list[$package->slug] ?? $package;
$type = ucfirst(preg_replace('/s$/', '', $package->package_type));
$method = 'is' . $type . 'Installed';
$installed = $this->gpm->{$method}($package->slug);
return !$installed ? '<magenta>not installed</magenta>' : '<cyan>installed</cyan>';
}
/**
* @param Package $package
* @return string
*/
private function enabled(Package $package): string
{
$package = $list[$package->slug] ?? $package;
$type = ucfirst(preg_replace('/s$/', '', $package->package_type));
$method = 'is' . $type . 'Installed';
$installed = $this->gpm->{$method}($package->slug);
$result = '';
if ($installed) {
$method = 'is' . $type . 'Enabled';
$enabled = $this->gpm->{$method}($package->slug);
if ($enabled === true) {
$result = '<cyan>enabled</cyan>';
} elseif ($enabled === false) {
$result = '<red>disabled</red>';
}
}
return $result;
}
/**
* @param Packages $data
* @return Packages
*/
public function filter(Packages $data): Packages
{
// filtering and sorting
if ($this->options['plugins-only']) {
unset($data['themes']);
}
if ($this->options['themes-only']) {
unset($data['plugins']);
}
$filter = [
$this->options['desc'],
$this->options['disabled'],
$this->options['enabled'],
$this->options['filter'],
$this->options['installed-only'],
$this->options['updates-only'],
];
if (count(array_filter($filter))) {
foreach ($data as $type => $packages) {
foreach ($packages as $slug => $package) {
$filter = true;
// Filtering by string
if ($this->options['filter']) {
$filter = preg_grep('/(' . implode('|', $this->options['filter']) . ')/i', [$slug, $package->name]);
}
// Filtering updatables only
if ($filter && ($this->options['installed-only'] || $this->options['enabled'] || $this->options['disabled'])) {
$method = ucfirst(preg_replace('/s$/', '', $package->package_type));
$function = 'is' . $method . 'Installed';
$filter = $this->gpm->{$function}($package->slug);
}
// Filtering updatables only
if ($filter && $this->options['updates-only']) {
$method = ucfirst(preg_replace('/s$/', '', $package->package_type));
$function = 'is' . $method . 'Updatable';
$filter = $this->gpm->{$function}($package->slug);
}
// Filtering enabled only
if ($filter && $this->options['enabled']) {
$method = ucfirst(preg_replace('/s$/', '', $package->package_type));
// Check if packaged is enabled.
$function = 'is' . $method . 'Enabled';
$filter = $this->gpm->{$function}($package->slug);
}
// Filtering disabled only
if ($filter && $this->options['disabled']) {
$method = ucfirst(preg_replace('/s$/', '', $package->package_type));
// Check if package is disabled.
$function = 'is' . $method . 'Enabled';
$enabled_filter = $this->gpm->{$function}($package->slug);
// Apply filtering results.
if (!( $enabled_filter === false)) {
$filter = false;
}
}
if (!$filter) {
unset($data[$type][$slug]);
}
}
}
}
return $data;
}
/**
* @param AbstractPackageCollection|Plugins|Themes $packages
* @return array
*/
public function sort(AbstractPackageCollection $packages): array
{
$key = $this->options['sort'];
// Sorting only works once.
return $packages->sort(
function ($a, $b) use ($key) {
switch ($key) {
case 'author':
return strcmp($a->{$key}['name'], $b->{$key}['name']);
default:
return strcmp($a->$key, $b->$key);
}
},
$this->options['desc'] ? true : false
);
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use function strlen;
/**
* Class InfoCommand
* @package Grav\Console\Gpm
*/
class InfoCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var GPM */
protected $gpm;
/** @var string */
protected $all_yes;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('info')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force fetching the new data remotely'
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addArgument(
'package',
InputArgument::REQUIRED,
'The package of which more informations are desired. Use the "index" command for a list of packages'
)
->setDescription('Shows more informations about a package')
->setHelp('The <info>info</info> shows more information about a package');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$this->gpm = new GPM($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->displayGPMRelease();
$foundPackage = $this->gpm->findPackage($input->getArgument('package'));
if (!$foundPackage) {
$io->writeln("The package <cyan>'{$input->getArgument('package')}'</cyan> was not found in the Grav repository.");
$io->newLine();
$io->writeln('You can list all the available packages by typing:');
$io->writeln(" <green>{$this->argv} index</green>");
$io->newLine();
return 1;
}
$io->writeln("Found package <cyan>'{$input->getArgument('package')}'</cyan> under the '<green>" . ucfirst($foundPackage->package_type) . "</green>' section");
$io->newLine();
$io->writeln("<cyan>{$foundPackage->name}</cyan> [{$foundPackage->slug}]");
$io->writeln(str_repeat('-', strlen($foundPackage->name) + strlen($foundPackage->slug) + 3));
$io->writeln('<white>' . strip_tags($foundPackage->description_plain) . '</white>');
$io->newLine();
$packageURL = '';
if (isset($foundPackage->author['url'])) {
$packageURL = '<' . $foundPackage->author['url'] . '>';
}
$io->writeln('<green>' . str_pad(
'Author',
12
) . ':</green> ' . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL);
foreach ([
'version',
'keywords',
'date',
'homepage',
'demo',
'docs',
'guide',
'repository',
'bugs',
'zipball_url',
'license'
] as $info) {
if (isset($foundPackage->{$info})) {
$name = ucfirst($info);
$data = $foundPackage->{$info};
if ($info === 'zipball_url') {
$name = 'Download';
}
if ($info === 'date') {
$name = 'Last Update';
$data = date('D, j M Y, H:i:s, P ', strtotime($data));
}
$name = str_pad($name, 12);
$io->writeln("<green>{$name}:</green> {$data}");
}
}
$type = rtrim($foundPackage->package_type, 's');
$updatable = $this->gpm->{'is' . $type . 'Updatable'}($foundPackage->slug);
$installed = $this->gpm->{'is' . $type . 'Installed'}($foundPackage->slug);
// display current version if installed and different
if ($installed && $updatable) {
$local = $this->gpm->{'getInstalled'. $type}($foundPackage->slug);
$io->newLine();
$io->writeln("Currently installed version: <magenta>{$local->version}</magenta>");
$io->newLine();
}
// display changelog information
$question = new ConfirmationQuestion(
'Would you like to read the changelog? [y|N] ',
false
);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
$changelog = $foundPackage->changelog;
$io->newLine();
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) {
return "\n" . ucfirst($match[1]) . ':';
}, $log['content']);
$io->writeln("<cyan>{$title}</cyan>");
$io->writeln(str_repeat('-', strlen($title)));
$io->writeln($content);
$io->newLine();
$question = new ConfirmationQuestion('Press [ENTER] to continue or [q] to quit ', true);
$answer = $this->all_yes ? false : $io->askQuestion($question);
if (!$answer) {
break;
}
$io->newLine();
}
}
$io->newLine();
if ($installed && $updatable) {
$io->writeln('You can update this package by typing:');
$io->writeln(" <green>{$this->argv} update</green> <cyan>{$foundPackage->slug}</cyan>");
} else {
$io->writeln('You can install this package by typing:');
$io->writeln(" <green>{$this->argv} install</green> <cyan>{$foundPackage->slug}</cyan>");
}
$io->newLine();
return 0;
}
}
@@ -0,0 +1,720 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Exception;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Licenses;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Remote\Package;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZipArchive;
use function array_key_exists;
use function count;
use function define;
define('GIT_REGEX', '/http[s]?:\/\/(?:.*@)?(github|bitbucket)(?:.org|.com)\/.*\/(.*)/');
/**
* Class InstallCommand
* @package Grav\Console\Gpm
*/
class InstallCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var GPM */
protected $gpm;
/** @var string */
protected $destination;
/** @var string */
protected $file;
/** @var string */
protected $tmp;
/** @var bool */
protected $use_symlinks;
/** @var array */
protected $demo_processing = [];
/** @var string */
protected $all_yes;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('install')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'destination',
'd',
InputOption::VALUE_OPTIONAL,
'The destination where the package should be installed at. By default this would be where the grav instance has been launched from',
GRAV_ROOT
)
->addArgument(
'package',
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
'Package(s) to install. Use "bin/gpm index" to list packages. Use "bin/gpm direct-install" to install a specific version'
)
->setDescription('Performs the installation of plugins and themes')
->setHelp('The <info>install</info> command allows to install plugins and themes');
}
/**
* Allows to set the GPM object, used for testing the class
*
* @param GPM $gpm
*/
public function setGpm(GPM $gpm): void
{
$this->gpm = $gpm;
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('GPM Install');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
$this->gpm = new GPM($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->displayGPMRelease();
$this->destination = realpath($input->getOption('destination'));
$packages = array_map('strtolower', $input->getArgument('package'));
$this->data = $this->gpm->findPackages($packages);
$this->loadLocalConfig();
if (!Installer::isGravInstance($this->destination) ||
!Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
) {
$io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
return 1;
}
$io->newLine();
if (!$this->data['total']) {
$io->writeln('Nothing to install.');
$io->newLine();
return 0;
}
if (count($this->data['not_found'])) {
$io->writeln('These packages were not found on Grav: <red>' . implode(
'</red>, <red>',
array_keys($this->data['not_found'])
) . '</red>');
}
unset($this->data['not_found'], $this->data['total']);
if (null !== $this->local_config) {
// Symlinks available, ask if Grav should use them
$this->use_symlinks = false;
$question = new ConfirmationQuestion('Should Grav use the symlinks if available? [y|N] ', false);
$answer = $this->all_yes ? false : $io->askQuestion($question);
if ($answer) {
$this->use_symlinks = true;
}
}
$io->newLine();
try {
$dependencies = $this->gpm->getDependencies($packages);
} catch (Exception $e) {
//Error out if there are incompatible packages requirements and tell which ones, and what to do
//Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken
$io->writeln("<red>{$e->getMessage()}</red>");
return 1;
}
if ($dependencies) {
try {
$this->installDependencies($dependencies, 'install', 'The following dependencies need to be installed...');
$this->installDependencies($dependencies, 'update', 'The following dependencies need to be updated...');
$this->installDependencies($dependencies, 'ignore', "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false);
} catch (Exception $e) {
$io->writeln('<red>Installation aborted</red>');
return 1;
}
$io->writeln('<green>Dependencies are OK</green>');
$io->newLine();
}
//We're done installing dependencies. Install the actual packages
foreach ($this->data as $data) {
foreach ($data as $package_name => $package) {
if (array_key_exists($package_name, $dependencies)) {
$io->writeln("<green>Package {$package_name} already installed as dependency</green>");
} else {
$is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path);
if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) {
$this->processPackage($package, false);
} else {
if (Installer::lastErrorCode() == Installer::EXISTS) {
try {
$this->askConfirmationIfMajorVersionUpdated($package);
$this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data));
} catch (Exception $e) {
$io->writeln("<red>{$e->getMessage()}</red>");
return 1;
}
$question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> is already installed, overwrite? [y|N] ", false);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
$is_update = true;
$this->processPackage($package, $is_update);
} else {
$io->writeln("<yellow>Package {$package_name} not overwritten</yellow>");
}
} else {
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$io->writeln("<red>Cannot overwrite existing symlink for </red><cyan>{$package_name}</cyan>");
$io->newLine();
}
}
}
}
}
}
if (count($this->demo_processing) > 0) {
foreach ($this->demo_processing as $package) {
$this->installDemoContent($package);
}
}
// clear cache after successful upgrade
$this->clearCache();
return 0;
}
/**
* If the package is updated from an older major release, show warning and ask confirmation
*
* @param Package $package
* @return void
*/
public function askConfirmationIfMajorVersionUpdated(Package $package): void
{
$io = $this->getIO();
$package_name = $package->name;
$new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug);
$old_version = $package->version;
$major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0];
if ($major_version_changed) {
if ($this->all_yes) {
$io->writeln("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>");
return;
}
$question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>. Be sure to read what changed with the new major release. Continue? [y|N] ", false);
if (!$io->askQuestion($question)) {
$io->writeln("<yellow>Package {$package_name} not updated</yellow>");
exit;
}
}
}
/**
* Given a $dependencies list, filters their type according to $type and
* shows $message prior to listing them to the user. Then asks the user a confirmation prior
* to installing them.
*
* @param array $dependencies The dependencies array
* @param string $type The type of dependency to show: install, update, ignore
* @param string $message A message to be shown prior to listing the dependencies
* @param bool $required A flag that determines if the installation is required or optional
* @return void
* @throws Exception
*/
public function installDependencies(array $dependencies, string $type, string $message, bool $required = true): void
{
$io = $this->getIO();
$packages = array_filter($dependencies, static function ($action) use ($type) {
return $action === $type;
});
if (count($packages) > 0) {
$io->writeln($message);
foreach ($packages as $dependencyName => $dependencyVersion) {
$io->writeln(" |- Package <cyan>{$dependencyName}</cyan>");
}
$io->newLine();
if ($type === 'install') {
$questionAction = 'Install';
} else {
$questionAction = 'Update';
}
if (count($packages) === 1) {
$questionArticle = 'this';
} else {
$questionArticle = 'these';
}
if (count($packages) === 1) {
$questionNoun = 'package';
} else {
$questionNoun = 'packages';
}
$question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
foreach ($packages as $dependencyName => $dependencyVersion) {
$package = $this->gpm->findPackage($dependencyName);
$this->processPackage($package, $type === 'update');
}
$io->newLine();
} elseif ($required) {
throw new Exception();
}
}
}
/**
* @param Package|null $package
* @param bool $is_update True if the package is an update
* @return void
*/
private function processPackage(?Package $package, bool $is_update = false): void
{
$io = $this->getIO();
if (!$package) {
$io->writeln('<red>Package not found on the GPM!</red>');
$io->newLine();
return;
}
$symlink = false;
if ($this->use_symlinks) {
if (!isset($package->version) || $this->getSymlinkSource($package)) {
$symlink = true;
}
}
$symlink ? $this->processSymlink($package) : $this->processGpm($package, $is_update);
$this->processDemo($package);
}
/**
* Add package to the queue to process the demo content, if demo content exists
*
* @param Package $package
* @return void
*/
private function processDemo(Package $package): void
{
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
$this->demo_processing[] = $package;
}
}
/**
* Prompt to install the demo content of a package
*
* @param Package $package
* @return void
*/
private function installDemoContent(Package $package): void
{
$io = $this->getIO();
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
$dest_dir = $this->destination . DS . 'user';
$pages_dir = $dest_dir . DS . 'pages';
// Demo content exists, prompt to install it.
$io->writeln("<white>Attention: </white><cyan>{$package->name}</cyan> contains demo content");
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln(" '- <red>Skipped!</red> ");
$io->newLine();
return;
}
// if pages folder exists in demo
if (file_exists($demo_dir . DS . 'pages')) {
$pages_backup = 'pages.' . date('m-d-Y-H-i-s');
$question = new ConfirmationQuestion('This will backup your current `user/pages` folder to `user/' . $pages_backup . '`, continue? [y|N]', false);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$io->writeln(" '- <red>Skipped!</red> ");
$io->newLine();
return;
}
// backup current pages folder
if (file_exists($dest_dir)) {
if (rename($pages_dir, $dest_dir . DS . $pages_backup)) {
$io->writeln(' |- Backing up pages... <green>ok</green>');
} else {
$io->writeln(' |- Backing up pages... <red>failed</red>');
}
}
}
// Confirmation received, copy over the data
$io->writeln(' |- Installing demo content... <green>ok</green> ');
Folder::rcopy($demo_dir, $dest_dir);
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
}
/**
* @param Package $package
* @return array|false
*/
private function getGitRegexMatches(Package $package)
{
if (isset($package->repository)) {
$repository = $package->repository;
} else {
return false;
}
preg_match(GIT_REGEX, $repository, $matches);
return $matches;
}
/**
* @param Package $package
* @return string|false
*/
private function getSymlinkSource(Package $package)
{
$matches = $this->getGitRegexMatches($package);
foreach ($this->local_config as $paths) {
if (Utils::endsWith($matches[2], '.git')) {
$repo_dir = preg_replace('/\.git$/', '', $matches[2]);
} else {
$repo_dir = $matches[2];
}
$paths = (array) $paths;
foreach ($paths as $repo) {
$path = rtrim($repo, '/') . '/' . $repo_dir;
if (file_exists($path)) {
return $path;
}
}
}
return false;
}
/**
* @param Package $package
* @return void
*/
private function processSymlink(Package $package): void
{
$io = $this->getIO();
exec('cd ' . $this->destination);
$to = $this->destination . DS . $package->install_path;
$from = $this->getSymlinkSource($package);
$io->writeln("Preparing to Symlink <cyan>{$package->name}</cyan>");
$io->write(' |- Checking source... ');
if (file_exists($from)) {
$io->writeln('<green>ok</green>');
$io->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} elseif (file_exists($to)) {
$io->writeln(" '- <red>Symlink cannot overwrite an existing package, please remove first</red>");
$io->newLine();
} else {
symlink($from, $to);
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Symlinking package... <green>ok</green> ');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
return;
}
$io->writeln('<red>not found!</red>');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
}
/**
* @param Package $package
* @param bool $is_update
* @return bool
*/
private function processGpm(Package $package, bool $is_update = false)
{
$io = $this->getIO();
$version = $package->available ?? $package->version;
$license = Licenses::get($package->slug);
$io->writeln("Preparing to install <cyan>{$package->name}</cyan> [v{$version}]");
$io->write(' |- Downloading package... 0%');
$this->file = $this->downloadPackage($package, $license);
if (!$this->file) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
return false;
}
$io->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} else {
$io->write(' |- Installing package... ');
$installation = $this->installPackage($package, $is_update);
if (!$installation) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} else {
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
return true;
}
}
return false;
}
/**
* @param Package $package
* @param string|null $license
* @return string|null
*/
private function downloadPackage(Package $package, string $license = null)
{
$io = $this->getIO();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid();
$filename = $package->slug . basename($package->zipball_url);
$filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename);
$query = '';
if (!empty($package->premium)) {
$query = json_encode(array_merge(
$package->premium,
[
'slug' => $package->slug,
'filename' => $package->premium['filename'],
'license_key' => $license,
'sid' => md5(GRAV_ROOT)
]
));
$query = '?d=' . base64_encode($query);
}
try {
$output = Response::get($package->zipball_url . $query, [], [$this, 'progress']);
} catch (Exception $e) {
$error = str_replace("\n", "\n | '- ", $e->getMessage());
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Downloading package... <red>error</red> ');
$io->writeln(" | '- " . $error);
return null;
}
Folder::create($this->tmp);
$io->write("\x0D");
$io->write(' |- Downloading package... 100%');
$io->newLine();
file_put_contents($this->tmp . DS . $filename, $output);
return $this->tmp . DS . $filename;
}
/**
* @param Package $package
* @return bool
*/
private function checkDestination(Package $package): bool
{
$io = $this->getIO();
Installer::isValidDestination($this->destination . DS . $package->install_path);
if (Installer::lastErrorCode() === Installer::IS_LINK) {
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
if ($this->all_yes) {
$io->writeln(" | '- <yellow>Skipped automatically.</yellow>");
return false;
}
$question = new ConfirmationQuestion(
" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
false
);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln(" | '- <red>You decided to not delete the symlink automatically.</red>");
return false;
}
unlink($this->destination . DS . $package->install_path);
}
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
return true;
}
/**
* Install a package
*
* @param Package $package
* @param bool $is_update True if it's an update. False if it's an install
* @return bool
*/
private function installPackage(Package $package, bool $is_update = false): bool
{
$io = $this->getIO();
$type = $package->package_type;
Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]);
$error_code = Installer::lastErrorCode();
Folder::delete($this->tmp);
if ($error_code) {
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Installing package... <red>error</red> ');
$io->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$message = Installer::getMessage();
if ($message) {
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(" |- {$message}");
}
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Installing package... <green>ok</green> ');
return true;
}
/**
* @param array $progress
* @return void
*/
public function progress(array $progress): void
{
$io = $this->getIO();
$io->write("\x0D");
$io->write(' |- Downloading package... ' . str_pad(
$progress['percent'],
5,
' ',
STR_PAD_LEFT
) . '%');
}
}
@@ -0,0 +1,344 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Exception;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav;
use Grav\Console\GpmCommand;
use Grav\Installer\Install;
use RuntimeException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZipArchive;
use function is_callable;
use function strlen;
/**
* Class SelfupgradeCommand
* @package Grav\Console\Gpm
*/
class SelfupgradeCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var string */
protected $file;
/** @var array */
protected $types = ['plugins', 'themes'];
/** @var string|null */
private $tmp;
/** @var Upgrader */
private $upgrader;
/** @var string */
protected $all_yes;
/** @var string */
protected $overwrite;
/** @var int */
protected $timeout;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('self-upgrade')
->setAliases(['selfupgrade', 'selfupdate'])
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'overwrite',
'o',
InputOption::VALUE_NONE,
'Option to overwrite packages if they already exist'
)
->addOption(
'timeout',
't',
InputOption::VALUE_OPTIONAL,
'Option to set the timeout in seconds when downloading the update (0 for no timeout)',
30
)
->setDescription('Detects and performs an update of Grav itself when available')
->setHelp('The <info>update</info> command updates Grav itself when a new version is available');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('GPM Self Upgrade');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
$this->upgrader = new Upgrader($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->overwrite = $input->getOption('overwrite');
$this->timeout = (int) $input->getOption('timeout');
$this->displayGPMRelease();
$update = $this->upgrader->getAssets()['grav-update'];
$local = $this->upgrader->getLocalVersion();
$remote = $this->upgrader->getRemoteVersion();
$release = strftime('%c', strtotime($this->upgrader->getReleaseDate()));
if (!$this->upgrader->meetsRequirements()) {
$io->writeln('<red>ATTENTION:</red>');
$io->writeln(' Grav has increased the minimum PHP requirement.');
$io->writeln(' You are currently running PHP <red>' . phpversion() . '</red>, but PHP <green>' . $this->upgrader->minPHPVersion() . '</green> is required.');
$io->writeln(' Additional information: <white>http://getgrav.org/blog/changing-php-requirements</white>');
$io->newLine();
$io->writeln('Selfupgrade aborted.');
$io->newLine();
return 1;
}
if (!$this->overwrite && !$this->upgrader->isUpgradable()) {
$io->writeln("You are already running the latest version of <green>Grav v{$local}</green>");
$io->writeln("which was released on {$release}");
$config = Grav::instance()['config'];
$schema = $config->get('versions.core.grav.schema');
if ($schema !== GRAV_SCHEMA && version_compare($schema, GRAV_SCHEMA, '<')) {
$io->newLine();
$io->writeln('However post-install scripts have not been run.');
if (!$this->all_yes) {
$question = new ConfirmationQuestion(
'Would you like to run the scripts? [Y|n] ',
true
);
$answer = $io->askQuestion($question);
} else {
$answer = true;
}
if ($answer) {
// Finalize installation.
Install::instance()->finalize();
$io->write(' |- Running post-install scripts... ');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
}
return 0;
}
Installer::isValidDestination(GRAV_ROOT . '/system');
if (Installer::IS_LINK === Installer::lastErrorCode()) {
$io->writeln('<red>ATTENTION:</red> Grav is symlinked, cannot upgrade, aborting...');
$io->newLine();
$io->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}.");
return 1;
}
// not used but preloaded just in case!
new ArrayInput([]);
$io->writeln("Grav v<cyan>{$remote}</cyan> is now available [release date: {$release}].");
$io->writeln('You are currently using v<cyan>' . GRAV_VERSION . '</cyan>.');
if (!$this->all_yes) {
$question = new ConfirmationQuestion(
'Would you like to read the changelog before proceeding? [y|N] ',
false
);
$answer = $io->askQuestion($question);
if ($answer) {
$changelog = $this->upgrader->getChangelog(GRAV_VERSION);
$io->newLine();
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) {
return "\n" . ucfirst($match[1]) . ':';
}, $log['content']);
$io->writeln($title);
$io->writeln(str_repeat('-', strlen($title)));
$io->writeln($content);
$io->newLine();
}
$question = new ConfirmationQuestion('Press [ENTER] to continue.', true);
$io->askQuestion($question);
}
$question = new ConfirmationQuestion('Would you like to upgrade now? [y|N] ', false);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln('Aborting...');
return 1;
}
}
$io->newLine();
$io->writeln("Preparing to upgrade to v<cyan>{$remote}</cyan>..");
$io->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%");
$this->file = $this->download($update);
$io->write(' |- Installing upgrade... ');
$installation = $this->upgrade();
$error = 0;
if (!$installation) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
$error = 1;
} else {
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
if ($this->tmp && is_dir($this->tmp)) {
Folder::delete($this->tmp);
}
return $error;
}
/**
* @param array $package
* @return string
*/
private function download(array $package): string
{
$io = $this->getIO();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/grav-update-' . uniqid('', false);
$options = [
'timeout' => $this->timeout,
];
$output = Response::get($package['download'], $options, [$this, 'progress']);
Folder::create($this->tmp);
$io->write("\x0D");
$io->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%");
$io->newLine();
file_put_contents($this->tmp . DS . $package['name'], $output);
return $this->tmp . DS . $package['name'];
}
/**
* @return bool
*/
private function upgrade(): bool
{
$io = $this->getIO();
$this->upgradeGrav($this->file);
$errorCode = Installer::lastErrorCode();
if ($errorCode) {
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Installing upgrade... <red>error</red> ');
$io->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$io->writeln(' |- Installing upgrade... <green>ok</green> ');
return true;
}
/**
* @param array $progress
* @return void
*/
public function progress(array $progress): void
{
$io = $this->getIO();
$io->write("\x0D");
$io->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad(
$progress['percent'],
5,
' ',
STR_PAD_LEFT
) . '%');
}
/**
* @param int|float $size
* @param int $precision
* @return string
*/
public function formatBytes($size, int $precision = 2): string
{
$base = log($size) / log(1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
return round(1024 ** ($base - floor($base)), $precision) . $suffixes[(int)floor($base)];
}
/**
* @param string $zip
* @return void
*/
private function upgradeGrav(string $zip): void
{
try {
$folder = Installer::unZip($zip, $this->tmp . '/zip');
if ($folder === false) {
throw new RuntimeException(Installer::lastErrorMsg());
}
$script = $folder . '/system/install.php';
if ((file_exists($script) && $install = include $script) && is_callable($install)) {
$install($zip);
} else {
throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
}
} catch (Exception $e) {
Installer::setError($e->getMessage());
}
}
}
@@ -0,0 +1,312 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Local;
use Grav\Common\GPM\Remote;
use Grav\Common\Grav;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Throwable;
use function count;
use function in_array;
use function is_array;
/**
* Class UninstallCommand
* @package Grav\Console\Gpm
*/
class UninstallCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var GPM */
protected $gpm;
/** @var string */
protected $destination;
/** @var string */
protected $file;
/** @var string */
protected $tmp;
/** @var array */
protected $dependencies = [];
/** @var string */
protected $all_yes;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('uninstall')
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addArgument(
'package',
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
'The package(s) that are desired to be removed. Use the "index" command for a list of packages'
)
->setDescription('Performs the uninstallation of plugins and themes')
->setHelp('The <info>uninstall</info> command allows to uninstall plugins and themes');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$this->gpm = new GPM();
$this->all_yes = $input->getOption('all-yes');
$packages = array_map('strtolower', $input->getArgument('package'));
$this->data = ['total' => 0, 'not_found' => []];
$total = 0;
foreach ($packages as $package) {
$plugin = $this->gpm->getInstalledPlugin($package);
$theme = $this->gpm->getInstalledTheme($package);
if ($plugin || $theme) {
$this->data[strtolower($package)] = $plugin ?: $theme;
$total++;
} else {
$this->data['not_found'][] = $package;
}
}
$this->data['total'] = $total;
$io->newLine();
if (!$this->data['total']) {
$io->writeln('Nothing to uninstall.');
$io->newLine();
return 0;
}
if (count($this->data['not_found'])) {
$io->writeln('These packages were not found installed: <red>' . implode(
'</red>, <red>',
$this->data['not_found']
) . '</red>');
}
unset($this->data['not_found'], $this->data['total']);
// Plugins need to be initialized in order to make clear-cache to work.
try {
$this->initializePlugins();
} catch (Throwable $e) {
$io->writeln("<red>Some plugins failed to initialize: {$e->getMessage()}</red>");
}
$error = 0;
foreach ($this->data as $slug => $package) {
$io->writeln("Preparing to uninstall <cyan>{$package->name}</cyan> [v{$package->version}]");
$io->write(' |- Checking destination... ');
$checks = $this->checkDestination($slug, $package);
if (!$checks) {
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
$error = 1;
} else {
$uninstall = $this->uninstallPackage($slug, $package);
if (!$uninstall) {
$io->writeln(" '- <red>Uninstallation failed or aborted.</red>");
$error = 1;
} else {
$io->writeln(" '- <green>Success!</green> ");
}
}
}
// clear cache after successful upgrade
$this->clearCache();
return $error;
}
/**
* @param string $slug
* @param Local\Package|Remote\Package $package
* @param bool $is_dependency
* @return bool
*/
private function uninstallPackage($slug, $package, $is_dependency = false): bool
{
$io = $this->getIO();
if (!$slug) {
return false;
}
//check if there are packages that have this as a dependency. Abort and show list
$dependent_packages = $this->gpm->getPackagesThatDependOnPackage($slug);
if (count($dependent_packages) > ($is_dependency ? 1 : 0)) {
$io->newLine(2);
$io->writeln('<red>Uninstallation failed.</red>');
$io->newLine();
if (count($dependent_packages) > ($is_dependency ? 2 : 1)) {
$io->writeln('The installed packages <cyan>' . implode('</cyan>, <cyan>', $dependent_packages) . '</cyan> depends on this package. Please remove those first.');
} else {
$io->writeln('The installed package <cyan>' . implode('</cyan>, <cyan>', $dependent_packages) . '</cyan> depends on this package. Please remove it first.');
}
$io->newLine();
return false;
}
if (isset($package->dependencies)) {
$dependencies = $package->dependencies;
if ($is_dependency) {
foreach ($dependencies as $key => $dependency) {
if (in_array($dependency['name'], $this->dependencies, true)) {
unset($dependencies[$key]);
}
}
} elseif (count($dependencies) > 0) {
$io->writeln(' `- Dependencies found...');
$io->newLine();
}
foreach ($dependencies as $dependency) {
$this->dependencies[] = $dependency['name'];
if (is_array($dependency)) {
$dependency = $dependency['name'];
}
if ($dependency === 'grav' || $dependency === 'php') {
continue;
}
$dependencyPackage = $this->gpm->findPackage($dependency);
$dependency_exists = $this->packageExists($dependency, $dependencyPackage);
if ($dependency_exists == Installer::EXISTS) {
$io->writeln("A dependency on <cyan>{$dependencyPackage->name}</cyan> [v{$dependencyPackage->version}] was found");
$question = new ConfirmationQuestion(" |- Uninstall <cyan>{$dependencyPackage->name}</cyan>? [y|N] ", false);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
$uninstall = $this->uninstallPackage($dependency, $dependencyPackage, true);
if (!$uninstall) {
$io->writeln(" '- <red>Uninstallation failed or aborted.</red>");
} else {
$io->writeln(" '- <green>Success!</green> ");
}
$io->newLine();
} else {
$io->writeln(" '- <yellow>You decided not to uninstall {$dependencyPackage->name}.</yellow>");
$io->newLine();
}
}
}
}
$locator = Grav::instance()['locator'];
$path = $locator->findResource($package->package_type . '://' . $slug);
Installer::uninstall($path);
$errorCode = Installer::lastErrorCode();
if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) {
$io->writeln(" |- Uninstalling {$package->name} package... <red>error</red> ");
$io->writeln(" | '- <yellow>" . Installer::lastErrorMsg() . '</yellow>');
return false;
}
$message = Installer::getMessage();
if ($message) {
$io->writeln(" |- {$message}");
}
if (!$is_dependency && $this->dependencies) {
$io->writeln("Finishing up uninstalling <cyan>{$package->name}</cyan>");
}
$io->writeln(" |- Uninstalling {$package->name} package... <green>ok</green> ");
return true;
}
/**
* @param string $slug
* @param Local\Package|Remote\Package $package
* @return bool
*/
private function checkDestination(string $slug, $package): bool
{
$io = $this->getIO();
$exists = $this->packageExists($slug, $package);
if ($exists === Installer::IS_LINK) {
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
if ($this->all_yes) {
$io->writeln(" | '- <yellow>Skipped automatically.</yellow>");
return false;
}
$question = new ConfirmationQuestion(
" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
false
);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln(" | '- <red>You decided not to delete the symlink automatically.</red>");
return false;
}
}
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
return true;
}
/**
* Check if package exists
*
* @param string $slug
* @param Local\Package|Remote\Package $package
* @return int
*/
private function packageExists(string $slug, $package): int
{
$path = Grav::instance()['locator']->findResource($package->package_type . '://' . $slug);
Installer::isValidDestination($path);
return Installer::lastErrorCode();
}
}
@@ -0,0 +1,289 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Upgrader;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZipArchive;
use function array_key_exists;
use function count;
/**
* Class UpdateCommand
* @package Grav\Console\Gpm
*/
class UpdateCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var string */
protected $destination;
/** @var string */
protected $file;
/** @var array */
protected $types = ['plugins', 'themes'];
/** @var GPM */
protected $gpm;
/** @var string */
protected $all_yes;
/** @var string */
protected $overwrite;
/** @var Upgrader */
protected $upgrader;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('update')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addOption(
'destination',
'd',
InputOption::VALUE_OPTIONAL,
'The grav instance location where the updates should be applied to. By default this would be where the grav cli has been launched from',
GRAV_ROOT
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'overwrite',
'o',
InputOption::VALUE_NONE,
'Option to overwrite packages if they already exist'
)
->addOption(
'plugins',
'p',
InputOption::VALUE_NONE,
'Update only plugins'
)
->addOption(
'themes',
't',
InputOption::VALUE_NONE,
'Update only themes'
)
->addArgument(
'package',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
'The package or packages that is desired to update. By default all available updates will be applied.'
)
->setDescription('Detects and performs an update of plugins and themes when available')
->setHelp('The <info>update</info> command updates plugins and themes when a new version is available');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('GPM Update');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
$this->upgrader = new Upgrader($input->getOption('force'));
$local = $this->upgrader->getLocalVersion();
$remote = $this->upgrader->getRemoteVersion();
if ($local !== $remote) {
$io->writeln('<yellow>WARNING</yellow>: A new version of Grav is available. You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working.');
$io->newLine();
$question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln('<red>Update aborted. Exiting...</red>');
return 1;
}
}
$this->gpm = new GPM($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->overwrite = $input->getOption('overwrite');
$this->displayGPMRelease();
$this->destination = realpath($input->getOption('destination'));
if (!Installer::isGravInstance($this->destination)) {
$io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
exit;
}
if ($input->getOption('plugins') === false && $input->getOption('themes') === false) {
$list_type = ['plugins' => true, 'themes' => true];
} else {
$list_type['plugins'] = $input->getOption('plugins');
$list_type['themes'] = $input->getOption('themes');
}
if ($this->overwrite) {
$this->data = $this->gpm->getInstallable($list_type);
$description = ' can be overwritten';
} else {
$this->data = $this->gpm->getUpdatable($list_type);
$description = ' need updating';
}
$only_packages = array_map('strtolower', $input->getArgument('package'));
if (!$this->overwrite && !$this->data['total']) {
$io->writeln('Nothing to update.');
return 0;
}
$io->write("Found <green>{$this->gpm->countInstalled()}</green> packages installed of which <magenta>{$this->data['total']}</magenta>{$description}");
$limit_to = $this->userInputPackages($only_packages);
$io->newLine();
unset($this->data['total'], $limit_to['total']);
// updates review
$slugs = [];
$index = 1;
foreach ($this->data as $packages) {
foreach ($packages as $slug => $package) {
if (!array_key_exists($slug, $limit_to) && count($only_packages)) {
continue;
}
if (!$package->available) {
$package->available = $package->version;
}
$io->writeln(
// index
str_pad((string)$index++, 2, '0', STR_PAD_LEFT) . '. ' .
// name
'<cyan>' . str_pad($package->name, 15) . '</cyan> ' .
// version
"[v<magenta>{$package->version}</magenta> -> v<green>{$package->available}</green>]"
);
$slugs[] = $slug;
}
}
if (!$this->all_yes) {
// prompt to continue
$io->newLine();
$question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true);
$answer = $io->askQuestion($question);
if (!$answer) {
$io->writeln('<red>Update aborted. Exiting...</red>');
return 1;
}
}
// finally update
$install_command = $this->getApplication()->find('install');
$args = new ArrayInput([
'command' => 'install',
'package' => $slugs,
'-f' => $input->getOption('force'),
'-d' => $this->destination,
'-y' => true
]);
$command_exec = $install_command->run($args, $io);
if ($command_exec != 0) {
$io->writeln('<red>Error:</red> An error occurred while trying to install the packages');
return 1;
}
return 0;
}
/**
* @param array $only_packages
* @return array
*/
private function userInputPackages(array $only_packages): array
{
$io = $this->getIO();
$found = ['total' => 0];
$ignore = [];
if (!count($only_packages)) {
$io->newLine();
} else {
foreach ($only_packages as $only_package) {
$find = $this->gpm->findPackage($only_package);
if (!$find || (!$this->overwrite && !$this->gpm->isUpdatable($find->slug))) {
$name = $find->slug ?? $only_package;
$ignore[$name] = $name;
} else {
$found[$find->slug] = $find;
$found['total']++;
}
}
if ($found['total']) {
$list = $found;
unset($list['total']);
$list = array_keys($list);
if ($found['total'] !== $this->data['total']) {
$io->write(", only <magenta>{$found['total']}</magenta> will be updated");
}
$io->newLine();
$io->writeln('Limiting updates for only <cyan>' . implode(
'</cyan>, <cyan>',
$list
) . '</cyan>');
}
if (count($ignore)) {
$io->newLine();
$io->writeln('Packages not found or not requiring updates: <red>' . implode(
'</red>, <red>',
$ignore
) . '</red>');
}
}
return $found;
}
}
@@ -0,0 +1,125 @@
<?php
/**
* @package Grav\Console\Gpm
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav;
use Grav\Console\GpmCommand;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function count;
/**
* Class VersionCommand
* @package Grav\Console\Gpm
*/
class VersionCommand extends GpmCommand
{
/** @var GPM */
protected $gpm;
/**
* @return void
*/
protected function configure(): void
{
$this
->setName('version')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addArgument(
'package',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
'The package or packages that is desired to know the version of. By default and if not specified this would be grav'
)
->setDescription('Shows the version of an installed package. If available also shows pending updates.')
->setHelp('The <info>version</info> command displays the current version of a package installed and, if available, the available version of pending updates');
}
/**
* @return int
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$this->gpm = new GPM($input->getOption('force'));
$packages = $input->getArgument('package');
$installed = false;
if (!count($packages)) {
$packages = ['grav'];
}
foreach ($packages as $package) {
$package = strtolower($package);
$name = null;
$version = null;
$updatable = false;
if ($package === 'grav') {
$name = 'Grav';
$version = GRAV_VERSION;
$upgrader = new Upgrader();
if ($upgrader->isUpgradable()) {
$updatable = " [upgradable: v<green>{$upgrader->getRemoteVersion()}</green>]";
}
} else {
// get currently installed version
$locator = Grav::instance()['locator'];
$blueprints_path = $locator->findResource('plugins://' . $package . DS . 'blueprints.yaml');
if (!file_exists($blueprints_path)) { // theme?
$blueprints_path = $locator->findResource('themes://' . $package . DS . 'blueprints.yaml');
if (!file_exists($blueprints_path)) {
continue;
}
}
$file = YamlFile::instance($blueprints_path);
$package_yaml = $file->content();
$file->free();
$version = $package_yaml['version'];
if (!$version) {
continue;
}
$installed = $this->gpm->findPackage($package);
if ($installed) {
$name = $installed->name;
if ($this->gpm->isUpdatable($package)) {
$updatable = " [updatable: v<green>{$installed->available}</green>]";
}
}
}
$updatable = $updatable ?: '';
if ($installed || $package === 'grav') {
$io->writeln("You are running <white>{$name}</white> v<cyan>{$version}</cyan>{$updatable}");
} else {
$io->writeln("Package <red>{$package}</red> not found");
}
}
return 0;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ConsoleCommand
* @package Grav\Console
*/
class GpmCommand extends Command
{
use ConsoleTrait;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setupConsole($input, $output);
$grav = Grav::instance();
$grav['config']->init();
$grav['uri']->init();
// @phpstan-ignore-next-line
$grav['accounts'];
return $this->serve();
}
/**
* Override with your implementation.
*
* @return int
*/
protected function serve()
{
// Return error.
return 1;
}
/**
* @return void
*/
protected function displayGPMRelease()
{
/** @var Config $config */
$config = Grav::instance()['config'];
$io = $this->getIO();
$io->newLine();
$io->writeln('GPM Releases Configuration: <yellow>' . ucfirst($config->get('system.gpm.releases')) . '</yellow>');
$io->newLine();
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ConsoleCommand
* @package Grav\Console
*/
class GravCommand extends Command
{
use ConsoleTrait;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->setupConsole($input, $output);
// Old versions of Grav called this command after grav upgrade.
// We need make this command to work with older ConsoleTrait:
if (method_exists($this, 'initializeGrav')) {
$this->initializeGrav();
}
return $this->serve();
}
/**
* Override with your implementation.
*
* @return int
*/
protected function serve()
{
// Return error.
return 1;
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package Grav\Console\Plugin
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Plugin;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Plugins;
use Grav\Console\ConsoleCommand;
/**
* Class InfoCommand
* @package Grav\Console\Gpm
*/
class PluginListCommand extends ConsoleCommand
{
protected static $defaultName = 'plugins:list';
/**
* @return void
*/
protected function configure(): void
{
$this->setHidden(true);
}
/**
* @return int
*/
protected function serve(): int
{
$bin = $this->argv;
$pattern = '([A-Z]\w+Command\.php)';
$io = $this->getIO();
$io->newLine();
$io->writeln('<red>Usage:</red>');
$io->writeln(" {$bin} [slug] [command] [arguments]");
$io->newLine();
$io->writeln('<red>Example:</red>');
$io->writeln(" {$bin} error log -l 1 --trace");
$io->newLine();
$io->writeln('<red>Plugins with CLI available:</red>');
$plugins = Plugins::all();
$index = 0;
foreach ($plugins as $name => $plugin) {
if (!$plugin->enabled) {
continue;
}
$list = Folder::all("plugins://{$name}", ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 1]);
if (!$list) {
continue;
}
$index++;
$num = str_pad((string)$index, 2, '0', STR_PAD_LEFT);
$io->writeln(' ' . $num . '. <red>' . str_pad($name, 15) . "</red> <white>{$bin} {$name} list</white>");
}
return 0;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @package Grav\Console\TerminalObjects
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\TerminalObjects;
/**
* Class Table
* @package Grav\Console\TerminalObjects
* @deprecated 1.7 Use Symfony Console Table
*/
class Table extends \League\CLImate\TerminalObject\Basic\Table
{
/**
* @return array
*/
public function result()
{
$this->column_widths = $this->getColumnWidths();
$this->table_width = $this->getWidth();
$this->border = $this->getBorder();
$this->buildHeaderRow();
foreach ($this->data as $key => $columns) {
$this->rows[] = $this->buildRow($columns);
}
$this->rows[] = $this->border;
return $this->rows;
}
}