PageRenderTime 52ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/Advisor.php

https://gitlab.com/trungthao379/phpmyadmin
PHP | 547 lines | 335 code | 49 blank | 163 comment | 38 complexity | 9f0d00efea849344cdea003490e5f570 MD5 | raw file
  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 PMA\libraries;
  10. use \Exception;
  11. use PMA\libraries\URL;
  12. require_once 'libraries/advisor.lib.php';
  13. /**
  14. * Advisor class
  15. *
  16. * @package PhpMyAdmin
  17. */
  18. class Advisor
  19. {
  20. protected $variables;
  21. protected $parseResult;
  22. protected $runResult;
  23. /**
  24. * Get variables
  25. *
  26. * @return mixed
  27. */
  28. public function getVariables()
  29. {
  30. return $this->variables;
  31. }
  32. /**
  33. * Set variables
  34. *
  35. * @param array $variables Variables
  36. *
  37. * @return Advisor
  38. */
  39. public function setVariables($variables)
  40. {
  41. $this->variables = $variables;
  42. return $this;
  43. }
  44. /**
  45. * Set a variable and its value
  46. *
  47. * @param string|int $variable Variable to set
  48. * @param mixed $value Value to set
  49. *
  50. * @return $this
  51. */
  52. public function setVariable($variable, $value)
  53. {
  54. $this->variables[$variable] = $value;
  55. return $this;
  56. }
  57. /**
  58. * Get parseResult
  59. *
  60. * @return mixed
  61. */
  62. public function getParseResult()
  63. {
  64. return $this->parseResult;
  65. }
  66. /**
  67. * Set parseResult
  68. *
  69. * @param array $parseResult Parse result
  70. *
  71. * @return Advisor
  72. */
  73. public function setParseResult($parseResult)
  74. {
  75. $this->parseResult = $parseResult;
  76. return $this;
  77. }
  78. /**
  79. * Get runResult
  80. *
  81. * @return mixed
  82. */
  83. public function getRunResult()
  84. {
  85. return $this->runResult;
  86. }
  87. /**
  88. * Set runResult
  89. *
  90. * @param array $runResult Run result
  91. *
  92. * @return Advisor
  93. */
  94. public function setRunResult($runResult)
  95. {
  96. $this->runResult = $runResult;
  97. return $this;
  98. }
  99. /**
  100. * Parses and executes advisor rules
  101. *
  102. * @return array with run and parse results
  103. */
  104. public function run()
  105. {
  106. // HowTo: A simple Advisory system in 3 easy steps.
  107. // Step 1: Get some variables to evaluate on
  108. $this->setVariables(
  109. array_merge(
  110. $GLOBALS['dbi']->fetchResult('SHOW GLOBAL STATUS', 0, 1),
  111. $GLOBALS['dbi']->fetchResult('SHOW GLOBAL VARIABLES', 0, 1)
  112. )
  113. );
  114. // Add total memory to variables as well
  115. include_once 'libraries/sysinfo.lib.php';
  116. $sysinfo = PMA_getSysInfo();
  117. $memory = $sysinfo->memory();
  118. $this->variables['system_memory']
  119. = isset($memory['MemTotal']) ? $memory['MemTotal'] : 0;
  120. // Step 2: Read and parse the list of rules
  121. $this->setParseResult(static::parseRulesFile());
  122. // Step 3: Feed the variables to the rules and let them fire. Sets
  123. // $runResult
  124. $this->runRules();
  125. return array(
  126. 'parse' => array('errors' => $this->parseResult['errors']),
  127. 'run' => $this->runResult
  128. );
  129. }
  130. /**
  131. * Stores current error in run results.
  132. *
  133. * @param string $description description of an error.
  134. * @param Exception $exception exception raised
  135. *
  136. * @return void
  137. */
  138. public function storeError($description, $exception)
  139. {
  140. $this->runResult['errors'][] = $description
  141. . ' '
  142. . sprintf(
  143. __('PHP threw following error: %s'),
  144. $exception->getMessage()
  145. );
  146. }
  147. /**
  148. * Executes advisor rules
  149. *
  150. * @return boolean
  151. */
  152. public function runRules()
  153. {
  154. $this->setRunResult(
  155. array(
  156. 'fired' => array(),
  157. 'notfired' => array(),
  158. 'unchecked' => array(),
  159. 'errors' => array(),
  160. )
  161. );
  162. foreach ($this->parseResult['rules'] as $rule) {
  163. $this->variables['value'] = 0;
  164. $precond = true;
  165. if (isset($rule['precondition'])) {
  166. try {
  167. $precond = $this->ruleExprEvaluate($rule['precondition']);
  168. } catch (Exception $e) {
  169. $this->storeError(
  170. sprintf(
  171. __('Failed evaluating precondition for rule \'%s\'.'),
  172. $rule['name']
  173. ),
  174. $e
  175. );
  176. continue;
  177. }
  178. }
  179. if (! $precond) {
  180. $this->addRule('unchecked', $rule);
  181. } else {
  182. try {
  183. $value = $this->ruleExprEvaluate($rule['formula']);
  184. } catch (Exception $e) {
  185. $this->storeError(
  186. sprintf(
  187. __('Failed calculating value for rule \'%s\'.'),
  188. $rule['name']
  189. ),
  190. $e
  191. );
  192. continue;
  193. }
  194. $this->variables['value'] = $value;
  195. try {
  196. if ($this->ruleExprEvaluate($rule['test'])) {
  197. $this->addRule('fired', $rule);
  198. } else {
  199. $this->addRule('notfired', $rule);
  200. }
  201. } catch (Exception $e) {
  202. $this->storeError(
  203. sprintf(
  204. __('Failed running test for rule \'%s\'.'),
  205. $rule['name']
  206. ),
  207. $e
  208. );
  209. }
  210. }
  211. }
  212. return true;
  213. }
  214. /**
  215. * Escapes percent string to be used in format string.
  216. *
  217. * @param string $str string to escape
  218. *
  219. * @return string
  220. */
  221. public static function escapePercent($str)
  222. {
  223. return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str);
  224. }
  225. /**
  226. * Wrapper function for translating.
  227. *
  228. * @param string $str the string
  229. * @param string $param the parameters
  230. *
  231. * @return string
  232. */
  233. public function translate($str, $param = null)
  234. {
  235. $string = _gettext(self::escapePercent($str));
  236. if (! is_null($param)) {
  237. $params = $this->ruleExprEvaluate('array(' . $param . ')');
  238. } else {
  239. $params = array();
  240. }
  241. return vsprintf($string, $params);
  242. }
  243. /**
  244. * Splits justification to text and formula.
  245. *
  246. * @param array $rule the rule
  247. *
  248. * @return string[]
  249. */
  250. public static function splitJustification($rule)
  251. {
  252. $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2);
  253. if (count($jst) > 1) {
  254. return array($jst[0], $jst[1]);
  255. }
  256. return array($rule['justification']);
  257. }
  258. /**
  259. * Adds a rule to the result list
  260. *
  261. * @param string $type type of rule
  262. * @param array $rule rule itself
  263. *
  264. * @return void
  265. */
  266. public function addRule($type, $rule)
  267. {
  268. switch ($type) {
  269. case 'notfired':
  270. case 'fired':
  271. $jst = self::splitJustification($rule);
  272. if (count($jst) > 1) {
  273. try {
  274. /* Translate */
  275. $str = $this->translate($jst[0], $jst[1]);
  276. } catch (Exception $e) {
  277. $this->storeError(
  278. sprintf(
  279. __('Failed formatting string for rule \'%s\'.'),
  280. $rule['name']
  281. ),
  282. $e
  283. );
  284. return;
  285. }
  286. $rule['justification'] = $str;
  287. } else {
  288. $rule['justification'] = $this->translate($rule['justification']);
  289. }
  290. $rule['id'] = $rule['name'];
  291. $rule['name'] = $this->translate($rule['name']);
  292. $rule['issue'] = $this->translate($rule['issue']);
  293. // Replaces {server_variable} with 'server_variable'
  294. // linking to server_variables.php
  295. $rule['recommendation'] = preg_replace(
  296. '/\{([a-z_0-9]+)\}/Ui',
  297. '<a href="server_variables.php' . URL::getCommon()
  298. . '&filter=\1">\1</a>',
  299. $this->translate($rule['recommendation'])
  300. );
  301. // Replaces external Links with PMA_linkURL() generated links
  302. $rule['recommendation'] = preg_replace_callback(
  303. '#href=("|\')(https?://[^\1]+)\1#i',
  304. array($this, 'replaceLinkURL'),
  305. $rule['recommendation']
  306. );
  307. break;
  308. }
  309. $this->runResult[$type][] = $rule;
  310. }
  311. /**
  312. * Callback for wrapping links with PMA_linkURL
  313. *
  314. * @param array $matches List of matched elements form preg_replace_callback
  315. *
  316. * @return string Replacement value
  317. */
  318. private function replaceLinkURL($matches)
  319. {
  320. return 'href="' . PMA_linkURL($matches[2]) . '" target="_blank"';
  321. }
  322. /**
  323. * Callback for evaluating fired() condition.
  324. *
  325. * @param array $matches List of matched elements form preg_replace_callback
  326. *
  327. * @return string Replacement value
  328. */
  329. private function ruleExprEvaluateFired($matches)
  330. {
  331. // No list of fired rules
  332. if (!isset($this->runResult['fired'])) {
  333. return '0';
  334. }
  335. // Did matching rule fire?
  336. foreach ($this->runResult['fired'] as $rule) {
  337. if ($rule['id'] == $matches[2]) {
  338. return '1';
  339. }
  340. }
  341. return '0';
  342. }
  343. /**
  344. * Callback for evaluating variables in expression.
  345. *
  346. * @param array $matches List of matched elements form preg_replace_callback
  347. *
  348. * @return string Replacement value
  349. */
  350. private function ruleExprEvaluateVariable($matches)
  351. {
  352. if (! isset($this->variables[$matches[1]])) {
  353. return $matches[1];
  354. }
  355. if (is_numeric($this->variables[$matches[1]])) {
  356. return $this->variables[$matches[1]];
  357. } else {
  358. return '\'' . addslashes($this->variables[$matches[1]]) . '\'';
  359. }
  360. }
  361. /**
  362. * Runs a code expression, replacing variable names with their respective
  363. * values
  364. *
  365. * @param string $expr expression to evaluate
  366. *
  367. * @return integer result of evaluated expression
  368. *
  369. * @throws Exception
  370. */
  371. public function ruleExprEvaluate($expr)
  372. {
  373. // Evaluate fired() conditions
  374. $expr = preg_replace_callback(
  375. '/fired\s*\(\s*(\'|")(.*)\1\s*\)/Ui',
  376. array($this, 'ruleExprEvaluateFired'),
  377. $expr
  378. );
  379. // Evaluate variables
  380. $expr = preg_replace_callback(
  381. '/\b(\w+)\b/',
  382. array($this, 'ruleExprEvaluateVariable'),
  383. $expr
  384. );
  385. $value = 0;
  386. $err = 0;
  387. // Actually evaluate the code
  388. ob_start();
  389. try {
  390. eval('$value = ' . $expr . ';');
  391. $err = ob_get_contents();
  392. } catch (Exception $e) {
  393. // In normal operation, there is just output in the buffer,
  394. // but when running under phpunit, error in eval raises exception
  395. $err = $e->getMessage();
  396. }
  397. ob_end_clean();
  398. // Error handling
  399. if ($err) {
  400. throw new Exception(
  401. strip_tags($err)
  402. . '<br />Executed code: $value = ' . htmlspecialchars($expr) . ';'
  403. );
  404. }
  405. return $value;
  406. }
  407. /**
  408. * Reads the rule file into an array, throwing errors messages on syntax
  409. * errors.
  410. *
  411. * @return array with parsed data
  412. */
  413. public static function parseRulesFile()
  414. {
  415. $file = file('libraries/advisory_rules.txt', FILE_IGNORE_NEW_LINES);
  416. $errors = array();
  417. $rules = array();
  418. $lines = array();
  419. $ruleSyntax = array(
  420. 'name', 'formula', 'test', 'issue', 'recommendation', 'justification'
  421. );
  422. $numRules = count($ruleSyntax);
  423. $numLines = count($file);
  424. $ruleNo = -1;
  425. $ruleLine = -1;
  426. for ($i = 0; $i < $numLines; $i++) {
  427. $line = $file[$i];
  428. if ($line == "" || $line[0] == '#') {
  429. continue;
  430. }
  431. // Reading new rule
  432. if (substr($line, 0, 4) == 'rule') {
  433. if ($ruleLine > 0) {
  434. $errors[] = sprintf(
  435. __(
  436. 'Invalid rule declaration on line %1$s, expected line '
  437. . '%2$s of previous rule.'
  438. ),
  439. $i + 1,
  440. $ruleSyntax[$ruleLine++]
  441. );
  442. continue;
  443. }
  444. if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) {
  445. $ruleLine = 1;
  446. $ruleNo++;
  447. $rules[$ruleNo] = array('name' => $match[1]);
  448. $lines[$ruleNo] = array('name' => $i + 1);
  449. if (isset($match[3])) {
  450. $rules[$ruleNo]['precondition'] = $match[3];
  451. $lines[$ruleNo]['precondition'] = $i + 1;
  452. }
  453. } else {
  454. $errors[] = sprintf(
  455. __('Invalid rule declaration on line %s.'),
  456. $i + 1
  457. );
  458. }
  459. continue;
  460. } else {
  461. if ($ruleLine == -1) {
  462. $errors[] = sprintf(
  463. __('Unexpected characters on line %s.'),
  464. $i + 1
  465. );
  466. }
  467. }
  468. // Reading rule lines
  469. if ($ruleLine > 0) {
  470. if (!isset($line[0])) {
  471. continue; // Empty lines are ok
  472. }
  473. // Non tabbed lines are not
  474. if ($line[0] != "\t") {
  475. $errors[] = sprintf(
  476. __(
  477. 'Unexpected character on line %1$s. Expected tab, but '
  478. . 'found "%2$s".'
  479. ),
  480. $i + 1,
  481. $line[0]
  482. );
  483. continue;
  484. }
  485. $rules[$ruleNo][$ruleSyntax[$ruleLine]] = chop(
  486. mb_substr($line, 1)
  487. );
  488. $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1;
  489. ++$ruleLine;
  490. }
  491. // Rule complete
  492. if ($ruleLine == $numRules) {
  493. $ruleLine = -1;
  494. }
  495. }
  496. return array('rules' => $rules, 'lines' => $lines, 'errors' => $errors);
  497. }
  498. }