Advisor.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  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.
  6. *
  7. * @package PhpMyAdmin
  8. */
  9. namespace PhpMyAdmin;
  10. use Exception;
  11. use PhpMyAdmin\Core;
  12. use PhpMyAdmin\DatabaseInterface;
  13. use PhpMyAdmin\SysInfo;
  14. use PhpMyAdmin\Url;
  15. use PhpMyAdmin\Util;
  16. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  17. /**
  18. * Advisor class
  19. *
  20. * @package PhpMyAdmin
  21. */
  22. class Advisor
  23. {
  24. protected $dbi;
  25. protected $variables;
  26. protected $globals;
  27. protected $parseResult;
  28. protected $runResult;
  29. protected $expression;
  30. /**
  31. * Constructor
  32. *
  33. * @param DatabaseInterface $dbi DatabaseInterface object
  34. * @param ExpressionLanguage $expression ExpressionLanguage object
  35. */
  36. public function __construct(DatabaseInterface $dbi, ExpressionLanguage $expression)
  37. {
  38. $this->dbi = $dbi;
  39. $this->expression = $expression;
  40. /*
  41. * Register functions for ExpressionLanguage, we intentionally
  42. * do not implement support for compile as we do not use it.
  43. */
  44. $this->expression->register(
  45. 'round',
  46. function (){},
  47. function ($arguments, $num) {
  48. return round($num);
  49. }
  50. );
  51. $this->expression->register(
  52. 'substr',
  53. function (){},
  54. function ($arguments, $string, $start, $length) {
  55. return substr($string, $start, $length);
  56. }
  57. );
  58. $this->expression->register(
  59. 'preg_match',
  60. function (){},
  61. function ($arguments, $pattern , $subject) {
  62. return preg_match($pattern, $subject);
  63. }
  64. );
  65. $this->expression->register(
  66. 'ADVISOR_bytime',
  67. function (){},
  68. function ($arguments, $num, $precision) {
  69. return self::byTime($num, $precision);
  70. }
  71. );
  72. $this->expression->register(
  73. 'ADVISOR_timespanFormat',
  74. function (){},
  75. function ($arguments, $seconds) {
  76. return self::timespanFormat($seconds);
  77. }
  78. );
  79. $this->expression->register(
  80. 'ADVISOR_formatByteDown',
  81. function (){},
  82. function ($arguments, $value, $limes = 6, $comma = 0) {
  83. return self::formatByteDown($value, $limes, $comma);
  84. }
  85. );
  86. $this->expression->register(
  87. 'fired',
  88. function (){},
  89. function ($arguments, $value) {
  90. if (!isset($this->runResult['fired'])) {
  91. return 0;
  92. }
  93. // Did matching rule fire?
  94. foreach ($this->runResult['fired'] as $rule) {
  95. if ($rule['id'] == $value) {
  96. return '1';
  97. }
  98. }
  99. return '0';
  100. }
  101. );
  102. /* Some global variables for advisor */
  103. $this->globals = array(
  104. 'PMA_MYSQL_INT_VERSION' => $this->dbi->getVersion(),
  105. );
  106. }
  107. /**
  108. * Get variables
  109. *
  110. * @return mixed
  111. */
  112. public function getVariables()
  113. {
  114. return $this->variables;
  115. }
  116. /**
  117. * Set variables
  118. *
  119. * @param array $variables Variables
  120. *
  121. * @return Advisor
  122. */
  123. public function setVariables(array $variables)
  124. {
  125. $this->variables = $variables;
  126. return $this;
  127. }
  128. /**
  129. * Set a variable and its value
  130. *
  131. * @param string|int $variable Variable to set
  132. * @param mixed $value Value to set
  133. *
  134. * @return $this
  135. */
  136. public function setVariable($variable, $value)
  137. {
  138. $this->variables[$variable] = $value;
  139. return $this;
  140. }
  141. /**
  142. * Get parseResult
  143. *
  144. * @return mixed
  145. */
  146. public function getParseResult()
  147. {
  148. return $this->parseResult;
  149. }
  150. /**
  151. * Set parseResult
  152. *
  153. * @param array $parseResult Parse result
  154. *
  155. * @return Advisor
  156. */
  157. public function setParseResult(array $parseResult)
  158. {
  159. $this->parseResult = $parseResult;
  160. return $this;
  161. }
  162. /**
  163. * Get runResult
  164. *
  165. * @return mixed
  166. */
  167. public function getRunResult()
  168. {
  169. return $this->runResult;
  170. }
  171. /**
  172. * Set runResult
  173. *
  174. * @param array $runResult Run result
  175. *
  176. * @return Advisor
  177. */
  178. public function setRunResult(array $runResult)
  179. {
  180. $this->runResult = $runResult;
  181. return $this;
  182. }
  183. /**
  184. * Parses and executes advisor rules
  185. *
  186. * @return array with run and parse results
  187. */
  188. public function run()
  189. {
  190. // HowTo: A simple Advisory system in 3 easy steps.
  191. // Step 1: Get some variables to evaluate on
  192. $this->setVariables(
  193. array_merge(
  194. $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1),
  195. $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1)
  196. )
  197. );
  198. // Add total memory to variables as well
  199. $sysinfo = SysInfo::get();
  200. $memory = $sysinfo->memory();
  201. $this->variables['system_memory']
  202. = isset($memory['MemTotal']) ? $memory['MemTotal'] : 0;
  203. // Step 2: Read and parse the list of rules
  204. $this->setParseResult(static::parseRulesFile());
  205. // Step 3: Feed the variables to the rules and let them fire. Sets
  206. // $runResult
  207. $this->runRules();
  208. return array(
  209. 'parse' => array('errors' => $this->parseResult['errors']),
  210. 'run' => $this->runResult
  211. );
  212. }
  213. /**
  214. * Stores current error in run results.
  215. *
  216. * @param string $description description of an error.
  217. * @param Exception $exception exception raised
  218. *
  219. * @return void
  220. */
  221. public function storeError($description, $exception)
  222. {
  223. $this->runResult['errors'][] = $description
  224. . ' '
  225. . sprintf(
  226. __('Error when evaluating: %s'),
  227. $exception->getMessage()
  228. );
  229. }
  230. /**
  231. * Executes advisor rules
  232. *
  233. * @return boolean
  234. */
  235. public function runRules()
  236. {
  237. $this->setRunResult(
  238. array(
  239. 'fired' => array(),
  240. 'notfired' => array(),
  241. 'unchecked' => array(),
  242. 'errors' => array(),
  243. )
  244. );
  245. foreach ($this->parseResult['rules'] as $rule) {
  246. $this->variables['value'] = 0;
  247. $precond = true;
  248. if (isset($rule['precondition'])) {
  249. try {
  250. $precond = $this->ruleExprEvaluate($rule['precondition']);
  251. } catch (Exception $e) {
  252. $this->storeError(
  253. sprintf(
  254. __('Failed evaluating precondition for rule \'%s\'.'),
  255. $rule['name']
  256. ),
  257. $e
  258. );
  259. continue;
  260. }
  261. }
  262. if (! $precond) {
  263. $this->addRule('unchecked', $rule);
  264. } else {
  265. try {
  266. $value = $this->ruleExprEvaluate($rule['formula']);
  267. } catch (Exception $e) {
  268. $this->storeError(
  269. sprintf(
  270. __('Failed calculating value for rule \'%s\'.'),
  271. $rule['name']
  272. ),
  273. $e
  274. );
  275. continue;
  276. }
  277. $this->variables['value'] = $value;
  278. try {
  279. if ($this->ruleExprEvaluate($rule['test'])) {
  280. $this->addRule('fired', $rule);
  281. } else {
  282. $this->addRule('notfired', $rule);
  283. }
  284. } catch (Exception $e) {
  285. $this->storeError(
  286. sprintf(
  287. __('Failed running test for rule \'%s\'.'),
  288. $rule['name']
  289. ),
  290. $e
  291. );
  292. }
  293. }
  294. }
  295. return true;
  296. }
  297. /**
  298. * Escapes percent string to be used in format string.
  299. *
  300. * @param string $str string to escape
  301. *
  302. * @return string
  303. */
  304. public static function escapePercent($str)
  305. {
  306. return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str);
  307. }
  308. /**
  309. * Wrapper function for translating.
  310. *
  311. * @param string $str the string
  312. * @param string $param the parameters
  313. *
  314. * @return string
  315. */
  316. public function translate($str, $param = null)
  317. {
  318. $string = _gettext(self::escapePercent($str));
  319. if (! is_null($param)) {
  320. $params = $this->ruleExprEvaluate('[' . $param . ']');
  321. } else {
  322. $params = array();
  323. }
  324. return vsprintf($string, $params);
  325. }
  326. /**
  327. * Splits justification to text and formula.
  328. *
  329. * @param array $rule the rule
  330. *
  331. * @return string[]
  332. */
  333. public static function splitJustification(array $rule)
  334. {
  335. $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2);
  336. if (count($jst) > 1) {
  337. return array($jst[0], $jst[1]);
  338. }
  339. return array($rule['justification']);
  340. }
  341. /**
  342. * Adds a rule to the result list
  343. *
  344. * @param string $type type of rule
  345. * @param array $rule rule itself
  346. *
  347. * @return void
  348. */
  349. public function addRule($type, array $rule)
  350. {
  351. switch ($type) {
  352. case 'notfired':
  353. case 'fired':
  354. $jst = self::splitJustification($rule);
  355. if (count($jst) > 1) {
  356. try {
  357. /* Translate */
  358. $str = $this->translate($jst[0], $jst[1]);
  359. } catch (Exception $e) {
  360. $this->storeError(
  361. sprintf(
  362. __('Failed formatting string for rule \'%s\'.'),
  363. $rule['name']
  364. ),
  365. $e
  366. );
  367. return;
  368. }
  369. $rule['justification'] = $str;
  370. } else {
  371. $rule['justification'] = $this->translate($rule['justification']);
  372. }
  373. $rule['id'] = $rule['name'];
  374. $rule['name'] = $this->translate($rule['name']);
  375. $rule['issue'] = $this->translate($rule['issue']);
  376. // Replaces {server_variable} with 'server_variable'
  377. // linking to server_variables.php
  378. $rule['recommendation'] = preg_replace_callback(
  379. '/\{([a-z_0-9]+)\}/Ui',
  380. array($this, 'replaceVariable'),
  381. $this->translate($rule['recommendation'])
  382. );
  383. // Replaces external Links with Core::linkURL() generated links
  384. $rule['recommendation'] = preg_replace_callback(
  385. '#href=("|\')(https?://[^\1]+)\1#i',
  386. array($this, 'replaceLinkURL'),
  387. $rule['recommendation']
  388. );
  389. break;
  390. }
  391. $this->runResult[$type][] = $rule;
  392. }
  393. /**
  394. * Callback for wrapping links with Core::linkURL
  395. *
  396. * @param array $matches List of matched elements form preg_replace_callback
  397. *
  398. * @return string Replacement value
  399. */
  400. private function replaceLinkURL(array $matches)
  401. {
  402. return 'href="' . Core::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"';
  403. }
  404. /**
  405. * Callback for wrapping variable edit links
  406. *
  407. * @param array $matches List of matched elements form preg_replace_callback
  408. *
  409. * @return string Replacement value
  410. */
  411. private function replaceVariable(array $matches)
  412. {
  413. return '<a href="server_variables.php' . Url::getCommon(array('filter' => $matches[1]))
  414. . '">' . htmlspecialchars($matches[1]) . '</a>';
  415. }
  416. /**
  417. * Runs a code expression, replacing variable names with their respective
  418. * values
  419. *
  420. * @param string $expr expression to evaluate
  421. *
  422. * @return integer result of evaluated expression
  423. *
  424. * @throws Exception
  425. */
  426. public function ruleExprEvaluate($expr)
  427. {
  428. // Actually evaluate the code
  429. // This can throw exception
  430. $value = $this->expression->evaluate(
  431. $expr,
  432. array_merge($this->variables, $this->globals)
  433. );
  434. return $value;
  435. }
  436. /**
  437. * Reads the rule file into an array, throwing errors messages on syntax
  438. * errors.
  439. *
  440. * @return array with parsed data
  441. */
  442. public static function parseRulesFile()
  443. {
  444. $filename = 'libraries/advisory_rules.txt';
  445. $file = file($filename, FILE_IGNORE_NEW_LINES);
  446. $errors = array();
  447. $rules = array();
  448. $lines = array();
  449. if ($file === false) {
  450. $errors[] = sprintf(
  451. __('Error in reading file: The file \'%s\' does not exist or is not readable!'),
  452. $filename
  453. );
  454. return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
  455. }
  456. $ruleSyntax = array(
  457. 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
  458. );
  459. $numRules = count($ruleSyntax);
  460. $numLines = count($file);
  461. $ruleNo = -1;
  462. $ruleLine = -1;
  463. for ($i = 0; $i < $numLines; $i++) {
  464. $line = $file[$i];
  465. if ($line == "" || $line[0] == '#') {
  466. continue;
  467. }
  468. // Reading new rule
  469. if (substr($line, 0, 4) == 'rule') {
  470. if ($ruleLine > 0) {
  471. $errors[] = sprintf(
  472. __(
  473. 'Invalid rule declaration on line %1$s, expected line '
  474. . '%2$s of previous rule.'
  475. ),
  476. $i + 1,
  477. $ruleSyntax[$ruleLine++]
  478. );
  479. continue;
  480. }
  481. if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
  482. $ruleLine = 1;
  483. $ruleNo++;
  484. $rules[$ruleNo] = array('name' => $match[1]);
  485. $lines[$ruleNo] = array('name' => $i + 1);
  486. if (isset($match[3])) {
  487. $rules[$ruleNo]['precondition'] = $match[3];
  488. $lines[$ruleNo]['precondition'] = $i + 1;
  489. }
  490. } else {
  491. $errors[] = sprintf(
  492. __('Invalid rule declaration on line %s.'),
  493. $i + 1
  494. );
  495. }
  496. continue;
  497. } else {
  498. if ($ruleLine == -1) {
  499. $errors[] = sprintf(
  500. __('Unexpected characters on line %s.'),
  501. $i + 1
  502. );
  503. }
  504. }
  505. // Reading rule lines
  506. if ($ruleLine > 0) {
  507. if (!isset($line[0])) {
  508. continue; // Empty lines are ok
  509. }
  510. // Non tabbed lines are not
  511. if ($line[0] != "\t") {
  512. $errors[] = sprintf(
  513. __(
  514. 'Unexpected character on line %1$s. Expected tab, but '
  515. . 'found "%2$s".'
  516. ),
  517. $i + 1,
  518. $line[0]
  519. );
  520. continue;
  521. }
  522. $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
  523. mb_substr($line, 1)
  524. );
  525. $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1;
  526. ++$ruleLine;
  527. }
  528. // Rule complete
  529. if ($ruleLine == $numRules) {
  530. $ruleLine = -1;
  531. }
  532. }
  533. return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
  534. }
  535. /**
  536. * Formats interval like 10 per hour
  537. *
  538. * @param integer $num number to format
  539. * @param integer $precision required precision
  540. *
  541. * @return string formatted string
  542. */
  543. public static function byTime($num, $precision)
  544. {
  545. if ($num >= 1) { // per second
  546. $per = __('per second');
  547. } elseif ($num * 60 >= 1) { // per minute
  548. $num = $num * 60;
  549. $per = __('per minute');
  550. } elseif ($num * 60 * 60 >= 1 ) { // per hour
  551. $num = $num * 60 * 60;
  552. $per = __('per hour');
  553. } else {
  554. $num = $num * 60 * 60 * 24;
  555. $per = __('per day');
  556. }
  557. $num = round($num, $precision);
  558. if ($num == 0) {
  559. $num = '<' . pow(10, -$precision);
  560. }
  561. return "$num $per";
  562. }
  563. /**
  564. * Wrapper for PhpMyAdmin\Util::timespanFormat
  565. *
  566. * This function is used when evaluating advisory_rules.txt
  567. *
  568. * @param int $seconds the timespan
  569. *
  570. * @return string the formatted value
  571. */
  572. public static function timespanFormat($seconds)
  573. {
  574. return Util::timespanFormat($seconds);
  575. }
  576. /**
  577. * Wrapper around PhpMyAdmin\Util::formatByteDown
  578. *
  579. * This function is used when evaluating advisory_rules.txt
  580. *
  581. * @param double $value the value to format
  582. * @param int $limes the sensitiveness
  583. * @param int $comma the number of decimals to retain
  584. *
  585. * @return string the formatted value with unit
  586. */
  587. public static function formatByteDown($value, $limes = 6, $comma = 0)
  588. {
  589. return implode(' ', Util::formatByteDown($value, $limes, $comma));
  590. }
  591. }