Advisor.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Server\SysInfo\SysInfo;
  5. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  6. use Throwable;
  7. use function array_merge;
  8. use function htmlspecialchars;
  9. use function implode;
  10. use function pow;
  11. use function preg_match;
  12. use function preg_replace_callback;
  13. use function round;
  14. use function sprintf;
  15. use function strpos;
  16. use function substr;
  17. use function vsprintf;
  18. /**
  19. * A simple rules engine, that executes the rules in the advisory_rules files.
  20. */
  21. class Advisor
  22. {
  23. private const GENERIC_RULES_FILE = 'libraries/advisory_rules_generic.php';
  24. private const BEFORE_MYSQL80003_RULES_FILE = 'libraries/advisory_rules_mysql_before80003.php';
  25. /** @var DatabaseInterface */
  26. private $dbi;
  27. /** @var array */
  28. private $variables;
  29. /** @var array */
  30. private $globals;
  31. /** @var array */
  32. private $rules;
  33. /** @var array */
  34. private $runResult;
  35. /** @var ExpressionLanguage */
  36. private $expression;
  37. /**
  38. * @param DatabaseInterface $dbi DatabaseInterface object
  39. * @param ExpressionLanguage $expression ExpressionLanguage object
  40. */
  41. public function __construct(DatabaseInterface $dbi, ExpressionLanguage $expression)
  42. {
  43. $this->dbi = $dbi;
  44. $this->expression = $expression;
  45. /*
  46. * Register functions for ExpressionLanguage, we intentionally
  47. * do not implement support for compile as we do not use it.
  48. */
  49. $this->expression->register(
  50. 'round',
  51. static function () {
  52. },
  53. /**
  54. * @param array $arguments
  55. * @param float $num
  56. */
  57. static function ($arguments, $num) {
  58. return round($num);
  59. }
  60. );
  61. $this->expression->register(
  62. 'substr',
  63. static function () {
  64. },
  65. /**
  66. * @param array $arguments
  67. * @param string $string
  68. * @param int $start
  69. * @param int $length
  70. */
  71. static function ($arguments, $string, $start, $length) {
  72. return substr($string, $start, $length);
  73. }
  74. );
  75. $this->expression->register(
  76. 'preg_match',
  77. static function () {
  78. },
  79. /**
  80. * @param array $arguments
  81. * @param string $pattern
  82. * @param string $subject
  83. */
  84. static function ($arguments, $pattern, $subject) {
  85. return preg_match($pattern, $subject);
  86. }
  87. );
  88. $this->expression->register(
  89. 'ADVISOR_bytime',
  90. static function () {
  91. },
  92. /**
  93. * @param array $arguments
  94. * @param float $num
  95. * @param int $precision
  96. */
  97. static function ($arguments, $num, $precision) {
  98. return self::byTime($num, $precision);
  99. }
  100. );
  101. $this->expression->register(
  102. 'ADVISOR_timespanFormat',
  103. static function () {
  104. },
  105. /**
  106. * @param array $arguments
  107. * @param string $seconds
  108. */
  109. static function ($arguments, $seconds) {
  110. return Util::timespanFormat((int) $seconds);
  111. }
  112. );
  113. $this->expression->register(
  114. 'ADVISOR_formatByteDown',
  115. static function () {
  116. },
  117. /**
  118. * @param array $arguments
  119. * @param int $value
  120. * @param int $limes
  121. * @param int $comma
  122. */
  123. static function ($arguments, $value, $limes = 6, $comma = 0) {
  124. return implode(' ', (array) Util::formatByteDown($value, $limes, $comma));
  125. }
  126. );
  127. $this->expression->register(
  128. 'fired',
  129. static function () {
  130. },
  131. /**
  132. * @param array $arguments
  133. * @param int $value
  134. */
  135. function ($arguments, $value) {
  136. if (! isset($this->runResult['fired'])) {
  137. return 0;
  138. }
  139. // Did matching rule fire?
  140. foreach ($this->runResult['fired'] as $rule) {
  141. if ($rule['id'] == $value) {
  142. return '1';
  143. }
  144. }
  145. return '0';
  146. }
  147. );
  148. /* Some global variables for advisor */
  149. $this->globals = [
  150. 'PMA_MYSQL_INT_VERSION' => $this->dbi->getVersion(),
  151. ];
  152. }
  153. private function setVariables(): void
  154. {
  155. $globalStatus = $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1);
  156. $globalVariables = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1);
  157. $sysInfo = SysInfo::get();
  158. $memory = $sysInfo->memory();
  159. $systemMemory = ['system_memory' => $memory['MemTotal'] ?? 0];
  160. $this->variables = array_merge($globalStatus, $globalVariables, $systemMemory);
  161. }
  162. /**
  163. * @param string|int $variable Variable to set
  164. * @param mixed $value Value to set
  165. */
  166. public function setVariable($variable, $value): void
  167. {
  168. $this->variables[$variable] = $value;
  169. }
  170. private function setRules(): void
  171. {
  172. $isMariaDB = strpos($this->variables['version'], 'MariaDB') !== false;
  173. $genericRules = include ROOT_PATH . self::GENERIC_RULES_FILE;
  174. if (! $isMariaDB && $this->globals['PMA_MYSQL_INT_VERSION'] >= 80003) {
  175. $this->rules = $genericRules;
  176. return;
  177. }
  178. $extraRules = include ROOT_PATH . self::BEFORE_MYSQL80003_RULES_FILE;
  179. $this->rules = array_merge($genericRules, $extraRules);
  180. }
  181. /**
  182. * @return array
  183. */
  184. public function getRunResult(): array
  185. {
  186. return $this->runResult;
  187. }
  188. /**
  189. * @return array
  190. */
  191. public function run(): array
  192. {
  193. $this->setVariables();
  194. $this->setRules();
  195. $this->runRules();
  196. return $this->runResult;
  197. }
  198. /**
  199. * Stores current error in run results.
  200. *
  201. * @param string $description description of an error.
  202. * @param Throwable $exception exception raised
  203. */
  204. private function storeError(string $description, Throwable $exception): void
  205. {
  206. $this->runResult['errors'][] = $description . ' ' . sprintf(
  207. __('Error when evaluating: %s'),
  208. $exception->getMessage()
  209. );
  210. }
  211. /**
  212. * Executes advisor rules
  213. */
  214. private function runRules(): void
  215. {
  216. $this->runResult = [
  217. 'fired' => [],
  218. 'notfired' => [],
  219. 'unchecked' => [],
  220. 'errors' => [],
  221. ];
  222. foreach ($this->rules as $rule) {
  223. $this->variables['value'] = 0;
  224. $precondition = true;
  225. if (isset($rule['precondition'])) {
  226. try {
  227. $precondition = $this->evaluateRuleExpression($rule['precondition']);
  228. } catch (Throwable $e) {
  229. $this->storeError(
  230. sprintf(
  231. __('Failed evaluating precondition for rule \'%s\'.'),
  232. $rule['name']
  233. ),
  234. $e
  235. );
  236. continue;
  237. }
  238. }
  239. if (! $precondition) {
  240. $this->addRule('unchecked', $rule);
  241. continue;
  242. }
  243. try {
  244. $value = $this->evaluateRuleExpression($rule['formula']);
  245. } catch (Throwable $e) {
  246. $this->storeError(
  247. sprintf(
  248. __('Failed calculating value for rule \'%s\'.'),
  249. $rule['name']
  250. ),
  251. $e
  252. );
  253. continue;
  254. }
  255. $this->variables['value'] = $value;
  256. try {
  257. if ($this->evaluateRuleExpression($rule['test'])) {
  258. $this->addRule('fired', $rule);
  259. } else {
  260. $this->addRule('notfired', $rule);
  261. }
  262. } catch (Throwable $e) {
  263. $this->storeError(
  264. sprintf(
  265. __('Failed running test for rule \'%s\'.'),
  266. $rule['name']
  267. ),
  268. $e
  269. );
  270. }
  271. }
  272. }
  273. /**
  274. * Adds a rule to the result list
  275. *
  276. * @param string $type type of rule
  277. * @param array $rule rule itself
  278. */
  279. public function addRule(string $type, array $rule): void
  280. {
  281. if ($type !== 'notfired' && $type !== 'fired') {
  282. $this->runResult[$type][] = $rule;
  283. return;
  284. }
  285. if (isset($rule['justification_formula'])) {
  286. try {
  287. $params = $this->evaluateRuleExpression('[' . $rule['justification_formula'] . ']');
  288. } catch (Throwable $e) {
  289. $this->storeError(
  290. sprintf(__('Failed formatting string for rule \'%s\'.'), $rule['name']),
  291. $e
  292. );
  293. return;
  294. }
  295. $rule['justification'] = vsprintf($rule['justification'], $params);
  296. }
  297. // Replaces {server_variable} with 'server_variable'
  298. // linking to /server/variables
  299. $rule['recommendation'] = preg_replace_callback(
  300. '/\{([a-z_0-9]+)\}/Ui',
  301. function (array $matches) {
  302. return $this->replaceVariable($matches);
  303. },
  304. $rule['recommendation']
  305. );
  306. $rule['issue'] = preg_replace_callback(
  307. '/\{([a-z_0-9]+)\}/Ui',
  308. function (array $matches) {
  309. return $this->replaceVariable($matches);
  310. },
  311. $rule['issue']
  312. );
  313. // Replaces external Links with Core::linkURL() generated links
  314. $rule['recommendation'] = preg_replace_callback(
  315. '#href=("|\')(https?://[^"\']+)\1#i',
  316. function (array $matches) {
  317. return $this->replaceLinkURL($matches);
  318. },
  319. $rule['recommendation']
  320. );
  321. $this->runResult[$type][] = $rule;
  322. }
  323. /**
  324. * Callback for wrapping links with Core::linkURL
  325. *
  326. * @param array $matches List of matched elements form preg_replace_callback
  327. *
  328. * @return string Replacement value
  329. */
  330. private function replaceLinkURL(array $matches): string
  331. {
  332. return 'href="' . Core::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"';
  333. }
  334. /**
  335. * Callback for wrapping variable edit links
  336. *
  337. * @param array $matches List of matched elements form preg_replace_callback
  338. *
  339. * @return string Replacement value
  340. */
  341. private function replaceVariable(array $matches): string
  342. {
  343. return '<a href="' . Url::getFromRoute('/server/variables', ['filter' => $matches[1]])
  344. . '">' . htmlspecialchars($matches[1]) . '</a>';
  345. }
  346. /**
  347. * Runs a code expression, replacing variable names with their respective values
  348. *
  349. * @return mixed result of evaluated expression
  350. */
  351. private function evaluateRuleExpression(string $expression)
  352. {
  353. return $this->expression->evaluate($expression, array_merge($this->variables, $this->globals));
  354. }
  355. /**
  356. * Formats interval like 10 per hour
  357. *
  358. * @param float $num number to format
  359. * @param int $precision required precision
  360. *
  361. * @return string formatted string
  362. */
  363. public static function byTime(float $num, int $precision): string
  364. {
  365. if ($num >= 1) { // per second
  366. $per = __('per second');
  367. } elseif ($num * 60 >= 1) { // per minute
  368. $num *= 60;
  369. $per = __('per minute');
  370. } elseif ($num * 60 * 60 >= 1) { // per hour
  371. $num *= 60 * 60;
  372. $per = __('per hour');
  373. } else {
  374. $num *= 24 * 60 * 60;
  375. $per = __('per day');
  376. }
  377. $num = round($num, $precision);
  378. if ($num == 0) {
  379. $num = '<' . pow(10, -$precision);
  380. }
  381. return $num . ' ' . $per;
  382. }
  383. }