3 declare(strict_types
=1);
7 use PhpMyAdmin\Server\SysInfo\SysInfo
;
8 use Symfony\Component\ExpressionLanguage\ExpressionLanguage
;
10 use function array_merge
;
11 use function htmlspecialchars
;
14 use function preg_match
;
15 use function preg_replace_callback
;
20 use function vsprintf
;
23 * A simple rules engine, that executes the rules in the advisory_rules files.
27 private const GENERIC_RULES_FILE
= 'libraries/advisory_rules_generic.php';
28 private const BEFORE_MYSQL80003_RULES_FILE
= 'libraries/advisory_rules_mysql_before80003.php';
30 /** @var DatabaseInterface */
45 /** @var ExpressionLanguage */
49 * @param DatabaseInterface $dbi DatabaseInterface object
50 * @param ExpressionLanguage $expression ExpressionLanguage object
52 public function __construct(DatabaseInterface
$dbi, ExpressionLanguage
$expression)
55 $this->expression
= $expression;
57 * Register functions for ExpressionLanguage, we intentionally
58 * do not implement support for compile as we do not use it.
60 $this->expression
->register(
65 * @param array $arguments
68 static function ($arguments, $num) {
72 $this->expression
->register(
77 * @param array $arguments
78 * @param string $string
82 static function ($arguments, $string, $start, $length) {
83 return substr($string, $start, $length);
86 $this->expression
->register(
91 * @param array $arguments
92 * @param string $pattern
93 * @param string $subject
95 static function ($arguments, $pattern, $subject) {
96 return preg_match($pattern, $subject);
99 $this->expression
->register(
104 * @param array $arguments
106 * @param int $precision
108 static function ($arguments, $num, $precision) {
109 return self
::byTime($num, $precision);
112 $this->expression
->register(
113 'ADVISOR_timespanFormat',
117 * @param array $arguments
118 * @param string $seconds
120 static function ($arguments, $seconds) {
121 return Util
::timespanFormat((int) $seconds);
124 $this->expression
->register(
125 'ADVISOR_formatByteDown',
129 * @param array $arguments
134 static function ($arguments, $value, $limes = 6, $comma = 0) {
135 return implode(' ', (array) Util
::formatByteDown($value, $limes, $comma));
138 $this->expression
->register(
143 * @param array $arguments
146 function ($arguments, $value) {
147 if (! isset($this->runResult
['fired'])) {
151 // Did matching rule fire?
152 foreach ($this->runResult
['fired'] as $rule) {
153 if ($rule['id'] == $value) {
161 /* Some global variables for advisor */
163 'PMA_MYSQL_INT_VERSION' => $this->dbi
->getVersion(),
167 private function setVariables(): void
169 $globalStatus = $this->dbi
->fetchResult('SHOW GLOBAL STATUS', 0, 1);
170 $globalVariables = $this->dbi
->fetchResult('SHOW GLOBAL VARIABLES', 0, 1);
172 $sysInfo = SysInfo
::get();
173 $memory = $sysInfo->memory();
174 $systemMemory = ['system_memory' => $memory['MemTotal'] ??
0];
176 $this->variables
= array_merge($globalStatus, $globalVariables, $systemMemory);
180 * @param string|int $variable Variable to set
181 * @param mixed $value Value to set
183 public function setVariable($variable, $value): void
185 $this->variables
[$variable] = $value;
188 private function setRules(): void
190 $isMariaDB = strpos($this->variables
['version'], 'MariaDB') !== false;
191 $genericRules = include ROOT_PATH
. self
::GENERIC_RULES_FILE
;
193 if (! $isMariaDB && $this->globals
['PMA_MYSQL_INT_VERSION'] >= 80003) {
194 $this->rules
= $genericRules;
199 $extraRules = include ROOT_PATH
. self
::BEFORE_MYSQL80003_RULES_FILE
;
200 $this->rules
= array_merge($genericRules, $extraRules);
206 public function getRunResult(): array
208 return $this->runResult
;
214 public function run(): array
216 $this->setVariables();
220 return $this->runResult
;
224 * Stores current error in run results.
226 * @param string $description description of an error.
227 * @param Throwable $exception exception raised
229 private function storeError(string $description, Throwable
$exception): void
231 $this->runResult
['errors'][] = $description . ' ' . sprintf(
232 __('Error when evaluating: %s'),
233 $exception->getMessage()
238 * Executes advisor rules
240 private function runRules(): void
249 foreach ($this->rules
as $rule) {
250 $this->variables
['value'] = 0;
251 $precondition = true;
253 if (isset($rule['precondition'])) {
255 $precondition = $this->evaluateRuleExpression($rule['precondition']);
256 } catch (Throwable
$e) {
259 __('Failed evaluating precondition for rule \'%s\'.'),
268 if (! $precondition) {
269 $this->addRule('unchecked', $rule);
275 $value = $this->evaluateRuleExpression($rule['formula']);
276 } catch (Throwable
$e) {
279 __('Failed calculating value for rule \'%s\'.'),
287 $this->variables
['value'] = $value;
290 if ($this->evaluateRuleExpression($rule['test'])) {
291 $this->addRule('fired', $rule);
293 $this->addRule('notfired', $rule);
295 } catch (Throwable
$e) {
298 __('Failed running test for rule \'%s\'.'),
308 * Adds a rule to the result list
310 * @param string $type type of rule
311 * @param array $rule rule itself
313 public function addRule(string $type, array $rule): void
315 if ($type !== 'notfired' && $type !== 'fired') {
316 $this->runResult
[$type][] = $rule;
321 if (isset($rule['justification_formula'])) {
323 $params = $this->evaluateRuleExpression('[' . $rule['justification_formula'] . ']');
324 } catch (Throwable
$e) {
326 sprintf(__('Failed formatting string for rule \'%s\'.'), $rule['name']),
333 $rule['justification'] = vsprintf($rule['justification'], $params);
336 // Replaces {server_variable} with 'server_variable'
337 // linking to /server/variables
338 $rule['recommendation'] = preg_replace_callback(
339 '/\{([a-z_0-9]+)\}/Ui',
340 function (array $matches) {
341 return $this->replaceVariable($matches);
343 $rule['recommendation']
346 // Replaces external Links with Core::linkURL() generated links
347 $rule['recommendation'] = preg_replace_callback(
348 '#href=("|\')(https?://[^\1]+)\1#i',
349 function (array $matches) {
350 return $this->replaceLinkURL($matches);
352 $rule['recommendation']
355 $this->runResult
[$type][] = $rule;
359 * Callback for wrapping links with Core::linkURL
361 * @param array $matches List of matched elements form preg_replace_callback
363 * @return string Replacement value
365 private function replaceLinkURL(array $matches): string
367 return 'href="' . Core
::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"';
371 * Callback for wrapping variable edit links
373 * @param array $matches List of matched elements form preg_replace_callback
375 * @return string Replacement value
377 private function replaceVariable(array $matches): string
379 return '<a href="' . Url
::getFromRoute('/server/variables', ['filter' => $matches[1]])
380 . '">' . htmlspecialchars($matches[1]) . '</a>';
384 * Runs a code expression, replacing variable names with their respective values
386 * @return mixed result of evaluated expression
388 private function evaluateRuleExpression(string $expression)
390 return $this->expression
->evaluate($expression, array_merge($this->variables
, $this->globals
));
394 * Formats interval like 10 per hour
396 * @param float $num number to format
397 * @param int $precision required precision
399 * @return string formatted string
401 public static function byTime(float $num, int $precision): string
403 if ($num >= 1) { // per second
404 $per = __('per second');
405 } elseif ($num * 60 >= 1) { // per minute
407 $per = __('per minute');
408 } elseif ($num * 60 * 60 >= 1) { // per hour
410 $per = __('per hour');
412 $num *= 24 * 60 * 60;
413 $per = __('per day');
416 $num = round($num, $precision);
419 $num = '<' . pow(10, -$precision);
422 return $num . ' ' . $per;