2 /* vim: set expandtab sw=4 ts=4 sts=4: */
4 * A simple rules engine, that parses and executes the rules in advisory_rules.txt.
5 * Adjusted to phpMyAdmin.
9 namespace PMA\libraries
;
12 use PMA\libraries\URL
;
14 require_once 'libraries/advisor.lib.php';
24 protected $parseResult;
32 public function getVariables()
34 return $this->variables
;
40 * @param array $variables Variables
44 public function setVariables($variables)
46 $this->variables
= $variables;
52 * Set a variable and its value
54 * @param string|int $variable Variable to set
55 * @param mixed $value Value to set
59 public function setVariable($variable, $value)
61 $this->variables
[$variable] = $value;
71 public function getParseResult()
73 return $this->parseResult
;
79 * @param array $parseResult Parse result
83 public function setParseResult($parseResult)
85 $this->parseResult
= $parseResult;
95 public function getRunResult()
97 return $this->runResult
;
103 * @param array $runResult Run result
107 public function setRunResult($runResult)
109 $this->runResult
= $runResult;
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
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
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
159 public function storeError($description, $exception)
161 $this->runResult
['errors'][] = $description
164 __('PHP threw following error: %s'),
165 $exception->getMessage()
170 * Executes advisor rules
174 public function runRules()
179 'notfired' => array(),
180 'unchecked' => array(),
185 foreach ($this->parseResult
['rules'] as $rule) {
186 $this->variables
['value'] = 0;
189 if (isset($rule['precondition'])) {
191 $precond = $this->ruleExprEvaluate($rule['precondition']);
192 } catch (Exception
$e) {
195 __('Failed evaluating precondition for rule \'%s\'.'),
205 $this->addRule('unchecked', $rule);
208 $value = $this->ruleExprEvaluate($rule['formula']);
209 } catch (Exception
$e) {
212 __('Failed calculating value for rule \'%s\'.'),
220 $this->variables
['value'] = $value;
223 if ($this->ruleExprEvaluate($rule['test'])) {
224 $this->addRule('fired', $rule);
226 $this->addRule('notfired', $rule);
228 } catch (Exception
$e) {
231 __('Failed running test for rule \'%s\'.'),
244 * Escapes percent string to be used in format string.
246 * @param string $str string to escape
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
263 public function translate($str, $param = null)
265 $string = _gettext(self
::escapePercent($str));
266 if (! is_null($param)) {
267 $params = $this->ruleExprEvaluate('array(' . $param . ')');
271 return vsprintf($string, $params);
275 * Splits justification to text and formula.
277 * @param array $rule the rule
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
298 public function addRule($type, $rule)
303 $jst = self
::splitJustification($rule);
304 if (count($jst) > 1) {
307 $str = $this->translate($jst[0], $jst[1]);
308 } catch (Exception
$e) {
311 __('Failed formatting string for rule \'%s\'.'),
319 $rule['justification'] = $str;
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']
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'])) {
386 // Did matching rule fire?
387 foreach ($this->runResult
['fired'] as $rule) {
388 if ($rule['id'] == $matches[2]) {
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];
407 if (is_numeric($match)) {
412 'round', 'substr', 'preg_match', 'array',
413 'ADVISOR_bytime', 'ADVISOR_timespanFormat',
414 'ADVISOR_formatByteDown'
416 if (in_array($match, $functions)) {
419 /* Unknown variable */
420 if (! isset($this->variables
[$match])) {
424 if (is_numeric($this->variables
[$match])) {
425 return $this->variables
[$match];
427 return '\'' . addslashes($this->variables
[$match]) . '\'';
432 * Runs a code expression, replacing variable names with their respective
435 * @param string $expr expression to evaluate
437 * @return integer result of evaluated expression
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'),
449 // Evaluate variables
450 $expr = preg_replace_callback(
452 array($this, 'ruleExprEvaluateVariable'),
458 // Actually evaluate the code
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();
473 // Remove PHP 7.2 and newer notice (it's not really interesting for user)
476 ' (this will throw an Error in a future version of PHP)',
480 . '<br />Executed code: $value = ' . htmlspecialchars($expr) . ';'
487 * Reads the rule file into an array, throwing errors messages on syntax
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
);
501 if ($file === FALSE) {
503 __('Error in reading file: The file \'%s\' does not exist or is not readable!'),
506 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
510 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
512 $numRules = count($ruleSyntax);
513 $numLines = count($file);
517 for ($i = 0; $i < $numLines; $i++
) {
519 if ($line == "" ||
$line[0] == '#') {
524 if (substr($line, 0, 4) == 'rule') {
528 'Invalid rule declaration on line %1$s, expected line '
529 . '%2$s of previous rule.'
532 $ruleSyntax[$ruleLine++
]
536 if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
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;
547 __('Invalid rule declaration on line %s.'),
553 if ($ruleLine == -1) {
555 __('Unexpected characters on line %s.'),
561 // Reading rule lines
563 if (!isset($line[0])) {
564 continue; // Empty lines are ok
566 // Non tabbed lines are not
567 if ($line[0] != "\t") {
570 'Unexpected character on line %1$s. Expected tab, but '
578 $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
581 $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i +
1;
586 if ($ruleLine == $numRules) {
591 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);