Welcome to mirror list, hosted at ThFree Co, Russian Federation.

ConfigDelete.php « Commands « CoreAdminHome « plugins - github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 3e72bdce54f7dd140f5562df5698cfa2e14284ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
<?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>";
    }
}