Translated using Weblate (Slovenian)
[phpmyadmin.git] / libraries / classes / Advisor.php
bloba3981a0e46bb317442d503a52e91be72fa00e959
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\Server\SysInfo\SysInfo;
8 use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
9 use Throwable;
10 use function array_merge;
11 use function htmlspecialchars;
12 use function implode;
13 use function pow;
14 use function preg_match;
15 use function preg_replace_callback;
16 use function round;
17 use function sprintf;
18 use function strpos;
19 use function substr;
20 use function vsprintf;
22 /**
23 * A simple rules engine, that executes the rules in the advisory_rules files.
25 class Advisor
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 */
31 private $dbi;
33 /** @var array */
34 private $variables;
36 /** @var array */
37 private $globals;
39 /** @var array */
40 private $rules;
42 /** @var array */
43 private $runResult;
45 /** @var ExpressionLanguage */
46 private $expression;
48 /**
49 * @param DatabaseInterface $dbi DatabaseInterface object
50 * @param ExpressionLanguage $expression ExpressionLanguage object
52 public function __construct(DatabaseInterface $dbi, ExpressionLanguage $expression)
54 $this->dbi = $dbi;
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(
61 'round',
62 static function () {
64 /**
65 * @param array $arguments
66 * @param float $num
68 static function ($arguments, $num) {
69 return round($num);
72 $this->expression->register(
73 'substr',
74 static function () {
76 /**
77 * @param array $arguments
78 * @param string $string
79 * @param int $start
80 * @param int $length
82 static function ($arguments, $string, $start, $length) {
83 return substr($string, $start, $length);
86 $this->expression->register(
87 'preg_match',
88 static function () {
90 /**
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(
100 'ADVISOR_bytime',
101 static function () {
104 * @param array $arguments
105 * @param float $num
106 * @param int $precision
108 static function ($arguments, $num, $precision) {
109 return self::byTime($num, $precision);
112 $this->expression->register(
113 'ADVISOR_timespanFormat',
114 static function () {
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',
126 static function () {
129 * @param array $arguments
130 * @param int $value
131 * @param int $limes
132 * @param int $comma
134 static function ($arguments, $value, $limes = 6, $comma = 0) {
135 return implode(' ', (array) Util::formatByteDown($value, $limes, $comma));
138 $this->expression->register(
139 'fired',
140 static function () {
143 * @param array $arguments
144 * @param int $value
146 function ($arguments, $value) {
147 if (! isset($this->runResult['fired'])) {
148 return 0;
151 // Did matching rule fire?
152 foreach ($this->runResult['fired'] as $rule) {
153 if ($rule['id'] == $value) {
154 return '1';
158 return '0';
161 /* Some global variables for advisor */
162 $this->globals = [
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;
196 return;
199 $extraRules = include ROOT_PATH . self::BEFORE_MYSQL80003_RULES_FILE;
200 $this->rules = array_merge($genericRules, $extraRules);
204 * @return array
206 public function getRunResult(): array
208 return $this->runResult;
212 * @return array
214 public function run(): array
216 $this->setVariables();
217 $this->setRules();
218 $this->runRules();
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
242 $this->runResult = [
243 'fired' => [],
244 'notfired' => [],
245 'unchecked' => [],
246 'errors' => [],
249 foreach ($this->rules as $rule) {
250 $this->variables['value'] = 0;
251 $precondition = true;
253 if (isset($rule['precondition'])) {
254 try {
255 $precondition = $this->evaluateRuleExpression($rule['precondition']);
256 } catch (Throwable $e) {
257 $this->storeError(
258 sprintf(
259 __('Failed evaluating precondition for rule \'%s\'.'),
260 $rule['name']
264 continue;
268 if (! $precondition) {
269 $this->addRule('unchecked', $rule);
271 continue;
274 try {
275 $value = $this->evaluateRuleExpression($rule['formula']);
276 } catch (Throwable $e) {
277 $this->storeError(
278 sprintf(
279 __('Failed calculating value for rule \'%s\'.'),
280 $rule['name']
284 continue;
287 $this->variables['value'] = $value;
289 try {
290 if ($this->evaluateRuleExpression($rule['test'])) {
291 $this->addRule('fired', $rule);
292 } else {
293 $this->addRule('notfired', $rule);
295 } catch (Throwable $e) {
296 $this->storeError(
297 sprintf(
298 __('Failed running test for rule \'%s\'.'),
299 $rule['name']
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;
318 return;
321 if (isset($rule['justification_formula'])) {
322 try {
323 $params = $this->evaluateRuleExpression('[' . $rule['justification_formula'] . ']');
324 } catch (Throwable $e) {
325 $this->storeError(
326 sprintf(__('Failed formatting string for rule \'%s\'.'), $rule['name']),
330 return;
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
406 $num *= 60;
407 $per = __('per minute');
408 } elseif ($num * 60 * 60 >= 1) { // per hour
409 $num *= 60 * 60;
410 $per = __('per hour');
411 } else {
412 $num *= 24 * 60 * 60;
413 $per = __('per day');
416 $num = round($num, $precision);
418 if ($num == 0) {
419 $num = '<' . pow(10, -$precision);
422 return $num . ' ' . $per;