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', static function (): void { }, /** * @param array $arguments * @param float $num */ static function ($arguments, $num) { return round($num); } ); $this->expression->register( 'substr', static function (): void { }, /** * @param array $arguments * @param string $string * @param int $start * @param int $length */ static function ($arguments, $string, $start, $length) { return substr($string, $start, $length); } ); $this->expression->register( 'preg_match', static function (): void { }, /** * @param array $arguments * @param string $pattern * @param string $subject */ static function ($arguments, $pattern, $subject) { return preg_match($pattern, $subject); } ); $this->expression->register( 'ADVISOR_bytime', static function (): void { }, /** * @param array $arguments * @param float $num * @param int $precision */ static function ($arguments, $num, $precision) { return self::byTime($num, $precision); } ); $this->expression->register( 'ADVISOR_timespanFormat', static function (): void { }, /** * @param array $arguments * @param string $seconds */ static function ($arguments, $seconds) { return Util::timespanFormat((int) $seconds); } ); $this->expression->register( 'ADVISOR_formatByteDown', static function (): void { }, /** * @param array $arguments * @param int $value * @param int $limes * @param int $comma */ static function ($arguments, $value, $limes = 6, $comma = 0) { return implode(' ', (array) Util::formatByteDown($value, $limes, $comma)); } ); $this->expression->register( 'fired', static function (): void { }, /** * @param array $arguments * @param int $value */ 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(), 'IS_MARIADB' => $this->dbi->isMariaDB(), ]; } private function setVariables(): void { $globalStatus = $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1); $globalVariables = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1); $sysInfo = SysInfo::get(); $memory = $sysInfo->memory(); $systemMemory = ['system_memory' => $memory['MemTotal'] ?? 0]; $this->variables = array_merge($globalStatus, $globalVariables, $systemMemory); } /** * @param string|int $variable Variable to set * @param mixed $value Value to set */ public function setVariable($variable, $value): void { $this->variables[$variable] = $value; } private function setRules(): void { $isMariaDB = str_contains($this->variables['version'], 'MariaDB'); $genericRules = include ROOT_PATH . self::GENERIC_RULES_FILE; if (! $isMariaDB && $this->globals['PMA_MYSQL_INT_VERSION'] >= 80003) { $this->rules = $genericRules; return; } $extraRules = include ROOT_PATH . self::BEFORE_MYSQL80003_RULES_FILE; $this->rules = array_merge($genericRules, $extraRules); } /** * @return array */ public function getRunResult(): array { return $this->runResult; } /** * @return array */ public function run(): array { $this->setVariables(); $this->setRules(); $this->runRules(); return $this->runResult; } /** * Stores current error in run results. * * @param string $description description of an error. * @param Throwable $exception exception raised */ private function storeError(string $description, Throwable $exception): void { $this->runResult['errors'][] = $description . ' ' . sprintf( __('Error when evaluating: %s'), $exception->getMessage() ); } /** * Executes advisor rules */ private function runRules(): void { $this->runResult = [ 'fired' => [], 'notfired' => [], 'unchecked' => [], 'errors' => [], ]; foreach ($this->rules as $rule) { $this->variables['value'] = 0; $precondition = true; if (isset($rule['precondition'])) { try { $precondition = $this->evaluateRuleExpression($rule['precondition']); } catch (Throwable $e) { $this->storeError( sprintf( __('Failed evaluating precondition for rule \'%s\'.'), $rule['name'] ), $e ); continue; } } if (! $precondition) { $this->addRule('unchecked', $rule); continue; } try { $value = $this->evaluateRuleExpression($rule['formula']); } catch (Throwable $e) { $this->storeError( sprintf( __('Failed calculating value for rule \'%s\'.'), $rule['name'] ), $e ); continue; } $this->variables['value'] = $value; try { if ($this->evaluateRuleExpression($rule['test'])) { $this->addRule('fired', $rule); } else { $this->addRule('notfired', $rule); } } catch (Throwable $e) { $this->storeError( sprintf( __('Failed running test for rule \'%s\'.'), $rule['name'] ), $e ); } } } /** * Adds a rule to the result list * * @param string $type type of rule * @param array $rule rule itself */ public function addRule(string $type, array $rule): void { if ($type !== 'notfired' && $type !== 'fired') { $this->runResult[$type][] = $rule; return; } if (isset($rule['justification_formula'])) { try { $params = $this->evaluateRuleExpression('[' . $rule['justification_formula'] . ']'); } catch (Throwable $e) { $this->storeError( sprintf(__('Failed formatting string for rule \'%s\'.'), $rule['name']), $e ); return; } $rule['justification'] = vsprintf($rule['justification'], $params); } // Replaces {server_variable} with 'server_variable' // linking to /server/variables $rule['recommendation'] = preg_replace_callback( '/\{([a-z_0-9]+)\}/Ui', function (array $matches) { return $this->replaceVariable($matches); }, $rule['recommendation'] ); $rule['issue'] = preg_replace_callback( '/\{([a-z_0-9]+)\}/Ui', function (array $matches) { return $this->replaceVariable($matches); }, $rule['issue'] ); // Replaces external Links with Core::linkURL() generated links $rule['recommendation'] = preg_replace_callback( '#href=("|\')(https?://[^"\']+)\1#i', function (array $matches) { return $this->replaceLinkURL($matches); }, $rule['recommendation'] ); $this->runResult[$type][] = $rule; } /** * 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 '' . htmlspecialchars($matches[1]) . ''; } /** * Runs a code expression, replacing variable names with their respective values * * @return mixed result of evaluated expression */ private function evaluateRuleExpression(string $expression) { return $this->expression->evaluate($expression, array_merge($this->variables, $this->globals)); } /** * Formats interval like 10 per hour * * @param float $num number to format * @param int $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 *= 60 * 60; $per = __('per hour'); } else { $num *= 24 * 60 * 60; $per = __('per day'); } $num = round($num, $precision); if ($num == 0) { $num = '<' . 10 ** (-$precision); } return $num . ' ' . $per; } }