site-accueil-insa/matomo/plugins/CoreAdminHome/Commands/ConfigDelete.php

282 lines
12 KiB
PHP

<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreAdminHome\Commands;
use Piwik\Config;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Settings\FieldConfig;
use Piwik\Settings\Plugin\SystemConfigSetting;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigDelete extends ConsoleCommand
{
// Message output if no matching setting is found.
private const MSG_NOTHING_FOUND = 'Nothing found';
// Message output on success.
private const MSG_SUCCESS = 'Success: The setting has been deleted';
protected function configure()
{
$this->setName('config:delete');
$this->setDescription('Delete a config setting');
$this->addArgument(
'argument',
InputArgument::OPTIONAL,
"A config setting in the format Section.key or Section.array_key[], e.g. 'Database.username' or 'PluginsInstalled.PluginsInstalled.CustomDimensions'"
);
$this->addOption('section', 's', InputOption::VALUE_REQUIRED, 'The section the INI config setting belongs to.');
$this->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'The name of the INI config setting.');
$this->addOption('value', 'i', InputOption::VALUE_REQUIRED, 'For arrays, specify the array value to be deleted.');
$this->setHelp("This command can be used to delete a INI config setting.
You can delete config values per the two sections below, where:
- [Section] is the name of the section, e.g. database or General.
- [config_setting_name] the name of the setting, e.g. username.
- [array_value] For arrays, the specific array value to delete.
(1) By settings options --section=[Section] and --key=[config_setting_name], and optionally --value=[array_value]. Examples:
#Delete this setting.
$ ./console %command.name% --section=database --key=username
#Delete one value in an array:
$ ./console %command.name% --section=PluginsInstalled --key=PluginsInstalled --value=DevicesDetection
OR
(2) By using a command argument in the format [Section].[config_setting_name].[array_value]. Examples:
#Delete this setting.
$ ./console %command.name% 'database.username'
#Delete one value in an array:
$ ./console %command.name% 'PluginsInstalled.PluginsInstalled.DevicesDetection'
NOTES:
- Settings may still appear to exist if they are set in global.ini.php or elsewhere.
- Section names, key names, and array values are all case-sensitive; so e.g. --section=Database fails but --section=database works. Look in config.ini.php and global.ini.php for the proper case.
- If no matching section/setting is found, the string \"" . self::MSG_NOTHING_FOUND . "\" shows.
- For safety, this tool cannot be used to delete a whole section of settings or an array of values in a single command.
");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// Gather options, then discard ones that are empty so we do not need to check for empty later.
$options = array_filter([
'section' => $input->getOption('section'),
'key' => $input->getOption('key'),
'value' => $input->getOption('value'),
]);
$argument = trim($input->getArgument('argument') ?? '');
// Sanity check inputs.
switch (true) {
case empty($argument) && empty($options):
throw new \InvalidArgumentException('You must set either an argument or set options --section, --key and optional --value');
case (!empty($argument) && !empty($options)):
throw new \InvalidArgumentException('You cannot set both an argument (' . serialize($argument) . ') and options (' . serialize($argument) . ')');
case empty($argument) && (!isset($options['section']) || empty($options['section']) || !isset($options['key']) || empty($options['key'])):
throw new \InvalidArgumentException('When using options, --section and --key must be set');
case (!empty($argument)):
$settingStr = $argument;
break;
case (!empty($options)):
$settingStr = implode('.', $options);
break;
default:
// We should not get here, but just in case.
throw new \Exception('Some unexpected error occurred parsing input values');
}
// Convenience wrapper used to augment SystemConfigSetting without extending SystemConfigSetting or adding random properties to the instance.
$settingWrapped = (object) [
'setting' => null,
'isArray' => false,
'arrayVal' => '',
];
// Parse the $settingStr into a $settingWrapped object.
$settingWrapped = self::parseSettingStr($settingStr, $settingWrapped);
// Check the setting exists and user has permissions, then populates the $settingWrapped properties.
$settingWrapped = $this->checkAndPopulate($settingWrapped);
if (!isset($settingWrapped->setting) || empty($settingWrapped->setting)) {
$output->writeln(self::wrapInTag('comment', self::MSG_NOTHING_FOUND));
} else {
// Pass both static and array config items out to the delete logic.
$result = $this->deleteConfigSetting($settingWrapped);
if ($result) {
$output->writeln($this->wrapInTag('info', self::MSG_SUCCESS));
}
}
}
/**
* Check the setting exists and user has permissions, then return a new, populated SystemConfigSetting wrapper.
*
* @param object $settingWrapped A wrapped SystemConfigSetting object e.g. what is returned from parseSettingStr().
* @return object A new wrapped SystemConfigSetting object.
*/
private function checkAndPopulate(object $settingWrapped): object
{
// Sanity check inputs.
if (!($settingWrapped->setting instanceof SystemConfigSetting)) {
throw new \InvalidArgumentException('This function expects $settingWrapped->setting to be a SystemConfigSetting instance');
}
$config = Config::getInstance();
// Check the setting exists and user has permissions. If so, put it in the wrapper.
switch (true) {
case empty($sectionName = $settingWrapped->setting->getConfigSectionName()):
throw new \InvalidArgumentException('A section name must be specified');
case empty($settingName = $settingWrapped->setting->getName()):
throw new \InvalidArgumentException('A setting name must be specified');
case empty($section = $config->__get($sectionName)):
return new \stdClass();
case empty($section = (object) $section) || !isset($section->$settingName):
return new \stdClass();
default:
// We have a valid scalar or array setting in a valid section, so just fall out of the switch statement.
break;
}
$settingWrappedNew = clone($settingWrapped);
$settingWrappedNew->isArray = is_array($section->$settingName);
if (!$settingWrappedNew->isArray && !empty($settingWrappedNew->arrayVal)) {
throw new \InvalidArgumentException('This config setting is not an array');
}
if ($settingWrappedNew->isArray) {
if (empty($settingWrappedNew->arrayVal)) {
throw new \InvalidArgumentException('This config setting is an array, but no array value was specified for deletion');
}
if (false === array_search($settingWrappedNew->arrayVal, $section->$settingName)) {
return new \stdClass();
}
}
return $settingWrappedNew;
}
/**
* Delete a single config section.setting or section.setting[array_key].
*
* @param object $settingWrapped Wrapper around a setting object describing what to get e.g. from self::make().
* @return bool True on success. If the delete fails, throws an exception.
*/
private function deleteConfigSetting(object $settingWrapped): bool
{
// Sanity check inputs.
if (!($settingWrapped->setting instanceof SystemConfigSetting)) {
throw new \InvalidArgumentException('This function expects $settingWrapped->setting to be a SystemConfigSetting instance');
}
// Make easy shortcuts to some info.
$sectionName = $settingWrapped->setting->getConfigSectionName();
$settingName = $settingWrapped->setting->getName();
// Get the actual config section.
$config = Config::getInstance();
$section = $config->$sectionName;
$setting = $section[$settingName];
// Do the delete.
// This does not do the job with value=null or value='': $config->setSetting($sectionName, $settingName, $value).
switch (true) {
case $settingWrapped->isArray === true && empty($settingWrapped->arrayVal):
throw new \InvalidArgumentException('This function refuses to delete config arrays. See usage for how to delete config array values.');
case $settingWrapped->isArray === true:
// Array config values.
$key = array_search($settingWrapped->arrayVal, $setting);
if ($key !== false) {
unset($setting[$key]);
}
// Save the setting into the section.
$section[$settingName] = $setting;
break;
default:
// Scalar config values.
// Remove the setting from the section.
unset($section[$settingName]);
break;
}
// Save the section into the config.
$config->$sectionName = $section;
// Save the config.
$config->forceSave();
return true;
}
/**
* Build a SystemConfigSetting object from a string.
*
* @param string $settingStr Config setting string to parse.
* @return object A new wrapped SystemConfigSetting object.
*/
public static function parseSettingStr(string $settingStr, object $settingWrapped): object
{
$matches = [];
if (!preg_match('/^([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?(?:\[\])?(?:\.([a-zA-Z0-9_]+))?/', $settingStr, $matches) || empty($matches[1])) {
throw new \InvalidArgumentException("Invalid input string='{$settingStr}': expected section.name or section.name[]");
}
$settingName = $matches[2] ?? null;
$arrayVal = $matches[3] ?? null;
$systemConfigSetting = new SystemConfigSetting(
// Setting name.
$settingName,
// Default value.
'',
// Type.
FieldConfig::TYPE_STRING,
// Plugin name.
'core',
// Section name.
$matches[1]
);
$settingWrappedNew = clone($settingWrapped);
$settingWrappedNew->setting = $systemConfigSetting;
if ($settingWrappedNew->isArray = !empty($arrayVal)) {
$settingWrappedNew->arrayVal = $arrayVal;
}
return $settingWrappedNew;
}
/**
* Wrap the input string in an open and closing HTML/XML tag.
* E.g. wrap_in_tag('info', 'my string') returns '<info>my string</info>'
*
* @param string $tagname Tag name to wrap the string in.
* @param string $str String to wrap with the tag.
* @return string The wrapped string.
*/
public static function wrapInTag(string $tagname, string $str): string
{
return "<$tagname>$str</$tagname>";
}
}