diff options
| author | Charles <sircharlesaze@gmail.com> | 2020-01-09 10:55:03 +0100 |
|---|---|---|
| committer | Charles <sircharlesaze@gmail.com> | 2020-01-09 13:09:38 +0100 |
| commit | 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa (patch) | |
| tree | 5c691241355c943a3c68ddb06b8cf8c60aa11319 /srcs/phpmyadmin/libraries/classes/Advisor.php | |
| parent | 7e0d85db834d6351ed85d01e5126ac31dc510b86 (diff) | |
| download | ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.gz ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.bz2 ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.zip | |
phpmyadmin working
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Advisor.php')
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Advisor.php | 707 |
1 files changed, 707 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Advisor.php b/srcs/phpmyadmin/libraries/classes/Advisor.php new file mode 100644 index 0000000..6dad8b6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Advisor.php @@ -0,0 +1,707 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * A simple rules engine, that parses and executes the rules in advisory_rules.txt. + * Adjusted to phpMyAdmin. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin; + +use Exception; +use PhpMyAdmin\Core; +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\SysInfo; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Throwable; +use function array_merge_recursive; + +/** + * Advisor class + * + * @package PhpMyAdmin + */ +class Advisor +{ + public const GENERIC_RULES_FILE = 'libraries/advisory_rules_generic.txt'; + public const BEFORE_MYSQL80003_RULES_FILE = 'libraries/advisory_rules_mysql_before80003.txt'; + + protected $dbi; + protected $variables; + protected $globals; + protected $parseResult; + protected $runResult; + protected $expression; + + /** + * Constructor + * + * @param DatabaseInterface $dbi DatabaseInterface object + * @param ExpressionLanguage $expression ExpressionLanguage object + */ + public function __construct(DatabaseInterface $dbi, ExpressionLanguage $expression) + { + $this->dbi = $dbi; + $this->expression = $expression; + /* + * Register functions for ExpressionLanguage, we intentionally + * do not implement support for compile as we do not use it. + */ + $this->expression->register( + 'round', + function () { + }, + function ($arguments, $num) { + return round($num); + } + ); + $this->expression->register( + 'substr', + function () { + }, + function ($arguments, $string, $start, $length) { + return substr($string, $start, $length); + } + ); + $this->expression->register( + 'preg_match', + function () { + }, + function ($arguments, $pattern, $subject) { + return preg_match($pattern, $subject); + } + ); + $this->expression->register( + 'ADVISOR_bytime', + function () { + }, + function ($arguments, $num, $precision) { + return self::byTime($num, $precision); + } + ); + $this->expression->register( + 'ADVISOR_timespanFormat', + function () { + }, + function ($arguments, $seconds) { + return self::timespanFormat((int) $seconds); + } + ); + $this->expression->register( + 'ADVISOR_formatByteDown', + function () { + }, + function ($arguments, $value, $limes = 6, $comma = 0) { + return self::formatByteDown($value, $limes, $comma); + } + ); + $this->expression->register( + 'fired', + function () { + }, + function ($arguments, $value) { + if (! isset($this->runResult['fired'])) { + return 0; + } + + // Did matching rule fire? + foreach ($this->runResult['fired'] as $rule) { + if ($rule['id'] == $value) { + return '1'; + } + } + + return '0'; + } + ); + /* Some global variables for advisor */ + $this->globals = [ + 'PMA_MYSQL_INT_VERSION' => $this->dbi->getVersion(), + ]; + } + + /** + * Get variables + * + * @return mixed + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Set variables + * + * @param array $variables Variables + * + * @return Advisor + */ + public function setVariables(array $variables): self + { + $this->variables = $variables; + + return $this; + } + + /** + * Set a variable and its value + * + * @param string|int $variable Variable to set + * @param mixed $value Value to set + * + * @return Advisor + */ + public function setVariable($variable, $value): self + { + $this->variables[$variable] = $value; + + return $this; + } + + /** + * Get parseResult + * + * @return mixed + */ + public function getParseResult() + { + return $this->parseResult; + } + + /** + * Set parseResult + * + * @param array $parseResult Parse result + * + * @return Advisor + */ + public function setParseResult(array $parseResult): self + { + $this->parseResult = $parseResult; + + return $this; + } + + /** + * Get runResult + * + * @return mixed + */ + public function getRunResult() + { + return $this->runResult; + } + + /** + * Set runResult + * + * @param array $runResult Run result + * + * @return Advisor + */ + public function setRunResult(array $runResult): self + { + $this->runResult = $runResult; + + return $this; + } + + /** + * Parses and executes advisor rules + * + * @return array with run and parse results + */ + public function run(): array + { + // HowTo: A simple Advisory system in 3 easy steps. + + // Step 1: Get some variables to evaluate on + $this->setVariables( + array_merge( + $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1), + $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1) + ) + ); + + // Add total memory to variables as well + $sysinfo = SysInfo::get(); + $memory = $sysinfo->memory(); + $this->variables['system_memory'] + = isset($memory['MemTotal']) ? $memory['MemTotal'] : 0; + + $ruleFiles = $this->defineRulesFiles(); + + // Step 2: Read and parse the list of rules + $parsedResults = []; + foreach ($ruleFiles as $ruleFile) { + $parsedResults[] = $this->parseRulesFile($ruleFile); + } + $this->setParseResult(array_merge_recursive(...$parsedResults)); + + // Step 3: Feed the variables to the rules and let them fire. Sets + // $runResult + $this->runRules(); + + return [ + 'parse' => ['errors' => $this->parseResult['errors']], + 'run' => $this->runResult, + ]; + } + + /** + * Stores current error in run results. + * + * @param string $description description of an error. + * @param Throwable $exception exception raised + * + * @return void + */ + public function storeError(string $description, Throwable $exception): void + { + $this->runResult['errors'][] = $description + . ' ' + . sprintf( + __('Error when evaluating: %s'), + $exception->getMessage() + ); + } + + /** + * Executes advisor rules + * + * @return boolean + */ + public function runRules(): bool + { + $this->setRunResult( + [ + 'fired' => [], + 'notfired' => [], + 'unchecked' => [], + 'errors' => [], + ] + ); + + foreach ($this->parseResult['rules'] as $rule) { + $this->variables['value'] = 0; + $precond = true; + + if (isset($rule['precondition'])) { + try { + $precond = $this->ruleExprEvaluate($rule['precondition']); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed evaluating precondition for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + continue; + } + } + + if (! $precond) { + $this->addRule('unchecked', $rule); + } else { + try { + $value = $this->ruleExprEvaluate($rule['formula']); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed calculating value for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + continue; + } + + $this->variables['value'] = $value; + + try { + if ($this->ruleExprEvaluate($rule['test'])) { + $this->addRule('fired', $rule); + } else { + $this->addRule('notfired', $rule); + } + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed running test for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + } + } + } + + return true; + } + + /** + * Escapes percent string to be used in format string. + * + * @param string $str string to escape + * + * @return string + */ + public static function escapePercent(string $str): string + { + return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str); + } + + /** + * Wrapper function for translating. + * + * @param string $str the string + * @param string $param the parameters + * + * @return string + * @throws Exception + */ + public function translate(string $str, ?string $param = null): string + { + $string = _gettext(self::escapePercent($str)); + if ($param !== null) { + $params = $this->ruleExprEvaluate('[' . $param . ']'); + } else { + $params = []; + } + return vsprintf($string, $params); + } + + /** + * Splits justification to text and formula. + * + * @param array $rule the rule + * + * @return string[] + */ + public static function splitJustification(array $rule): array + { + $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2); + if (count($jst) > 1) { + return [ + $jst[0], + $jst[1], + ]; + } + return [$rule['justification']]; + } + + /** + * Adds a rule to the result list + * + * @param string $type type of rule + * @param array $rule rule itself + * + * @return void + * @throws Exception + */ + public function addRule(string $type, array $rule): void + { + switch ($type) { + case 'notfired': + case 'fired': + $jst = self::splitJustification($rule); + if (count($jst) > 1) { + try { + /* Translate */ + $str = $this->translate($jst[0], $jst[1]); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed formatting string for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + return; + } + + $rule['justification'] = $str; + } else { + $rule['justification'] = $this->translate($rule['justification']); + } + $rule['id'] = $rule['name']; + $rule['name'] = $this->translate($rule['name']); + $rule['issue'] = $this->translate($rule['issue']); + + // Replaces {server_variable} with 'server_variable' + // linking to server_variables.php + $rule['recommendation'] = preg_replace_callback( + '/\{([a-z_0-9]+)\}/Ui', + [ + $this, + 'replaceVariable', + ], + $this->translate($rule['recommendation']) + ); + + // Replaces external Links with Core::linkURL() generated links + $rule['recommendation'] = preg_replace_callback( + '#href=("|\')(https?://[^\1]+)\1#i', + [ + $this, + 'replaceLinkURL', + ], + $rule['recommendation'] + ); + break; + } + + $this->runResult[$type][] = $rule; + } + + /** + * Defines the rules files to use + * + * @return array + */ + protected function defineRulesFiles(): array + { + $isMariaDB = false !== strpos($this->getVariables()['version'], 'MariaDB'); + $ruleFiles = [self::GENERIC_RULES_FILE]; + // If MariaDB (= not MySQL) OR MYSQL < 8.0.3, add another rules file. + if ($isMariaDB || $this->globals['PMA_MYSQL_INT_VERSION'] < 80003) { + $ruleFiles[] = self::BEFORE_MYSQL80003_RULES_FILE; + } + return $ruleFiles; + } + + /** + * Callback for wrapping links with Core::linkURL + * + * @param array $matches List of matched elements form preg_replace_callback + * + * @return string Replacement value + */ + private function replaceLinkURL(array $matches): string + { + return 'href="' . Core::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"'; + } + + /** + * Callback for wrapping variable edit links + * + * @param array $matches List of matched elements form preg_replace_callback + * + * @return string Replacement value + */ + private function replaceVariable(array $matches): string + { + return '<a href="server_variables.php' . Url::getCommon(['filter' => $matches[1]]) + . '">' . htmlspecialchars($matches[1]) . '</a>'; + } + + /** + * Runs a code expression, replacing variable names with their respective + * values + * + * @param string $expr expression to evaluate + * + * @return mixed result of evaluated expression + * + * @throws Exception + */ + public function ruleExprEvaluate(string $expr) + { + // Actually evaluate the code + // This can throw exception + $value = $this->expression->evaluate( + $expr, + array_merge($this->variables, $this->globals) + ); + + return $value; + } + + /** + * Reads the rule file into an array, throwing errors messages on syntax + * errors. + * + * @param string $filename Name of file to parse + * + * @return array with parsed data + */ + public static function parseRulesFile(string $filename): array + { + $file = file($filename, FILE_IGNORE_NEW_LINES); + + $errors = []; + $rules = []; + $lines = []; + + if ($file === false) { + $errors[] = sprintf( + __('Error in reading file: The file \'%s\' does not exist or is not readable!'), + $filename + ); + return [ + 'rules' => $rules, + 'lines' => $lines, + 'errors' => $errors, + ]; + } + + $ruleSyntax = [ + 'name', + 'formula', + 'test', + 'issue', + 'recommendation', + 'justification', + ]; + $numRules = count($ruleSyntax); + $numLines = count($file); + $ruleNo = -1; + $ruleLine = -1; + + for ($i = 0; $i < $numLines; $i++) { + $line = $file[$i]; + if ($line == "" || $line[0] == '#') { + continue; + } + + // Reading new rule + if (substr($line, 0, 4) == 'rule') { + if ($ruleLine > 0) { + $errors[] = sprintf( + __( + 'Invalid rule declaration on line %1$s, expected line ' + . '%2$s of previous rule.' + ), + $i + 1, + $ruleSyntax[$ruleLine++] + ); + continue; + } + if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) { + $ruleLine = 1; + $ruleNo++; + $rules[$ruleNo] = ['name' => $match[1]]; + $lines[$ruleNo] = ['name' => $i + 1]; + if (isset($match[3])) { + $rules[$ruleNo]['precondition'] = $match[3]; + $lines[$ruleNo]['precondition'] = $i + 1; + } + } else { + $errors[] = sprintf( + __('Invalid rule declaration on line %s.'), + $i + 1 + ); + } + continue; + } elseif ($ruleLine == -1) { + $errors[] = sprintf( + __('Unexpected characters on line %s.'), + $i + 1 + ); + } + + // Reading rule lines + if ($ruleLine > 0) { + if (! isset($line[0])) { + continue; // Empty lines are ok + } + // Non tabbed lines are not + if ($line[0] != "\t") { + $errors[] = sprintf( + __( + 'Unexpected character on line %1$s. Expected tab, but ' + . 'found "%2$s".' + ), + $i + 1, + $line[0] + ); + continue; + } + $rules[$ruleNo][$ruleSyntax[$ruleLine]] = rtrim( + mb_substr($line, 1) + ); + $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1; + ++$ruleLine; + } + + // Rule complete + if ($ruleLine == $numRules) { + $ruleLine = -1; + } + } + + return [ + 'rules' => $rules, + 'lines' => $lines, + 'errors' => $errors, + ]; + } + + /** + * Formats interval like 10 per hour + * + * @param float $num number to format + * @param integer $precision required precision + * + * @return string formatted string + */ + public static function byTime(float $num, int $precision): string + { + if ($num >= 1) { // per second + $per = __('per second'); + } elseif ($num * 60 >= 1) { // per minute + $num *= 60; + $per = __('per minute'); + } elseif ($num * 60 * 60 >= 1) { // per hour + $num = $num * 60 * 60; + $per = __('per hour'); + } else { + $num = $num * 60 * 60 * 24; + $per = __('per day'); + } + + $num = round($num, $precision); + + if ($num == 0) { + $num = '<' . pow(10, -$precision); + } + + return "$num $per"; + } + + /** + * Wrapper for PhpMyAdmin\Util::timespanFormat + * + * This function is used when evaluating advisory_rules.txt + * + * @param int $seconds the timespan + * + * @return string the formatted value + */ + public static function timespanFormat(int $seconds): string + { + return Util::timespanFormat($seconds); + } + + /** + * Wrapper around PhpMyAdmin\Util::formatByteDown + * + * This function is used when evaluating advisory_rules.txt + * + * @param double|string $value the value to format + * @param int $limes the sensitiveness + * @param int $comma the number of decimals to retain + * + * @return string the formatted value with unit + */ + public static function formatByteDown($value, int $limes = 6, int $comma = 0): string + { + return implode(' ', Util::formatByteDown($value, $limes, $comma)); + } +} |
