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;
138 // Step 2: Read and parse the list of rules
139 $this->setParseResult(static::parseRulesFile());
140 // Step 3: Feed the variables to the rules and let them fire. Sets
145 'parse' => array('errors' => $this->parseResult
['errors']),
146 'run' => $this->runResult
151 * Stores current error in run results.
153 * @param string $description description of an error.
154 * @param Exception $exception exception raised
158 public function storeError($description, $exception)
160 $this->runResult
['errors'][] = $description
163 __('PHP threw following error: %s'),
164 $exception->getMessage()
169 * Executes advisor rules
173 public function runRules()
178 'notfired' => array(),
179 'unchecked' => array(),
184 foreach ($this->parseResult
['rules'] as $rule) {
185 $this->variables
['value'] = 0;
188 if (isset($rule['precondition'])) {
190 $precond = $this->ruleExprEvaluate($rule['precondition']);
191 } catch (Exception
$e) {
194 __('Failed evaluating precondition for rule \'%s\'.'),
204 $this->addRule('unchecked', $rule);
207 $value = $this->ruleExprEvaluate($rule['formula']);
208 } catch (Exception
$e) {
211 __('Failed calculating value for rule \'%s\'.'),
219 $this->variables
['value'] = $value;
222 if ($this->ruleExprEvaluate($rule['test'])) {
223 $this->addRule('fired', $rule);
225 $this->addRule('notfired', $rule);
227 } catch (Exception
$e) {
230 __('Failed running test for rule \'%s\'.'),
243 * Escapes percent string to be used in format string.
245 * @param string $str string to escape
249 public static function escapePercent($str)
251 return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str);
255 * Wrapper function for translating.
257 * @param string $str the string
258 * @param string $param the parameters
262 public function translate($str, $param = null)
264 $string = _gettext(self
::escapePercent($str));
265 if (! is_null($param)) {
266 $params = $this->ruleExprEvaluate('array(' . $param . ')');
270 return vsprintf($string, $params);
274 * Splits justification to text and formula.
276 * @param array $rule the rule
280 public static function splitJustification($rule)
282 $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2);
283 if (count($jst) > 1) {
284 return array($jst[0], $jst[1]);
286 return array($rule['justification']);
290 * Adds a rule to the result list
292 * @param string $type type of rule
293 * @param array $rule rule itself
297 public function addRule($type, $rule)
302 $jst = self
::splitJustification($rule);
303 if (count($jst) > 1) {
306 $str = $this->translate($jst[0], $jst[1]);
307 } catch (Exception
$e) {
310 __('Failed formatting string for rule \'%s\'.'),
318 $rule['justification'] = $str;
320 $rule['justification'] = $this->translate($rule['justification']);
322 $rule['id'] = $rule['name'];
323 $rule['name'] = $this->translate($rule['name']);
324 $rule['issue'] = $this->translate($rule['issue']);
326 // Replaces {server_variable} with 'server_variable'
327 // linking to server_variables.php
328 $rule['recommendation'] = preg_replace(
329 '/\{([a-z_0-9]+)\}/Ui',
330 '<a href="server_variables.php' . URL
::getCommon()
331 . '&filter=\1">\1</a>',
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 evaluating fired() condition.
362 * @param array $matches List of matched elements form preg_replace_callback
364 * @return string Replacement value
366 private function ruleExprEvaluateFired($matches)
368 // No list of fired rules
369 if (!isset($this->runResult
['fired'])) {
373 // Did matching rule fire?
374 foreach ($this->runResult
['fired'] as $rule) {
375 if ($rule['id'] == $matches[2]) {
384 * Callback for evaluating variables in expression.
386 * @param array $matches List of matched elements form preg_replace_callback
388 * @return string Replacement value
390 private function ruleExprEvaluateVariable($matches)
392 if (! isset($this->variables
[$matches[1]])) {
395 if (is_numeric($this->variables
[$matches[1]])) {
396 return $this->variables
[$matches[1]];
398 return '\'' . addslashes($this->variables
[$matches[1]]) . '\'';
403 * Runs a code expression, replacing variable names with their respective
406 * @param string $expr expression to evaluate
408 * @return integer result of evaluated expression
412 public function ruleExprEvaluate($expr)
414 // Evaluate fired() conditions
415 $expr = preg_replace_callback(
416 '/fired\s*\(\s*(\'|")(.*)\1\s*\)/Ui',
417 array($this, 'ruleExprEvaluateFired'),
420 // Evaluate variables
421 $expr = preg_replace_callback(
423 array($this, 'ruleExprEvaluateVariable'),
429 // Actually evaluate the code
432 // TODO: replace by using symfony/expression-language
433 eval('$value = ' . $expr . ';');
434 $err = ob_get_contents();
435 } catch (Exception
$e) {
436 // In normal operation, there is just output in the buffer,
437 // but when running under phpunit, error in eval raises exception
438 $err = $e->getMessage();
446 . '<br />Executed code: $value = ' . htmlspecialchars($expr) . ';'
453 * Reads the rule file into an array, throwing errors messages on syntax
456 * @return array with parsed data
458 public static function parseRulesFile()
460 $filename = 'libraries/advisory_rules.txt';
461 $file = file($filename, FILE_IGNORE_NEW_LINES
);
467 if ($file === FALSE) {
469 __('Error in reading file: The file \'%s\' does not exist or is not readable!'),
472 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
476 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
478 $numRules = count($ruleSyntax);
479 $numLines = count($file);
483 for ($i = 0; $i < $numLines; $i++
) {
485 if ($line == "" ||
$line[0] == '#') {
490 if (substr($line, 0, 4) == 'rule') {
494 'Invalid rule declaration on line %1$s, expected line '
495 . '%2$s of previous rule.'
498 $ruleSyntax[$ruleLine++
]
502 if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
505 $rules[$ruleNo] = array('name' => $match[1]);
506 $lines[$ruleNo] = array('name' => $i +
1);
507 if (isset($match[3])) {
508 $rules[$ruleNo]['precondition'] = $match[3];
509 $lines[$ruleNo]['precondition'] = $i +
1;
513 __('Invalid rule declaration on line %s.'),
519 if ($ruleLine == -1) {
521 __('Unexpected characters on line %s.'),
527 // Reading rule lines
529 if (!isset($line[0])) {
530 continue; // Empty lines are ok
532 // Non tabbed lines are not
533 if ($line[0] != "\t") {
536 'Unexpected character on line %1$s. Expected tab, but '
544 $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
547 $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i +
1;
552 if ($ruleLine == $numRules) {
557 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);