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;
+ }
}