diff --git a/src/Glpi/Console/AbstractCommand.php b/src/Glpi/Console/AbstractCommand.php index c9b7d624d2c..9d1d67e8eca 100644 --- a/src/Glpi/Console/AbstractCommand.php +++ b/src/Glpi/Console/AbstractCommand.php @@ -35,18 +35,22 @@ namespace Glpi\Console; +use Auth; use DBmysql; use Glpi\Console\Command\GlpiCommandInterface; use Glpi\Console\Exception\EarlyExitException; use Glpi\System\RequirementsManager; use Override; +use Session; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; +use User; use function Safe\preg_replace; @@ -377,4 +381,33 @@ final protected function outputMessage(string $message, int $verbosity = OutputI ); } } + + /** + * Load user in session. + * + * @throws InvalidArgumentException + */ + protected function loadUserSession(string $username): void + { + $user = new User(); + if ($user->getFromDBbyName($username)) { + // Store computed output parameters + $lang = $_SESSION['glpilanguage']; + $session_use_mode = $_SESSION['glpi_use_mode']; + + $auth = new Auth(); + $auth->auth_succeded = true; + $auth->user = $user; + Session::init($auth); + + // Force usage of computed output parameters + $_SESSION['glpilanguage'] = $lang; + $_SESSION['glpi_use_mode'] = $session_use_mode; + Session::loadLanguage(); + } else { + throw new InvalidArgumentException( + __('User name defined by --username option is invalid.') + ); + } + } } diff --git a/src/Glpi/Console/Marketplace/DownloadCommand.php b/src/Glpi/Console/Marketplace/DownloadCommand.php index 0a27bb7c899..72975bc8e43 100644 --- a/src/Glpi/Console/Marketplace/DownloadCommand.php +++ b/src/Glpi/Console/Marketplace/DownloadCommand.php @@ -85,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // If the plugin is already downloaded, refuse to download it again if (!$input->getOption('force') && is_dir(GLPI_MARKETPLACE_DIR . '/' . $plugin)) { if (Controller::hasVcsDirectory($plugin)) { - $error_msg = sprintf(__('Plugin "%s" as a local source versioning directory.'), $plugin); + $error_msg = sprintf(__('Plugin "%s" has a local source versioning directory.'), $plugin); $error_msg .= "\n" . __('To avoid overwriting a potential branch under development, downloading is disabled.'); } else { $error_msg = sprintf(__('Plugin "%s" is already downloaded. Use --force to force it to re-download.'), $plugin); @@ -96,8 +96,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $controller = new Controller($plugin); if ($controller->canBeDownloaded($version)) { $result = $controller->downloadPlugin(false, $version); - $success_msg = sprintf(__("Plugin %s downloaded successfully"), $plugin); - $error_msg = sprintf(__("Plugin %s could not be downloaded"), $plugin); + $success_msg = sprintf(__('Plugin "%s" downloaded successfully'), $plugin); + $error_msg = sprintf(__('Plugin "%s" could not be downloaded'), $plugin); if ($result) { $output->writeln("$success_msg"); } else { diff --git a/src/Glpi/Console/Marketplace/UpdateLocalPluginsCommand.php b/src/Glpi/Console/Marketplace/UpdateLocalPluginsCommand.php new file mode 100644 index 00000000000..814528afec7 --- /dev/null +++ b/src/Glpi/Console/Marketplace/UpdateLocalPluginsCommand.php @@ -0,0 +1,216 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Console\Marketplace; + +use Glpi\Console\AbstractCommand; +use Glpi\Marketplace\Api\Plugins; +use Glpi\Marketplace\Controller; +use GLPINetwork; +use Plugin; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Throwable; + +final class UpdateLocalPluginsCommand extends AbstractCommand +{ + protected function configure() + { + parent::configure(); + + $this->setName('marketplace:update_local_plugins'); + $this->setDescription(__('Download up-to-date sources for all local plugins and process updates of active plugins')); + + $this->addOption( + 'username', + 'u', + InputOption::VALUE_REQUIRED, + __('Name of user used during installation script (among other things to set plugin admin rights)') + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Controller::isCLIAllowed()) { + $output->writeln("" . __('Access to the marketplace CLI commands is disallowed by the GLPI configuration') . ""); + return self::FAILURE; + } + + if (!GLPINetwork::isRegistered()) { + $output->writeln("" . __("The GLPI Network registration key is missing or invalid") . ""); + return self::FAILURE; + } + + $username = $input->getOption('username'); + if ($username !== null) { + $this->loadUserSession($username); + } + + $has_errors = false; + + $plugins_manager = new Plugin(); + $plugins_api = new Plugins(); + + $local_plugins_data = $plugins_manager->find(); + $local_versions = \array_column($local_plugins_data, 'version', 'directory'); + $active_plugins = \array_column( + \array_filter($local_plugins_data, fn($plugin_data) => $plugin_data['state'] === Plugin::ACTIVATED), + 'id', + 'directory' + ); + + // Update all plugins sources, to be sure that all plugins have the latest version. + $updated_plugins = []; + foreach ($local_versions as $plugin_key => $local_version) { + $exists_on_filesystem = $plugins_manager->isLoadable($plugin_key); + + $latest_version = $plugins_api->getPlugin($plugin_key)['version'] ?? null; + + if ($latest_version === null) { + $msg = '' + . sprintf(__('Plugin "%s" is not available for your GLPI version.'), $plugin_key) + . '' + ; + $output->writeln($msg); + continue; + } + + if ($exists_on_filesystem && \version_compare($local_version, $latest_version, '<') === false) { + $msg = '' + . sprintf(__('Plugin "%s" is already up-to-date.'), $plugin_key) + . '' + ; + $output->writeln($msg); + continue; + } + + $controller = new Controller($plugin_key); + if (!$controller->canBeOverwritten()) { + if ($controller::hasVcsDirectory($plugin_key)) { + $msg = '' + . sprintf(__('Plugin "%s" has a local source versioning directory.'), $plugin_key) + . ' ' + . __('To avoid overwriting a potential branch under development, downloading is disabled.') + . '' + ; + } else { + $msg = '' + . sprintf(__('Plugin "%s" has an available update but its directory is not writable.'), $plugin_key) + . '' + ; + } + + $output->writeln($msg); + continue; + } + + $result = $controller->downloadPlugin(false, $latest_version); + if ($result) { + $updated_plugins[] = $plugin_key; + $output->writeln('' . sprintf(__('Plugin "%s" downloaded successfully'), $plugin_key) . ''); + } else { + $has_errors = true; + $output->writeln( + '' . sprintf(__('Plugin "%s" could not be downloaded'), $plugin_key) . '', + OutputInterface::VERBOSITY_QUIET + ); + $this->outputSessionBufferedMessages([WARNING, ERROR]); + } + } + + // Automatically process plugin update and reactivation of active plugins. + if (count($active_plugins) > 0) { + \asort($active_plugins); + + Plugin::forcePluginsExecution(true); // Temporarly force the plugins execution + foreach ($active_plugins as $plugin_key => $plugin_id) { + if (!\in_array($plugin_key, $updated_plugins, true)) { + continue; + } + + $plugin = new Plugin(); + + try { + $plugin->install($plugin_id); + $installed = \in_array($plugin->fields['state'], [Plugin::NOTACTIVATED, Plugin::TOBECONFIGURED]); + } catch (Throwable $e) { + global $PHPLOGGER; + $PHPLOGGER->error( + sprintf('Error while installing plugin `%s`, error was: `%s`.', $plugin_key, $e->getMessage()), + ['exception' => $e] + ); + + $installed = false; + } + if (!$installed) { + $has_errors = true; + $output->writeln( + '' . sprintf(__('Plugin "%s" installation failed.'), $plugin_key) . '', + OutputInterface::VERBOSITY_QUIET + ); + $this->outputSessionBufferedMessages([WARNING, ERROR]); + continue; + } + + try { + $activated = $plugin->activate($plugin_id); + } catch (Throwable $e) { + global $PHPLOGGER; + $PHPLOGGER->error( + sprintf('Error while activating plugin `%s`, error was: `%s`.', $plugin_key, $e->getMessage()), + ['exception' => $e] + ); + + $activated = false; + } + + if (!$activated) { + $has_errors = true; + $output->writeln( + '' . sprintf(__('Plugin "%s" activation failed.'), $plugin_key) . '', + OutputInterface::VERBOSITY_QUIET + ); + $this->outputSessionBufferedMessages([WARNING, ERROR]); + continue; + } + + $output->writeln('' . sprintf(__('Plugin "%1$s" has been updated and reactivated.'), $plugin_key) . '', ); + } + Plugin::forcePluginsExecution(false); + } + + return $has_errors ? self::FAILURE : self::SUCCESS; + } +} diff --git a/src/Glpi/Console/Plugin/InstallCommand.php b/src/Glpi/Console/Plugin/InstallCommand.php index 5814ff5f929..7f8d5a68241 100644 --- a/src/Glpi/Console/Plugin/InstallCommand.php +++ b/src/Glpi/Console/Plugin/InstallCommand.php @@ -35,19 +35,11 @@ namespace Glpi\Console\Plugin; -use Auth; use Plugin; -use Session; -use Symfony\Component\Console\Exception\InvalidArgumentException; -use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -use User; -use function Safe\ob_end_clean; -use function Safe\ob_start; use function Safe\opendir; class InstallCommand extends AbstractPluginCommand @@ -94,28 +86,15 @@ protected function configure() ); } - protected function interact(InputInterface $input, OutputInterface $output) - { - - parent::interact($input, $output); - - if (null === $input->getOption('username')) { - $question_helper = new QuestionHelper(); - $value = $question_helper->ask( - $input, - $output, - new Question('User to use:') - ); - $input->setOption('username', $value); - } - } - protected function execute(InputInterface $input, OutputInterface $output) { $this->normalizeInput($input); - $this->loadUserSession($input->getOption('username')); + $username = $input->getOption('username'); + if ($username !== null) { + $this->loadUserSession($username); + } $directories = $input->getArgument('directory'); $force = $input->getOption('force'); @@ -230,39 +209,6 @@ protected function getDirectoryChoiceChoices() return $choices; } - /** - * Load user in session. - * - * @param string $username - * @return void - * - * @throws InvalidArgumentException - */ - private function loadUserSession($username) - { - - $user = new User(); - if ($user->getFromDBbyName($username)) { - // Store computed output parameters - $lang = $_SESSION['glpilanguage']; - $session_use_mode = $_SESSION['glpi_use_mode']; - - $auth = new Auth(); - $auth->auth_succeded = true; - $auth->user = $user; - Session::init($auth); - - // Force usage of computed output parameters - $_SESSION['glpilanguage'] = $lang; - $_SESSION['glpi_use_mode'] = $session_use_mode; - Session::loadLanguage(); - } else { - throw new InvalidArgumentException( - __('User name defined by --username option is invalid.') - ); - } - } - /** * Check if plugin is already installed. * @@ -342,26 +288,6 @@ private function canRunInstallMethod($directory, $allow_reinstall) return false; } - // Check prerequisites - ob_start(); - $requirements_met = $plugin->checkVersions($directory); - $check_function = 'plugin_' . $directory . '_check_prerequisites'; - if ($requirements_met && function_exists($check_function)) { - $requirements_met = $check_function(); - } - $ob_contents = ob_get_contents(); - ob_end_clean(); - if (!$requirements_met) { - $this->output->writeln( - [ - '' . sprintf(__('Plugin "%s" requirements not met.'), $directory) . '', - '' . $ob_contents . '', - ], - OutputInterface::VERBOSITY_QUIET - ); - return false; - } - return true; } diff --git a/src/Plugin.php b/src/Plugin.php index 925ba06e90d..197fdb47bb1 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -181,6 +181,11 @@ class Plugin extends CommonDBTM */ private static $loaded_plugins = []; + /** + * Indicates whether the plugins execution is forced. + */ + private static bool $force_plugins_execution = false; + /** * Store additional infos for each plugins * @@ -738,6 +743,9 @@ protected function getFilesystemPluginKeys(): array $this->filesystem_plugin_keys[] = $plugin_directory->getFilename(); } + + $this->filesystem_plugin_keys = array_unique($this->filesystem_plugin_keys); + asort($this->filesystem_plugin_keys); } return $this->filesystem_plugin_keys; @@ -1154,6 +1162,38 @@ public function install($ID, array $params = []) self::load($this->fields['directory'], true); // Load plugin hooks + $msg = ''; + + ob_start(); + $can_install = $this->checkVersions($this->fields['directory']); + if (!$can_install) { + $msg .= ' ' . htmlescape(ob_get_contents()) . ""; + } + ob_end_clean(); + + $check_function = 'plugin_' . $this->fields['directory'] . '_check_prerequisites'; + if (function_exists($check_function)) { + ob_start(); + $requirements_met = $check_function(); + $msg = ''; + if (!$requirements_met) { + $can_install = false; + $msg .= ' ' . htmlescape(ob_get_contents()) . ''; + } + ob_end_clean(); + } + + if (!$can_install) { + $this->unload($this->fields['directory']); + + Session::addMessageAfterRedirect( + htmlescape(sprintf(__('Plugin %1$s prerequisites are not matching, it cannot be installed.'), $this->fields['name'])) . ' ' . $msg, + true, + ERROR + ); + return; + } + $install_function = 'plugin_' . $this->fields['directory'] . '_install'; if (function_exists($install_function)) { $DB->disableTableCaching(); //prevents issues on table/fieldExists upgrading from old versions @@ -3280,6 +3320,10 @@ public function isPluginsExecutionSuspended(): bool { global $CFG_GLPI; + if (self::$force_plugins_execution) { + return false; + } + return in_array( $CFG_GLPI['plugins_execution_mode'] ?? null, [ @@ -3288,4 +3332,12 @@ public function isPluginsExecutionSuspended(): bool ] ); } + + /** + * (un)force the plugins execution for the current PHP process. + */ + public static function forcePluginsExecution(bool $force): void + { + self::$force_plugins_execution = $force; + } }