Advisor.class.php 15 KB

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