1<?php
2
3declare(strict_types=1);
4
5namespace PhpMyAdmin\Controllers\Server;
6
7use PhpMyAdmin\Controllers\AbstractController;
8use PhpMyAdmin\DatabaseInterface;
9use PhpMyAdmin\Html\Generator;
10use PhpMyAdmin\Providers\ServerVariables\ServerVariablesProvider;
11use PhpMyAdmin\Response;
12use PhpMyAdmin\Template;
13use PhpMyAdmin\Url;
14use PhpMyAdmin\Util;
15use function header;
16use function htmlspecialchars;
17use function implode;
18use function in_array;
19use function is_numeric;
20use function mb_strtolower;
21use function pow;
22use function preg_match;
23use function str_replace;
24use function strtolower;
25use function trim;
26
27/**
28 * Handles viewing and editing server variables
29 */
30class VariablesController extends AbstractController
31{
32    /** @var DatabaseInterface */
33    private $dbi;
34
35    /**
36     * @param Response          $response
37     * @param DatabaseInterface $dbi
38     */
39    public function __construct($response, Template $template, $dbi)
40    {
41        parent::__construct($response, $template);
42        $this->dbi = $dbi;
43    }
44
45    public function index(): void
46    {
47        global $err_url;
48
49        $params = ['filter' => $_GET['filter'] ?? null];
50        $err_url = Url::getFromRoute('/');
51
52        if ($this->dbi->isSuperUser()) {
53            $this->dbi->selectDb('mysql');
54        }
55
56        $filterValue = ! empty($params['filter']) ? $params['filter'] : '';
57
58        $this->addScriptFiles(['server/variables.js']);
59
60        $variables = [];
61        $serverVarsResult = $this->dbi->tryQuery('SHOW SESSION VARIABLES;');
62        if ($serverVarsResult !== false) {
63            $serverVarsSession = [];
64            while ($arr = $this->dbi->fetchRow($serverVarsResult)) {
65                $serverVarsSession[$arr[0]] = $arr[1];
66            }
67            $this->dbi->freeResult($serverVarsResult);
68
69            $serverVars = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES;', 0, 1);
70
71            // list of static (i.e. non-editable) system variables
72            $staticVariables = ServerVariablesProvider::getImplementation()->getStaticVariables();
73
74            foreach ($serverVars as $name => $value) {
75                $hasSessionValue = isset($serverVarsSession[$name])
76                    && $serverVarsSession[$name] !== $value;
77                $docLink = Generator::linkToVarDocumentation(
78                    $name,
79                    $this->dbi->isMariaDB(),
80                    str_replace('_', '&nbsp;', $name)
81                );
82
83                [$formattedValue, $isEscaped] = $this->formatVariable($name, $value);
84                if ($hasSessionValue) {
85                    [$sessionFormattedValue] = $this->formatVariable(
86                        $name,
87                        $serverVarsSession[$name]
88                    );
89                }
90
91                $variables[] = [
92                    'name' => $name,
93                    'is_editable' => ! in_array(strtolower($name), $staticVariables),
94                    'doc_link' => $docLink,
95                    'value' => $formattedValue,
96                    'is_escaped' => $isEscaped,
97                    'has_session_value' => $hasSessionValue,
98                    'session_value' => $sessionFormattedValue ?? null,
99                ];
100            }
101        }
102
103        $this->render('server/variables/index', [
104            'variables' => $variables,
105            'filter_value' => $filterValue,
106            'is_superuser' => $this->dbi->isSuperUser(),
107            'is_mariadb' => $this->dbi->isMariaDB(),
108        ]);
109    }
110
111    /**
112     * Handle the AJAX request for a single variable value
113     *
114     * @param array $params Request parameters
115     */
116    public function getValue(array $params): void
117    {
118        if (! $this->response->isAjax()) {
119            return;
120        }
121
122        // Send with correct charset
123        header('Content-Type: text/html; charset=UTF-8');
124        // Do not use double quotes inside the query to avoid a problem
125        // when server is running in ANSI_QUOTES sql_mode
126        $varValue = $this->dbi->fetchSingleRow(
127            'SHOW GLOBAL VARIABLES WHERE Variable_name=\''
128            . $this->dbi->escapeString($params['name']) . '\';',
129            'NUM'
130        );
131
132        $json = [
133            'message' => $varValue[1],
134        ];
135
136        $variableType = ServerVariablesProvider::getImplementation()->getVariableType($params['name']);
137
138        if ($variableType === 'byte') {
139            $json['message'] = implode(
140                ' ',
141                Util::formatByteDown($varValue[1], 3, 3)
142            );
143        }
144
145        $this->response->addJSON($json);
146    }
147
148    /**
149     * Handle the AJAX request for setting value for a single variable
150     *
151     * @param array $vars Request parameters
152     */
153    public function setValue(array $vars): void
154    {
155        $params = [
156            'varName' => $vars['name'],
157            'varValue' => $_POST['varValue'] ?? null,
158        ];
159
160        if (! $this->response->isAjax()) {
161            return;
162        }
163
164        $value = (string) $params['varValue'];
165        $variableName = (string) $params['varName'];
166        $matches = [];
167        $variableType = ServerVariablesProvider::getImplementation()->getVariableType($variableName);
168
169        if ($variableType === 'byte' && preg_match(
170            '/^\s*(\d+(\.\d+)?)\s*(mb|kb|mib|kib|gb|gib)\s*$/i',
171            $value,
172            $matches
173        )) {
174            $exp = [
175                'kb' => 1,
176                'kib' => 1,
177                'mb' => 2,
178                'mib' => 2,
179                'gb' => 3,
180                'gib' => 3,
181            ];
182            $value = (float) $matches[1] * pow(
183                1024,
184                $exp[mb_strtolower($matches[3])]
185            );
186        } else {
187            $value = $this->dbi->escapeString($value);
188        }
189
190        if (! is_numeric($value)) {
191            $value = "'" . $value . "'";
192        }
193
194        $json = [];
195        if (! preg_match('/[^a-zA-Z0-9_]+/', $params['varName'])
196            && $this->dbi->query(
197                'SET GLOBAL ' . $params['varName'] . ' = ' . $value
198            )
199        ) {
200            // Some values are rounded down etc.
201            $varValue = $this->dbi->fetchSingleRow(
202                'SHOW GLOBAL VARIABLES WHERE Variable_name="'
203                . $this->dbi->escapeString($params['varName'])
204                . '";',
205                'NUM'
206            );
207            [$formattedValue, $isHtmlFormatted] = $this->formatVariable(
208                $params['varName'],
209                $varValue[1]
210            );
211
212            if ($isHtmlFormatted === false) {
213                $json['variable'] = htmlspecialchars($formattedValue);
214            } else {
215                $json['variable'] = $formattedValue;
216            }
217        } else {
218            $this->response->setRequestStatus(false);
219            $json['error'] = __('Setting variable failed');
220        }
221
222        $this->response->addJSON($json);
223    }
224
225    /**
226     * Format Variable
227     *
228     * @param string     $name  variable name
229     * @param int|string $value variable value
230     *
231     * @return array formatted string and bool if string is HTML formatted
232     */
233    private function formatVariable($name, $value): array
234    {
235        $isHtmlFormatted = false;
236        $formattedValue = $value;
237
238        if (is_numeric($value)) {
239            $variableType = ServerVariablesProvider::getImplementation()->getVariableType($name);
240
241            if ($variableType === 'byte') {
242                $isHtmlFormatted = true;
243                $formattedValue = trim(
244                    $this->template->render(
245                        'server/variables/format_variable',
246                        [
247                            'valueTitle' => Util::formatNumber($value, 0),
248                            'value' => implode(' ', Util::formatByteDown($value, 3, 3)),
249                        ]
250                    )
251                );
252            } else {
253                $formattedValue = Util::formatNumber($value, 0);
254            }
255        }
256
257        return [
258            $formattedValue,
259            $isHtmlFormatted,
260        ];
261    }
262}
263