first commit

This commit is contained in:
2025-08-07 13:15:31 +01:00
commit d903893b4c
21854 changed files with 4461308 additions and 0 deletions

View File

@ -0,0 +1,202 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DB;
use Glpi\Console\Command\GlpiCommandInterface;
use Glpi\System\RequirementsManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
abstract class AbstractCommand extends Command implements GlpiCommandInterface {
/**
* @var DB
*/
protected $db;
/**
* @var InputInterface
*/
protected $input;
/**
* @var OutputInterface
*/
protected $output;
/**
* Flag to indicate if command requires a BD connection.
*
* @var boolean
*/
protected $requires_db = true;
protected function initialize(InputInterface $input, OutputInterface $output) {
$this->input = $input;
$this->output = $output;
$this->initDbConnection();
}
/**
* Check database connection.
*
* @throws RuntimeException
*
* @return void
*/
protected function initDbConnection() {
global $DB;
if ($this->requires_db && (!($DB instanceof DB) || !$DB->connected)) {
throw new RuntimeException(__('Unable to connect to database.'));
}
$this->db = $DB;
}
/**
* Correctly write output messages when a progress bar is displayed.
*
* @param string|array $messages
* @param ProgressBar $progress_bar
* @param integer $verbosity
*
* @return void
*/
protected function writelnOutputWithProgressBar($messages,
ProgressBar $progress_bar,
$verbosity = OutputInterface::VERBOSITY_NORMAL) {
if ($verbosity > $this->output->getVerbosity()) {
return; // Do nothing if message will not be output due to its too high verbosity
}
$progress_bar->clear();
$this->output->writeln(
$messages,
$verbosity
);
$progress_bar->display();
}
/**
* Output session buffered messages.
*
* @param array $levels_to_output
*
* @return void
*/
protected function outputSessionBufferedMessages($levels_to_output = [INFO, WARNING, ERROR]) {
if (empty($_SESSION['MESSAGE_AFTER_REDIRECT'])) {
return;
}
$msg_levels = [
INFO => [
'tag' => 'info',
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
],
WARNING => [
'tag' => 'comment',
'verbosity' => OutputInterface::VERBOSITY_NORMAL,
],
ERROR => [
'tag' => 'error',
'verbosity' => OutputInterface::VERBOSITY_QUIET,
],
];
foreach ($msg_levels as $key => $options) {
if (!in_array($key, $levels_to_output)) {
continue;
}
if (!array_key_exists($key, $_SESSION['MESSAGE_AFTER_REDIRECT'])) {
continue;
}
foreach ($_SESSION['MESSAGE_AFTER_REDIRECT'][$key] as $message) {
$message = strip_tags(preg_replace('/<br\s*\/?>/', ' ', $message)); // Output raw text
$this->output->writeln(
"<{$options['tag']}>{$message}</{$options['tag']}>",
$options['verbosity']
);
}
}
}
/**
* Output a warning in an optionnal requirement is missing.
*
* @return void
*/
protected function outputWarningOnMissingOptionnalRequirements() {
if ($this->output->isQuiet()) {
return;
}
$db = property_exists($this, 'db') ? $this->db : null;
$requirements_manager = new RequirementsManager();
$core_requirements = $requirements_manager->getCoreRequirementList(
$db instanceof \DBmysql && $db->connected ? $db : null
);
if ($core_requirements->hasMissingOptionalRequirements()) {
$message = __('Some optional system requirements are missing.')
. ' '
. __('Run "php bin/console glpi:system:check_requirements" for more details.');
$this->output->writeln(
'<comment>' . $message . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
}
}
public function mustCheckMandatoryRequirements(): bool {
return true;
}
}

View File

@ -0,0 +1,476 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Config;
use DB;
use GLPI;
use Glpi\Application\ErrorHandler;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Glpi\Console\Command\GlpiCommandInterface;
use Glpi\System\RequirementsManager;
use Plugin;
use Session;
use Toolbox;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
class Application extends BaseApplication {
/**
* Error code returned when system requirements are missing.
*
* @var integer
*/
const ERROR_MISSING_REQUIREMENTS = 128; // start application codes at 128 be sure to be different from commands codes
/**
* Pointer to $CFG_GLPI.
* @var array
*/
private $config;
/**
* @var ErrorHandler
*/
private $error_handler;
/**
* @var DB
*/
private $db;
/**
* @var OutputInterface
*/
private $output;
public function __construct() {
parent::__construct('GLPI CLI', GLPI_VERSION);
$this->initApplication();
$this->initDb();
$this->initSession();
$this->initCache();
$this->initConfig();
$this->computeAndLoadOutputLang();
// Load core commands only to check if called command prevent or not usage of plugins
// Plugin commands will be loaded later
$loader = new CommandLoader(false);
$this->setCommandLoader($loader);
$use_plugins = $this->usePlugins();
if ($use_plugins) {
$this->loadActivePlugins();
$loader->registerPluginsCommands();
}
}
protected function getDefaultInputDefinition() {
$definition = new InputDefinition(
[
new InputArgument(
'command',
InputArgument::REQUIRED,
__('The command to execute')
),
new InputOption(
'--help',
'-h',
InputOption::VALUE_NONE,
__('Display this help message')
),
new InputOption(
'--quiet',
'-q',
InputOption::VALUE_NONE,
__('Do not output any message')
),
new InputOption(
'--verbose',
'-v|vv|vvv',
InputOption::VALUE_NONE,
__('Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug')
),
new InputOption(
'--version',
'-V',
InputOption::VALUE_NONE,
__('Display this application version')
),
new InputOption(
'--ansi',
null,
InputOption::VALUE_NONE,
__('Force ANSI output')
),
new InputOption(
'--no-ansi',
null,
InputOption::VALUE_NONE,
__('Disable ANSI output')
),
new InputOption(
'--no-interaction',
'-n',
InputOption::VALUE_NONE,
__('Do not ask any interactive question')
),
new InputOption(
'--config-dir',
null,
InputOption::VALUE_OPTIONAL,
__('Configuration directory to use')
),
new InputOption(
'--no-plugins',
null,
InputOption::VALUE_NONE,
__('Disable GLPI plugins (unless commands forces plugins loading)')
),
new InputOption(
'--lang',
null,
InputOption::VALUE_OPTIONAL,
__('Output language (default value is existing GLPI "language" configuration or "en_GB")')
)
]
);
return $definition;
}
protected function configureIO(InputInterface $input, OutputInterface $output) {
global $CFG_GLPI;
$this->output = $output;
$this->error_handler->setOutputHandler($output);
parent::configureIO($input, $output);
// Trigger error on invalid lang. This is not done before as error handler would not be set.
$lang = $input->getParameterOption('--lang', null, true);
if (null !== $lang && !array_key_exists($lang, $CFG_GLPI['languages'])) {
throw new RuntimeException(
sprintf(__('Invalid "--lang" option value "%s".'), $lang)
);
}
if ($output->getVerbosity() === OutputInterface::VERBOSITY_DEBUG) {
Toolbox::setDebugMode(Session::DEBUG_MODE, 0, 0, 1);
}
}
/**
* Returns output handler.
*
* @return OutputInterface
*/
public function getOutput() {
return $this->output;
}
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) {
$begin_time = microtime(true);
if ($command instanceof GlpiCommandInterface && $command->mustCheckMandatoryRequirements()
&& !$this->checkCoreMandatoryRequirements()) {
return self::ERROR_MISSING_REQUIREMENTS;
}
$result = parent::doRunCommand($command, $input, $output);
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
$output->writeln(
sprintf(
__('Time elapsed: %s.'),
Helper::formatTime(microtime(true) - $begin_time)
)
);
$output->writeln(
sprintf(
__('Memory usage: %s.'),
Helper::formatMemory(memory_get_peak_usage(true))
)
);
}
return $result;
}
/**
* Initalize GLPI.
*
* @global array $CFG_GLPI
* @global GLPI $GLPI
*
* @return void
*/
private function initApplication() {
// Disable debug at bootstrap (will be re-enabled later if requested by verbosity level).
global $CFG_GLPI;
$CFG_GLPI = array_merge(
$CFG_GLPI,
[
'debug_sql' => 0,
'debug_vars' => 0,
]
);
global $GLPI;
$GLPI = new GLPI();
$GLPI->initLogger();
$this->error_handler = $GLPI->initErrorHandler();
Config::detectRootDoc();
}
/**
* Initialize database connection.
*
* @global DB $DB
*
* @return void
*
* @throws RuntimeException
*/
private function initDb() {
if (!class_exists('DB', false) || !class_exists('mysqli', false)) {
return;
}
global $DB;
$DB = new DB();
$this->db = $DB;
if (!$this->db->connected) {
return;
}
ob_start();
$checkdb = Config::displayCheckDbEngine();
$message = ob_get_clean();
if ($checkdb > 0) {
throw new RuntimeException($message);
}
}
/**
* Initialize GLPI session.
* This is mandatory to init cache and load languages.
*
* @TODO Do not use session for console.
*
* @return void
*/
private function initSession() {
if (!is_writable(GLPI_SESSION_DIR)) {
throw new RuntimeException(
sprintf(__('Cannot write in "%s" directory.'), GLPI_SESSION_DIR)
);
}
Session::setPath();
Session::start();
// Default value for use mode
$_SESSION['glpi_use_mode'] = Session::NORMAL_MODE;
}
/**
* Initialize GLPI cache.
*
* @global Laminas\Cache\Storage\StorageInterface $GLPI_CACHE
*
* @return void
*/
private function initCache() {
global $GLPI_CACHE;
$GLPI_CACHE = Config::getCache('cache_db');
}
/**
* Initialize GLPI configuration.
*
* @global array $CFG_GLPI
*
* @return void
*/
private function initConfig() {
global $CFG_GLPI;
$this->config = &$CFG_GLPI;
if (!($this->db instanceof DB) || !$this->db->connected) {
return;
}
Config::loadLegacyConfiguration(false);
}
/**
* Compute and load output language.
*
* @return void
*
* @throws RuntimeException
*/
private function computeAndLoadOutputLang() {
// 1. Check in command line arguments
$input = new ArgvInput();
$lang = $input->getParameterOption('--lang', null, true);
if (null !== $lang && !$this->isLanguageValid($lang)) {
// Unset requested lang if invalid
$lang = null;
}
// 2. Check in GLPI configuration
if (null === $lang && array_key_exists('language', $this->config)
&& $this->isLanguageValid($this->config['language'])) {
$lang = $this->config['language'];
}
// 3. Use default value
if (null === $lang) {
$lang = 'en_GB';
}
$_SESSION['glpilanguage'] = $lang;
Session::loadLanguage('', $this->usePlugins());
}
/**
* Check if a language is valid.
*
* @param string $language
*
* @return boolean
*/
private function isLanguageValid($language) {
return is_array($this->config)
&& array_key_exists('languages', $this->config)
&& array_key_exists($language, $this->config['languages']);
}
/**
* Load active plugins.
*
* @return void
*/
private function loadActivePlugins() {
if (!($this->db instanceof DB) || !$this->db->connected) {
return;
}
$plugin = new Plugin();
$plugin->init(true);
}
/**
* Whether or not plugins have to be used.
*
* @return boolean
*/
private function usePlugins() {
$input = new ArgvInput();
try {
$command = $this->find($this->getCommandName($input));
if ($command instanceof ForceNoPluginsOptionCommandInterface) {
return !$command->getNoPluginsOptionValue();
}
} catch (CommandNotFoundException $e) {
// Command will not be found at this point if it is a plugin command
$command = null; // Say hello to CS checker
}
return !$input->hasParameterOption('--no-plugins', true);
}
/**
* Check if core mandatory requirements are OK.
*
* @return boolean true if requirements are OK, false otherwise
*/
private function checkCoreMandatoryRequirements(): bool {
$db = property_exists($this, 'db') ? $this->db : null;
$requirements_manager = new RequirementsManager();
$core_requirements = $requirements_manager->getCoreRequirementList(
$db instanceof \DBmysql && $db->connected ? $db : null
);
if ($core_requirements->hasMissingMandatoryRequirements()) {
$message = __('Some mandatory system requirements are missing.')
. ' '
. __('Run "php bin/console glpi:system:check_requirements" for more details.');
$this->output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
return true;
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Command;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
interface ForceNoPluginsOptionCommandInterface {
/**
* Defines whether or not command prevents plugins to be loaded.
*
* @return boolean
*/
public function getNoPluginsOptionValue();
}

View File

@ -0,0 +1,47 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Command;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
interface GlpiCommandInterface {
/**
* Defines whether or mandatory requirements must be checked before running command.
*
* @return boolean
*/
public function mustCheckMandatoryRequirements(): bool;
}

View File

@ -0,0 +1,309 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DirectoryIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use SplFileInfo;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Core and plugins command loader.
*
* @since 9.4.0
*/
class CommandLoader implements CommandLoaderInterface {
/**
* Root directory path to search on.
* @var string
*/
private $rootdir;
/**
* Found commands.
*
* @var Command[]
*/
private $commands = [];
/**
* @param string $rootdir Root directory path of application.
*/
public function __construct($include_plugins = true, $rootdir = GLPI_ROOT) {
$this->rootdir = $rootdir;
$this->findCoreCommands();
$this->findToolsCommands();
if ($include_plugins) {
$this->findPluginCommands();
}
}
public function get($name) {
if (!array_key_exists($name, $this->commands)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->commands[$name];
}
public function has($name) {
return array_key_exists($name, $this->commands);
}
public function getNames() {
return array_keys($this->commands);
}
/**
* Register plugin commands in command list.
*
* @return void
*/
public function registerPluginsCommands() {
$this->findPluginCommands();
}
/**
* Find all core commands.
*
* return void
*/
private function findCoreCommands() {
$basedir = $this->rootdir . DIRECTORY_SEPARATOR . 'inc';
$core_files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($basedir),
RecursiveIteratorIterator::SELF_FIRST
);
/** @var SplFileInfo $file */
foreach ($core_files as $file) {
if (!$file->isReadable() || !$file->isFile()) {
continue;
}
$class = $this->getCommandClassnameFromFile(
$file,
$basedir,
['', NS_GLPI]
);
if (null === $class) {
continue;
}
$this->registerCommand(new $class());
}
}
/**
* Find all plugins (active or not) commands.
*
* @return void
*/
private function findPluginCommands() {
$basedir = $this->rootdir . DIRECTORY_SEPARATOR . 'plugins';
$plugins_directories = new DirectoryIterator($basedir);
/** @var SplFileInfo $plugin_directory */
foreach ($plugins_directories as $plugin_directory) {
if (in_array($plugin_directory->getFilename(), ['.', '..'])) {
continue;
}
$plugin_basedir = $plugin_directory->getPathname() . DIRECTORY_SEPARATOR . 'inc';
if (!is_readable($plugin_basedir) || !is_dir($plugin_basedir)) {
continue;
}
$plugin_files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($plugin_basedir),
RecursiveIteratorIterator::SELF_FIRST
);
/** @var SplFileInfo $file */
foreach ($plugin_files as $file) {
if (!$file->isReadable() || !$file->isFile()) {
continue;
}
$class = $this->getCommandClassnameFromFile(
$file,
$plugin_basedir,
[
NS_PLUG . ucfirst($plugin_directory->getFilename()) . '\\',
'Plugin' . ucfirst($plugin_directory->getFilename()),
]
);
if (null === $class) {
continue;
}
$this->registerCommand(new $class());
}
}
}
/**
* Find all "tools" commands.
*
* return void
*/
private function findToolsCommands() {
$basedir = $this->rootdir . DIRECTORY_SEPARATOR . 'tools';
if (!is_dir($basedir)) {
return;
}
$tools_files = new DirectoryIterator($basedir);
/** @var SplFileInfo $file */
foreach ($tools_files as $file) {
if (!$file->isReadable() || !$file->isFile()) {
continue;
}
$class = $this->getCommandClassnameFromFile(
$file,
$basedir
);
if (null === $class) {
continue;
}
$this->registerCommand(new $class());
}
}
/**
* Register a command on self.
*
* @param Command $command
*
* @return void
*/
private function registerCommand(Command $command) {
$this->commands[$command->getName()] = $command;
$aliases = $command->getAliases();
foreach ($aliases as $alias) {
$this->commands[$alias] = $command;
}
}
/**
* Return classname of command contained in file, if file contains one.
*
* @param SplFileInfo $file File to inspect
* @param string $basedir Directory containing classes (eg GLPI_ROOT . '/inc')
* @param string $prefixes Possible prefixes to add to classname (eg 'PluginExample', 'GlpiPlugin\Example')
*
* @return null|string
*/
private function getCommandClassnameFromFile(SplFileInfo $file, $basedir, array $prefixes = []) {
// Check if file is readable and contained classname finishes by "command"
if (!$file->isReadable() || !$file->isFile()
|| !preg_match('/^(.*)command\.class\.php$/', $file->getFilename())) {
return null;
}
// Extract expected classname from filename.
// Classname will be lowercased, but it is ok for PHP.
$classname = str_replace(
['.class.php', DIRECTORY_SEPARATOR],
['', '\\'],
$this->getRelativePath($basedir, $file->getPathname())
);
if (empty($prefixes)) {
$prefixes = [''];
}
foreach ($prefixes as $prefix) {
$classname_to_check = $prefix . $classname;
include_once($file->getPathname()); // Required as ReflectionClass will not use autoload
if (!class_exists($classname_to_check, false)) {
// Try with other prefixes.
// Needed as a file located in root source dir of Glpi can be either namespaced either not.
continue;
}
$reflectionClass = new ReflectionClass($classname_to_check);
if ($reflectionClass->isInstantiable() && $reflectionClass->isSubclassOf(Command::class)) {
return $classname_to_check;
}
}
return null;
}
/**
* Returns path relative to basedir.
*
* @param string $basedir
* @param string $filepath
* @return string
*/
private function getRelativePath($basedir, $filepath) {
// Strip (multiple) ending directory separator to normalize input
while (strrpos($basedir, DIRECTORY_SEPARATOR) == strlen($basedir) - 1) {
$basedir = substr($basedir, 0, -1);
}
// Assume that filepath is prefixed by basedir
// Cannot use realpath to normalize path as it will not work when using a virtual fs (unit tests)
return str_replace($basedir . DIRECTORY_SEPARATOR, '', $filepath);
}
}

View File

@ -0,0 +1,348 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Config;
use DBConnection;
use Glpi\Console\AbstractCommand;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
abstract class AbstractConfigureCommand extends AbstractCommand implements ForceNoPluginsOptionCommandInterface {
/**
* Error code returned if DB configuration is aborted by user.
*
* @var integer
*/
const ABORTED_BY_USER = -1;
/**
* Error code returned if DB configuration succeed.
*
* @var integer
*/
const SUCCESS = 0;
/**
* Error code returned if DB connection initialization fails.
*
* @var integer
*/
const ERROR_DB_CONNECTION_FAILED = 1;
/**
* Error code returned if DB engine is unsupported.
*
* @var integer
*/
const ERROR_DB_ENGINE_UNSUPPORTED = 2;
/**
* Error code returned when trying to configure and having a DB config already set.
*
* @var integer
*/
const ERROR_DB_CONFIG_ALREADY_SET = 3;
/**
* Error code returned when failing to save database configuration file.
*
* @var integer
*/
const ERROR_DB_CONFIG_FILE_NOT_SAVED = 4;
protected function configure() {
parent::configure();
$this->setName('glpi:database:install');
$this->setAliases(['db:install']);
$this->setDescription('Install database schema');
$this->addOption(
'db-host',
'H',
InputOption::VALUE_OPTIONAL,
__('Database host'),
'localhost'
);
$this->addOption(
'db-name',
'd',
InputOption::VALUE_REQUIRED,
__('Database name')
);
$this->addOption(
'db-password',
'p',
InputOption::VALUE_OPTIONAL,
__('Database password (will be prompted for value if option passed without value)'),
'' // Empty string by default (enable detection of null if passed without value)
);
$this->addOption(
'db-port',
'P',
InputOption::VALUE_OPTIONAL,
__('Database port')
);
$this->addOption(
'db-user',
'u',
InputOption::VALUE_REQUIRED,
__('Database user')
);
$this->addOption(
'reconfigure',
'r',
InputOption::VALUE_NONE,
__('Reconfigure database, override configuration file if it already exists')
);
}
protected function interact(InputInterface $input, OutputInterface $output) {
$questions = [
'db-name' => new Question(__('Database name:'), ''), // Required
'db-user' => new Question(__('Database user:'), ''), // Required
'db-password' => new Question(__('Database password:'), ''), // Prompt if null (passed without value)
];
$questions['db-password']->setHidden(true); // Make password input hidden
foreach ($questions as $name => $question) {
if (null === $input->getOption($name)) {
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$value = $question_helper->ask($input, $output, $question);
$input->setOption($name, $value);
}
}
}
protected function initDbConnection() {
return; // Prevent DB connection
}
/**
* Save database configuration file.
*
* @param InputInterface $input
* @param OutputInterface $output
* @throws InvalidArgumentException
* @return string
*/
protected function configureDatabase(InputInterface $input, OutputInterface $output) {
$db_pass = $input->getOption('db-password');
$db_host = $input->getOption('db-host');
$db_name = $input->getOption('db-name');
$db_port = $input->getOption('db-port');
$db_user = $input->getOption('db-user');
$db_hostport = $db_host . (!empty($db_port) ? ':' . $db_port : '');
$reconfigure = $input->getOption('reconfigure');
if (file_exists(GLPI_CONFIG_DIR . '/config_db.php') && !$reconfigure) {
// Prevent overriding of existing DB
$output->writeln(
'<error>' . __('Database configuration already exists. Use --reconfigure option to override existing configuration.') . '</error>'
);
return self::ERROR_DB_CONFIG_ALREADY_SET;
}
$this->validateConfigInput($input);
$run = $this->askForDbConfigConfirmation(
$input,
$output,
$db_hostport,
$db_name,
$db_user
);
if (!$run) {
$output->writeln(
'<comment>' . __('Configuration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return self::ABORTED_BY_USER;
}
$mysqli = new \mysqli();
if (intval($db_port) > 0) {
// Network port
@$mysqli->connect($db_host, $db_user, $db_pass, null, $db_port);
} else {
// Unix Domain Socket
@$mysqli->connect($db_host, $db_user, $db_pass, null, 0, $db_port);
}
if (0 !== $mysqli->connect_errno) {
$message = sprintf(
__('Database connection failed with message "(%s) %s".'),
$mysqli->connect_errno,
$mysqli->connect_error
);
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_DB_CONNECTION_FAILED;
}
ob_start();
$db_version_data = $mysqli->query('SELECT version()')->fetch_array();
$checkdb = Config::displayCheckDbEngine(false, $db_version_data[0]);
$message = ob_get_clean();
if ($checkdb > 0) {
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_DB_ENGINE_UNSUPPORTED;
}
$db_name = $mysqli->real_escape_string($db_name);
$output->writeln(
'<comment>' . __('Saving configuration file...') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
if (!DBConnection::createMainConfig($db_hostport, $db_user, $db_pass, $db_name)) {
$message = sprintf(
__('Cannot write configuration file "%s".'),
GLPI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'config_db.php'
);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_DB_CONFIG_FILE_NOT_SAVED;
}
return self::SUCCESS;
}
public function getNoPluginsOptionValue() {
return true;
}
/**
* Check if DB is already configured.
*
* @return boolean
*/
protected function isDbAlreadyConfigured() {
return file_exists(GLPI_CONFIG_DIR . '/config_db.php');
}
/**
* Validate configuration variables from input.
*
* @param InputInterface $input
*
* @throws InvalidArgumentException
*/
protected function validateConfigInput(InputInterface $input) {
$db_name = $input->getOption('db-name');
$db_user = $input->getOption('db-user');
$db_pass = $input->getOption('db-password');
if (empty($db_name)) {
throw new InvalidArgumentException(
__('Database name defined by --db-name option cannot be empty.')
);
}
if (empty($db_user)) {
throw new InvalidArgumentException(
__('Database user defined by --db-user option cannot be empty.')
);
}
if (null === $db_pass) {
// Will be null if option used without value and without interaction
throw new InvalidArgumentException(
__('--db-password option value cannot be null.')
);
}
}
/**
* Ask user to confirm DB configuration.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return boolean
*/
protected function askForDbConfigConfirmation(
InputInterface $input,
OutputInterface $output,
$db_hostport,
$db_name,
$db_user) {
$informations = new Table($output);
$informations->addRow([__('Database host'), $db_hostport]);
$informations->addRow([__('Database name'), $db_name]);
$informations->addRow([__('Database user'), $db_user]);
$informations->render();
if ($input->getOption('no-interaction')) {
// Consider that config is validated if user require no interaction
return true;
}
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
return $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use SebastianBergmann\Diff\Differ;
class CheckCommand extends AbstractCommand {
/**
* Error code returned when failed to read empty SQL file.
*
* @var integer
*/
const ERROR_UNABLE_TO_READ_EMPTYSQL = 1;
protected function configure() {
parent::configure();
$this->setName('glpi:database:check');
$this->setAliases(['db:check']);
$this->setDescription(__('Check for schema differences between current database and installation file.'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$differ = new Differ();
if (false === ($empty_file = realpath(GLPI_ROOT . '/install/mysql/glpi-empty.sql'))
|| false === ($empty_sql = file_get_contents($empty_file))) {
$message = sprintf(__('Unable to read installation file "%s".'), $empty_file);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_UNABLE_TO_READ_EMPTYSQL;
}
$matches = [];
preg_match_all('/CREATE TABLE `(.+)`[^;]+/', $empty_sql, $matches);
$empty_tables_names = $matches[1];
$empty_tables_schemas = $matches[0];
foreach ($empty_tables_schemas as $index => $table_schema) {
$table_name = $empty_tables_names[$index];
$output->writeln(
sprintf(__('Processing table "%s"...'), $table_name),
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$base_table_struct = $this->db->getTableSchema($table_name, $table_schema);
$existing_table_struct = $this->db->getTableSchema($table_name);
if ($existing_table_struct['schema'] != $base_table_struct['schema']) {
$message = sprintf(__('Table schema differs for table "%s".'), $table_name);
$output->writeln(
'<info>' . $message . '</info>',
OutputInterface::VERBOSITY_QUIET
);
$output->write(
$differ->diff($base_table_struct['schema'], $existing_table_struct['schema'])
);
}
}
return 0; // Success
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigureCommand extends AbstractConfigureCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:database:configure');
$this->setAliases(['db:configure']);
$this->setDescription('Define database configuration');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$result = $this->configureDatabase($input, $output);
if (self::ABORTED_BY_USER === $result) {
return 0; // Considered as success
}
return $result;
}
}

View File

@ -0,0 +1,342 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DB;
use GLPIKey;
use Toolbox;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class InstallCommand extends AbstractConfigureCommand {
/**
* Error code returned when failing to create database.
*
* @var integer
*/
const ERROR_DB_CREATION_FAILED = 5;
/**
* Error code returned when trying to install and having a DB already containing glpi_* tables.
*
* @var integer
*/
const ERROR_DB_ALREADY_CONTAINS_TABLES = 6;
/**
* Error code returned when failing to create database schema.
*
* @var integer
*/
const ERROR_SCHEMA_CREATION_FAILED = 7;
/**
* Error code returned when failing to create encryption key file.
*
* @var integer
*/
const ERROR_CANNOT_CREATE_ENCRYPTION_KEY_FILE = 8;
protected function configure() {
parent::configure();
$this->setName('glpi:database:install');
$this->setAliases(['db:install']);
$this->setDescription('Install database schema');
$this->addOption(
'default-language',
'L',
InputOption::VALUE_OPTIONAL,
__('Default language of GLPI'),
'en_GB'
);
$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
__('Force execution of installation, overriding existing database')
);
}
protected function initialize(InputInterface $input, OutputInterface $output) {
parent::initialize($input, $output);
$this->outputWarningOnMissingOptionnalRequirements();
}
protected function interact(InputInterface $input, OutputInterface $output) {
if ($this->isDbAlreadyConfigured()
&& $this->isInputContainingConfigValues($input, $output)
&& !$input->getOption('reconfigure')) {
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$reconfigure = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(
__('Command input contains configuration options that may override existing configuration.')
. PHP_EOL
. __('Do you want to reconfigure database ?') . ' [Yes/no]',
true
)
);
$input->setOption('reconfigure', $reconfigure);
}
if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) {
parent::interact($input, $output);
}
}
protected function execute(InputInterface $input, OutputInterface $output) {
global $DB;
$default_language = $input->getOption('default-language');
$force = $input->getOption('force');
if ($this->isDbAlreadyConfigured()
&& $this->isInputContainingConfigValues($input, $output)
&& !$input->getOption('reconfigure')) {
// Prevent overriding of existing DB when input contains configuration values and
// --reconfigure option is not used.
$output->writeln(
'<error>' . __('Database configuration already exists. Use --reconfigure option to override existing configuration.') . '</error>'
);
return self::ERROR_DB_CONFIG_ALREADY_SET;
}
if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) {
$result = $this->configureDatabase($input, $output);
if (self::ABORTED_BY_USER === $result) {
return 0; // Considered as success
} else if (self::SUCCESS !== $result) {
return $result; // Fail with error code
}
$db_host = $input->getOption('db-host');
$db_port = $input->getOption('db-port');
$db_hostport = $db_host . (!empty($db_port) ? ':' . $db_port : '');
$db_name = $input->getOption('db-name');
$db_user = $input->getOption('db-user');
$db_pass = $input->getOption('db-password');
} else {
// Ask to confirm installation based on existing configuration.
// $DB->dbhost can be array when using round robin feature
$db_hostport = is_array($DB->dbhost) ? $DB->dbhost[0] : $DB->dbhost;
$hostport = explode(':', $db_hostport);
$db_host = $hostport[0];
if (count($hostport) < 2) {
// Host only case
$db_port = null;
} else {
// Host:port case or :Socket case
$db_port = $hostport[1];
}
$db_name = $DB->dbdefault;
$db_user = $DB->dbuser;
$db_pass = rawurldecode($DB->dbpassword); //rawurldecode as in DBmysql::connect()
$run = $this->askForDbConfigConfirmation(
$input,
$output,
$db_hostport,
$db_name,
$db_user
);
if (!$run) {
$output->writeln(
'<comment>' . __('Installation aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
// Create security key
$glpikey = new GLPIKey();
if (!$glpikey->keyExists() && !$glpikey->generate()) {
$message = __('Security key cannot be generated!');
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_CANNOT_CREATE_ENCRYPTION_KEY_FILE;
}
$mysqli = new \mysqli();
if (intval($db_port) > 0) {
// Network port
@$mysqli->connect($db_host, $db_user, $db_pass, null, $db_port);
} else {
// Unix Domain Socket
@$mysqli->connect($db_host, $db_user, $db_pass, null, 0, $db_port);
}
if (0 !== $mysqli->connect_errno) {
$message = sprintf(
__('Database connection failed with message "(%s) %s".'),
$mysqli->connect_errno,
$mysqli->connect_error
);
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_DB_CONNECTION_FAILED;
}
// Create database or select existing one
$output->writeln(
'<comment>' . __('Creating the database...') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
if (!$mysqli->query('CREATE DATABASE IF NOT EXISTS `' . $db_name .'`')
|| !$mysqli->select_db($db_name)) {
$message = sprintf(
__('Database creation failed with message "(%s) %s".'),
$mysqli->errno,
$mysqli->error
);
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_DB_CREATION_FAILED;
}
// Prevent overriding of existing DB
$tables_result = $mysqli->query(
"SELECT COUNT(table_name)
FROM information_schema.tables
WHERE table_schema = '{$db_name}'
AND table_type = 'BASE TABLE'
AND table_name LIKE 'glpi_%'"
);
if (!$tables_result) {
throw new RuntimeException('Unable to check GLPI tables existence.');
}
if ($tables_result->fetch_array()[0] > 0 && !$force) {
$output->writeln(
'<error>' . __('Database already contains "glpi_*" tables. Use --force option to override existing database.') . '</error>'
);
return self::ERROR_DB_ALREADY_CONTAINS_TABLES;
}
if ($DB instanceof DB) {
// If global $DB is set at this point, it means that configuration file has been loaded
// prior to reconfiguration.
// As configuration is part of a class, it cannot be reloaded and class properties
// have to be updated manually in order to make `Toolbox::createSchema()` work correctly.
$DB->dbhost = $db_hostport;
$DB->dbuser = $db_user;
$DB->dbpassword = rawurlencode($db_pass);
$DB->dbdefault = $db_name;
$DB->clearSchemaCache();
$DB->connect();
$db_instance = $DB;
} else {
include_once (GLPI_CONFIG_DIR . "/config_db.php");
$db_instance = new DB();
}
$output->writeln(
'<comment>' . __('Loading default schema...') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
// TODO Get rid of output buffering
ob_start();
Toolbox::createSchema($default_language, $db_instance);
$message = ob_get_clean();
if (!empty($message)) {
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
return self::ERROR_SCHEMA_CREATION_FAILED;
}
$output->writeln('<info>' . __('Installation done.') . '</info>');
return 0; // Success
}
/**
* Check if DB config should be set by current command run.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return boolean
*/
private function shouldSetDBConfig(InputInterface $input, OutputInterface $output) {
return $input->getOption('reconfigure') || !file_exists(GLPI_CONFIG_DIR . '/config_db.php');
}
/**
* Check if input contains DB config options.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return boolean
*/
private function isInputContainingConfigValues(InputInterface $input, OutputInterface $output) {
$config_options = [
'db-host',
'db-port',
'db-name',
'db-user',
'db-password',
];
foreach ($config_options as $option) {
$default_value = $this->getDefinition()->getOption($option)->getDefault();
$input_value = $input->getOption($option);
if ($default_value !== $input_value) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Migration;
use Session;
use Update;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class UpdateCommand extends AbstractCommand implements ForceNoPluginsOptionCommandInterface {
/**
* Error code returned when trying to update from an unstable version.
*
* @var integer
*/
const ERROR_NO_UNSTABLE_UPDATE = 1;
/**
* Error code returned when security key file is missing.
*
* @var integer
*/
const ERROR_MISSING_SECURITY_KEY_FILE = 2;
protected function configure() {
parent::configure();
$this->setName('glpi:database:update');
$this->setAliases(['db:update']);
$this->setDescription(__('Update database schema to new version'));
$this->addOption(
'allow-unstable',
'u',
InputOption::VALUE_NONE,
__('Allow update to an unstable version')
);
$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
__('Force execution of update from v-1 version of GLPI even if schema did not changed')
);
}
protected function initialize(InputInterface $input, OutputInterface $output) {
parent::initialize($input, $output);
$this->outputWarningOnMissingOptionnalRequirements();
$this->db->disableTableCaching();
}
protected function execute(InputInterface $input, OutputInterface $output) {
$allow_unstable = $input->getOption('allow-unstable');
$force = $input->getOption('force');
$no_interaction = $input->getOption('no-interaction'); // Base symfony/console option
$update = new Update($this->db);
// Initialize entities
$_SESSION['glpidefault_entity'] = 0;
Session::initEntityProfiles(2);
Session::changeProfile(4);
// Display current/future state informations
$currents = $update->getCurrents();
$current_version = $currents['version'];
$current_db_version = $currents['dbversion'];
global $migration; // Migration scripts are using global migrations
$migration = new Migration(GLPI_SCHEMA_VERSION);
$migration->setOutputHandler($output);
$update->setMigration($migration);
$informations = new Table($output);
$informations->setHeaders(['', __('Current'), _n('Target', 'Targets', 1)]);
$informations->addRow([__('Database host'), $this->db->dbhost, '']);
$informations->addRow([__('Database name'), $this->db->dbdefault, '']);
$informations->addRow([__('Database user'), $this->db->dbuser, '']);
$informations->addRow([__('GLPI version'), $current_version, GLPI_VERSION]);
$informations->addRow([__('GLPI database version'), $current_db_version, GLPI_SCHEMA_VERSION]);
$informations->render();
if (defined('GLPI_PREVER')) {
// Prevent unstable update unless explicitly asked
if (!$allow_unstable && version_compare($current_db_version, GLPI_SCHEMA_VERSION, 'ne')) {
$output->writeln(
sprintf(
'<error>' . __('%s is not a stable release. Please upgrade manually or add --allow-unstable option.') . '</error>',
GLPI_SCHEMA_VERSION
),
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_NO_UNSTABLE_UPDATE;
}
}
if (version_compare($current_db_version, GLPI_SCHEMA_VERSION, 'eq') && !$force) {
$output->writeln('<info>' . __('No migration needed.') . '</info>');
return 0;
}
if ($update->isExpectedSecurityKeyFileMissing()) {
$output->writeln(
sprintf(
'<error>' . __('The key file "%s" used to encrypt/decrypt sensitive data is missing. You should retrieve it from your previous installation or encrypted data will be unreadable.') . '</error>',
$update->getExpectedSecurityKeyFilePath()
),
OutputInterface::VERBOSITY_QUIET
);
if ($no_interaction) {
return self::ERROR_MISSING_SECURITY_KEY_FILE;
}
}
if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Update aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
$update->doUpdates($current_version);
if (version_compare($current_db_version, GLPI_SCHEMA_VERSION, 'ne')) {
// Migration is considered as done as Update class has the responsibility
// to run updates if schema has changed (even for "pre-versions".
$output->writeln('<info>' . __('Migration done.') . '</info>');
} else if ($force) {
// Replay last update script even if there is no schema change.
// It can be used in dev environment when update script has been updated/fixed.
include_once(GLPI_ROOT . '/install/update_951_952.php');
update951to952();
$output->writeln('<info>' . __('Last migration replayed.') . '</info>');
}
return 0; // Success
}
public function getNoPluginsOptionValue() {
return true;
}
}

View File

@ -0,0 +1,479 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Ldap;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use AuthLDAP;
use User;
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class SynchronizeUsersCommand extends AbstractCommand {
/**
* Error code returned if LDAP connection failed.
*
* @var integer
*/
const ERROR_LDAP_CONNECTION_FAILED = 1;
/**
* Error code returned if LDAP limit exceeded.
*
* @var integer
*/
const ERROR_LDAP_LIMIT_EXCEEDED = 2;
protected function configure() {
global $CFG_GLPI;
parent::configure();
$this->setName('glpi:ldap:synchronize_users');
$this->setAliases(['ldap:sync']);
$this->setDescription(__('Synchronize users against LDAP server informations'));
$this->addOption(
'only-create-new',
'c',
InputOption::VALUE_NONE,
__('Only create new users')
);
$this->addOption(
'only-update-existing',
'u',
InputOption::VALUE_NONE,
__('Only update existing users')
);
$this->addOption(
'ldap-server-id',
's',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
__('Synchronize only users attached to this LDAP server')
);
$this->addOption(
'ldap-filter',
'f',
InputOption::VALUE_OPTIONAL,
__('Filter to apply on LDAP search')
);
$this->addOption(
'begin-date',
null,
InputOption::VALUE_OPTIONAL,
sprintf(
__('Begin date to apply in "modifyTimestamp" filter (see %s for supported formats)'),
'http://php.net/manual/en/datetime.formats.php'
)
);
$this->addOption(
'end-date',
null,
InputOption::VALUE_OPTIONAL,
sprintf(
__('End date to apply in "modifyTimestamp" filter (see %s for supported formats)'),
'http://php.net/manual/en/datetime.formats.php'
)
);
$strategies = AuthLDAP::getLdapDeletedUserActionOptions();
$description = sprintf(
__('Force strategy used for deleted users (current configured action: "%s")'),
(isset($CFG_GLPI['user_deleted_ldap']) ? $CFG_GLPI['user_deleted_ldap'] : __('unknown'))
);
$description .= "\n" . __('Possible values are:') . "\n";
$description .= implode(
"\n",
array_map(
function ($key, $value) { return '- ' . sprintf(__('%1$s: %2$s'), $key, $value); },
array_keys($strategies),
$strategies
)
);
$this->addOption(
'deleted-user-strategy',
'd',
InputOption::VALUE_OPTIONAL,
$description
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
global $CFG_GLPI;
$this->validateInput($input);
$only_create = $input->getOption('only-create-new');
$only_update = $input->getOption('only-update-existing');
$actions = [];
if ($only_create) {
$actions = [
AuthLDAP::ACTION_IMPORT, // Import unexisting users
];
} else if ($only_update) {
$actions = [
AuthLDAP::ACTION_SYNCHRONIZE, // Update existing users but does not handle deleted ones
];
} else {
$actions = [
AuthLDAP::ACTION_IMPORT, // Import unexisting users
AuthLDAP::ACTION_ALL, // Update existing users and handle deleted ones
];
$deleted_user_strategy = $input->getOption('deleted-user-strategy');
if (null !== $deleted_user_strategy) {
$CFG_GLPI['user_deleted_ldap'] = $deleted_user_strategy;
}
}
$servers_id = $input->getOption('ldap-server-id');
if (empty($servers_id)) {
$servers_iterator = $this->db->request(
[
'SELECT' => 'id',
'FROM' => AuthLDAP::getTable(),
'WHERE' => [
'is_active' => 1,
],
]
);
if ($servers_iterator->count() === 0) {
$output->writeln('<info>' . __('No active LDAP server found.') . '</info>');
return 0;
}
foreach ($servers_iterator as $server) {
$servers_id[] = $server['id'];
}
}
$ldap_filter = $input->getOption('ldap-filter');
$begin_date = $input->getOption('begin-date');
$end_date = $input->getOption('end-date');
if (!$input->getOption('no-interaction')) {
// Ask for confirmation (unless --no-interaction)
$servers_iterator = $this->db->request(
[
'SELECT' => ['id', 'name'],
'FROM' => AuthLDAP::getTable(),
'WHERE' => [
'id' => $servers_id,
],
]
);
$servers_names = [];
foreach ($servers_iterator as $server) {
$servers_names[] = sprintf(__('%1$s (%2$s)'), $server['name'], $server['id']);
}
$informations = new Table($output);
$informations->addRow([__('LDAP servers'), implode(', ', $servers_names)]);
$informations->addRow([__('LDAP filter'), $ldap_filter]);
$informations->addRow([__('Begin date'), $begin_date]);
$informations->addRow([__('End date'), $end_date]);
$informations->render();
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Synchronization aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
foreach ($servers_id as $server_id) {
$server = new AuthLDAP();
if (!$server->getFromDB($server_id)) {
throw new RuntimeException(__('Unable to load LDAP server informations.'));
}
if (!$server->isActive()) {
// Can happen if id is specified in command call
$message = sprintf(
__('LDAP server "%s" is inactive, no synchronization will be done against it.'),
$server_id
);
$output->writeln('<info>' . $message . '</info>');
continue;
}
$output->writeln(
'<info>' . sprintf(__('Processing LDAP server "%s"...'), $server_id) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
foreach ($actions as $action) {
$results = [
AuthLDAP::USER_IMPORTED => 0,
AuthLDAP::USER_SYNCHRONIZED => 0,
AuthLDAP::USER_DELETED_LDAP => 0,
];
$limitexceeded = false;
$users = AuthLDAP::getAllUsers(
[
'authldaps_id' => $server_id,
'mode' => $action,
'ldap_filter' => null !== $ldap_filter ? $ldap_filter : '',
'script' => true,
'begin_date' => null !== $begin_date ? $begin_date : '',
'end_date' => null !== $end_date ? $end_date : '',
],
$results,
$limitexceeded
);
if (false === $users) {
if ($limitexceeded) {
$message = sprintf(
__('LDAP server "%s" size limit exceeded.'),
$server_id
);
$code = self::ERROR_LDAP_LIMIT_EXCEEDED;
} else {
$message = sprintf(
__('Error while contacting the LDAP server "%s".'),
$server_id
);
$code = self::ERROR_LDAP_CONNECTION_FAILED;
}
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return $code;
}
$action_message = '';
switch ($action) {
case AuthLDAP::ACTION_IMPORT;
$action_message = __('Import new users from server "%s"...');
break;
case AuthLDAP::ACTION_SYNCHRONIZE;
$action_message = __('Update existing users with server "%s"...');
break;
case AuthLDAP::ACTION_ALL;
$action_message = __('Synchronize users with server "%s"...');
break;
}
$output->writeln(
'<info>' . sprintf($action_message, $server_id) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
if (count($users) === 0) {
$output->writeln(
'<info>' . __('No users found.') . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
continue;
}
$users_progress_bar = new ProgressBar($output, count($users));
$users_progress_bar->start();
foreach ($users as $user) {
$users_progress_bar->advance(1);
$user_sync_field = null;
if ($server->isSyncFieldEnabled()) {
$sync_field = $server->fields['sync_field'];
if (isset($user[$sync_field])) {
$user_sync_field = $server->getFieldValue($user, $sync_field);
}
}
$existing_user = $server->getLdapExistingUser(
$user['user'],
$server_id,
$user_sync_field
);
if ($existing_user instanceof User && $action == AuthLDAP::ACTION_IMPORT) {
continue; // Do not update existing user if current action is only import
}
$user_field = 'name';
$id_field = $server->fields['login_field'];
$value = $user['user'];
if ($server->isSyncFieldEnabled()
&& (!($existing_user instanceof User)
|| !empty($existing_user->fields['sync_field']))) {
$value = $user_sync_field;
$user_field = 'sync_field';
$id_field = $server->fields['sync_field'];
}
$result = AuthLDAP::ldapImportUserByServerId(
[
'method' => AuthLDAP::IDENTIFIER_LOGIN,
'value' => $value,
'identifier_field' => $id_field,
'user_field' => $user_field
],
$action,
$server_id
);
if (false !== $result) {
$results[$result['action']] += 1;
} else {
$this->writelnOutputWithProgressBar(
sprintf(__('Unable to synchronize user "%s".'), $user['user']),
$users_progress_bar,
OutputInterface::VERBOSITY_VERBOSE
);
}
}
$users_progress_bar->finish();
$output->write(PHP_EOL);
}
if ($output->getVerbosity() > OutputInterface::VERBOSITY_QUIET) {
$result_output = new Table($output);
$result_output->setHeaders(
[
__('LDAP server'),
__('Imported'),
__('Synchronized'),
__('Deleted from LDAP'),
]
);
$result_output->addRow(
[
$server_id,
$results[AuthLDAP::USER_IMPORTED],
$results[AuthLDAP::USER_SYNCHRONIZED],
$results[AuthLDAP::USER_DELETED_LDAP],
]
);
$result_output->render();
}
}
return 0; // Success
}
/**
* Validate command input.
*
* @param InputInterface $input
*
* @return void
*
* @throws InvalidArgumentException
*/
private function validateInput(InputInterface $input) {
$only_create = $input->getOption('only-create-new');
$only_update = $input->getOption('only-update-existing');
if (false !== $only_create && false !== $only_update) {
throw new InvalidArgumentException(
__('Option --only-create-new is not compatible with option --only-update-existing.')
);
}
$servers_id = $input->getOption('ldap-server-id');
$server = new AuthLDAP();
foreach ($servers_id as $server_id) {
if (!$server->getFromDB($server_id)) {
throw new InvalidArgumentException(
sprintf(__('--ldap-server-id value "%s" is not a valid LDAP server id.'), $server_id)
);
}
}
foreach (['begin-date', 'end-date'] as $option_name) {
// Convert date to 'Y:m:d H:i:s' formatted string
$date = $input->getOption($option_name);
if (null !== $date) {
$parsed_date = strtotime($date);
if (false === $parsed_date) {
throw new InvalidArgumentException(
sprintf(__('Unable to parse --%1$s value "%2$s".'), $option_name, $date)
);
}
$input->setOption($option_name, date('Y:m:d H:i:s', $parsed_date));
}
}
$begin_date = $input->getOption('begin-date');
$end_date = $input->getOption('end-date');
if ($begin_date > $end_date) {
throw new InvalidArgumentException(
__('Option --begin-date value has to be lower than option --end-date value.')
);
}
$deleted_user_strategy = $input->getOption('deleted-user-strategy');
if (null !== $deleted_user_strategy) {
$strategies = AuthLDAP::getLdapDeletedUserActionOptions();
if (!in_array($deleted_user_strategy, array_keys($strategies))) {
throw new InvalidArgumentException(
sprintf(
__('--deleted-user-strategy value "%s" is not valid.'),
$deleted_user_strategy
)
);
}
}
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Maintenance;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Config;
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DisableMaintenanceModeCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:maintenance:disable');
$this->setAliases(
[
'maintenance:disable',
]
);
$this->setDescription(__('Disable maintenance mode'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$config = new Config();
$config->setConfigurationValues('core', ['maintenance_mode' => '0']);
$output->writeln('<info>' . __('Maintenance mode disabled.') . '</info>');
return 0; // Success
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Maintenance;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Config;
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class EnableMaintenanceModeCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:maintenance:enable');
$this->setAliases(
[
'maintenance:enable',
]
);
$this->setDescription(__('Enable maintenance mode'));
$this->addOption(
'text',
't',
InputOption::VALUE_OPTIONAL,
__('Text to display during maintenance')
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
global $CFG_GLPI;
$values = [
'maintenance_mode' => '1'
];
if ($input->hasOption('text')) {
$values['maintenance_text'] = $input->getOption('text');
}
$config = new Config();
$config->setConfigurationValues('core', $values);
$message = sprintf(
__('Maintenance mode activated. Backdoor using: %s'),
$CFG_GLPI['url_base'] . '/index.php?skipMaintenance=1'
);
$output->writeln('<info>' . $message . '</info>');
return 0; // Success
}
}

View File

@ -0,0 +1,746 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
* Yves TESNIERE
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Migration;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Appliance;
use ApplianceType;
use ApplianceEnvironment;
use Appliance_Item;
use Appliance_Item_Relation;
use Change_Item;
use Contract_Item;
use DB;
use Document_Item;
use Domain;
use Infocom;
use Location;
use Network;
use Toolbox;
use Glpi\Console\AbstractCommand;
use Item_Problem;
use Item_Project;
use Item_Ticket;
use KnowbaseItem_Item;
use Log;
use Profile;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Exception\RuntimeException;
class AppliancesPluginToCoreCommand extends AbstractCommand {
/**
* Error code returned if plugin version or plugin data is invalid.
*
* @var integer
*/
const ERROR_PLUGIN_VERSION_OR_DATA_INVALID = 1;
/**
* Error code returned if import failed.
*
* @var integer
*/
const ERROR_PLUGIN_IMPORT_FAILED = 2;
/**
* list of possible relations of the plugin indexed by their correspond integer in the plugin
*
* @var array
*/
const PLUGIN_RELATION_TYPES = [
1 => Location::class,
2 => Network::class,
3 => Domain::class,
];
/**
* list of usefull plugin tables and fields
*
* @var array
*/
const PLUGIN_APPLIANCE_TABLES = [
"glpi_plugin_appliances_appliances" => [
"id",
"entities_id",
"is_recursive",
"name",
"is_deleted",
"plugin_appliances_appliancetypes_id",
"comment",
"locations_id",
"plugin_appliances_environments_id",
"users_id",
"users_id_tech",
"groups_id",
"groups_id_tech",
"relationtype",
"date_mod",
"states_id",
"externalid",
"serial",
"otherserial"
],
"glpi_plugin_appliances_appliancetypes" => ["id","entities_id","is_recursive","name","comment"],
"glpi_plugin_appliances_appliances_items" => ["id", "plugin_appliances_appliances_id","items_id","itemtype"],
"glpi_plugin_appliances_environments" => ["id","name","comment" ],
"glpi_plugin_appliances_relations" => ["id","plugin_appliances_appliances_items_id","relations_id"]
];
/**
* itemtype corresponding to appliance in plugin
*
* @var string
*/
const PLUGIN_APPLIANCE_ITEMTYPE = "PluginAppliancesAppliance";
/**
* itemtype corresponding to appliance in core
*
* @var string
*/
const CORE_APPLIANCE_ITEMTYPE = "Appliance";
protected function configure() {
parent::configure();
$this->setName('glpi:migration:appliances_plugin_to_core');
$this->setDescription(__('Migrate Appliances plugin data into GLPI core tables'));
$this->addOption(
'skip-errors',
's',
InputOption::VALUE_NONE,
__('Do not exit on import errors')
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$no_interaction = $input->getOption('no-interaction');
if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
$output->writeln([
__('You are about to launch migration of Appliances plugin data into GLPI core tables.'),
__('Any previous appliance created in core will be lost.'),
__('It is better to make a backup of your existing data before continuing.')
]
);
/**
* @var QuestionHelper $question_helper
*/
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(
'<comment>' . __('Do you want to launch migration ?') . ' [yes/No]</comment>',
false
)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
if (!$this->checkPlugin()) {
return self::ERROR_PLUGIN_VERSION_OR_DATA_INVALID;
}
if (!$this->migratePlugin()) {
return self::ERROR_PLUGIN_IMPORT_FAILED;
}
$output->writeln('<info>' . __('Migration done.') . '</info>');
return 0; // Success
}
/**
* Check that required tables exists and fields are OK for migration.
*
* @return bool
*/
private function checkPlugin(): bool {
$missing_tables = false;
foreach (self::PLUGIN_APPLIANCE_TABLES as $table => $fields) {
if (!$this->db->tableExists($table)) {
$this->output->writeln(
'<error>' . sprintf(__('Appliances plugin table "%s" is missing.'), $table) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$missing_tables = true;
} else {
foreach ($fields as $field) {
if (!$this->db->fieldExists($table, $field)) {
$this->output->writeln(
'<error>' . sprintf(__('Appliances plugin field "%s" is missing.'), $table.'.'.$field) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$missing_tables = true;
}
}
}
}
if ($missing_tables) {
$this->output->writeln(
'<error>' . __('Migration cannot be done.') . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
return true;
}
/**
* Clean data from core tables.
*
* @throws RuntimeException
*/
private function cleanCoreTables() {
$core_tables = [
Appliance::getTable(),
ApplianceType::getTable(),
ApplianceEnvironment::getTable(),
Appliance_Item::getTable(),
Appliance_Item_Relation::getTable(),
];
foreach ($core_tables as $table) {
$result = $this->db->query('TRUNCATE ' . DB::quoteName($table));
if (!$result) {
throw new RuntimeException(
sprintf('Unable to truncate table "%s"', $table)
);
}
}
$table = Infocom::getTable();
$result = $this->db->delete($table, [
'itemtype' => self::CORE_APPLIANCE_ITEMTYPE
]);
if (!$result) {
throw new RuntimeException(
sprintf('Unable to clean table "%s"', $table)
);
}
}
/**
* Copy plugin tables to backup tables from plugin to core keeping same ID.
*
* @return bool
*/
private function migratePlugin(): bool {
$this->cleanCoreTables();
return $this->createApplianceTypes()
&& $this->createApplianceEnvironments()
&& $this->createApplianceRelations()
&& $this->createApplianceItems()
&& $this->createAppliances()
&& $this->updateItemtypes()
&& $this->updateProfilesApplianceRights();
}
/**
* Update profile rights (Associable items to a ticket).
*
* @return bool
*/
private function updateProfilesApplianceRights(): bool {
$this->output->writeln(
'<comment>' . __('Updating profiles...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$table = Profile::getTable();
$result = $this->db->query(
sprintf(
"UPDATE %s SET helpdesk_item_type = REPLACE(helpdesk_item_type, '%s', '%s')",
DB::quoteName($table),
self::PLUGIN_APPLIANCE_ITEMTYPE,
self::CORE_APPLIANCE_ITEMTYPE
)
);
if (false === $result) {
$this->outputImportError(
sprintf(__('Unable to update "%s" in profiles.'), __('Associable items to a ticket'))
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
return true;
}
/**
* Rename itemtype in core tables.
*
* @return bool
*/
private function updateItemtypes(): bool {
$this->output->writeln(
'<comment>' . __('Updating GLPI itemtypes...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$itemtypes_tables = [
Item_Ticket::getTable(),
Item_Problem::getTable(),
Change_Item::getTable(),
Item_Project::getTable(),
Log::getTable(),
Infocom::getTable(),
Document_Item::getTable(),
Contract_Item::getTable(),
KnowbaseItem_Item::getTable(),
];
foreach ($itemtypes_tables as $itemtype_table) {
$result = $this->db->update($itemtype_table, [
'itemtype' => self::CORE_APPLIANCE_ITEMTYPE,
], [
'itemtype' => self::PLUGIN_APPLIANCE_ITEMTYPE,
]);
if (false === $result) {
$this->outputImportError(
sprintf(
__('Migration of table "%s" failed with message "(%s) %s".'),
$itemtype_table,
$this->db->errno(),
$this->db->error()
)
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
return true;
}
/**
* Create appliance items.
*
* @return bool
*/
private function createApplianceItems(): bool {
$this->output->writeln(
'<comment>' . __('Creating Appliance Items...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$iterator = $this->db->request([
'FROM' => 'glpi_plugin_appliances_appliances_items'
]);
if (!count($iterator)) {
return true;
}
$progress_bar = new ProgressBar($this->output);
foreach ($progress_bar->iterate($iterator) as $item) {
$this->writelnOutputWithProgressBar(
sprintf(
__('Importing Appliance item "%d"...'),
(int) $item['id']
),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$app_fields = Toolbox::sanitize([
'id' => $item['id'],
'appliances_id' => $item['plugin_appliances_appliances_id'],
'items_id' => $item['items_id'],
'itemtype' => $item['itemtype']
]);
$appi = new Appliance_Item();
if (!($appi_id = $appi->getFromDBByCrit($app_fields))) {
$appi_id = $appi->add($app_fields);
}
if (false === $appi_id) {
$this->outputImportError(
sprintf(__('Unable to create Appliance item %d.'), (int) $item['id']),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
$this->output->write(PHP_EOL);
return true;
}
/**
* Create appliance environments.
*
* @return bool
*/
private function createApplianceEnvironments(): bool {
$this->output->writeln(
'<comment>' . __('Creating Appliance Environment...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$iterator = $this->db->request([
'FROM' => 'glpi_plugin_appliances_environments'
]);
if (!count($iterator)) {
return true;
}
$progress_bar = new ProgressBar($this->output);
foreach ($progress_bar->iterate($iterator) as $env) {
$this->writelnOutputWithProgressBar(
sprintf(
__('Importing environment "%s"...'),
$env['name']
),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$app_fields = Toolbox::sanitize([
'id' => $env['id'],
'name' => $env['name'],
'comment' => $env['comment']
]);
$appe = new ApplianceEnvironment();
if (!($appe_id = $appe->getFromDBByCrit($app_fields))) {
$appe_id = $appe->add($app_fields);
}
if (false === $appe_id) {
$this->outputImportError(
sprintf(__('Unable to create Appliance environment %s (%d).'), $env['name'], (int) $env['id']),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
$this->output->write(PHP_EOL);
return true;
}
/**
* Create appliances.
*
* @return bool
*/
private function createAppliances(): bool {
$this->output->writeln(
'<comment>'. __('Creating Appliances...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$iterator = $this->db->request([
'FROM' => 'glpi_plugin_appliances_appliances'
]);
if (!count($iterator)) {
return true;
}
$progress_bar = new ProgressBar($this->output);
foreach ($progress_bar->iterate($iterator) as $appliance) {
$this->writelnOutputWithProgressBar(
sprintf(
__('Importing appliance "%s"...'),
$appliance['name']
),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$app_fields = Toolbox::sanitize([
'id' => $appliance['id'],
'entities_id' => $appliance['entities_id'],
'is_recursive' => $appliance['is_recursive'],
'name' => $appliance['name'],
'is_deleted' => $appliance['is_deleted'],
'appliancetypes_id' => $appliance['plugin_appliances_appliancetypes_id'],
'comment' => $appliance['comment'],
'locations_id' => $appliance['locations_id'],
'manufacturers_id' => '0',
'applianceenvironments_id' => $appliance['plugin_appliances_environments_id'],
'users_id' => $appliance['users_id'],
'users_id_tech' => $appliance['users_id_tech'],
'groups_id' => $appliance['groups_id'],
'groups_id_tech' => $appliance['groups_id_tech'],
'date_mod' => $appliance['date_mod'],
'is_helpdesk_visible' => $appliance['is_helpdesk_visible'],
'states_id' => $appliance['states_id'],
'externalidentifier' => $appliance['externalid'],
'serial' => $appliance['serial'],
'otherserial' => $appliance['otherserial']
]);
$app = new Appliance();
if (!($app_id = $app->getFromDBByCrit($app_fields))) {
$app_id = $app->add($app_fields);
}
if (false === $app_id) {
$this->outputImportError(
sprintf(__('Unable to create Appliance %s (%d).'), $appliance['name'], (int) $appliance['id']),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
$this->output->write(PHP_EOL);
return true;
}
/**
* Create appliance types.
*
* @return bool
*/
private function createApplianceTypes(): bool {
$this->output->writeln(
'<comment>' . __('Creating Appliance types...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$iterator = $this->db->request([
'FROM' => 'glpi_plugin_appliances_appliancetypes'
]);
if (!count($iterator)) {
return true;
}
$progress_bar = new ProgressBar($this->output);
foreach ($progress_bar->iterate($iterator) as $type) {
$this->writelnOutputWithProgressBar(
sprintf(
__('Importing type "%s"...'),
$type['name']
),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$appt_fields = Toolbox::sanitize([
'id' => $type['id'],
'entities_id' => $type['entities_id'],
'is_recursive' => $type['is_recursive'],
'name' => $type['name'],
'comment' => $type['comment'],
'externalidentifier' => $type['externalid']
]);
$appt = new ApplianceType();
if (!($appt_id = $appt->getFromDBByCrit($appt_fields))) {
$appt_id = $appt->add($appt_fields);
}
if (false === $appt_id) {
$this->outputImportError(
sprintf(__('Unable to create Appliance environment %s (%d).'), $type['name'], (int) $type['id']),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
$this->output->write(PHP_EOL);
return true;
}
/**
* Create appliance relations.
*
* @return bool
*/
private function createApplianceRelations(): bool {
$this->output->writeln(
'<comment>' . __('Creating Appliance relations...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$iterator = $this->db->request([
'SELECT' => ['rel.*', 'app.relationtype'],
'FROM' => 'glpi_plugin_appliances_relations AS rel',
'INNER JOIN' => [
'glpi_plugin_appliances_appliances_items AS items' => [
'ON' => [
'items' => 'id',
'rel' => 'plugin_appliances_appliances_items_id'
]
],
'glpi_plugin_appliances_appliances AS app' => [
'ON' => [
'app' => 'id',
'items' => 'plugin_appliances_appliances_id'
]
]
]
]);
if (!count($iterator)) {
return true;
}
$progress_bar = new ProgressBar($this->output);
foreach ($progress_bar->iterate($iterator) as $row) {
$this->writelnOutputWithProgressBar(
sprintf(
__('Importing relation "%s"...'),
$row['id']
),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$itemtype = self::PLUGIN_RELATION_TYPES[$row['relationtype']] ?? "";
if ($itemtype == "") {
$this->outputImportError(
sprintf(
__('Unable to found relation type %s from Appliance Item Relation %d.'),
$row['relationtype'],
(int) $row['id']
),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
} else {
continue; // Skip this relation
}
}
$appr_fields = Toolbox::sanitize([
'id' => $row['id'],
'appliances_items_id' => $row['plugin_appliances_appliances_items_id'],
'itemtype' => $itemtype,
'items_id' => $row['relations_id']
]);
$appr = new Appliance_Item_Relation();
if (!($appr_id = $appr->getFromDBByCrit($appr_fields))) {
$appr_id = $appr->add($appr_fields);
}
if (false === $appr_id) {
$this->outputImportError(
sprintf(__('Unable to create Appliance Item Relation %d.'), (int) $row['id']),
$progress_bar
);
if (!$this->input->getOption('skip-errors')) {
return false;
}
}
}
$this->output->write(PHP_EOL);
return true;
}
/**
* Output import error message.
*
* @param string $message
* @param ProgressBar|null $progress_bar
*
* @return void
*/
private function outputImportError($message, ProgressBar $progress_bar = null) {
$skip_errors = $this->input->getOption('skip-errors');
$verbosity = $skip_errors
? OutputInterface::VERBOSITY_NORMAL
: OutputInterface::VERBOSITY_QUIET;
$message = '<error>' . $message . '</error>';
if ($skip_errors && $progress_bar instanceof ProgressBar) {
$this->writelnOutputWithProgressBar(
$message,
$progress_bar,
$verbosity
);
} else {
if (!$skip_errors && $progress_bar instanceof ProgressBar) {
$this->output->write(PHP_EOL); // Keep progress bar last state and go to next line
}
$this->output->writeln(
$message,
$verbosity
);
}
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Migration;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use CommonDBTM;
use Glpi\Console\AbstractCommand;
use Log;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class BuildMissingTimestampsCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:migration:build_missing_timestamps');
$this->setDescription(__('Set missing `date_creation` and `date_mod` values using log entries.'));
$this->setHidden(true); // Hide this command as it is when migrating from really old GLPI version
}
protected function execute(InputInterface $input, OutputInterface $output) {
$tables_iterator = $this->db->request(
[
'SELECT' => [
'table_name AS TABLE_NAME',
'column_name AS COLUMN_NAME',
],
'FROM' => 'information_schema.columns',
'WHERE' => [
'table_schema' => $this->db->dbdefault,
'table_name' => ['LIKE', 'glpi_%'],
'column_name' => ['date_creation', 'date_mod'],
],
'ORDER' => ['table_name', 'column_name'],
]
);
$log_table = Log::getTable();
foreach ($tables_iterator as $table_info) {
$table = $table_info['TABLE_NAME'];
$itemtype = getItemTypeForTable($table);
$column = $table_info['COLUMN_NAME'];
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue; // getItemTypeForTable() may not return a class name ("UNKNOWN" for example)
}
/* @var $item CommonDBTM */
$item = new $itemtype();
if (!$item->dohistory) {
continue; // Skip items that does not have an history
}
$output->writeln(
'<comment>' . sprintf(__('Filling `%s`.`%s`...'), $table, $column) . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
$target_date = $column === 'date_creation' ? 'MIN(`date_mod`)' : 'MAX(`date_mod`)';
$result = $this->db->query(
"
UPDATE `$table`
LEFT JOIN (
SELECT $target_date AS `date_mod`, `itemtype`, `items_id`
FROM `$log_table`
GROUP BY `itemtype`, `items_id`
) as `logs`
ON `logs`.`itemtype` = '$itemtype' AND `logs`.`items_id` = `$table`.`id`
SET `$table`.`$column` = `logs`.`date_mod` WHERE `$table`.`$column` IS NULL
"
);
if (false === $result) {
$message = sprintf(
__('Update of `%s`.`%s` failed with message "(%s) %s".'),
$table,
$column,
$this->db->errno(),
$this->db->error()
);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
}
}
$output->writeln('<info>' . __('Migration done.') . '</info>');
return 0; // Success
}
}

View File

@ -0,0 +1,758 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Migration;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use CommonDBTM;
use DB;
use DomainType;
use Domain;
use Domain_Item;
use Plugin;
use Toolbox;
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Question\ChoiceQuestion;
class DomainsPluginToCoreCommand extends AbstractCommand {
/**
* Error code returned if plugin version or plugin data is invalid.
*
* @var integer
*/
const ERROR_PLUGIN_VERSION_OR_DATA_INVALID = 1;
/**
* Error code returned if import failed.
*
* @var integer
*/
const ERROR_PLUGIN_IMPORT_FAILED = 1;
/**
* Version of Domains plugin required for this migration.
* @var string
*/
const DOMAINS_REQUIRED_VERSION = '2.1.0';
/**
* Imported elements mapping.
*
* @var array
*/
private $elements_mapping;
protected function configure() {
parent::configure();
$this->setName('glpi:migration:domains_plugin_to_core');
$this->setDescription(__('Migrate Domains plugin data into GLPI core tables'));
$this->addOption(
'update-plugin',
'u',
InputOption::VALUE_NONE,
sprintf(
__('Run Domains plugin update (you need version %s files to do this)'),
self::DOMAINS_REQUIRED_VERSION
)
);
$this->addOption(
'without-plugin',
'w',
InputOption::VALUE_NONE,
sprintf(
__('Enable migration without plugin files (we cannot validate that plugin data are compatible with supported %s version)'),
self::DOMAINS_REQUIRED_VERSION
)
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->elements_mapping = []; // Clear elements mapping
$no_interaction = $input->getOption('no-interaction');
if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
$output->writeln(
[
__('You are about to launch migration of Domains plugin data into GLPI core tables.'),
__('It is better to make a backup of your existing data before continuing.')
]
);
/** @var QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(
'<comment>' . __('Do you want to launch migration ?') . ' [yes/No]</comment>',
false
)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
if (!$this->checkPlugin()) {
return self::ERROR_PLUGIN_VERSION_OR_DATA_INVALID;
}
if (!$this->migratePlugin()) {
return self::ERROR_PLUGIN_IMPORT_FAILED;
}
$output->writeln('<info>' . __('Migration done.') . '</info>');
return 0; // Success
}
/**
* Check that plugin state and existing data are OK for migration.
*
* @throws LogicException
*
* @return boolean
*/
private function checkPlugin() {
$check_version = !$this->input->getOption('without-plugin');
if ($check_version) {
$this->output->writeln(
'<comment>' . __('Checking plugin version...') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
$plugin = new Plugin();
$plugin->checkPluginState('domains');
if (!$plugin->getFromDBbyDir('domains')) {
$message = __('Domains plugin is not part of GLPI plugin list. It has never been installed or has been cleaned.')
. ' '
. sprintf(
__('You have to install Domains plugin files in version %s to be able to continue.'),
self::DOMAINS_REQUIRED_VERSION
);
$this->output->writeln(
[
'<error>' . $message . '</error>',
],
OutputInterface::VERBOSITY_QUIET
);
return false;
}
$is_version_ok = self::DOMAINS_REQUIRED_VERSION === $plugin->fields['version'];
if (!$is_version_ok) {
$message = sprintf(
__('You have to install Domains plugin files in version %s to be able to continue.'),
self::DOMAINS_REQUIRED_VERSION
);
$this->output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
$is_installable = in_array(
$plugin->fields['state'],
[
Plugin::TOBECLEANED, // Can be in this state if check was done without the plugin dir
Plugin::NOTINSTALLED, // Can be not installed if plugin has been cleaned in plugin list
Plugin::NOTUPDATED, // Plugin 1.8.0 version has never been installed
]
);
if ($is_installable) {
if ($this->input->getOption('update-plugin')) {
$message = sprintf(
__('Migrating plugin to %s version...'),
self::DOMAINS_REQUIRED_VERSION
);
$this->output->writeln(
'<info>' . $message . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
ob_start();
$plugin->install($plugin->fields['id']);
ob_end_clean();
// Reload and check migration result
$plugin->getFromDB($plugin->fields['id']);
if (!in_array($plugin->fields['state'], [Plugin::TOBECONFIGURED, Plugin::NOTACTIVATED])) {
$message = sprintf(
__('Plugin migration to %s version failed.'),
self::DOMAINS_REQUIRED_VERSION
);
$this->output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
} else {
$message = sprintf(
__('Domains plugin data has to be updated to %s version. It can be done using the --update-plugin option.'),
self::DOMAINS_REQUIRED_VERSION
);
$this->output->writeln(
'<comment>' . $message . '</comment>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
}
$is_state_ok = in_array(
$plugin->fields['state'],
[
Plugin::ACTIVATED, // Should not be possible as 1.8.0 is not compatible with 9.3
Plugin::TOBECONFIGURED, // Should not be possible as check_config of plugin returns always true
Plugin::NOTACTIVATED,
]
);
if (!$is_state_ok) {
// Should not happens as installation should put plugin in awaited state
throw new LogicException('Unexpected plugin state.');
}
}
$domains_tables = [
'glpi_plugin_domains_configs',
'glpi_plugin_domains_domains',
'glpi_plugin_domains_domains_items',
'glpi_plugin_domains_domaintypes',
];
$missing_tables = false;
foreach ($domains_tables as $table) {
if (!$this->db->tableExists($table)) {
$this->output->writeln(
'<error>' . sprintf(__('Domains plugin table "%s" is missing.'), $table) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$missing_tables = true;
}
}
if ($missing_tables) {
$this->output->writeln(
'<error>' . __('Migration cannot be done.') . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
return true;
}
private function migratePlugin() {
$failure = !$this->importDomainTypes()
|| !$this->importDomains()
|| !$this->importDomainItems();
return !$failure;
}
/**
* Migrate domain types
*
* @return boolean
*/
protected function importDomainTypes() {
$has_errors = false;
$this->output->writeln(
'<comment>' . __('Importing domains types...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$types_iterator = $this->db->request([
'FROM' => 'glpi_plugin_domains_domaintypes',
'ORDER' => 'id ASC'
]);
$core_types = [];
$coret_iterator = $this->db->request([
'SELECT' => ['id', 'name'],
'FROM' => DomainType::getTable()
]);
while ($row = $coret_iterator->next()) {
$core_types[$row['name']] = $row['id'];
}
if ($types_iterator->count()) {
$progress_bar = new ProgressBar($this->output, $types_iterator->count());
$progress_bar->start();
foreach ($types_iterator as $typ) {
$progress_bar->advance(1);
$core_type = null;
if (isset($core_types[$typ['name']])) {
$core_type = $core_types[$typ['name']];
$message = sprintf(
__('Updating existing domain type %s...'),
$typ['name']
);
} else {
$message = sprintf(
__('Importing domain type %s...'),
$typ['name']
);
}
$this->writelnOutputWithProgressBar(
$message,
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$type_input = [
'name' => $typ['name'],
'entities_id' => $typ['entities_id'],
'comment' => $typ['comment'],
];
$type_input = Toolbox::addslashes_deep($type_input);
$domaintype = new DomainType();
if ($core_type !== null) {
$res = (bool)$domaintype->update($type_input + ['id' => $core_type]);
} else {
$new_tid = (int)$domaintype->add($type_input);
$res = $new_tid > 0;
if ($res) {
$core_types[$typ['name']] = $new_tid;
}
}
if (!$res) {
$has_errors = true;
$message = sprintf(
$core_type === null ?
__('Unable to add domain type %s.') :
__('Unable to update domain type %s.'),
$typ['name']
);
$this->outputImportError($message, $progress_bar);
return false;
}
$this->addElementToMapping(
'PluginDomainsDomaintype',
$typ['id'],
'DomainType',
$new_tid ?? $core_type
);
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
} else {
$this->output->writeln(
'<comment>' . __('No domains types found.') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
}
return !$has_errors;
}
/**
* Migrate domains
*
* @throws LogicException
*
* @return boolean
*/
protected function importDomains() {
$has_errors = false;
$this->output->writeln(
'<comment>' . __('Importing domains...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$domains_iterator = $this->db->request([
'FROM' => 'glpi_plugin_domains_domains',
'ORDER' => 'id ASC'
]);
$core_domains = [];
$cored_iterator = $this->db->request([
'SELECT' => ['id', 'name'],
'FROM' => Domain::getTable()
]);
while ($row = $cored_iterator->next()) {
$core_domains[$row['name']] = $row['id'];
}
if ($domains_iterator->count()) {
$progress_bar = new ProgressBar($this->output, $domains_iterator->count());
$progress_bar->start();
foreach ($domains_iterator as $dom) {
$progress_bar->advance(1);
$core_dom = null;
if (isset($core_domains[$dom['name']])) {
$core_dom = $core_domains[$dom['name']];
$message = sprintf(
__('Updating existing domain %s...'),
$dom['name']
);
} else {
$message = sprintf(
__('Importing domain %s...'),
$dom['name']
);
}
$this->writelnOutputWithProgressBar(
$message,
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$mapped_type = $this->getCorrespondingItem('PluginDomainsDomaintype', $dom['plugin_domains_domaintypes_id']);
if ($dom['plugin_domains_domaintypes_id'] != 0 && $mapped_type === null) {
$message = sprintf(
__('Unable to find mapping for type %s.'),
$dom['plugin_domains_domaintypes_id']
);
$this->outputImportError($message, $progress_bar);
return false;
}
$types_id = $mapped_type !== null ? $mapped_type->fields['id'] : 0;
$domain_input = [
'name' => $dom['name'],
'entities_id' => $dom['entities_id'],
'is_recursive' => $dom['is_recursive'],
'domaintypes_id' => $types_id,
'date_creation' => $dom['date_creation'],
'date_expiration' => $dom['date_expiration'],
'users_id_tech' => $dom['users_id_tech'],
'groups_id_tech' => $dom['groups_id_tech'],
//suppliers_id not present in core
'comment' => $dom['comment'],
'others' => $dom['others'],
'is_helpdesk_visible' => $dom['is_helpdesk_visible'],
'date_mod' => $dom['date_mod'],
'is_deleted' => $dom['is_deleted']
];
$domain_input = Toolbox::addslashes_deep($domain_input);
$domain = new Domain();
if ($core_dom !== null) {
$res = (bool)$domain->update($domain_input + ['id' => $core_dom]);
} else {
$new_did = (int)$domain->add($domain_input);
$res = $new_did > 0;
if ($res) {
$core_domains[$dom['name']] = $new_did;
}
}
if (!$res) {
$has_errors = true;
$message = sprintf(
$core_dom === null ?
__('Unable to add domain %s.') :
__('Unable to update domain %s.'),
$dom['name']
);
$this->outputImportError($message, $progress_bar);
return false;
}
$this->addElementToMapping(
'PluginDomainsDomains',
$dom['id'],
'Domain',
$new_did ?? $core_dom
);
//handle infocoms
$infocom = new \Infocom();
$infocom_input = [
'itemtype' => 'Domain',
'items_id' => $new_did ?? $core_dom,
'suppliers_id' => $dom['suppliers_id'],
'entities_id' => $dom['entities_id'],
'is_recursive' => $dom['is_recursive']
];
if ($core_dom === null) {
$infocom->add($infocom_input);
} else {
$found = $infocom->getFromDBByCrit([
'itemtype' => 'Domain',
'items_id' => $core_dom
]);
if ($found) {
$infocom_input['id'] = $infocom->fields['id'];
$infocom->update($infocom_input);
} else {
$infocom->add($infocom_input);
}
}
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
} else {
$this->output->writeln(
'<comment>' . __('No domains found.') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
}
return !$has_errors;
}
/**
* Migrate domain items
*
* @return boolean
*/
protected function importDomainItems() {
$has_errors = false;
$this->output->writeln(
'<comment>' . __('Importing domains items...') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
$items_iterator = $this->db->request([
'FROM' => 'glpi_plugin_domains_domains_items',
'ORDER' => 'id ASC'
]);
$core_items = [];
$coreitems_iterator = $this->db->request([
'FROM' => Domain_Item::getTable()
]);
while ($row = $coreitems_iterator->next()) {
$core_items[$row['domains_id'].$row['itemtype'].$row['items_id']] = $row['id'];
}
if ($items_iterator->count()) {
$progress_bar = new ProgressBar($this->output, $items_iterator->count());
$progress_bar->start();
foreach ($items_iterator as $itm) {
$progress_bar->advance(1);
$core_item = null;
$mapped_domain = $this->getCorrespondingItem('PluginDomainsDomains', $itm['plugin_domains_domains_id']);
if ($mapped_domain === null) {
$message = sprintf(
__('Unable to find corresponding domain for item %s (%s).'),
$itm['itemtype'],
$itm['items_id']
);
$this->outputImportError($message, $progress_bar);
// Do not block migration as this error is probably resulting in presence of obsolete data in DB
continue;
}
$domains_id = $mapped_domain->fields['id'];
if (isset($core_items[$domains_id.$itm['itemtype'].$itm['items_id']])) {
$core_item = $core_items[$domains_id.$itm['itemtype'].$itm['items_id']];
$message = sprintf(
__('Skip existing domain item %s...'),
$domains_id . ' ' . $itm['itemtype'] . ' ' . $itm['items_id']
);
} else {
$message = sprintf(
__('Importing domain item %s...'),
$domains_id . ' ' . $itm['itemtype'] . ' ' . $itm['items_id']
);
}
$this->writelnOutputWithProgressBar(
$message,
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
if ($core_item !== null) {
//if it already exist in DB, there is nothing to change
continue;
}
$item_input = [
'domains_id' => $domains_id,
'itemtype' => $itm['itemtype'],
'items_id' => $itm['items_id'],
'domainrelations_id' => 0
];
$item_input = Toolbox::addslashes_deep($item_input);
$item = new Domain_Item();
$new_iid = (int)$item->add($item_input);
$res = $new_iid > 0;
if ($res) {
$core_items[$domains_id.$itm['itemtype'].$itm['items_id']] = $new_iid;
}
if (!$res) {
$has_errors = true;
$message = sprintf(
$core_item === null ?
__('Unable to add domain item %s.') :
__('Unable to update domain item %s.'),
$domains_id . ' ' . $itm['itemtype'] . ' ' . $itm['items_id']
);
$this->outputImportError($message, $progress_bar);
return false;
}
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
} else {
$this->output->writeln(
'<comment>' . __('No domains items found.') . '</comment>',
OutputInterface::VERBOSITY_NORMAL
);
}
return !$has_errors;
}
/**
* Add an element to mapping.
*
* @param string $old_itemtype
* @param integer $old_id
* @param string $new_itemtype
* @param integer $new_id
*
* @return void
*/
private function addElementToMapping($old_itemtype, $old_id, $new_itemtype, $new_id) {
if (!array_key_exists($old_itemtype, $this->elements_mapping)) {
$this->elements_mapping[$old_itemtype] = [];
}
$this->elements_mapping[$old_itemtype][$old_id] = [
'itemtype' => $new_itemtype,
'id' => $new_id,
];
}
/**
* Returns item corresponding to itemtype and id.
* If item has been migrated to another itemtype, il will return the new item.
*
* @param string $itemtype
* @param integer $id
*
* @return null|CommonDBTM
*/
private function getCorrespondingItem($itemtype, $id) {
if (array_key_exists($itemtype, $this->elements_mapping)
&& array_key_exists($id, $this->elements_mapping[$itemtype])) {
// Element exists in mapping, get new element
$mapping = $this->elements_mapping[$itemtype][$id];
$id = $mapping['id'];
$itemtype = $mapping['itemtype'];
}
if (!class_exists($itemtype)) {
return null;
}
$item = new $itemtype();
if ($id !== 0 && !$item->getFromDB($id)) {
return null;
}
return $item;
}
/**
* Output import error message.
*
* @param string $message
* @param ProgressBar|null $progress_bar
*
* @return void
*/
private function outputImportError($message, ProgressBar $progress_bar = null) {
$verbosity = OutputInterface::VERBOSITY_QUIET;
$message = '<error>' . $message . '</error>';
if ($progress_bar instanceof ProgressBar) {
$this->writelnOutputWithProgressBar(
$message,
$progress_bar,
$verbosity
);
} else {
if ($progress_bar instanceof ProgressBar) {
$this->output->write(PHP_EOL); // Keep progress bar last state and go to next line
}
$this->output->writeln(
$message,
$verbosity
);
}
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Migration;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DB;
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class MyIsamToInnoDbCommand extends AbstractCommand {
/**
* Error code returned when failed to migrate one table.
*
* @var integer
*/
const ERROR_TABLE_MIGRATION_FAILED = 1;
protected function configure() {
parent::configure();
$this->setName('glpi:migration:myisam_to_innodb');
$this->setDescription(__('Migrate MyISAM tables to InnoDB'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$no_interaction = $input->getOption('no-interaction'); // Base symfony/console option
$myisam_tables = $this->db->getMyIsamTables();
$output->writeln(
sprintf(
'<info>' . __('Found %s table(s) using MyISAM engine.') . '</info>',
$myisam_tables->count()
)
);
if (0 === $myisam_tables->count()) {
$output->writeln('<info>' . __('No migration needed.') . '</info>');
return 0;
}
if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
/** @var QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
while ($table = $myisam_tables->next()) {
$table_name = DB::quoteName($table['TABLE_NAME']);
$output->writeln(
'<comment>' . sprintf(__('Migrating table "%s"...'), $table_name) . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
$result = $this->db->query(sprintf('ALTER TABLE %s ENGINE = InnoDB', $table_name));
if (false === $result) {
$message = sprintf(
__('Migration of table "%s" failed with message "(%s) %s".'),
$table_name,
$this->db->errno(),
$this->db->error()
);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_TABLE_MIGRATION_FAILED;
}
}
$output->writeln('<info>' . __('Migration done.') . '</info>');
return 0; // Success
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,216 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Migration;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class TimestampsCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:migration:timestamps');
$this->setDescription(__('Convert "datetime" fields to "timestamp" to use timezones.'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
//convert db
// we are going to update datetime, date and time (?) types to timestamp type
$tbl_iterator = $this->db->request([
'SELECT' => ['information_schema.columns.table_name as TABLE_NAME'],
'DISTINCT' => true,
'FROM' => 'information_schema.columns',
'INNER JOIN' => [
'information_schema.tables' => [
'ON' => [
'information_schema.tables.table_name',
'information_schema.columns.table_name', [
'AND' => ['information_schema.tables.table_type' => 'BASE TABLE']
]
]
]
],
'WHERE' => [
'information_schema.columns.table_schema' => $this->db->dbdefault,
'information_schema.columns.data_type' => 'datetime'
],
'ORDER' => [
'information_schema.columns.table_name'
]
]);
$output->writeln(
sprintf(
'<info>' . __('Found %s table(s) requiring migration.') . '</info>',
$tbl_iterator->count()
)
);
if ($tbl_iterator->count() === 0) {
$output->writeln('<info>' . __('No migration needed.') . '</info>');
return 0; // Success
}
if (!$input->getOption('no-interaction')) {
// Ask for confirmation (unless --no-interaction)
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
$progress_bar = new ProgressBar($output, $tbl_iterator->count());
$progress_bar->start();
while ($table = $tbl_iterator->next()) {
$progress_bar->advance(1);
$tablealter = ''; // init by default
// get accurate info from information_schema to perform correct alter
$col_iterator = $this->db->request([
'SELECT' => [
'table_name AS TABLE_NAME',
'column_name AS COLUMN_NAME',
'column_default AS COLUMN_DEFAULT',
'column_comment AS COLUMN_COMMENT',
'is_nullable AS IS_NULLABLE',
],
'FROM' => 'information_schema.columns',
'WHERE' => [
'table_schema' => $this->db->dbdefault,
'table_name' => $table['TABLE_NAME'],
'data_type' => 'datetime'
]
]);
while ($column = $col_iterator->next()) {
$nullable = false;
$default = null;
//check if nullable
if ('YES' === $column['IS_NULLABLE']) {
$nullable = true;
}
//guess default value
if (is_null($column['COLUMN_DEFAULT']) && !$nullable) { // no default
$default = null;
} else if ((is_null($column['COLUMN_DEFAULT']) || strtoupper($column['COLUMN_DEFAULT']) == 'NULL') && $nullable) {
$default = "NULL";
} else if (!is_null($column['COLUMN_DEFAULT']) && strtoupper($column['COLUMN_DEFAULT']) != 'NULL') {
if ($column['COLUMN_DEFAULT'] < '1970-01-01 00:00:01') {
// Prevent default value to be out of range (lower to min possible value)
$defaultDate = new \DateTime('1970-01-01 00:00:01', new \DateTimeZone('UTC'));
$defaultDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$default = $defaultDate->format("Y-m-d H:i:s");
} else if ($column['COLUMN_DEFAULT'] > '2038-01-19 03:14:07') {
// Prevent default value to be out of range (greater to max possible value)
$defaultDate = new \DateTime('2038-01-19 03:14:07', new \DateTimeZone('UTC'));
$defaultDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$default = $defaultDate->format("Y-m-d H:i:s");
} else {
$default = $column['COLUMN_DEFAULT'];
}
}
//build alter
$tablealter .= "\n\t MODIFY COLUMN ".$this->db->quoteName($column['COLUMN_NAME'])." TIMESTAMP";
if ($nullable) {
$tablealter .= " NULL";
} else {
$tablealter .= " NOT NULL";
}
if ($default !== null) {
if ($default !== 'NULL') {
$default = "'" . $this->db->escape($default) . "'";
}
$tablealter .= " DEFAULT $default";
}
if ($column['COLUMN_COMMENT'] != '') {
$tablealter .= " COMMENT '".$this->db->escape($column['COLUMN_COMMENT'])."'";
}
$tablealter .= ",";
}
$tablealter = rtrim($tablealter, ",");
// apply alter to table
$query = "ALTER TABLE " . $this->db->quoteName($table['TABLE_NAME']) . " " . $tablealter.";\n";
$this->writelnOutputWithProgressBar(
'<comment>' . sprintf(__('Running %s'), $query) . '</comment>',
$progress_bar,
OutputInterface::VERBOSITY_VERBOSE
);
$result = $this->db->query($query);
if (false === $result) {
$message = sprintf(
__('Update of `%s` failed with message "(%s) %s".'),
$table['TABLE_NAME'],
$this->db->errno(),
$this->db->error()
);
$this->writelnOutputWithProgressBar(
'<error>' . $message . '</error>',
$progress_bar,
OutputInterface::VERBOSITY_QUIET
);
}
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
$output->writeln('<info>' . __('Migration done.') . '</info>');
return 0; // Success
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Plugin;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
abstract class AbstractPluginCommand extends AbstractCommand implements ForceNoPluginsOptionCommandInterface {
/**
* Wildcard value to target all directories.
*
* @var string
*/
const DIRECTORY_ALL = '*';
protected function configure() {
parent::configure();
$this->addOption(
'all',
'a',
InputOption::VALUE_NONE,
__('Run command on all plugins')
);
$this->addArgument(
'directory',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
__('Plugin directory')
);
}
protected function interact(InputInterface $input, OutputInterface $output) {
$all = $input->getOption('all');
$directories = $input->getArgument('directory');
if ($all && !empty($directories)) {
throw new InvalidArgumentException(
__('Option --all is not compatible with usage of directory argument.')
);
}
if ($all) {
// Set wildcard value in directory argument
$input->setArgument('directory', [self::DIRECTORY_ALL]);
} else if (empty($directories)) {
// Ask for plugin list if directory argument is empty
$choices = $this->getDirectoryChoiceChoices();
$choices = array_merge(
[self::DIRECTORY_ALL => __('All plugins')],
$choices
);
if (!empty($choices)) {
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$question = new ChoiceQuestion(
$this->getDirectoryChoiceQuestion(),
$choices
);
$question->setAutocompleterValues(array_keys($choices));
$question->setMultiselect(true);
$answer = $question_helper->ask(
$input,
$output,
$question
);
$input->setArgument('directory', $answer);
}
}
}
public function getNoPluginsOptionValue() {
// Force no loading on plugins in plugin install process
return true;
}
/**
* Normalize input to symplify handling of specific arguments/options values.
*
* @param InputInterface $input
*
* @return void
*/
protected function normalizeInput(InputInterface $input) {
if ($input->getArgument('directory') === [self::DIRECTORY_ALL]) {
$input->setArgument('directory', array_keys($this->getDirectoryChoiceChoices()));
}
}
/**
* Returns question to ask if no directory argument has been passed.
*
* @return string
*/
abstract protected function getDirectoryChoiceQuestion();
/**
* Returns possible directory choices to suggest if no directory argument has been passed.
* Returns an array usable in a ChoiceQuestion object.
*
* @return string[]
*/
abstract protected function getDirectoryChoiceChoices();
}

View File

@ -0,0 +1,172 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Plugin;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ActivateCommand extends AbstractPluginCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:plugin:activate');
$this->setAliases(['plugin:activate']);
$this->setDescription('Activate plugin(s)');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->normalizeInput($input);
$directories = $input->getArgument('directory');
foreach ($directories as $directory) {
$output->writeln(
'<info>' . sprintf(__('Processing plugin "%s"...'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
if (!$this->canRunActivateMethod($directory)) {
continue;
}
$plugin = new Plugin();
$plugin->checkPluginState($directory); // Be sure that plugin informations are up to date in DB
if (!$plugin->getFromDBByCrit(['directory' => $directory])) {
$this->output->writeln(
'<error>' . sprintf(__('Unable to load plugin "%s" informations.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
continue;
}
if (!$plugin->activate($plugin->fields['id'])) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" activation failed.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$this->outputSessionBufferedMessages([WARNING, ERROR]);
continue;
}
$output->writeln(
'<info>' . sprintf(__('Plugin "%1$s" has been activated.'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
}
return 0; // Success
}
/**
* Check if activate method can be run for given plugin.
*
* @param string $directory
*
* @return boolean
*/
private function canRunActivateMethod($directory) {
$plugin = new Plugin();
// Check that directory is valid
$informations = $plugin->getInformationsFromDirectory($directory);
if (empty($informations)) {
$this->output->writeln(
'<error>' . sprintf(__('Invalid plugin directory "%s".'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
// Check current plugin state
$is_already_known = $plugin->getFromDBByCrit(['directory' => $directory]);
if (!$is_already_known) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" is not yet installed.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
if ($plugin->fields['state'] == Plugin::ACTIVATED) {
$this->output->writeln(
'<info>' . sprintf(__('Plugin "%s" is already active.'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
return false;
}
if (Plugin::NOTACTIVATED != $plugin->fields['state']) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" have to be installed and configured prior to activation.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
return true;
}
protected function getDirectoryChoiceQuestion() {
return __('Which plugin(s) do you want to activate (comma separated values) ?');
}
protected function getDirectoryChoiceChoices() {
$choices = [];
$plugin_iterator = $this->db->request(
[
'FROM' => Plugin::getTable(),
'WHERE' => [
'state' => Plugin::NOTACTIVATED
]
]
);
foreach ($plugin_iterator as $plugin) {
$choices[$plugin['directory']] = $plugin['name'];
}
ksort($choices, SORT_STRING);
return $choices;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Plugin;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DeactivateCommand extends AbstractPluginCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:plugin:deactivate');
$this->setAliases(['plugin:deactivate']);
$this->setDescription('Deactivate plugin(s)');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->normalizeInput($input);
$directories = $input->getArgument('directory');
foreach ($directories as $directory) {
$output->writeln(
'<info>' . sprintf(__('Processing plugin "%s"...'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
if (!$this->canRunDeactivateMethod($directory)) {
continue;
}
$plugin = new Plugin();
$plugin->checkPluginState($directory); // Be sure that plugin informations are up to date in DB
if (!$plugin->getFromDBByCrit(['directory' => $directory])) {
$this->output->writeln(
'<error>' . sprintf(__('Unable to load plugin "%s" informations.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
continue;
}
if (!$plugin->unactivate($plugin->fields['id'])) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" deactivation failed.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$this->outputSessionBufferedMessages([WARNING, ERROR]);
continue;
}
$output->writeln(
'<info>' . sprintf(__('Plugin "%1$s" has been deactivated.'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
}
return 0; // Success
}
/**
* Check if deactivate method can be run for given plugin.
*
* @param string $directory
*
* @return boolean
*/
private function canRunDeactivateMethod($directory) {
$plugin = new Plugin();
// Check that directory is valid
$informations = $plugin->getInformationsFromDirectory($directory);
if (empty($informations)) {
$this->output->writeln(
'<error>' . sprintf(__('Invalid plugin directory "%s".'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
// Check current plugin state
$is_already_known = $plugin->getFromDBByCrit(['directory' => $directory]);
if (!$is_already_known) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" is not yet installed.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
if (Plugin::ACTIVATED != $plugin->fields['state']) {
$this->output->writeln(
'<info>' . sprintf(__('Plugin "%s" is already inactive.'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
return false;
}
return true;
}
protected function getDirectoryChoiceQuestion() {
return __('Which plugin(s) do you want to deactivate (comma separated values) ?');
}
protected function getDirectoryChoiceChoices() {
$choices = [];
$plugin_iterator = $this->db->request(
[
'FROM' => Plugin::getTable(),
'WHERE' => [
'state' => Plugin::ACTIVATED
]
]
);
foreach ($plugin_iterator as $plugin) {
$choices[$plugin['directory']] = $plugin['name'];
}
ksort($choices, SORT_STRING);
return $choices;
}
}

View File

@ -0,0 +1,355 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Plugin;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Auth;
use Plugin;
use Session;
use User;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class InstallCommand extends AbstractPluginCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:plugin:install');
$this->setAliases(['plugin:install']);
$this->setDescription('Run plugin(s) installation script');
$this->addOption(
'param',
'p',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
__('Additionnal parameters to pass to the plugin install hook function')
. PHP_EOL
. __('"-p foo" will set "foo" param value to true')
. PHP_EOL
. __('"-p foo=bar" will set "foo" param value to "bar"')
. PHP_EOL
);
$this->addUsage('-p foo=bar -p force myplugin');
$this->addOption(
'username',
'u',
InputOption::VALUE_REQUIRED,
__('Name of user used during installation script (among other things to set plugin admin rights)')
);
$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
__('Force execution of installation, even if plugin is already installed')
);
}
protected function interact(InputInterface $input, OutputInterface $output) {
parent::interact($input, $output);
if (null === $input->getOption('username')) {
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$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'));
$directories = $input->getArgument('directory');
$force = $input->getOption('force');
$params = $this->getAdditionnalParameters($input);
foreach ($directories as $directory) {
$output->writeln(
'<info>' . sprintf(__('Processing plugin "%s"...'), $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
if (!$this->canRunInstallMethod($directory, $force)) {
continue;
}
$plugin = new Plugin();
$plugin->checkPluginState($directory); // Be sure that plugin informations are up to date in DB
if (!$plugin->getFromDBByCrit(['directory' => $directory])) {
$this->output->writeln(
'<error>' . sprintf(__('Unable to load plugin "%s" informations.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
continue;
}
$plugin->install($plugin->fields['id'], $params);
// Check state after installation
if (!in_array($plugin->fields['state'], [Plugin::NOTACTIVATED, Plugin::TOBECONFIGURED])) {
$this->output->writeln(
'<error>' . sprintf(__('Plugin "%s" installation failed.'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
$this->outputSessionBufferedMessages([WARNING, ERROR]);
continue;
}
$message = Plugin::TOBECONFIGURED == $plugin->fields['state']
? __('Plugin "%1$s" has been installed and must be configured.')
: __('Plugin "%1$s" has been installed and can be activated.');
$output->writeln(
'<info>' . sprintf($message, $directory) . '</info>',
OutputInterface::VERBOSITY_NORMAL
);
}
return 0; // Success
}
protected function getDirectoryChoiceQuestion() {
return __('Which plugin(s) do you want to install (comma separated values) ?');
}
protected function getDirectoryChoiceChoices() {
$only_not_installed = !$this->input->getOption('force');
// Fetch directory list
$directories = [];
foreach (PLUGINS_DIRECTORIES as $plugins_directory) {
$directory_handle = opendir($plugins_directory);
while (false !== ($filename = readdir($directory_handle))) {
if (!in_array($filename, ['.svn', '.', '..'])
&& is_dir($plugins_directory . DIRECTORY_SEPARATOR . $filename)) {
$directories[] = $filename;
}
}
}
// Fetch plugins informations
$choices = [];
foreach ($directories as $directory) {
$plugin = new Plugin();
$informations = $plugin->getInformationsFromDirectory($directory);
if (empty($informations)) {
continue; // Ignore directory if not able to load plugin informations.
}
if ($only_not_installed
&& ($this->isAlreadyInstalled($directory)
|| (array_key_exists('oldname', $informations)
&& $this->isAlreadyInstalled($informations['oldname'])))) {
continue;
}
$choices[$directory] = array_key_exists('name', $informations)
? $informations['name']
: $directory;
}
ksort($choices, SORT_STRING);
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.
*
* @param string $directory
*
* @return array
*/
private function isAlreadyInstalled($directory) {
$plugin = new Plugin();
$is_already_known = $plugin->getFromDBByCrit(['directory' => $directory]);
$installed_states = [
Plugin::ACTIVATED,
Plugin::TOBECONFIGURED,
Plugin::NOTACTIVATED,
];
return $is_already_known && in_array($plugin->fields['state'], $installed_states);
}
/**
* Check if install method can be run for given plugin.
*
* @param string $directory
* @param boolean $allow_reinstall
*
* @return boolean
*/
private function canRunInstallMethod($directory, $allow_reinstall) {
$plugin = new Plugin();
// Check that directory is valid
$informations = $plugin->getInformationsFromDirectory($directory);
if (empty($informations)) {
$this->output->writeln(
'<error>' . sprintf(__('Invalid plugin directory "%s".'), $directory) . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
// Check if plugin is not already installed
if (!$allow_reinstall
&& ($this->isAlreadyInstalled($directory)
|| (array_key_exists('oldname', $informations)
&& $this->isAlreadyInstalled($informations['oldname'])))) {
$message = sprintf(
__('Plugin "%s" is already installed. Use --force option to force reinstallation.'),
$directory
);
$this->output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return false;
}
Plugin::load($directory, true);
// Check that required functions exists
$function = 'plugin_' . $directory . '_install';
if (!function_exists($function)) {
$message = sprintf(
__('Plugin "%s" function "%s" is missing.'),
$directory,
$function
);
$this->output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
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(
[
'<error>' . sprintf(__('Plugin "%s" requirements not met.'), $directory) . '</error>',
'<error>' . $ob_contents . '</error>',
],
OutputInterface::VERBOSITY_QUIET
);
return false;
}
return true;
}
/**
* Extract additionnal parameters from input.
*
* @param InputInterface $input
*
* @return array
*/
private function getAdditionnalParameters(InputInterface $input) {
$input_params = $input->getOption('param');
$params = [];
foreach ($input_params as $input_param) {
$parts = explode('=', $input_param);
$params[$parts[0]] = isset($parts[1]) ? $parts[1] : true;
}
return $params;
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Rules;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ProcessSoftwareCategoryRulesCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:rules:process_software_category_rules');
$this->setAliases(['rules:process_software_category_rules']);
$this->setDescription(__('Process software category rules'));
$this->addOption(
'all',
'a',
InputOption::VALUE_NONE,
__('Process rule for all softwares, even those having already a defined category')
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$query = [
'SELECT' => [
'id',
],
'FROM' => \Software::getTable(),
];
if (!$input->getOption('all')) {
$query['WHERE'] = [
'softwarecategories_id' => 0
];
}
$software_iterator = $this->db->request($query);
$sofware_count = $software_iterator->count();
if ($sofware_count === 0) {
$output->writeln('<info>' . __('No software to process.') . '</info>');
return 0; // Success
}
$progress_bar = new ProgressBar($output, $sofware_count);
$progress_bar->start();
$processed_count = 0;
foreach ($software_iterator as $data) {
$progress_bar->advance(1);
$this->writelnOutputWithProgressBar(
sprintf(__('Processing software having id "%s".'), $data['id']),
$progress_bar,
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$software = new \Software();
if (!$software->getFromDB($data['id'])) {
$this->writelnOutputWithProgressBar(
sprintf(__('Unable to load software having id "%s".'), $data['id']),
$progress_bar,
OutputInterface::VERBOSITY_NORMAL
);
continue;
}
$rule_collection = new \RuleSoftwareCategoryCollection();
$input = $rule_collection->processAllRules(
[],
$software->fields,
[
'name' => $software->fields['name'],
'manufacturers_id' => $software->fields['manufacturers_id'],
]
);
$software->update($input);
$processed_count++;
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
$output->writeln(
'<info>' .sprintf(__('Number of softwares processed: %d.'), $processed_count) . '</info>'
);
return 0; // Success
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Rules;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
class ReplayDictionnaryRulesCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:rules:replay_dictionnary_rules');
$this->setAliases(['rules:replay_dictionnary_rules']);
$this->setDescription(__('Replay dictionnary rules on existing items'));
$this->addOption(
'dictionnary',
'd',
InputOption::VALUE_REQUIRED,
sprintf(
__('Dictionnary to use. Possible values are: %s'),
implode(', ', $this->getDictionnaryTypes())
)
);
$this->addOption(
'manufacturer-id',
'm',
InputOption::VALUE_REQUIRED,
__('If option is set, only items having given manufacturer ID will be processed.')
. "\n" . __('Currently only available for Software dictionnary.')
);
}
protected function interact(InputInterface $input, OutputInterface $output) {
if (empty($input->getOption('dictionnary'))) {
// Ask for dictionnary argument is empty
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$question = new ChoiceQuestion(
__('Which dictionnary do you want to replay ?'),
$this->getDictionnaryTypes()
);
$answer = $question_helper->ask(
$input,
$output,
$question
);
$input->setOption('dictionnary', $answer);
}
}
protected function execute(InputInterface $input, OutputInterface $output) {
$dictionnary = $input->getOption('dictionnary');
$rulecollection = \RuleCollection::getClassByType($dictionnary);
if (!in_array($dictionnary, $this->getDictionnaryTypes())
|| !($rulecollection instanceof \RuleCollection)) {
throw new InvalidArgumentException(
sprintf(__('Invalid "dictionnary" value.'))
);
}
$params = [];
if (null !== ($manufacturer_id = $input->getOption('manufacturer-id'))) {
$params['manufacturer'] = $manufacturer_id;
}
// Nota: implementations of RuleCollection::replayRulesOnExistingDB() are printing
// messages during execution on CLI mode.
// This could be improved by using the $output object to handle choosed verbosity level.
$rulecollection->replayRulesOnExistingDB(0, 0, [], $params);
return 0; // Success
}
/**
* Return list of available disctionnary types.
*
* @return string[]
*/
private function getDictionnaryTypes(): array {
global $CFG_GLPI;
$types = $CFG_GLPI['dictionnary_types'];
sort($types);
return $types;
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Security;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use GLPIKey;
class ChangekeyCommand extends AbstractCommand {
/**
* Error code returned when unable to renew key.
*
* @var integer
*/
const ERROR_UNABLE_TO_RENEW_KEY = 1;
protected function configure() {
parent::configure();
$this->setName('glpi:security:change_key');
$this->setDescription(__('Change password storage key and update values in database.'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$glpikey = new GLPIKey();
$fields = $glpikey->getFields();
$configs = $glpikey->getConfigs();
$conf_count = 0;
foreach ($configs as $config) {
$conf_count += count($config);
}
$output->writeln(
sprintf(
'<info>' . __('Found %1$s field(s) and %2$s configuration entries requiring migration.') . '</info>',
count($fields),
$conf_count
)
);
if (!$input->getOption('no-interaction')) {
// Ask for confirmation (unless --no-interaction)
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
$created = $glpikey->generate();
if (!$created) {
$output->writeln(
'<error>' . __('Unable to change security key!') . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_UNABLE_TO_RENEW_KEY;
}
$this->output->write(PHP_EOL);
$output->writeln('<info>' . __('New security key generated; database updated.') . '</info>');
return 0; // Success
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\System;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Glpi\System\RequirementsManager;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CheckRequirementsCommand extends AbstractCommand {
protected $requires_db = false;
protected function configure() {
parent::configure();
$this->setName('glpi:system:check_requirements');
$this->setAliases(['system:check_requirements']);
$this->setDescription(__('Check system requirements'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$requirements_manager = new RequirementsManager();
$core_requirements = $requirements_manager->getCoreRequirementList(
$this->db instanceof \DBmysql && $this->db->connected ? $this->db : null
);
$informations = new Table($output);
$informations->setHeaders(
[
__('Requirement'),
__('Status'),
__('Messages'),
]
);
/* @var \Glpi\System\Requirement\RequirementInterface $requirement */
foreach ($core_requirements as $requirement) {
if ($requirement->isOutOfContext()) {
continue; // skip requirement if not relevant
}
if ($requirement->isValidated()) {
$status = sprintf('<%s>[%s]</>', 'fg=black;bg=green', __('OK'));
} else {
$status = $requirement->isOptional()
? sprintf('<%s>[%s]</> ', 'fg=white;bg=yellow', __('WARNING'))
: sprintf('<%s>[%s]</> ', 'fg=white;bg=red', __('ERROR'));
}
$informations->addRow(
[
$requirement->getTitle(),
$status,
$requirement->isValidated() ? '' : implode("\n", $requirement->getValidationMessages())
]
);
}
$informations->render();
return 0; // Success
}
public function mustCheckMandatoryRequirements(): bool {
return false;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\System;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Console\AbstractCommand;
use Glpi\System\Status\StatusChecker;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CheckStatusCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:system:status');
$this->setAliases(['system:status']);
$this->setDescription(__('Check system status'));
$this->addOption('format', 'f', InputOption::VALUE_OPTIONAL,
'Output format [plain or json]', 'plain');
$this->addOption('private', 'p', InputOption::VALUE_NONE,
'Status information publicity. Private status information may contain potentially sensitive information such as version information.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$format = strtolower($input->getOption('format'));
$status = StatusChecker::getFullStatus(!$input->getOption('private'), $format === 'json');
if ($format === 'json') {
$output->writeln(json_encode($status, JSON_PRETTY_PRINT));
} else {
$output->writeln($status);
}
return 0; // Success
}
}

View File

@ -0,0 +1,213 @@
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Task;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use CronTask;
use Glpi\Console\AbstractCommand;
use Glpi\Event;
use QueryExpression;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
class UnlockCommand extends AbstractCommand {
protected function configure() {
parent::configure();
$this->setName('glpi:task:unlock');
$this->setAliases(['task:unlock']);
$this->setDescription(__('Unlock automatic tasks'));
$this->addOption(
'all',
'a',
InputOption::VALUE_NONE,
__('Unlock all tasks')
);
$this->addOption(
'cycle',
'c',
InputOption::VALUE_OPTIONAL,
__('Execution time (in cycles) from which the task is considered as stuck (delay = task frequency * cycle)'),
null // Has to be null to detect lack of definition of both 'cycle' and 'delay' to set default delay (see self::validateInput())
);
$this->addOption(
'delay',
'd',
InputOption::VALUE_OPTIONAL,
__('Execution time (in seconds) from which the task is considered as stuck (default: 1800)'),
null // Has to be null to detect lack of definition of both 'cycle' and 'delay' to set default delay (see self::validateInput())
);
$this->addOption(
'task',
't',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
__('Itemtype::name of task to unlock (e.g: "MailCollector::mailgate")')
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->validateInput($input);
$all = $input->getOption('all');
$cycle = $input->getOption('cycle');
$delay = $input->getOption('delay');
$tasks = $input->getOption('task');
if (null !== $cycle) {
$delay = $cycle . ' * ' . $this->db->quoteName('frequency');
}
$task_iterator = $this->db->request(
[
'SELECT' => [
'id',
new QueryExpression(
'CONCAT('
. $this->db->quoteName('itemtype')
. ', ' . $this->db->quoteValue('::')
. ', ' . $this->db->quoteName('name')
. ') AS ' . $this->db->quoteName('task')
)
],
'FROM' => CronTask::getTable(),
'WHERE' => [
'state' => CronTask::STATE_RUNNING,
new QueryExpression(
'UNIX_TIMESTAMP(' . $this->db->quoteName('lastrun') . ') + ' . $delay
. ' < UNIX_TIMESTAMP(NOW())'
)
]
]
);
$crontask = new CronTask();
$unlocked_count = 0;
foreach ($task_iterator as $task) {
if (!$all && !in_array($task['task'], $tasks)) {
$output->writeln(
'<comment>' . sprintf(__('Task "%s" is still running but not in the whitelist.'), $task['task']) . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
continue;
}
$input = [
'id' => $task['id'],
'state' => CronTask::STATE_WAITING,
];
if ($crontask->update($input)) {
$unlocked_count++;
$message = sprintf(__('Task "%s" unlocked.'), $task['task']);
$output->writeln('<info>' . $message . '</info>');
Event::log($task['id'], 'CronTask', 5, 'Configuration', $message);
} else {
$output->writeln(
sprintf(
'<error>' . __('An error occurs while trying to unlock "%s" task.') . '</error>',
$task['task']
),
OutputInterface::VERBOSITY_QUIET
);
}
}
$output->writeln(
'<info>' .sprintf(__('Number of tasks unlocked: %d.'), $unlocked_count) . '</info>'
);
return 0; // Success
}
/**
* Validate command input.
*
* @param InputInterface $input
*
* @return void
*
* @throws InvalidArgumentException
*/
private function validateInput(InputInterface $input) {
$all = $input->getOption('all');
$cycle = $input->getOption('cycle');
$delay = $input->getOption('delay');
$tasks = $input->getOption('task');
if (null !== $cycle && null !== $delay) {
throw new InvalidArgumentException(
__('Option --cycle is not compatible with option --delay.')
);
}
if (null !== $cycle && !preg_match('/^\d+$/', $cycle)) {
throw new InvalidArgumentException(
__('Option --cycle has to be an integer.')
);
}
if (null !== $delay && !preg_match('/^\d+$/', $delay)) {
throw new InvalidArgumentException(
__('Option --delay has to be an integer.')
);
}
if (null === $cycle && null === $delay) {
$input->setOption('delay', 1800); // Default delay
}
if ($all && !empty($tasks)) {
throw new InvalidArgumentException(
__('Option --all is not compatible with option --task.')
);
}
if (!$all && empty($tasks)) {
throw new InvalidArgumentException(
__('You have to specify which tasks to unlock using --all or --task options.')
);
}
}
}