Advisor.class.php 15 KB

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