Server IP : 85.214.239.14 / Your IP : 3.15.144.162 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /var/www/wordpress/phpMyAdmin/libraries/classes/ |
Upload File : |
<?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)); } }