Translated using Weblate (Slovenian)
[phpmyadmin.git] / libraries / Advisor.php
blob17db830e7cdcb2d7fd5262e7532fbd91a3efb511
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;
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
141 // $runResult
142 $this->runRules();
144 return array(
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
156 * @return void
158 public function storeError($description, $exception)
160 $this->runResult['errors'][] = $description
161 . ' '
162 . sprintf(
163 __('PHP threw following error: %s'),
164 $exception->getMessage()
169 * Executes advisor rules
171 * @return boolean
173 public function runRules()
175 $this->setRunResult(
176 array(
177 'fired' => array(),
178 'notfired' => array(),
179 'unchecked' => array(),
180 'errors' => array(),
184 foreach ($this->parseResult['rules'] as $rule) {
185 $this->variables['value'] = 0;
186 $precond = true;
188 if (isset($rule['precondition'])) {
189 try {
190 $precond = $this->ruleExprEvaluate($rule['precondition']);
191 } catch (Exception $e) {
192 $this->storeError(
193 sprintf(
194 __('Failed evaluating precondition for rule \'%s\'.'),
195 $rule['name']
199 continue;
203 if (! $precond) {
204 $this->addRule('unchecked', $rule);
205 } else {
206 try {
207 $value = $this->ruleExprEvaluate($rule['formula']);
208 } catch (Exception $e) {
209 $this->storeError(
210 sprintf(
211 __('Failed calculating value for rule \'%s\'.'),
212 $rule['name']
216 continue;
219 $this->variables['value'] = $value;
221 try {
222 if ($this->ruleExprEvaluate($rule['test'])) {
223 $this->addRule('fired', $rule);
224 } else {
225 $this->addRule('notfired', $rule);
227 } catch (Exception $e) {
228 $this->storeError(
229 sprintf(
230 __('Failed running test for rule \'%s\'.'),
231 $rule['name']
239 return true;
243 * Escapes percent string to be used in format string.
245 * @param string $str string to escape
247 * @return string
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
260 * @return string
262 public function translate($str, $param = null)
264 $string = _gettext(self::escapePercent($str));
265 if (! is_null($param)) {
266 $params = $this->ruleExprEvaluate('array(' . $param . ')');
267 } else {
268 $params = array();
270 return vsprintf($string, $params);
274 * Splits justification to text and formula.
276 * @param array $rule the rule
278 * @return string[]
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
295 * @return void
297 public function addRule($type, $rule)
299 switch ($type) {
300 case 'notfired':
301 case 'fired':
302 $jst = self::splitJustification($rule);
303 if (count($jst) > 1) {
304 try {
305 /* Translate */
306 $str = $this->translate($jst[0], $jst[1]);
307 } catch (Exception $e) {
308 $this->storeError(
309 sprintf(
310 __('Failed formatting string for rule \'%s\'.'),
311 $rule['name']
315 return;
318 $rule['justification'] = $str;
319 } else {
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']
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 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'])) {
370 return '0';
373 // Did matching rule fire?
374 foreach ($this->runResult['fired'] as $rule) {
375 if ($rule['id'] == $matches[2]) {
376 return '1';
380 return '0';
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]])) {
393 return $matches[1];
395 if (is_numeric($this->variables[$matches[1]])) {
396 return $this->variables[$matches[1]];
397 } else {
398 return '\'' . addslashes($this->variables[$matches[1]]) . '\'';
403 * Runs a code expression, replacing variable names with their respective
404 * values
406 * @param string $expr expression to evaluate
408 * @return integer result of evaluated expression
410 * @throws Exception
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'),
418 $expr
420 // Evaluate variables
421 $expr = preg_replace_callback(
422 '/\b(\w+)\b/',
423 array($this, 'ruleExprEvaluateVariable'),
424 $expr
426 $value = 0;
427 $err = 0;
429 // Actually evaluate the code
430 ob_start();
431 try {
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();
440 ob_end_clean();
442 // Error handling
443 if ($err) {
444 throw new Exception(
445 strip_tags($err)
446 . '<br />Executed code: $value = ' . htmlspecialchars($expr) . ';'
449 return $value;
453 * Reads the rule file into an array, throwing errors messages on syntax
454 * errors.
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);
463 $errors = array();
464 $rules = array();
465 $lines = array();
467 if ($file === FALSE) {
468 $errors[] = sprintf(
469 __('Error in reading file: The file \'%s\' does not exist or is not readable!'),
470 $filename
472 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
475 $ruleSyntax = array(
476 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
478 $numRules = count($ruleSyntax);
479 $numLines = count($file);
480 $ruleNo = -1;
481 $ruleLine = -1;
483 for ($i = 0; $i < $numLines; $i++) {
484 $line = $file[$i];
485 if ($line == "" || $line[0] == '#') {
486 continue;
489 // Reading new rule
490 if (substr($line, 0, 4) == 'rule') {
491 if ($ruleLine > 0) {
492 $errors[] = sprintf(
494 'Invalid rule declaration on line %1$s, expected line '
495 . '%2$s of previous rule.'
497 $i + 1,
498 $ruleSyntax[$ruleLine++]
500 continue;
502 if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
503 $ruleLine = 1;
504 $ruleNo++;
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;
511 } else {
512 $errors[] = sprintf(
513 __('Invalid rule declaration on line %s.'),
514 $i + 1
517 continue;
518 } else {
519 if ($ruleLine == -1) {
520 $errors[] = sprintf(
521 __('Unexpected characters on line %s.'),
522 $i + 1
527 // Reading rule lines
528 if ($ruleLine > 0) {
529 if (!isset($line[0])) {
530 continue; // Empty lines are ok
532 // Non tabbed lines are not
533 if ($line[0] != "\t") {
534 $errors[] = sprintf(
536 'Unexpected character on line %1$s. Expected tab, but '
537 . 'found "%2$s".'
539 $i + 1,
540 $line[0]
542 continue;
544 $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
545 mb_substr($line, 1)
547 $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1;
548 ++$ruleLine;
551 // Rule complete
552 if ($ruleLine == $numRules) {
553 $ruleLine = -1;
557 return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);