Translated using Weblate (Portuguese (Brazil))
[phpmyadmin.git] / libraries / Advisor.php
blobc18d9a728b038268abe9588752c1b0b785e73314
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * A simple rules engine, that parses and executes the rules in advisory_rules.txt.
5 * Adjusted to phpMyAdmin.
7 * @package PhpMyAdmin
8 */
9 namespace PMA\libraries;
11 use \Exception;
12 use PMA\libraries\URL;
14 require_once 'libraries/advisor.lib.php';
16 /**
17 * Advisor class
19 * @package PhpMyAdmin
21 class Advisor
23 protected $variables;
24 protected $parseResult;
25 protected $runResult;
27 /**
28 * Get variables
30 * @return mixed
32 public function getVariables()
34 return $this->variables;
37 /**
38 * Set variables
40 * @param array $variables Variables
42 * @return Advisor
44 public function setVariables($variables)
46 $this->variables = $variables;
48 return $this;
51 /**
52 * Set a variable and its value
54 * @param string|int $variable Variable to set
55 * @param mixed $value Value to set
57 * @return $this
59 public function setVariable($variable, $value)
61 $this->variables[$variable] = $value;
63 return $this;
66 /**
67 * Get parseResult
69 * @return mixed
71 public function getParseResult()
73 return $this->parseResult;
76 /**
77 * Set parseResult
79 * @param array $parseResult Parse result
81 * @return Advisor
83 public function setParseResult($parseResult)
85 $this->parseResult = $parseResult;
87 return $this;
90 /**
91 * Get runResult
93 * @return mixed
95 public function getRunResult()
97 return $this->runResult;
101 * Set runResult
103 * @param array $runResult Run result
105 * @return Advisor
107 public function setRunResult($runResult)
109 $this->runResult = $runResult;
111 return $this;
115 * Parses and executes advisor rules
117 * @return array with run and parse results
119 public function run()
121 // HowTo: A simple Advisory system in 3 easy steps.
123 // Step 1: Get some variables to evaluate on
124 $this->setVariables(
125 array_merge(
126 $GLOBALS['dbi']->fetchResult('SHOW GLOBAL STATUS', 0, 1),
127 $GLOBALS['dbi']->fetchResult('SHOW GLOBAL VARIABLES', 0, 1)
131 // Add total memory to variables as well
132 include_once 'libraries/sysinfo.lib.php';
133 $sysinfo = PMA_getSysInfo();
134 $memory = $sysinfo->memory();
135 $this->variables['system_memory']
136 = isset($memory['MemTotal']) ? $memory['MemTotal'] : 0;
137 $this->variables['PMA_MYSQL_INT_VERSION'] = PMA_MYSQL_INT_VERSION;
139 // Step 2: Read and parse the list of rules
140 $this->setParseResult(static::parseRulesFile());
141 // Step 3: Feed the variables to the rules and let them fire. Sets
142 // $runResult
143 $this->runRules();
145 return array(
146 'parse' => array('errors' => $this->parseResult['errors']),
147 'run' => $this->runResult
152 * Stores current error in run results.
154 * @param string $description description of an error.
155 * @param Exception $exception exception raised
157 * @return void
159 public function storeError($description, $exception)
161 $this->runResult['errors'][] = $description
162 . ' '
163 . sprintf(
164 __('PHP threw following error: %s'),
165 $exception->getMessage()
170 * Executes advisor rules
172 * @return boolean
174 public function runRules()
176 $this->setRunResult(
177 array(
178 'fired' => array(),
179 'notfired' => array(),
180 'unchecked' => array(),
181 'errors' => array(),
185 foreach ($this->parseResult['rules'] as $rule) {
186 $this->variables['value'] = 0;
187 $precond = true;
189 if (isset($rule['precondition'])) {
190 try {
191 $precond = $this->ruleExprEvaluate($rule['precondition']);
192 } catch (Exception $e) {
193 $this->storeError(
194 sprintf(
195 __('Failed evaluating precondition for rule \'%s\'.'),
196 $rule['name']
200 continue;
204 if (! $precond) {
205 $this->addRule('unchecked', $rule);
206 } else {
207 try {
208 $value = $this->ruleExprEvaluate($rule['formula']);
209 } catch (Exception $e) {
210 $this->storeError(
211 sprintf(
212 __('Failed calculating value for rule \'%s\'.'),
213 $rule['name']
217 continue;
220 $this->variables['value'] = $value;
222 try {
223 if ($this->ruleExprEvaluate($rule['test'])) {
224 $this->addRule('fired', $rule);
225 } else {
226 $this->addRule('notfired', $rule);
228 } catch (Exception $e) {
229 $this->storeError(
230 sprintf(
231 __('Failed running test for rule \'%s\'.'),
232 $rule['name']
240 return true;
244 * Escapes percent string to be used in format string.
246 * @param string $str string to escape
248 * @return string
250 public static function escapePercent($str)
252 return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str);
256 * Wrapper function for translating.
258 * @param string $str the string
259 * @param string $param the parameters
261 * @return string
263 public function translate($str, $param = null)
265 $string = _gettext(self::escapePercent($str));
266 if (! is_null($param)) {
267 $params = $this->ruleExprEvaluate('array(' . $param . ')');
268 } else {
269 $params = array();
271 return vsprintf($string, $params);
275 * Splits justification to text and formula.
277 * @param array $rule the rule
279 * @return string[]
281 public static function splitJustification($rule)
283 $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2);
284 if (count($jst) > 1) {
285 return array($jst[0], $jst[1]);
287 return array($rule['justification']);
291 * Adds a rule to the result list
293 * @param string $type type of rule
294 * @param array $rule rule itself
296 * @return void
298 public function addRule($type, $rule)
300 switch ($type) {
301 case 'notfired':
302 case 'fired':
303 $jst = self::splitJustification($rule);
304 if (count($jst) > 1) {
305 try {
306 /* Translate */
307 $str = $this->translate($jst[0], $jst[1]);
308 } catch (Exception $e) {
309 $this->storeError(
310 sprintf(
311 __('Failed formatting string for rule \'%s\'.'),
312 $rule['name']
316 return;
319 $rule['justification'] = $str;
320 } else {
321 $rule['justification'] = $this->translate($rule['justification']);
323 $rule['id'] = $rule['name'];
324 $rule['name'] = $this->translate($rule['name']);
325 $rule['issue'] = $this->translate($rule['issue']);
327 // Replaces {server_variable} with 'server_variable'
328 // linking to server_variables.php
329 $rule['recommendation'] = preg_replace_callback(
330 '/\{([a-z_0-9]+)\}/Ui',
331 array($this, 'replaceVariable'),
332 $this->translate($rule['recommendation'])
335 // Replaces external Links with PMA_linkURL() generated links
336 $rule['recommendation'] = preg_replace_callback(
337 '#href=("|\')(https?://[^\1]+)\1#i',
338 array($this, 'replaceLinkURL'),
339 $rule['recommendation']
341 break;
344 $this->runResult[$type][] = $rule;
348 * Callback for wrapping links with PMA_linkURL
350 * @param array $matches List of matched elements form preg_replace_callback
352 * @return string Replacement value
354 private function replaceLinkURL($matches)
356 return 'href="' . PMA_linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"';
360 * Callback for wrapping variable edit links
362 * @param array $matches List of matched elements form preg_replace_callback
364 * @return string Replacement value
366 private function replaceVariable($matches)
368 return '<a href="server_variables.php' . URL::getCommon(array('filter' => $matches[1]))
369 . '">' . htmlspecialchars($matches[1]). '</a>';
373 * Callback for evaluating fired() condition.
375 * @param array $matches List of matched elements form preg_replace_callback
377 * @return string Replacement value
379 private function ruleExprEvaluateFired($matches)
381 // No list of fired rules
382 if (!isset($this->runResult['fired'])) {
383 return '0';
386 // Did matching rule fire?
387 foreach ($this->runResult['fired'] as $rule) {
388 if ($rule['id'] == $matches[2]) {
389 return '1';
393 return '0';
397 * Callback for evaluating variables in expression.
399 * @param array $matches List of matched elements form preg_replace_callback
401 * @return string Replacement value
403 private function ruleExprEvaluateVariable($matches)
405 $match = $matches[1];
406 /* Numbers */
407 if (is_numeric($match)) {
408 return $match;
410 /* Functions */
411 $functions = array(
412 'round', 'substr', 'preg_match', 'array',
413 'ADVISOR_bytime', 'ADVISOR_timespanFormat',
414 'ADVISOR_formatByteDown'
416 if (in_array($match, $functions)) {
417 return $match;
419 /* Unknown variable */
420 if (! isset($this->variables[$match])) {
421 return $match;
423 /* Variable value */
424 if (is_numeric($this->variables[$match])) {
425 return $this->variables[$match];
426 } else {
427 return '\'' . addslashes($this->variables[$match]) . '\'';
432 * Runs a code expression, replacing variable names with their respective
433 * values
435 * @param string $expr expression to evaluate
437 * @return integer result of evaluated expression
439 * @throws Exception
441 public function ruleExprEvaluate($expr)
443 // Evaluate fired() conditions
444 $expr = preg_replace_callback(
445 '/fired\s*\(\s*(\'|")(.*)\1\s*\)/Ui',
446 array($this, 'ruleExprEvaluateFired'),
447 $expr
449 // Evaluate variables
450 $expr = preg_replace_callback(
451 '/\b(\w+)\b/',
452 array($this, 'ruleExprEvaluateVariable'),
453 $expr
455 $value = 0;
456 $err = 0;
458 // Actually evaluate the code
459 ob_start();
460 try {
461 // TODO: replace by using symfony/expression-language
462 eval('$value = ' . $expr . ';');
463 $err = ob_get_contents();
464 } catch (Exception $e) {
465 // In normal operation, there is just output in the buffer,
466 // but when running under phpunit, error in eval raises exception
467 $err = $e->getMessage();
469 ob_end_clean();
471 // Error handling
472 if ($err) {
473 // Remove PHP 7.2 and newer notice (it's not really interesting for user)
474 throw new Exception(
475 str_replace(
476 ' (this will throw an Error in a future version of PHP)',
478 strip_tags($err)
480 . '<br />Executed code: $value = ' . htmlspecialchars($expr) . ';'
483 return $value;
487 * Reads the rule file into an array, throwing errors messages on syntax
488 * errors.
490 * @return array with parsed data
492 public static function parseRulesFile()
494 $filename = 'libraries/advisory_rules.txt';
495 $file = file($filename, FILE_IGNORE_NEW_LINES);
497 $errors = array();
498 $rules = array();
499 $lines = array();
501 if ($file === FALSE) {
502 $errors[] = sprintf(
503 __('Error in reading file: The file \'%s\' does not exist or is not readable!'),
504 $filename
506 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
509 $ruleSyntax = array(
510 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
512 $numRules = count($ruleSyntax);
513 $numLines = count($file);
514 $ruleNo = -1;
515 $ruleLine = -1;
517 for ($i = 0; $i < $numLines; $i++) {
518 $line = $file[$i];
519 if ($line == "" || $line[0] == '#') {
520 continue;
523 // Reading new rule
524 if (substr($line, 0, 4) == 'rule') {
525 if ($ruleLine > 0) {
526 $errors[] = sprintf(
528 'Invalid rule declaration on line %1$s, expected line '
529 . '%2$s of previous rule.'
531 $i + 1,
532 $ruleSyntax[$ruleLine++]
534 continue;
536 if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
537 $ruleLine = 1;
538 $ruleNo++;
539 $rules[$ruleNo] = array('name' => $match[1]);
540 $lines[$ruleNo] = array('name' => $i + 1);
541 if (isset($match[3])) {
542 $rules[$ruleNo]['precondition'] = $match[3];
543 $lines[$ruleNo]['precondition'] = $i + 1;
545 } else {
546 $errors[] = sprintf(
547 __('Invalid rule declaration on line %s.'),
548 $i + 1
551 continue;
552 } else {
553 if ($ruleLine == -1) {
554 $errors[] = sprintf(
555 __('Unexpected characters on line %s.'),
556 $i + 1
561 // Reading rule lines
562 if ($ruleLine > 0) {
563 if (!isset($line[0])) {
564 continue; // Empty lines are ok
566 // Non tabbed lines are not
567 if ($line[0] != "\t") {
568 $errors[] = sprintf(
570 'Unexpected character on line %1$s. Expected tab, but '
571 . 'found "%2$s".'
573 $i + 1,
574 $line[0]
576 continue;
578 $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
579 mb_substr($line, 1)
581 $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1;
582 ++$ruleLine;
585 // Rule complete
586 if ($ruleLine == $numRules) {
587 $ruleLine = -1;
591 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);