Update website

This commit is contained in:
Guilhem Lavaux 2025-03-24 09:27:39 +01:00
parent a0b0d3dae7
commit ae7ef6ad45
3151 changed files with 566766 additions and 48 deletions

View file

@ -0,0 +1,431 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Server\SysInfo\SysInfo;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Throwable;
use function array_merge;
use function htmlspecialchars;
use function implode;
use function pow;
use function preg_match;
use function preg_replace_callback;
use function round;
use function sprintf;
use function strpos;
use function substr;
use function vsprintf;
/**
* A simple rules engine, that executes the rules in the advisory_rules files.
*/
class Advisor
{
private const GENERIC_RULES_FILE = 'libraries/advisory_rules_generic.php';
private const BEFORE_MYSQL80003_RULES_FILE = 'libraries/advisory_rules_mysql_before80003.php';
/** @var DatabaseInterface */
private $dbi;
/** @var array */
private $variables;
/** @var array */
private $globals;
/** @var array */
private $rules;
/** @var array */
private $runResult;
/** @var ExpressionLanguage */
private $expression;
/**
* @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',
static function () {
},
/**
* @param array $arguments
* @param float $num
*/
static function ($arguments, $num) {
return round($num);
}
);
$this->expression->register(
'substr',
static function () {
},
/**
* @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 () {
},
/**
* @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 () {
},
/**
* @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 () {
},
/**
* @param array $arguments
* @param string $seconds
*/
static function ($arguments, $seconds) {
return Util::timespanFormat((int) $seconds);
}
);
$this->expression->register(
'ADVISOR_formatByteDown',
static function () {
},
/**
* @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 () {
},
/**
* @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(),
];
}
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 = strpos($this->variables['version'], 'MariaDB') !== false;
$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 '<a href="' . Url::getFromRoute('/server/variables', ['filter' => $matches[1]])
. '">' . htmlspecialchars($matches[1]) . '</a>';
}
/**
* 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 = '<' . pow(10, -$precision);
}
return $num . ' ' . $per;
}
}

View file

@ -0,0 +1,396 @@
<?php
/**
* Handles bookmarking SQL queries
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use const PREG_SET_ORDER;
use function count;
use function is_array;
use function preg_match_all;
use function preg_replace;
use function str_replace;
use function strlen;
/**
* Handles bookmarking SQL queries
*/
class Bookmark
{
/**
* ID of the bookmark
*
* @var int
*/
private $id;
/**
* Database the bookmark belongs to
*
* @var string
*/
private $database;
/**
* The user to whom the bookmark belongs, empty for public bookmarks
*
* @var string
*/
private $currentUser;
/**
* Label of the bookmark
*
* @var string
*/
private $label;
/**
* SQL query that is bookmarked
*
* @var string
*/
private $query;
/** @var DatabaseInterface */
private $dbi;
/**
* Current user
*
* @var string
*/
private $user;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
*/
public function __construct(DatabaseInterface $dbi, string $user)
{
$this->dbi = $dbi;
$this->user = $user;
}
/**
* Returns the ID of the bookmark
*/
public function getId(): int
{
return (int) $this->id;
}
/**
* Returns the database of the bookmark
*/
public function getDatabase(): string
{
return $this->database;
}
/**
* Returns the user whom the bookmark belongs to
*/
public function getUser(): string
{
return $this->currentUser;
}
/**
* Returns the label of the bookmark
*/
public function getLabel(): string
{
return $this->label;
}
/**
* Returns the query
*/
public function getQuery(): string
{
return $this->query;
}
/**
* Adds a bookmark
*
* @return bool whether the INSERT succeeds or not
*
* @access public
*/
public function save(): bool
{
$cfgBookmark = self::getParams($this->user);
if (! is_array($cfgBookmark)) {
return false;
}
$query = 'INSERT INTO ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. ' (id, dbase, user, query, label) VALUES (NULL, '
. "'" . $this->dbi->escapeString($this->database) . "', "
. "'" . $this->dbi->escapeString($this->currentUser) . "', "
. "'" . $this->dbi->escapeString($this->query) . "', "
. "'" . $this->dbi->escapeString($this->label) . "')";
return $this->dbi->query($query, DatabaseInterface::CONNECT_CONTROL);
}
/**
* Deletes a bookmark
*
* @return bool true if successful
*
* @access public
*/
public function delete(): bool
{
$cfgBookmark = self::getParams($this->user);
if (! is_array($cfgBookmark)) {
return false;
}
$query = 'DELETE FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. ' WHERE id = ' . $this->id;
return $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL);
}
/**
* Returns the number of variables in a bookmark
*
* @return int number of variables
*/
public function getVariableCount(): int
{
$matches = [];
preg_match_all('/\[VARIABLE[0-9]*\]/', $this->query, $matches, PREG_SET_ORDER);
return count($matches);
}
/**
* Replace the placeholders in the bookmark query with variables
*
* @param array $variables array of variables
*
* @return string query with variables applied
*/
public function applyVariables(array $variables): string
{
// remove comments that encloses a variable placeholder
$query = (string) preg_replace(
'|/\*(.*\[VARIABLE[0-9]*\].*)\*/|imsU',
'${1}',
$this->query
);
// replace variable placeholders with values
$number_of_variables = $this->getVariableCount();
for ($i = 1; $i <= $number_of_variables; $i++) {
$var = '';
if (! empty($variables[$i])) {
$var = $this->dbi->escapeString($variables[$i]);
}
$query = str_replace('[VARIABLE' . $i . ']', $var, $query);
// backward compatibility
if ($i != 1) {
continue;
}
$query = str_replace('[VARIABLE]', $var, $query);
}
return $query;
}
/**
* Defines the bookmark parameters for the current user
*
* @param string $user Current user
*
* @return array|bool the bookmark parameters for the current user
*
* @access public
*/
public static function getParams(string $user)
{
global $dbi;
static $cfgBookmark = null;
if ($cfgBookmark !== null) {
return $cfgBookmark;
}
$relation = new Relation($dbi);
$cfgRelation = $relation->getRelationsParam();
if ($cfgRelation['bookmarkwork']) {
$cfgBookmark = [
'user' => $user,
'db' => $cfgRelation['db'],
'table' => $cfgRelation['bookmark'],
];
} else {
$cfgBookmark = false;
}
return $cfgBookmark;
}
/**
* Creates a Bookmark object from the parameters
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param array $bkm_fields the properties of the bookmark to add; here,
* $bkm_fields['bkm_sql_query'] is urlencoded
* @param bool $all_users whether to make the bookmark
* available for all users
*
* @return Bookmark|false
*/
public static function createBookmark(
DatabaseInterface $dbi,
string $user,
array $bkm_fields,
bool $all_users = false
) {
if (! (isset($bkm_fields['bkm_sql_query'], $bkm_fields['bkm_label'])
&& strlen($bkm_fields['bkm_sql_query']) > 0
&& strlen($bkm_fields['bkm_label']) > 0)
) {
return false;
}
$bookmark = new Bookmark($dbi, $user);
$bookmark->database = $bkm_fields['bkm_database'];
$bookmark->label = $bkm_fields['bkm_label'];
$bookmark->query = $bkm_fields['bkm_sql_query'];
$bookmark->currentUser = $all_users ? '' : $bkm_fields['bkm_user'];
return $bookmark;
}
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param array $row Resource used to build the bookmark
*/
protected static function createFromRow(
DatabaseInterface $dbi,
string $user,
$row
): Bookmark {
$bookmark = new Bookmark($dbi, $user);
$bookmark->id = $row['id'];
$bookmark->database = $row['dbase'];
$bookmark->currentUser = $row['user'];
$bookmark->label = $row['label'];
$bookmark->query = $row['query'];
return $bookmark;
}
/**
* Gets the list of bookmarks defined for the current database
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param string|false $db the current database name or false
*
* @return Bookmark[] the bookmarks list
*
* @access public
*/
public static function getList(
DatabaseInterface $dbi,
string $user,
$db = false
): array {
$cfgBookmark = self::getParams($user);
if (! is_array($cfgBookmark)) {
return [];
}
$query = 'SELECT * FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. " WHERE ( `user` = ''"
. " OR `user` = '" . $dbi->escapeString($cfgBookmark['user']) . "' )";
if ($db !== false) {
$query .= " AND dbase = '" . $dbi->escapeString($db) . "'";
}
$query .= ' ORDER BY label ASC';
$result = $dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL,
DatabaseInterface::QUERY_STORE
);
if (! empty($result)) {
$bookmarks = [];
foreach ($result as $row) {
$bookmarks[] = self::createFromRow($dbi, $user, $row);
}
return $bookmarks;
}
return [];
}
/**
* Retrieve a specific bookmark
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param string $db the current database name
* @param int|string $id an identifier of the bookmark to get
* @param string $id_field which field to look up the identifier
* @param bool $action_bookmark_all true: get all bookmarks regardless
* of the owning user
* @param bool $exact_user_match whether to ignore bookmarks with no user
*
* @return Bookmark|null the bookmark
*
* @access public
*/
public static function get(
DatabaseInterface $dbi,
string $user,
string $db,
$id,
string $id_field = 'id',
bool $action_bookmark_all = false,
bool $exact_user_match = false
): ?self {
$cfgBookmark = self::getParams($user);
if (! is_array($cfgBookmark)) {
return null;
}
$query = 'SELECT * FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. " WHERE dbase = '" . $dbi->escapeString($db) . "'";
if (! $action_bookmark_all) {
$query .= " AND (user = '"
. $dbi->escapeString($cfgBookmark['user']) . "'";
if (! $exact_user_match) {
$query .= " OR user = ''";
}
$query .= ')';
}
$query .= ' AND ' . Util::backquote($id_field)
. " = '" . $dbi->escapeString((string) $id) . "' LIMIT 1";
$result = $dbi->fetchSingleRow($query, 'ASSOC', DatabaseInterface::CONNECT_CONTROL);
if (! empty($result)) {
return self::createFromRow($dbi, $user, $result);
}
return null;
}
}

View file

@ -0,0 +1,352 @@
<?php
/**
* Contains functions used by browse foreigners
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use function asort;
use function ceil;
use function floor;
use function htmlspecialchars;
use function is_array;
use function mb_strlen;
use function mb_substr;
/**
* PhpMyAdmin\BrowseForeigners class
*/
class BrowseForeigners
{
/** @var int */
private $limitChars;
/** @var int */
private $maxRows;
/** @var int */
private $repeatCells;
/** @var bool */
private $showAll;
/** @var string */
private $themeImage;
/** @var Template */
public $template;
/**
* @param Template $template Template object
*/
public function __construct(Template $template)
{
global $cfg, $PMA_Theme;
$this->template = $template;
$this->limitChars = (int) $cfg['LimitChars'];
$this->maxRows = (int) $cfg['MaxRows'];
$this->repeatCells = (int) $cfg['RepeatCells'];
$this->showAll = (bool) $cfg['ShowAll'];
$this->themeImage = $PMA_Theme->getImgPath();
}
/**
* Function to get html for one relational key
*
* @param int $horizontal_count the current horizontal count
* @param string $header table header
* @param array $keys all the keys
* @param int $indexByKeyname index by keyname
* @param array $descriptions descriptions
* @param int $indexByDescription index by description
* @param string $current_value current value on the edit form
*
* @return array the generated html
*/
private function getHtmlForOneKey(
int $horizontal_count,
string $header,
array $keys,
int $indexByKeyname,
array $descriptions,
int $indexByDescription,
string $current_value
): array {
$horizontal_count++;
$output = '';
// whether the key name corresponds to the selected value in the form
$rightKeynameIsSelected = false;
$leftKeynameIsSelected = false;
if ($this->repeatCells > 0 && $horizontal_count > $this->repeatCells) {
$output .= $header;
$horizontal_count = 0;
}
// key names and descriptions for the left section,
// sorted by key names
$leftKeyname = $keys[$indexByKeyname];
[
$leftDescription,
$leftDescriptionTitle,
] = $this->getDescriptionAndTitle($descriptions[$indexByKeyname]);
// key names and descriptions for the right section,
// sorted by descriptions
$rightKeyname = $keys[$indexByDescription];
[
$rightDescription,
$rightDescriptionTitle,
] = $this->getDescriptionAndTitle($descriptions[$indexByDescription]);
$indexByDescription++;
if (! empty($current_value)) {
$rightKeynameIsSelected = $rightKeyname == $current_value;
$leftKeynameIsSelected = $leftKeyname == $current_value;
}
$output .= '<tr class="noclick">';
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $leftKeyname,
'description' => $leftDescription,
'title' => $leftDescriptionTitle,
'is_selected' => $leftKeynameIsSelected,
'nowrap' => true,
]);
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $leftKeyname,
'description' => $leftDescription,
'title' => $leftDescriptionTitle,
'is_selected' => $leftKeynameIsSelected,
'nowrap' => false,
]);
$output .= '<td width="20%">'
. '<img src="' . $this->themeImage . 'spacer.png" alt=""'
. ' width="1" height="1"></td>';
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $rightKeyname,
'description' => $rightDescription,
'title' => $rightDescriptionTitle,
'is_selected' => $rightKeynameIsSelected,
'nowrap' => false,
]);
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $rightKeyname,
'description' => $rightDescription,
'title' => $rightDescriptionTitle,
'is_selected' => $rightKeynameIsSelected,
'nowrap' => true,
]);
$output .= '</tr>';
return [
$output,
$horizontal_count,
$indexByDescription,
];
}
/**
* Function to get html for relational field selection
*
* @param string $db current database
* @param string $table current table
* @param string $field field
* @param array $foreignData foreign column data
* @param string|null $fieldkey field key
* @param string $current_value current columns's value
*/
public function getHtmlForRelationalFieldSelection(
string $db,
string $table,
string $field,
array $foreignData,
?string $fieldkey,
string $current_value
): string {
$gotopage = $this->getHtmlForGotoPage($foreignData);
$foreignShowAll = $this->template->render('table/browse_foreigners/show_all', [
'foreign_data' => $foreignData,
'show_all' => $this->showAll,
'max_rows' => $this->maxRows,
]);
$output = '<form class="ajax" '
. 'id="browse_foreign_form" name="browse_foreign_from" action="'
. Url::getFromRoute('/browse-foreigners')
. '" method="post"><fieldset>'
. Url::getHiddenInputs($db, $table)
. '<input type="hidden" name="field" value="' . htmlspecialchars($field)
. '">'
. '<input type="hidden" name="fieldkey" value="'
. (isset($fieldkey) ? htmlspecialchars($fieldkey) : '') . '">';
if (isset($_POST['rownumber'])) {
$output .= '<input type="hidden" name="rownumber" value="'
. htmlspecialchars((string) $_POST['rownumber']) . '">';
}
$filter_value = (isset($_POST['foreign_filter'])
? htmlspecialchars($_POST['foreign_filter'])
: '');
$output .= '<span class="formelement">'
. '<label for="input_foreign_filter">' . __('Search:') . '</label>'
. '<input type="text" name="foreign_filter" '
. 'id="input_foreign_filter" '
. 'value="' . $filter_value . '" data-old="' . $filter_value . '" '
. '>'
. '<input class="btn btn-primary" type="submit" name="submit_foreign_filter" value="'
. __('Go') . '">'
. '</span>'
. '<span class="formelement">' . $gotopage . '</span>'
. '<span class="formelement">' . $foreignShowAll . '</span>'
. '</fieldset>'
. '</form>';
$output .= '<table class="pma-table" width="100%" id="browse_foreign_table">';
if (! is_array($foreignData['disp_row'])) {
return $output . '</tbody>'
. '</table>';
}
$header = '<tr>
<th>' . __('Keyname') . '</th>
<th>' . __('Description') . '</th>
<td width="20%"></td>
<th>' . __('Description') . '</th>
<th>' . __('Keyname') . '</th>
</tr>';
$output .= '<thead>' . $header . '</thead>' . "\n"
. '<tfoot>' . $header . '</tfoot>' . "\n"
. '<tbody>' . "\n";
$descriptions = [];
$keys = [];
foreach ($foreignData['disp_row'] as $relrow) {
if ($foreignData['foreign_display'] != false) {
$descriptions[] = $relrow[$foreignData['foreign_display']] ?? '';
} else {
$descriptions[] = '';
}
$keys[] = $relrow[$foreignData['foreign_field']];
}
asort($keys);
$horizontal_count = 0;
$indexByDescription = 0;
foreach ($keys as $indexByKeyname => $value) {
[
$html,
$horizontal_count,
$indexByDescription,
] = $this->getHtmlForOneKey(
$horizontal_count,
$header,
$keys,
$indexByKeyname,
$descriptions,
$indexByDescription,
$current_value
);
$output .= $html;
}
$output .= '</tbody>'
. '</table>';
return $output;
}
/**
* Get the description (possibly truncated) and the title
*
* @param string $description the key name's description
*
* @return array the new description and title
*/
private function getDescriptionAndTitle(string $description): array
{
if (mb_strlen($description) <= $this->limitChars) {
$description = htmlspecialchars(
$description
);
$descriptionTitle = '';
} else {
$descriptionTitle = htmlspecialchars(
$description
);
$description = htmlspecialchars(
mb_substr(
$description,
0,
$this->limitChars
)
. '...'
);
}
return [
$description,
$descriptionTitle,
];
}
/**
* Function to get html for the goto page option
*
* @param array|null $foreignData foreign data
*/
private function getHtmlForGotoPage(?array $foreignData): string
{
$gotopage = '';
isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0;
if ($foreignData === null || ! is_array($foreignData['disp_row'])) {
return $gotopage;
}
$pageNow = (int) floor($pos / $this->maxRows) + 1;
$nbTotalPage = (int) ceil($foreignData['the_total'] / $this->maxRows);
if ($foreignData['the_total'] > $this->maxRows) {
$gotopage = Util::pageselector(
'pos',
$this->maxRows,
$pageNow,
$nbTotalPage,
200,
5,
5,
20,
10,
__('Page number:')
);
}
return $gotopage;
}
/**
* Function to get foreign limit
*
* @param string|null $foreignShowAll foreign navigation
*/
public function getForeignLimit(?string $foreignShowAll): ?string
{
if (isset($foreignShowAll) && $foreignShowAll == __('Show all')) {
return null;
}
isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0;
return 'LIMIT ' . $pos . ', ' . $this->maxRows . ' ';
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* MySQL charset metadata and manipulations
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use const SORT_STRING;
use function array_keys;
use function count;
use function explode;
use function is_string;
use function ksort;
/**
* Class used to manage MySQL charsets
*/
class Charsets
{
/**
* MySQL charsets map
*
* @var array
*/
public static $mysqlCharsetMap = [
'big5' => 'big5',
'cp-866' => 'cp866',
'euc-jp' => 'ujis',
'euc-kr' => 'euckr',
'gb2312' => 'gb2312',
'gbk' => 'gbk',
'iso-8859-1' => 'latin1',
'iso-8859-2' => 'latin2',
'iso-8859-7' => 'greek',
'iso-8859-8' => 'hebrew',
'iso-8859-8-i' => 'hebrew',
'iso-8859-9' => 'latin5',
'iso-8859-13' => 'latin7',
'iso-8859-15' => 'latin1',
'koi8-r' => 'koi8r',
'shift_jis' => 'sjis',
'tis-620' => 'tis620',
'utf-8' => 'utf8',
'windows-1250' => 'cp1250',
'windows-1251' => 'cp1251',
'windows-1252' => 'latin1',
'windows-1256' => 'cp1256',
'windows-1257' => 'cp1257',
];
/**
* The charset for the server
*
* @var Charset|null
*/
private static $serverCharset = null;
/** @var array<string, Charset> */
private static $charsets = [];
/** @var array<string, array<string, Collation>> */
private static $collations = [];
/**
* Loads charset data from the server
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
private static function loadCharsets(DatabaseInterface $dbi, bool $disableIs): void
{
/* Data already loaded */
if (count(self::$charsets) > 0) {
return;
}
if ($disableIs) {
$sql = 'SHOW CHARACTER SET';
} else {
$sql = 'SELECT `CHARACTER_SET_NAME` AS `Charset`,'
. ' `DEFAULT_COLLATE_NAME` AS `Default collation`,'
. ' `DESCRIPTION` AS `Description`,'
. ' `MAXLEN` AS `Maxlen`'
. ' FROM `information_schema`.`CHARACTER_SETS`';
}
$res = $dbi->query($sql);
self::$charsets = [];
while ($row = $dbi->fetchAssoc($res)) {
self::$charsets[$row['Charset']] = Charset::fromServer($row);
}
$dbi->freeResult($res);
ksort(self::$charsets, SORT_STRING);
}
/**
* Loads collation data from the server
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
private static function loadCollations(DatabaseInterface $dbi, bool $disableIs): void
{
/* Data already loaded */
if (count(self::$collations) > 0) {
return;
}
if ($disableIs) {
$sql = 'SHOW COLLATION';
} else {
$sql = 'SELECT `COLLATION_NAME` AS `Collation`,'
. ' `CHARACTER_SET_NAME` AS `Charset`,'
. ' `ID` AS `Id`,'
. ' `IS_DEFAULT` AS `Default`,'
. ' `IS_COMPILED` AS `Compiled`,'
. ' `SORTLEN` AS `Sortlen`'
. ' FROM `information_schema`.`COLLATIONS`';
}
$res = $dbi->query($sql);
self::$collations = [];
while ($row = $dbi->fetchAssoc($res)) {
self::$collations[$row['Charset']][$row['Collation']] = Collation::fromServer($row);
}
$dbi->freeResult($res);
foreach (array_keys(self::$collations) as $charset) {
ksort(self::$collations[$charset], SORT_STRING);
}
}
/**
* Get current server charset
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
public static function getServerCharset(DatabaseInterface $dbi, bool $disableIs): Charset
{
if (self::$serverCharset !== null) {
return self::$serverCharset;
}
self::loadCharsets($dbi, $disableIs);
$serverCharset = $dbi->getVariable('character_set_server');
if (! is_string($serverCharset)) {// MySQL 5.7.8 fallback, issue #15614
$serverCharset = $dbi->fetchValue('SELECT @@character_set_server;');
}
self::$serverCharset = self::$charsets[$serverCharset] ?? null;
// MySQL 8.0.11+ fallback, issue #16931
if (self::$serverCharset === null && $serverCharset === 'utf8mb3') {
// See: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-charset
// The utf8mb3 character set will be replaced by utf8mb4 in a future MySQL version.
// The utf8 character set is currently an alias for utf8mb3,
// but will at that point become a reference to utf8mb4.
// To avoid ambiguity about the meaning of utf8,
// consider specifying utf8mb4 explicitly for character set references instead of utf8.
// Warning: #3719 'utf8' is currently an alias for the character set UTF8MB3 [...]
return self::$charsets['utf8'];
}
if (self::$serverCharset === null) {// Fallback in case nothing is found
return Charset::fromServer(
[
'Charset' => __('Unknown'),
'Description' => __('Unknown'),
]
);
}
return self::$serverCharset;
}
/**
* Get all server charsets
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*
* @return array
*/
public static function getCharsets(DatabaseInterface $dbi, bool $disableIs): array
{
self::loadCharsets($dbi, $disableIs);
return self::$charsets;
}
/**
* Get all server collations
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*
* @return array
*/
public static function getCollations(DatabaseInterface $dbi, bool $disableIs): array
{
self::loadCollations($dbi, $disableIs);
return self::$collations;
}
/**
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
* @param string|null $name Collation name
*/
public static function findCollationByName(DatabaseInterface $dbi, bool $disableIs, ?string $name): ?Collation
{
$pieces = explode('_', (string) $name);
if ($pieces === false || ! isset($pieces[0])) {
return null;
}
$charset = $pieces[0];
$collations = self::getCollations($dbi, $disableIs);
return $collations[$charset][$name] ?? null;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* Value object class for a character set
*/
declare(strict_types=1);
namespace PhpMyAdmin\Charsets;
/**
* Value object class for a character set
*/
final class Charset
{
/**
* The character set name
*
* @var string
*/
private $name;
/**
* A description of the character set
*
* @var string
*/
private $description;
/**
* The default collation for the character set
*
* @var string
*/
private $defaultCollation;
/**
* The maximum number of bytes required to store one character
*
* @var int
*/
private $maxLength;
/**
* @param string $name Charset name
* @param string $description Description
* @param string $defaultCollation Default collation
* @param int $maxLength Maximum length
*/
private function __construct(
string $name,
string $description,
string $defaultCollation,
int $maxLength
) {
$this->name = $name;
$this->description = $description;
$this->defaultCollation = $defaultCollation;
$this->maxLength = $maxLength;
}
/**
* @param array $state State obtained from the database server
*
* @return Charset
*/
public static function fromServer(array $state): self
{
return new self(
$state['Charset'] ?? '',
$state['Description'] ?? '',
$state['Default collation'] ?? '',
(int) ($state['Maxlen'] ?? 0)
);
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getDefaultCollation(): string
{
return $this->defaultCollation;
}
public function getMaxLength(): int
{
return $this->maxLength;
}
}

View file

@ -0,0 +1,587 @@
<?php
/**
* Value object class for a collation
*/
declare(strict_types=1);
namespace PhpMyAdmin\Charsets;
use function count;
use function explode;
use function implode;
/**
* Value object class for a collation
*/
final class Collation
{
/**
* The collation name
*
* @var string
*/
private $name;
/**
* A description of the collation
*
* @var string
*/
private $description;
/**
* The name of the character set with which the collation is associated
*
* @var string
*/
private $charset;
/**
* The collation ID
*
* @var int
*/
private $id;
/**
* Whether the collation is the default for its character set
*
* @var bool
*/
private $isDefault;
/**
* Whether the character set is compiled into the server
*
* @var bool
*/
private $isCompiled;
/**
* Used for determining the memory used to sort strings in this collation
*
* @var int
*/
private $sortLength;
/**
* The collation pad attribute
*
* @var string
*/
private $padAttribute;
/**
* @param string $name Collation name
* @param string $charset Related charset
* @param int $id Collation ID
* @param bool $isDefault Whether is the default
* @param bool $isCompiled Whether the charset is compiled
* @param int $sortLength Sort length
* @param string $padAttribute Pad attribute
*/
private function __construct(
string $name,
string $charset,
int $id,
bool $isDefault,
bool $isCompiled,
int $sortLength,
string $padAttribute
) {
$this->name = $name;
$this->charset = $charset;
$this->id = $id;
$this->isDefault = $isDefault;
$this->isCompiled = $isCompiled;
$this->sortLength = $sortLength;
$this->padAttribute = $padAttribute;
$this->description = $this->buildDescription();
}
/**
* @param array $state State obtained from the database server
*/
public static function fromServer(array $state): self
{
return new self(
$state['Collation'] ?? '',
$state['Charset'] ?? '',
(int) ($state['Id'] ?? 0),
isset($state['Default']) && ($state['Default'] === 'Yes' || $state['Default'] === '1'),
isset($state['Compiled']) && ($state['Compiled'] === 'Yes' || $state['Compiled'] === '1'),
(int) ($state['Sortlen'] ?? 0),
$state['Pad_attribute'] ?? ''
);
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getCharset(): string
{
return $this->charset;
}
public function getId(): int
{
return $this->id;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function isCompiled(): bool
{
return $this->isCompiled;
}
public function getSortLength(): int
{
return $this->sortLength;
}
public function getPadAttribute(): string
{
return $this->padAttribute;
}
/**
* Returns description for given collation
*
* @return string collation description
*/
private function buildDescription(): string
{
$parts = explode('_', $this->getName());
$name = __('Unknown');
$variant = null;
$suffixes = [];
$unicode = false;
$unknown = false;
$level = 0;
foreach ($parts as $part) {
if ($level === 0) {
/* Next will be language */
$level = 1;
/* First should be charset */
$this->getNameForLevel0(
$unicode, // By reference
$unknown, // Same
$part,
$name, // By reference
$variant// Same
);
continue;
}
if ($level === 1) {
/* Next will be variant unless changed later */
$level = 4;
/* Locale name or code */
$found = true;
$this->getNameForLevel1(
$unicode,
$unknown,
$part,
$name, // By reference, will be changed
$level, // Also
$found// Same
);
if ($found) {
continue;
}
// Not parsed token, fall to next level
}
if ($level === 2) {
/* Next will be variant */
$level = 4;
/* Germal variant */
if ($part === 'pb') {
$name = _pgettext('Collation', 'German (phone book order)');
continue;
}
$name = _pgettext('Collation', 'German (dictionary order)');
// Not parsed token, fall to next level
}
if ($level === 3) {
/* Next will be variant */
$level = 4;
/* Spanish variant */
if ($part === 'trad') {
$name = _pgettext('Collation', 'Spanish (traditional)');
continue;
}
$name = _pgettext('Collation', 'Spanish (modern)');
// Not parsed token, fall to next level
}
if ($level === 4) {
/* Next will be suffix */
$level = 5;
/* Variant */
$found = true;
$variantFound = $this->getVariant($part);
if ($variantFound === null) {
$found = false;
} else {
$variant = $variantFound;
}
if ($found) {
continue;
}
// Not parsed token, fall to next level
}
if ($level < 5) {
continue;
}
/* Suffixes */
$suffixes = $this->addSuffixes($suffixes, $part);
}
return $this->buildName($name, $variant, $suffixes);
}
private function buildName(string $result, ?string $variant, array $suffixes): string
{
if ($variant !== null) {
$result .= ' (' . $variant . ')';
}
if (count($suffixes) > 0) {
$result .= ', ' . implode(', ', $suffixes);
}
return $result;
}
private function getVariant(string $part): ?string
{
switch ($part) {
case '0900':
return 'UCA 9.0.0';
case '520':
return 'UCA 5.2.0';
case 'mysql561':
return 'MySQL 5.6.1';
case 'mysql500':
return 'MySQL 5.0.0';
default:
return null;
}
}
private function addSuffixes(array $suffixes, string $part): array
{
switch ($part) {
case 'ci':
$suffixes[] = _pgettext('Collation variant', 'case-insensitive');
break;
case 'cs':
$suffixes[] = _pgettext('Collation variant', 'case-sensitive');
break;
case 'ai':
$suffixes[] = _pgettext('Collation variant', 'accent-insensitive');
break;
case 'as':
$suffixes[] = _pgettext('Collation variant', 'accent-sensitive');
break;
case 'ks':
$suffixes[] = _pgettext('Collation variant', 'kana-sensitive');
break;
case 'w2':
case 'l2':
$suffixes[] = _pgettext('Collation variant', 'multi-level');
break;
case 'bin':
$suffixes[] = _pgettext('Collation variant', 'binary');
break;
case 'nopad':
$suffixes[] = _pgettext('Collation variant', 'no-pad');
break;
}
return $suffixes;
}
private function getNameForLevel0(
bool &$unicode,
bool &$unknown,
string $part,
?string &$name,
?string &$variant
): void {
switch ($part) {
case 'binary':
$name = _pgettext('Collation', 'Binary');
break;
// Unicode charsets
case 'utf8mb4':
$variant = 'UCA 4.0.0';
// Fall through to other unicode
case 'ucs2':
case 'utf8':
case 'utf16':
case 'utf16le':
case 'utf16be':
case 'utf32':
$name = _pgettext('Collation', 'Unicode');
$unicode = true;
break;
// West European charsets
case 'ascii':
case 'cp850':
case 'dec8':
case 'hp8':
case 'latin1':
case 'macroman':
$name = _pgettext('Collation', 'West European');
break;
// Central European charsets
case 'cp1250':
case 'cp852':
case 'latin2':
case 'macce':
$name = _pgettext('Collation', 'Central European');
break;
// Russian charsets
case 'cp866':
case 'koi8r':
$name = _pgettext('Collation', 'Russian');
break;
// Chinese charsets
case 'gb2312':
case 'gbk':
$name = _pgettext('Collation', 'Simplified Chinese');
break;
case 'big5':
$name = _pgettext('Collation', 'Traditional Chinese');
break;
case 'gb18030':
$name = _pgettext('Collation', 'Chinese');
$unicode = true;
break;
// Japanese charsets
case 'sjis':
case 'ujis':
case 'cp932':
case 'eucjpms':
$name = _pgettext('Collation', 'Japanese');
break;
// Baltic charsets
case 'cp1257':
case 'latin7':
$name = _pgettext('Collation', 'Baltic');
break;
// Other
case 'armscii8':
case 'armscii':
$name = _pgettext('Collation', 'Armenian');
break;
case 'cp1251':
$name = _pgettext('Collation', 'Cyrillic');
break;
case 'cp1256':
$name = _pgettext('Collation', 'Arabic');
break;
case 'euckr':
$name = _pgettext('Collation', 'Korean');
break;
case 'hebrew':
$name = _pgettext('Collation', 'Hebrew');
break;
case 'geostd8':
$name = _pgettext('Collation', 'Georgian');
break;
case 'greek':
$name = _pgettext('Collation', 'Greek');
break;
case 'keybcs2':
$name = _pgettext('Collation', 'Czech-Slovak');
break;
case 'koi8u':
$name = _pgettext('Collation', 'Ukrainian');
break;
case 'latin5':
$name = _pgettext('Collation', 'Turkish');
break;
case 'swe7':
$name = _pgettext('Collation', 'Swedish');
break;
case 'tis620':
$name = _pgettext('Collation', 'Thai');
break;
default:
$name = _pgettext('Collation', 'Unknown');
$unknown = true;
break;
}
}
private function getNameForLevel1(
bool $unicode,
bool $unknown,
string $part,
?string &$name,
int &$level,
bool &$found
): void {
switch ($part) {
case 'general':
break;
case 'bulgarian':
case 'bg':
$name = _pgettext('Collation', 'Bulgarian');
break;
case 'chinese':
case 'cn':
case 'zh':
if ($unicode) {
$name = _pgettext('Collation', 'Chinese');
}
break;
case 'croatian':
case 'hr':
$name = _pgettext('Collation', 'Croatian');
break;
case 'czech':
case 'cs':
$name = _pgettext('Collation', 'Czech');
break;
case 'danish':
case 'da':
$name = _pgettext('Collation', 'Danish');
break;
case 'english':
case 'en':
$name = _pgettext('Collation', 'English');
break;
case 'esperanto':
case 'eo':
$name = _pgettext('Collation', 'Esperanto');
break;
case 'estonian':
case 'et':
$name = _pgettext('Collation', 'Estonian');
break;
case 'german1':
$name = _pgettext('Collation', 'German (dictionary order)');
break;
case 'german2':
$name = _pgettext('Collation', 'German (phone book order)');
break;
case 'german':
case 'de':
/* Name is set later */
$level = 2;
break;
case 'hungarian':
case 'hu':
$name = _pgettext('Collation', 'Hungarian');
break;
case 'icelandic':
case 'is':
$name = _pgettext('Collation', 'Icelandic');
break;
case 'japanese':
case 'ja':
$name = _pgettext('Collation', 'Japanese');
break;
case 'la':
$name = _pgettext('Collation', 'Classical Latin');
break;
case 'latvian':
case 'lv':
$name = _pgettext('Collation', 'Latvian');
break;
case 'lithuanian':
case 'lt':
$name = _pgettext('Collation', 'Lithuanian');
break;
case 'korean':
case 'ko':
$name = _pgettext('Collation', 'Korean');
break;
case 'myanmar':
case 'my':
$name = _pgettext('Collation', 'Burmese');
break;
case 'persian':
$name = _pgettext('Collation', 'Persian');
break;
case 'polish':
case 'pl':
$name = _pgettext('Collation', 'Polish');
break;
case 'roman':
$name = _pgettext('Collation', 'West European');
break;
case 'romanian':
case 'ro':
$name = _pgettext('Collation', 'Romanian');
break;
case 'ru':
$name = _pgettext('Collation', 'Russian');
break;
case 'si':
case 'sinhala':
$name = _pgettext('Collation', 'Sinhalese');
break;
case 'slovak':
case 'sk':
$name = _pgettext('Collation', 'Slovak');
break;
case 'slovenian':
case 'sl':
$name = _pgettext('Collation', 'Slovenian');
break;
case 'spanish':
$name = _pgettext('Collation', 'Spanish (modern)');
break;
case 'es':
/* Name is set later */
$level = 3;
break;
case 'spanish2':
$name = _pgettext('Collation', 'Spanish (traditional)');
break;
case 'swedish':
case 'sv':
$name = _pgettext('Collation', 'Swedish');
break;
case 'thai':
case 'th':
$name = _pgettext('Collation', 'Thai');
break;
case 'turkish':
case 'tr':
$name = _pgettext('Collation', 'Turkish');
break;
case 'ukrainian':
case 'uk':
$name = _pgettext('Collation', 'Ukrainian');
break;
case 'vietnamese':
case 'vi':
$name = _pgettext('Collation', 'Vietnamese');
break;
case 'unicode':
if ($unknown) {
$name = _pgettext('Collation', 'Unicode');
}
break;
default:
$found = false;
}
}
}

View file

@ -0,0 +1,370 @@
<?php
/**
* Get user's global privileges and some db-specific privileges
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\Utils\SessionCache;
use function mb_strpos;
use function mb_substr;
use function preg_match;
use function preg_replace;
use function strpos;
/**
* PhpMyAdmin\CheckUserPrivileges class
*/
class CheckUserPrivileges
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
*/
public function __construct(DatabaseInterface $dbi)
{
$this->dbi = $dbi;
}
/**
* Extracts details from a result row of a SHOW GRANT query
*
* @param string $row grant row
*
* @return array
*/
public function getItemsFromShowGrantsRow(string $row): array
{
$db_name_offset = mb_strpos($row, ' ON ') + 4;
$tblname_end_offset = mb_strpos($row, ' TO ');
$tblname_start_offset = false;
$__tblname_start_offset = mb_strpos($row, '`.', $db_name_offset);
if ($__tblname_start_offset && $__tblname_start_offset < $tblname_end_offset) {
$tblname_start_offset = $__tblname_start_offset + 1;
}
if ($tblname_start_offset === false) {
$tblname_start_offset = mb_strpos($row, '.', $db_name_offset);
}
$show_grants_dbname = mb_substr(
$row,
$db_name_offset,
$tblname_start_offset - $db_name_offset
);
$show_grants_dbname = Util::unQuote($show_grants_dbname, '`');
$show_grants_str = mb_substr(
$row,
6,
mb_strpos($row, ' ON ') - 6
);
$show_grants_tblname = mb_substr(
$row,
$tblname_start_offset + 1,
$tblname_end_offset - $tblname_start_offset - 1
);
$show_grants_tblname = Util::unQuote($show_grants_tblname, '`');
return [
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname,
];
}
/**
* Check if user has required privileges for
* performing 'Adjust privileges' operations
*
* @param string $show_grants_str string containing grants for user
* @param string $show_grants_dbname name of db extracted from grant string
* @param string $show_grants_tblname name of table extracted from grant string
*/
public function checkRequiredPrivilegesForAdjust(
string $show_grants_str,
string $show_grants_dbname,
string $show_grants_tblname
): void {
// '... ALL PRIVILEGES ON *.* ...' OR '... ALL PRIVILEGES ON `mysql`.* ..'
// OR
// SELECT, INSERT, UPDATE, DELETE .... ON *.* OR `mysql`.*
if ($show_grants_str !== 'ALL'
&& $show_grants_str !== 'ALL PRIVILEGES'
&& (mb_strpos(
$show_grants_str,
'SELECT, INSERT, UPDATE, DELETE'
) === false)
) {
return;
}
if ($show_grants_dbname === '*'
&& $show_grants_tblname === '*'
) {
$GLOBALS['col_priv'] = true;
$GLOBALS['db_priv'] = true;
$GLOBALS['proc_priv'] = true;
$GLOBALS['table_priv'] = true;
if ($show_grants_str === 'ALL PRIVILEGES'
|| $show_grants_str === 'ALL'
) {
$GLOBALS['is_reload_priv'] = true;
}
}
// check for specific tables in `mysql` db
// Ex. '... ALL PRIVILEGES on `mysql`.`columns_priv` .. '
if ($show_grants_dbname !== 'mysql') {
return;
}
switch ($show_grants_tblname) {
case 'columns_priv':
$GLOBALS['col_priv'] = true;
break;
case 'db':
$GLOBALS['db_priv'] = true;
break;
case 'procs_priv':
$GLOBALS['proc_priv'] = true;
break;
case 'tables_priv':
$GLOBALS['table_priv'] = true;
break;
case '*':
$GLOBALS['col_priv'] = true;
$GLOBALS['db_priv'] = true;
$GLOBALS['proc_priv'] = true;
$GLOBALS['table_priv'] = true;
break;
default:
}
}
/**
* sets privilege information extracted from SHOW GRANTS result
*
* Detection for some CREATE privilege.
*
* Since MySQL 4.1.2, we can easily detect current user's grants using $userlink
* (no control user needed) and we don't have to try any other method for
* detection
*
* @todo fix to get really all privileges, not only explicitly defined for this user
* from MySQL manual: (https://dev.mysql.com/doc/refman/5.0/en/show-grants.html)
* SHOW GRANTS displays only the privileges granted explicitly to the named
* account. Other privileges might be available to the account, but they are not
* displayed. For example, if an anonymous account exists, the named account
* might be able to use its privileges, but SHOW GRANTS will not display them.
*/
private function analyseShowGrant(): void
{
if (SessionCache::has('is_create_db_priv')) {
$GLOBALS['is_create_db_priv'] = SessionCache::get(
'is_create_db_priv'
);
$GLOBALS['is_reload_priv'] = SessionCache::get(
'is_reload_priv'
);
$GLOBALS['db_to_create'] = SessionCache::get(
'db_to_create'
);
$GLOBALS['dbs_where_create_table_allowed'] = SessionCache::get(
'dbs_where_create_table_allowed'
);
$GLOBALS['dbs_to_test'] = SessionCache::get(
'dbs_to_test'
);
$GLOBALS['db_priv'] = SessionCache::get(
'db_priv'
);
$GLOBALS['col_priv'] = SessionCache::get(
'col_priv'
);
$GLOBALS['table_priv'] = SessionCache::get(
'table_priv'
);
$GLOBALS['proc_priv'] = SessionCache::get(
'proc_priv'
);
return;
}
// defaults
$GLOBALS['is_create_db_priv'] = false;
$GLOBALS['is_reload_priv'] = false;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'] = [];
$GLOBALS['dbs_to_test'] = Utilities::getSystemSchemas();
$GLOBALS['proc_priv'] = false;
$GLOBALS['db_priv'] = false;
$GLOBALS['col_priv'] = false;
$GLOBALS['table_priv'] = false;
$rs_usr = $this->dbi->tryQuery('SHOW GRANTS');
if (! $rs_usr) {
return;
}
$re0 = '(^|(\\\\\\\\)+|[^\\\\])'; // non-escaped wildcards
$re1 = '(^|[^\\\\])(\\\)+'; // escaped wildcards
while ($row = $this->dbi->fetchRow($rs_usr)) {
[
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname,
] = $this->getItemsFromShowGrantsRow($row[0]);
if ($show_grants_dbname === '*') {
if ($show_grants_str !== 'USAGE') {
$GLOBALS['dbs_to_test'] = false;
}
} elseif ($GLOBALS['dbs_to_test'] !== false) {
$GLOBALS['dbs_to_test'][] = $show_grants_dbname;
}
if (mb_strpos($show_grants_str, 'RELOAD') !== false) {
$GLOBALS['is_reload_priv'] = true;
}
// check for the required privileges for adjust
$this->checkRequiredPrivilegesForAdjust(
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname
);
/**
* @todo if we find CREATE VIEW but not CREATE, do not offer
* the create database dialog box
*/
if ($show_grants_str !== 'ALL'
&& $show_grants_str !== 'ALL PRIVILEGES'
&& $show_grants_str !== 'CREATE'
&& strpos($show_grants_str, 'CREATE,') === false
) {
continue;
}
if ($show_grants_dbname === '*') {
// a global CREATE privilege
$GLOBALS['is_create_db_priv'] = true;
$GLOBALS['is_reload_priv'] = true;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'][] = '*';
// @todo we should not break here, cause GRANT ALL *.*
// could be revoked by a later rule like GRANT SELECT ON db.*
break;
}
// this array may contain wildcards
$GLOBALS['dbs_where_create_table_allowed'][] = $show_grants_dbname;
$dbname_to_test = Util::backquote($show_grants_dbname);
if ($GLOBALS['is_create_db_priv']) {
// no need for any more tests if we already know this
continue;
}
// does this db exist?
if ((! preg_match('/' . $re0 . '%|_/', $show_grants_dbname)
|| preg_match('/\\\\%|\\\\_/', $show_grants_dbname))
&& ($this->dbi->tryQuery(
'USE ' . preg_replace(
'/' . $re1 . '(%|_)/',
'\\1\\3',
$dbname_to_test
)
)
|| mb_substr((string) $this->dbi->getError(), 1, 4) == 1044)
) {
continue;
}
/**
* Do not handle the underscore wildcard
* (this case must be rare anyway)
*/
$GLOBALS['db_to_create'] = preg_replace(
'/' . $re0 . '%/',
'\\1',
$show_grants_dbname
);
$GLOBALS['db_to_create'] = preg_replace(
'/' . $re1 . '(%|_)/',
'\\1\\3',
$GLOBALS['db_to_create']
);
$GLOBALS['is_create_db_priv'] = true;
/**
* @todo collect $GLOBALS['db_to_create'] into an array,
* to display a drop-down in the "Create database" dialog
*/
// we don't break, we want all possible databases
//break;
}
$this->dbi->freeResult($rs_usr);
// must also cacheUnset() them in
// PhpMyAdmin\Plugins\Auth\AuthenticationCookie
SessionCache::set('is_create_db_priv', $GLOBALS['is_create_db_priv']);
SessionCache::set('is_reload_priv', $GLOBALS['is_reload_priv']);
SessionCache::set('db_to_create', $GLOBALS['db_to_create']);
SessionCache::set(
'dbs_where_create_table_allowed',
$GLOBALS['dbs_where_create_table_allowed']
);
SessionCache::set('dbs_to_test', $GLOBALS['dbs_to_test']);
SessionCache::set('proc_priv', $GLOBALS['proc_priv']);
SessionCache::set('table_priv', $GLOBALS['table_priv']);
SessionCache::set('col_priv', $GLOBALS['col_priv']);
SessionCache::set('db_priv', $GLOBALS['db_priv']);
}
/**
* Get user's global privileges and some db-specific privileges
*/
public function getPrivileges(): void
{
$username = '';
$current = $this->dbi->getCurrentUserAndHost();
if (! empty($current)) {
[$username] = $current;
}
// If MySQL is started with --skip-grant-tables
if ($username === '') {
$GLOBALS['is_create_db_priv'] = true;
$GLOBALS['is_reload_priv'] = true;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'] = ['*'];
$GLOBALS['dbs_to_test'] = false;
$GLOBALS['db_priv'] = true;
$GLOBALS['col_priv'] = true;
$GLOBALS['table_priv'] = true;
$GLOBALS['proc_priv'] = true;
} else {
$this->analyseShowGrant();
}
}
}

View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Routing;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
use PhpMyAdmin\Twig\CoreExtension;
use PhpMyAdmin\Twig\I18nExtension;
use PhpMyAdmin\Twig\MessageExtension;
use PhpMyAdmin\Twig\PluginsExtension;
use PhpMyAdmin\Twig\RelationExtension;
use PhpMyAdmin\Twig\SanitizeExtension;
use PhpMyAdmin\Twig\TableExtension;
use PhpMyAdmin\Twig\TrackerExtension;
use PhpMyAdmin\Twig\TransformationsExtension;
use PhpMyAdmin\Twig\UrlExtension;
use PhpMyAdmin\Twig\UtilExtension;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Twig\Cache\CacheInterface;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use function fclose;
use function fopen;
use function fwrite;
use function json_encode;
use function str_replace;
use function strpos;
use function is_file;
use function sprintf;
final class CacheWarmupCommand extends Command
{
/** @var string */
protected static $defaultName = 'cache:warmup';
protected function configure(): void
{
$this->setDescription('Warms up the Twig templates cache');
$this->addOption('twig', null, null, 'Warm up twig templates cache.');
$this->addOption('routing', null, null, 'Warm up routing cache.');
$this->setHelp('The <info>%command.name%</info> command warms up the cache of the Twig templates.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('twig') === true && $input->getOption('routing') === true) {
$output->writeln('Please specify --twig or --routing');
return 1;
}
if ($input->getOption('twig') === true) {
return $this->warmUpTwigCache($output);
}
if ($input->getOption('routing') === true) {
return $this->warmUpRoutingCache($output);
}
$output->writeln('Warming up all caches.', OutputInterface::VERBOSITY_VERBOSE);
$twigCode = $this->warmUptwigCache($output);
if ($twigCode !== 0) {
$output->writeln('Twig cache generation had an error.');
return $twigCode;
}
$routingCode = $this->warmUpTwigCache($output);
if ($routingCode !== 0) {
$output->writeln('Routing cache generation had an error.');
return $twigCode;
}
$output->writeln('Warm up of all caches done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
private function warmUpRoutingCache(OutputInterface $output): int
{
$output->writeln('Warming up the routing cache', OutputInterface::VERBOSITY_VERBOSE);
Routing::getDispatcher();
if (is_file(Routing::ROUTES_CACHE_FILE)) {
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
$output->writeln(
sprintf(
'Warm up did not work, the folder "%s" is probably not writable.',
CACHE_DIR
),
OutputInterface::VERBOSITY_NORMAL
);
return 1;
}
private function warmUpTwigCache(OutputInterface $output): int
{
global $cfg, $PMA_Config, $dbi;
$output->writeln('Warming up the twig cache', OutputInterface::VERBOSITY_VERBOSE);
$cfg['environment'] = 'production';
$PMA_Config = new Config(CONFIG_FILE);
$PMA_Config->set('environment', $cfg['environment']);
$dbi = new DatabaseInterface(new DbiDummy());
$tplDir = ROOT_PATH . 'templates';
$tmpDir = ROOT_PATH . 'twig-templates';
$loader = new FilesystemLoader($tplDir);
$twig = new Environment($loader, [
'auto_reload' => true,
'cache' => $tmpDir,
]);
$twig->setExtensions([
new CoreExtension(),
new I18nExtension(),
new MessageExtension(),
new PluginsExtension(),
new RelationExtension(),
new SanitizeExtension(),
new TableExtension(),
new TrackerExtension(),
new TransformationsExtension(),
new UrlExtension(),
new UtilExtension(),
]);
/** @var CacheInterface $twigCache */
$twigCache = $twig->getCache(false);
$output->writeln('Searching for files...', OutputInterface::VERBOSITY_VERY_VERBOSE);
$replacements = [];
$templates = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tplDir),
RecursiveIteratorIterator::LEAVES_ONLY
);
$output->writeln('Warming templates', OutputInterface::VERBOSITY_VERY_VERBOSE);
foreach ($templates as $file) {
// Skip test files
if (strpos($file->getPathname(), '/test/') !== false) {
continue;
}
// force compilation
if (! $file->isFile() || $file->getExtension() !== 'twig') {
continue;
}
$name = str_replace($tplDir . '/', '', $file->getPathname());
$output->writeln('Loading: ' . $name, OutputInterface::VERBOSITY_DEBUG);
if (Environment::MAJOR_VERSION === 3) {
$template = $twig->loadTemplate($twig->getTemplateClass($name), $name);
} else {// @phpstan-ignore-line Twig 2
$template = $twig->loadTemplate($name);// @phpstan-ignore-line Twig 2
}
// Generate line map
$cacheFilename = $twigCache->generateKey($name, $twig->getTemplateClass($name));
$template_file = 'templates/' . $name;
$cache_file = str_replace($tmpDir, 'twig-templates', $cacheFilename);
$replacements[$cache_file] = [$template_file, $template->getDebugInfo()];
}
$output->writeln('Writing replacements...', OutputInterface::VERBOSITY_VERY_VERBOSE);
// Store replacements in JSON
$handle = fopen($tmpDir . '/replace.json', 'w');
if ($handle === false) {
return 1;
}
fwrite($handle, (string) json_encode($replacements));
fclose($handle);
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use RangeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function file_put_contents;
use function preg_match;
use function sprintf;
final class SetVersionCommand extends Command
{
/** @var string */
protected static $defaultName = 'set-version';
/** @var string */
private static $generatedClassTemplate = <<<'PHP'
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
/**
* This class is generated by scripts/console.
*
* @see \PhpMyAdmin\Command\SetVersionCommand
*/
final class Version
{
// The VERSION_SUFFIX constant is defined at libraries/vendor_config.php
public const VERSION = '%1$u.%2$u.%3$u%4$s' . VERSION_SUFFIX;
public const SERIES = '%1$u.%2$u';
public const MAJOR = %1$u;
public const MINOR = %2$u;
public const PATCH = %3$u;
public const ID = %1$u%2$02u%3$02u;
public const PRE_RELEASE_NAME = '%5$s';
public const IS_DEV = %6$s;
}
PHP;
protected function configure(): void
{
$this->setDescription('Sets the version number');
$this->setHelp('This command generates the PhpMyAdmin\Version class based on the version number provided.');
$this->addArgument('version', InputArgument::REQUIRED, 'The version number');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $version */
$version = $input->getArgument('version');
$generatedClass = $this->getGeneratedClass($version);
if (! $this->writeGeneratedClassFile($generatedClass)) {
// failure
return 1;
}
$output->writeln('PhpMyAdmin\Version class successfully generated!');
// success
return 0;
}
private function getGeneratedClass(string $version): string
{
// Do not allow any major below 5
$return = preg_match('/^([5-9]+)\.(\d{1,2})\.(\d{1,2})(-([a-z0-9]+))?$/', $version, $matches);
if ($return === false || $return === 0) {
throw new RangeException('The version number is in the wrong format: ' . $version);
}
return sprintf(
self::$generatedClassTemplate,
$matches[1],
$matches[2],
$matches[3],
$matches[4] ?? '',
$matches[5] ?? '',
($matches[5] ?? '') === 'dev' ? 'true' : 'false'
);
}
private function writeGeneratedClassFile(string $generatedClass): bool
{
$result = file_put_contents(
ROOT_PATH . 'libraries/classes/Version.php',
$generatedClass
);
return $result !== false;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,577 @@
<?php
/**
* Config file management
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use function array_diff;
use function array_flip;
use function array_keys;
use function array_walk;
use function count;
use function is_array;
use function preg_replace;
/**
* Config file management class.
* Stores its data in $_SESSION
*/
class ConfigFile
{
/**
* Stores default PMA config from config.default.php
*
* @var array
*/
private $defaultCfg;
/**
* Stores allowed values for non-standard fields
*
* @var array
*/
private $cfgDb;
/**
* Stores original PMA config, not modified by user preferences
*
* @var array|null
*/
private $baseCfg;
/**
* Whether we are currently working in PMA Setup context
*
* @var bool
*/
private $isInSetup;
/**
* Keys which will be always written to config file
*
* @var array
*/
private $persistKeys = [];
/**
* Changes keys while updating config in {@link updateWithGlobalConfig()}
* or reading by {@link getConfig()} or {@link getConfigArray()}
*
* @var array
*/
private $cfgUpdateReadMapping = [];
/**
* Key filter for {@link set()}
*
* @var array|null
*/
private $setFilter;
/**
* Instance id (key in $_SESSION array, separate for each server -
* ConfigFile{server id})
*
* @var string
*/
private $id;
/**
* Result for {@link flattenArray()}
*
* @var array|null
*/
private $flattenArrayResult;
/**
* @param array|null $baseConfig base configuration read from
* {@link PhpMyAdmin\Config::$base_config},
* use only when not in PMA Setup
*/
public function __construct($baseConfig = null)
{
// load default config values
$cfg = &$this->defaultCfg;
include ROOT_PATH . 'libraries/config.default.php';
// load additional config information
$this->cfgDb = include ROOT_PATH . 'libraries/config.values.php';
// apply default values overrides
if (count($this->cfgDb['_overrides'])) {
foreach ($this->cfgDb['_overrides'] as $path => $value) {
Core::arrayWrite($path, $cfg, $value);
}
}
$this->baseCfg = $baseConfig;
$this->isInSetup = $baseConfig === null;
$this->id = 'ConfigFile' . $GLOBALS['server'];
if (isset($_SESSION[$this->id])) {
return;
}
$_SESSION[$this->id] = [];
}
/**
* Sets names of config options which will be placed in config file even if
* they are set to their default values (use only full paths)
*
* @param array $keys the names of the config options
*
* @return void
*/
public function setPersistKeys(array $keys)
{
// checking key presence is much faster than searching so move values
// to keys
$this->persistKeys = array_flip($keys);
}
/**
* Returns flipped array set by {@link setPersistKeys()}
*
* @return array
*/
public function getPersistKeysMap()
{
return $this->persistKeys;
}
/**
* By default ConfigFile allows setting of all configuration keys, use
* this method to set up a filter on {@link set()} method
*
* @param array|null $keys array of allowed keys or null to remove filter
*
* @return void
*/
public function setAllowedKeys($keys)
{
if ($keys === null) {
$this->setFilter = null;
return;
}
// checking key presence is much faster than searching so move values
// to keys
$this->setFilter = array_flip($keys);
}
/**
* Sets path mapping for updating config in
* {@link updateWithGlobalConfig()} or reading
* by {@link getConfig()} or {@link getConfigArray()}
*
* @param array $mapping Contains the mapping of "Server/config options"
* to "Server/1/config options"
*
* @return void
*/
public function setCfgUpdateReadMapping(array $mapping)
{
$this->cfgUpdateReadMapping = $mapping;
}
/**
* Resets configuration data
*
* @return void
*/
public function resetConfigData()
{
$_SESSION[$this->id] = [];
}
/**
* Sets configuration data (overrides old data)
*
* @param array $cfg Configuration options
*
* @return void
*/
public function setConfigData(array $cfg)
{
$_SESSION[$this->id] = $cfg;
}
/**
* Sets config value
*
* @param string $path Path
* @param mixed $value Value
* @param string $canonicalPath Canonical path
*
* @return void
*/
public function set($path, $value, $canonicalPath = null)
{
if ($canonicalPath === null) {
$canonicalPath = $this->getCanonicalPath($path);
}
if ($this->setFilter !== null
&& ! isset($this->setFilter[$canonicalPath])
) {
return;
}
// if the path isn't protected it may be removed
if (isset($this->persistKeys[$canonicalPath])) {
Core::arrayWrite($path, $_SESSION[$this->id], $value);
return;
}
$defaultValue = $this->getDefault($canonicalPath);
$removePath = $value === $defaultValue;
if ($this->isInSetup) {
// remove if it has a default value or is empty
$removePath = $removePath
|| (empty($value) && empty($defaultValue));
} else {
// get original config values not overwritten by user
// preferences to allow for overwriting options set in
// config.inc.php with default values
$instanceDefaultValue = Core::arrayRead(
$canonicalPath,
$this->baseCfg
);
// remove if it has a default value and base config (config.inc.php)
// uses default value
$removePath = $removePath
&& ($instanceDefaultValue === $defaultValue);
}
if ($removePath) {
Core::arrayRemove($path, $_SESSION[$this->id]);
return;
}
Core::arrayWrite($path, $_SESSION[$this->id], $value);
}
/**
* Flattens multidimensional array, changes indices to paths
* (eg. 'key/subkey').
* Used as array_walk() callback.
*
* @param mixed $value Value
* @param mixed $key Key
* @param mixed $prefix Prefix
*
* @return void
*/
private function flattenArray($value, $key, $prefix)
{
// no recursion for numeric arrays
if (is_array($value) && ! isset($value[0])) {
$prefix .= $key . '/';
array_walk(
$value,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
$prefix
);
} else {
$this->flattenArrayResult[$prefix . $key] = $value;
}
}
/**
* Returns default config in a flattened array
*
* @return array
*/
public function getFlatDefaultConfig()
{
$this->flattenArrayResult = [];
array_walk(
$this->defaultCfg,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$flatConfig = $this->flattenArrayResult;
$this->flattenArrayResult = null;
return $flatConfig;
}
/**
* Updates config with values read from given array
* (config will contain differences to defaults from config.defaults.php).
*
* @param array $cfg Configuration
*
* @return void
*/
public function updateWithGlobalConfig(array $cfg)
{
// load config array and flatten it
$this->flattenArrayResult = [];
array_walk(
$cfg,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$flatConfig = $this->flattenArrayResult;
$this->flattenArrayResult = null;
// save values map for translating a few user preferences paths,
// should be complemented by code reading from generated config
// to perform inverse mapping
foreach ($flatConfig as $path => $value) {
if (isset($this->cfgUpdateReadMapping[$path])) {
$path = $this->cfgUpdateReadMapping[$path];
}
$this->set($path, $value, $path);
}
}
/**
* Returns config value or $default if it's not set
*
* @param string $path Path of config file
* @param mixed $default Default values
*
* @return mixed
*/
public function get($path, $default = null)
{
return Core::arrayRead($path, $_SESSION[$this->id], $default);
}
/**
* Returns default config value or $default it it's not set ie. it doesn't
* exist in config.default.php ($cfg) and config.values.php
* ($_cfg_db['_overrides'])
*
* @param string $canonicalPath Canonical path
* @param mixed $default Default value
*
* @return mixed
*/
public function getDefault($canonicalPath, $default = null)
{
return Core::arrayRead($canonicalPath, $this->defaultCfg, $default);
}
/**
* Returns config value, if it's not set uses the default one; returns
* $default if the path isn't set and doesn't contain a default value
*
* @param string $path Path
* @param mixed $default Default value
*
* @return mixed
*/
public function getValue($path, $default = null)
{
$v = Core::arrayRead($path, $_SESSION[$this->id], null);
if ($v !== null) {
return $v;
}
$path = $this->getCanonicalPath($path);
return $this->getDefault($path, $default);
}
/**
* Returns canonical path
*
* @param string $path Path
*
* @return string
*/
public function getCanonicalPath($path)
{
return preg_replace('#^Servers/([\d]+)/#', 'Servers/1/', $path);
}
/**
* Returns config database entry for $path
*
* @param string $path path of the variable in config db
* @param mixed $default default value
*
* @return mixed
*/
public function getDbEntry($path, $default = null)
{
return Core::arrayRead($path, $this->cfgDb, $default);
}
/**
* Returns server count
*
* @return int
*/
public function getServerCount()
{
return isset($_SESSION[$this->id]['Servers'])
? count($_SESSION[$this->id]['Servers'])
: 0;
}
/**
* Returns server list
*
* @return array|null
*/
public function getServers()
{
return $_SESSION[$this->id]['Servers'] ?? null;
}
/**
* Returns DSN of given server
*
* @param int $server server index
*
* @return string
*/
public function getServerDSN($server)
{
if (! isset($_SESSION[$this->id]['Servers'][$server])) {
return '';
}
$path = 'Servers/' . $server;
$dsn = 'mysqli://';
if ($this->getValue($path . '/auth_type') === 'config') {
$dsn .= $this->getValue($path . '/user');
if (! empty($this->getValue($path . '/password'))) {
$dsn .= ':***';
}
$dsn .= '@';
}
if ($this->getValue($path . '/host') !== 'localhost') {
$dsn .= $this->getValue($path . '/host');
$port = $this->getValue($path . '/port');
if ($port) {
$dsn .= ':' . $port;
}
} else {
$dsn .= $this->getValue($path . '/socket');
}
return $dsn;
}
/**
* Returns server name
*
* @param int $id server index
*
* @return string
*/
public function getServerName($id)
{
if (! isset($_SESSION[$this->id]['Servers'][$id])) {
return '';
}
$verbose = $this->get('Servers/' . $id . '/verbose');
if (! empty($verbose)) {
return $verbose;
}
$host = $this->get('Servers/' . $id . '/host');
return empty($host) ? 'localhost' : $host;
}
/**
* Removes server
*
* @param int $server server index
*
* @return void
*/
public function removeServer($server)
{
if (! isset($_SESSION[$this->id]['Servers'][$server])) {
return;
}
$lastServer = $this->getServerCount();
for ($i = $server; $i < $lastServer; $i++) {
$_SESSION[$this->id]['Servers'][$i]
= $_SESSION[$this->id]['Servers'][$i + 1];
}
unset($_SESSION[$this->id]['Servers'][$lastServer]);
if (! isset($_SESSION[$this->id]['ServerDefault'])
|| $_SESSION[$this->id]['ServerDefault'] != $lastServer
) {
return;
}
unset($_SESSION[$this->id]['ServerDefault']);
}
/**
* Returns configuration array (full, multidimensional format)
*
* @return array
*/
public function getConfig()
{
$c = $_SESSION[$this->id];
foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
// if the key $c exists in $map_to
if (Core::arrayRead($mapTo, $c) === null) {
continue;
}
Core::arrayWrite($mapTo, $c, Core::arrayRead($mapFrom, $c));
Core::arrayRemove($mapFrom, $c);
}
return $c;
}
/**
* Returns configuration array (flat format)
*
* @return array
*/
public function getConfigArray()
{
$this->flattenArrayResult = [];
array_walk(
$_SESSION[$this->id],
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$c = $this->flattenArrayResult;
$this->flattenArrayResult = null;
$persistKeys = array_diff(
array_keys($this->persistKeys),
array_keys($c)
);
foreach ($persistKeys as $k) {
$c[$k] = $this->getDefault($this->getCanonicalPath($k));
}
foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
if (! isset($c[$mapFrom])) {
continue;
}
$c[$mapTo] = $c[$mapFrom];
unset($c[$mapFrom]);
}
return $c;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,310 @@
<?php
/**
* Form handling code.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use const E_USER_ERROR;
use function array_combine;
use function array_shift;
use function array_walk;
use function count;
use function gettype;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function ltrim;
use function mb_strpos;
use function mb_strrpos;
use function mb_substr;
use function str_replace;
use function trigger_error;
/**
* Base class for forms, loads default configuration options, checks allowed
* values etc.
*/
class Form
{
/**
* Form name
*
* @var string
*/
public $name;
/**
* Arbitrary index, doesn't affect class' behavior
*
* @var int
*/
public $index;
/**
* Form fields (paths), filled by {@link readFormPaths()}, indexed by field name
*
* @var array
*/
public $fields;
/**
* Stores default values for some fields (eg. pmadb tables)
*
* @var array
*/
public $default;
/**
* Caches field types, indexed by field names
*
* @var array
*/
private $fieldsTypes;
/**
* ConfigFile instance
*
* @var ConfigFile
*/
private $configFile;
/**
* A counter for the number of groups
*
* @var int
*/
private static $groupCounter = 0;
/**
* Reads default config values
*
* @param string $formName Form name
* @param array $form Form data
* @param ConfigFile $cf Config file instance
* @param int $index arbitrary index, stored in Form::$index
*/
public function __construct(
$formName,
array $form,
ConfigFile $cf,
$index = null
) {
$this->index = $index;
$this->configFile = $cf;
$this->loadForm($formName, $form);
}
/**
* Returns type of given option
*
* @param string $optionName path or field name
*
* @return string|null one of: boolean, integer, double, string, select, array
*/
public function getOptionType($optionName)
{
$key = ltrim(
mb_substr(
$optionName,
(int) mb_strrpos($optionName, '/')
),
'/'
);
return $this->fieldsTypes[$key] ?? null;
}
/**
* Returns allowed values for select fields
*
* @param string $optionPath Option path
*
* @return array
*/
public function getOptionValueList($optionPath)
{
$value = $this->configFile->getDbEntry($optionPath);
if ($value === null) {
trigger_error($optionPath . ' - select options not defined', E_USER_ERROR);
return [];
}
if (! is_array($value)) {
trigger_error($optionPath . ' - not a static value list', E_USER_ERROR);
return [];
}
// convert array('#', 'a', 'b') to array('a', 'b')
if (isset($value[0]) && $value[0] === '#') {
// remove first element ('#')
array_shift($value);
// $value has keys and value names, return it
return $value;
}
// convert value list array('a', 'b') to array('a' => 'a', 'b' => 'b')
$hasStringKeys = false;
$keys = [];
for ($i = 0, $nb = count($value); $i < $nb; $i++) {
if (! isset($value[$i])) {
$hasStringKeys = true;
break;
}
$keys[] = is_bool($value[$i]) ? (int) $value[$i] : $value[$i];
}
if (! $hasStringKeys) {
$value = array_combine($keys, $value);
}
// $value has keys and value names, return it
return $value;
}
/**
* array_walk callback function, reads path of form fields from
* array (see docs for \PhpMyAdmin\Config\Forms\BaseForm::getForms)
*
* @param mixed $value Value
* @param mixed $key Key
* @param mixed $prefix Prefix
*
* @return void
*/
private function readFormPathsCallback($value, $key, $prefix)
{
if (is_array($value)) {
$prefix .= $key . '/';
array_walk(
$value,
function ($value, $key, $prefix) {
$this->readFormPathsCallback($value, $key, $prefix);
},
$prefix
);
return;
}
if (! is_int($key)) {
$this->default[$prefix . $key] = $value;
$value = $key;
}
// add unique id to group ends
if ($value === ':group:end') {
$value .= ':' . self::$groupCounter++;
}
$this->fields[] = $prefix . $value;
}
/**
* Reset the group counter, function for testing purposes
*/
public static function resetGroupCounter(): void
{
self::$groupCounter = 0;
}
/**
* Reads form paths to {@link $fields}
*
* @param array $form Form
*
* @return void
*/
protected function readFormPaths(array $form)
{
// flatten form fields' paths and save them to $fields
$this->fields = [];
array_walk(
$form,
function ($value, $key, $prefix) {
$this->readFormPathsCallback($value, $key, $prefix);
},
''
);
// $this->fields is an array of the form: [0..n] => 'field path'
// change numeric indexes to contain field names (last part of the path)
$paths = $this->fields;
$this->fields = [];
foreach ($paths as $path) {
$key = ltrim(
mb_substr($path, (int) mb_strrpos($path, '/')),
'/'
);
$this->fields[$key] = $path;
}
// now $this->fields is an array of the form: 'field name' => 'field path'
}
/**
* Reads fields' types to $this->fieldsTypes
*
* @return void
*/
protected function readTypes()
{
$cf = $this->configFile;
foreach ($this->fields as $name => $path) {
if (mb_strpos((string) $name, ':group:') === 0) {
$this->fieldsTypes[$name] = 'group';
continue;
}
$v = $cf->getDbEntry($path);
if ($v !== null) {
$type = is_array($v) ? 'select' : $v;
} else {
$type = gettype($cf->getDefault($path));
}
$this->fieldsTypes[$name] = $type;
}
}
/**
* Remove slashes from group names
*
* @see issue #15836
*
* @param array $form The form data
*
* @return array
*/
protected function cleanGroupPaths(array $form): array
{
foreach ($form as &$name) {
if (! is_string($name)) {
continue;
}
if (mb_strpos($name, ':group:') !== 0) {
continue;
}
$name = str_replace('/', '-', $name);
}
return $form;
}
/**
* Reads form settings and prepares class to work with given subset of
* config file
*
* @param string $formName Form name
* @param array $form Form
*
* @return void
*/
public function loadForm($formName, array $form)
{
$this->name = $formName;
$form = $this->cleanGroupPaths($form);
$this->readFormPaths($form);
$this->readTypes();
}
}

View file

@ -0,0 +1,967 @@
<?php
/**
* Form management class, displays and processes forms
*
* Explanation of used terms:
* o work_path - original field path, eg. Servers/4/verbose
* o system_path - work_path modified so that it points to the first server,
* eg. Servers/1/verbose
* o translated_path - work_path modified for HTML field name, a path with
* slashes changed to hyphens, eg. Servers-4-verbose
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config\Forms\User\UserFormList;
use PhpMyAdmin\Html\MySQLDocumentation;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Util;
use const E_USER_WARNING;
use function array_flip;
use function array_keys;
use function array_search;
use function count;
use function explode;
use function function_exists;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_numeric;
use function mb_substr;
use function preg_match;
use function settype;
use function sprintf;
use function str_replace;
use function trigger_error;
use function trim;
/**
* Form management class, displays and processes forms
*/
class FormDisplay
{
/**
* ConfigFile instance
*
* @var ConfigFile
*/
private $configFile;
/**
* Form list
*
* @var Form[]
*/
private $forms = [];
/**
* Stores validation errors, indexed by paths
* [ Form_name ] is an array of form errors
* [path] is a string storing error associated with single field
*
* @var array
*/
private $errors = [];
/**
* Paths changed so that they can be used as HTML ids, indexed by paths
*
* @var array
*/
private $translatedPaths = [];
/**
* Server paths change indexes so we define maps from current server
* path to the first one, indexed by work path
*
* @var array
*/
private $systemPaths = [];
/**
* Language strings which will be sent to Messages JS variable
* Will be looked up in $GLOBALS: str{value} or strSetup{value}
*
* @var array
*/
private $jsLangStrings = [];
/**
* Tells whether forms have been validated
*
* @var bool
*/
private $isValidated = true;
/**
* Dictionary with user preferences keys
*
* @var array|null
*/
private $userprefsKeys;
/**
* Dictionary with disallowed user preferences keys
*
* @var array
*/
private $userprefsDisallow;
/** @var FormDisplayTemplate */
private $formDisplayTemplate;
/**
* @param ConfigFile $cf Config file instance
*/
public function __construct(ConfigFile $cf)
{
$this->formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']);
$this->jsLangStrings = [
'error_nan_p' => __('Not a positive number!'),
'error_nan_nneg' => __('Not a non-negative number!'),
'error_incorrect_port' => __('Not a valid port number!'),
'error_invalid_value' => __('Incorrect value!'),
'error_value_lte' => __('Value must be less than or equal to %s!'),
];
$this->configFile = $cf;
// initialize validators
Validator::getValidators($this->configFile);
}
/**
* Returns {@link ConfigFile} associated with this instance
*
* @return ConfigFile
*/
public function getConfigFile()
{
return $this->configFile;
}
/**
* Registers form in form manager
*
* @param string $formName Form name
* @param array $form Form data
* @param int $serverId 0 if new server, validation; >= 1 if editing a server
*
* @return void
*/
public function registerForm($formName, array $form, $serverId = null)
{
$this->forms[$formName] = new Form(
$formName,
$form,
$this->configFile,
$serverId
);
$this->isValidated = false;
foreach ($this->forms[$formName]->fields as $path) {
$workPath = $serverId === null
? $path
: str_replace('Servers/1/', 'Servers/' . $serverId . '/', $path);
$this->systemPaths[$workPath] = $path;
$this->translatedPaths[$workPath] = str_replace('/', '-', $workPath);
}
}
/**
* Processes forms, returns true on successful save
*
* @param bool $allowPartialSave allows for partial form saving
* on failed validation
* @param bool $checkFormSubmit whether check for $_POST['submit_save']
*
* @return bool whether processing was successful
*/
public function process($allowPartialSave = true, $checkFormSubmit = true)
{
if ($checkFormSubmit && ! isset($_POST['submit_save'])) {
return false;
}
// save forms
if (count($this->forms) > 0) {
return $this->save(array_keys($this->forms), $allowPartialSave);
}
return false;
}
/**
* Runs validation for all registered forms
*
* @return void
*/
private function validate()
{
if ($this->isValidated) {
return;
}
$paths = [];
$values = [];
foreach ($this->forms as $form) {
/** @var Form $form */
$paths[] = $form->name;
// collect values and paths
foreach ($form->fields as $path) {
$workPath = array_search($path, $this->systemPaths);
$values[$path] = $this->configFile->getValue($workPath);
$paths[] = $path;
}
}
// run validation
$errors = Validator::validate(
$this->configFile,
$paths,
$values,
false
);
// change error keys from canonical paths to work paths
if (is_array($errors) && count($errors) > 0) {
$this->errors = [];
foreach ($errors as $path => $errorList) {
$workPath = array_search($path, $this->systemPaths);
// field error
if (! $workPath) {
// form error, fix path
$workPath = $path;
}
$this->errors[$workPath] = $errorList;
}
}
$this->isValidated = true;
}
/**
* Outputs HTML for the forms under the menu tab
*
* @param bool $showRestoreDefault whether to show "restore default"
* button besides the input field
* @param array $jsDefault stores JavaScript code
* to be displayed
* @param array $js will be updated with javascript code
* @param bool $showButtons whether show submit and reset button
*
* @return string
*/
private function displayForms(
$showRestoreDefault,
array &$jsDefault,
array &$js,
$showButtons
) {
$htmlOutput = '';
$validators = Validator::getValidators($this->configFile);
foreach ($this->forms as $form) {
/** @var Form $form */
$formErrors = $this->errors[$form->name] ?? null;
$htmlOutput .= $this->formDisplayTemplate->displayFieldsetTop(
Descriptions::get('Form_' . $form->name),
Descriptions::get('Form_' . $form->name, 'desc'),
$formErrors,
['id' => $form->name]
);
foreach ($form->fields as $field => $path) {
$workPath = array_search($path, $this->systemPaths);
$translatedPath = $this->translatedPaths[$workPath];
// always true/false for user preferences display
// otherwise null
$userPrefsAllow = isset($this->userprefsKeys[$path])
? ! isset($this->userprefsDisallow[$path])
: null;
// display input
$htmlOutput .= $this->displayFieldInput(
$form,
$field,
$path,
$workPath,
$translatedPath,
$showRestoreDefault,
$userPrefsAllow,
$jsDefault
);
// register JS validators for this field
if (! isset($validators[$path])) {
continue;
}
$this->formDisplayTemplate->addJsValidate($translatedPath, $validators[$path], $js);
}
$htmlOutput .= $this->formDisplayTemplate->displayFieldsetBottom($showButtons);
}
return $htmlOutput;
}
/**
* Outputs HTML for forms
*
* @param bool $tabbedForm if true, use a form with tabs
* @param bool $showRestoreDefault whether show "restore default" button
* besides the input field
* @param bool $showButtons whether show submit and reset button
* @param string $formAction action attribute for the form
* @param array|null $hiddenFields array of form hidden fields (key: field
* name)
*
* @return string HTML for forms
*/
public function getDisplay(
$tabbedForm = false,
$showRestoreDefault = false,
$showButtons = true,
$formAction = null,
$hiddenFields = null
) {
static $jsLangSent = false;
$htmlOutput = '';
$js = [];
$jsDefault = [];
$htmlOutput .= $this->formDisplayTemplate->displayFormTop($formAction, 'post', $hiddenFields);
if ($tabbedForm) {
$tabs = [];
foreach ($this->forms as $form) {
$tabs[$form->name] = Descriptions::get('Form_' . $form->name);
}
$htmlOutput .= $this->formDisplayTemplate->displayTabsTop($tabs);
}
// validate only when we aren't displaying a "new server" form
$isNewServer = false;
foreach ($this->forms as $form) {
/** @var Form $form */
if ($form->index === 0) {
$isNewServer = true;
break;
}
}
if (! $isNewServer) {
$this->validate();
}
// user preferences
$this->loadUserprefsInfo();
// display forms
$htmlOutput .= $this->displayForms(
$showRestoreDefault,
$jsDefault,
$js,
$showButtons
);
if ($tabbedForm) {
$htmlOutput .= $this->formDisplayTemplate->displayTabsBottom();
}
$htmlOutput .= $this->formDisplayTemplate->displayFormBottom();
// if not already done, send strings used for validation to JavaScript
if (! $jsLangSent) {
$jsLangSent = true;
$jsLang = [];
foreach ($this->jsLangStrings as $strName => $strValue) {
$jsLang[] = "'" . $strName . "': '" . Sanitize::jsFormat($strValue, false) . '\'';
}
$js[] = "$.extend(Messages, {\n\t"
. implode(",\n\t", $jsLang) . '})';
}
$js[] = "$.extend(defaultValues, {\n\t"
. implode(",\n\t", $jsDefault) . '})';
return $htmlOutput . $this->formDisplayTemplate->displayJavascript($js);
}
/**
* Prepares data for input field display and outputs HTML code
*
* @param Form $form Form object
* @param string $field field name as it appears in $form
* @param string $systemPath field path, eg. Servers/1/verbose
* @param string $workPath work path, eg. Servers/4/verbose
* @param string $translatedPath work path changed so that it can be
* used as XHTML id
* @param bool $showRestoreDefault whether show "restore default" button
* besides the input field
* @param bool|null $userPrefsAllow whether user preferences are enabled
* for this field (null - no support,
* true/false - enabled/disabled)
* @param array $jsDefault array which stores JavaScript code
* to be displayed
*
* @return string|null HTML for input field
*/
private function displayFieldInput(
Form $form,
$field,
$systemPath,
$workPath,
$translatedPath,
$showRestoreDefault,
$userPrefsAllow,
array &$jsDefault
) {
$name = Descriptions::get($systemPath);
$description = Descriptions::get($systemPath, 'desc');
$value = $this->configFile->get($workPath);
$valueDefault = $this->configFile->getDefault($systemPath);
$valueIsDefault = false;
if ($value === null || $value === $valueDefault) {
$value = $valueDefault;
$valueIsDefault = true;
}
$opts = [
'doc' => $this->getDocLink($systemPath),
'show_restore_default' => $showRestoreDefault,
'userprefs_allow' => $userPrefsAllow,
'userprefs_comment' => Descriptions::get($systemPath, 'cmt'),
];
if (isset($form->default[$systemPath])) {
$opts['setvalue'] = (string) $form->default[$systemPath];
}
if (isset($this->errors[$workPath])) {
$opts['errors'] = $this->errors[$workPath];
}
$type = '';
switch ($form->getOptionType($field)) {
case 'string':
$type = 'text';
break;
case 'short_string':
$type = 'short_text';
break;
case 'double':
case 'integer':
$type = 'number_text';
break;
case 'boolean':
$type = 'checkbox';
break;
case 'select':
$type = 'select';
$opts['values'] = $form->getOptionValueList($form->fields[$field]);
break;
case 'array':
$type = 'list';
$value = (array) $value;
$valueDefault = (array) $valueDefault;
break;
case 'group':
// :group:end is changed to :group:end:{unique id} in Form class
$htmlOutput = '';
if (mb_substr($field, 7, 4) !== 'end:') {
$htmlOutput .= $this->formDisplayTemplate->displayGroupHeader(
mb_substr($field, 7)
);
} else {
$this->formDisplayTemplate->displayGroupFooter();
}
return $htmlOutput;
case 'NULL':
trigger_error('Field ' . $systemPath . ' has no type', E_USER_WARNING);
return null;
}
// detect password fields
if ($type === 'text'
&& (mb_substr($translatedPath, -9) === '-password'
|| mb_substr($translatedPath, -4) === 'pass'
|| mb_substr($translatedPath, -4) === 'Pass')
) {
$type = 'password';
}
// TrustedProxies requires changes before displaying
if ($systemPath === 'TrustedProxies') {
foreach ($value as $ip => &$v) {
if (preg_match('/^-\d+$/', $ip)) {
continue;
}
$v = $ip . ': ' . $v;
}
}
$this->setComments($systemPath, $opts);
// send default value to form's JS
$jsLine = '\'' . $translatedPath . '\': ';
switch ($type) {
case 'text':
case 'short_text':
case 'number_text':
case 'password':
$jsLine .= '\'' . Sanitize::escapeJsString($valueDefault) . '\'';
break;
case 'checkbox':
$jsLine .= $valueDefault ? 'true' : 'false';
break;
case 'select':
$valueDefaultJs = is_bool($valueDefault)
? (int) $valueDefault
: $valueDefault;
$jsLine .= '[\'' . Sanitize::escapeJsString($valueDefaultJs) . '\']';
break;
case 'list':
$val = $valueDefault;
if (isset($val['wrapper_params'])) {
unset($val['wrapper_params']);
}
$jsLine .= '\'' . Sanitize::escapeJsString(implode("\n", $val))
. '\'';
break;
}
$jsDefault[] = $jsLine;
return $this->formDisplayTemplate->displayInput(
$translatedPath,
$name,
$type,
$value,
$description,
$valueIsDefault,
$opts
);
}
/**
* Displays errors
*
* @return string|null HTML for errors
*/
public function displayErrors()
{
$this->validate();
if (count($this->errors) === 0) {
return null;
}
$htmlOutput = '';
foreach ($this->errors as $systemPath => $errorList) {
if (isset($this->systemPaths[$systemPath])) {
$name = Descriptions::get($this->systemPaths[$systemPath]);
} else {
$name = Descriptions::get('Form_' . $systemPath);
}
$htmlOutput .= $this->formDisplayTemplate->displayErrors($name, $errorList);
}
return $htmlOutput;
}
/**
* Reverts erroneous fields to their default values
*
* @return void
*/
public function fixErrors()
{
$this->validate();
if (count($this->errors) === 0) {
return;
}
$cf = $this->configFile;
foreach (array_keys($this->errors) as $workPath) {
if (! isset($this->systemPaths[$workPath])) {
continue;
}
$canonicalPath = $this->systemPaths[$workPath];
$cf->set($workPath, $cf->getDefault($canonicalPath));
}
}
/**
* Validates select field and casts $value to correct type
*
* @param string|bool $value Current value
* @param array $allowed List of allowed values
*/
private function validateSelect(&$value, array $allowed): bool
{
$valueCmp = is_bool($value)
? (int) $value
: $value;
foreach ($allowed as $vk => $v) {
// equality comparison only if both values are numeric or not numeric
// (allows to skip 0 == 'string' equalling to true)
// or identity (for string-string)
if (! (($vk == $value && ! (is_numeric($valueCmp) xor is_numeric($vk)))
|| $vk === $value)
) {
continue;
}
// keep boolean value as boolean
if (! is_bool($value)) {
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($value, gettype($vk));
}
return true;
}
return false;
}
/**
* Validates and saves form data to session
*
* @param array|string $forms array of form names
* @param bool $allowPartialSave allows for partial form saving on
* failed validation
*
* @return bool true on success (no errors and all saved)
*/
public function save($forms, $allowPartialSave = true)
{
$result = true;
$forms = (array) $forms;
$values = [];
$toSave = [];
$isSetupScript = $GLOBALS['PMA_Config']->get('is_setup');
if ($isSetupScript) {
$this->loadUserprefsInfo();
}
$this->errors = [];
foreach ($forms as $formName) {
if (! isset($this->forms[$formName])) {
continue;
}
/** @var Form $form */
$form = $this->forms[$formName];
// get current server id
$changeIndex = $form->index === 0
? $this->configFile->getServerCount() + 1
: false;
// grab POST values
foreach ($form->fields as $field => $systemPath) {
$workPath = array_search($systemPath, $this->systemPaths);
$key = $this->translatedPaths[$workPath];
$type = (string) $form->getOptionType($field);
// skip groups
if ($type === 'group') {
continue;
}
// ensure the value is set
if (! isset($_POST[$key])) {
// checkboxes aren't set by browsers if they're off
if ($type !== 'boolean') {
$this->errors[$form->name][] = sprintf(
__('Missing data for %s'),
'<i>' . Descriptions::get($systemPath) . '</i>'
);
$result = false;
continue;
}
$_POST[$key] = false;
}
// user preferences allow/disallow
if ($isSetupScript
&& isset($this->userprefsKeys[$systemPath])
) {
if (isset($this->userprefsDisallow[$systemPath], $_POST[$key . '-userprefs-allow'])
) {
unset($this->userprefsDisallow[$systemPath]);
} elseif (! isset($_POST[$key . '-userprefs-allow'])) {
$this->userprefsDisallow[$systemPath] = true;
}
}
// cast variables to correct type
switch ($type) {
case 'double':
$_POST[$key] = Util::requestString($_POST[$key]);
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($_POST[$key], 'float');
break;
case 'boolean':
case 'integer':
if ($_POST[$key] !== '') {
$_POST[$key] = Util::requestString($_POST[$key]);
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($_POST[$key], $type);
}
break;
case 'select':
$successfullyValidated = $this->validateSelect(
$_POST[$key],
$form->getOptionValueList($systemPath)
);
if (! $successfullyValidated) {
$this->errors[$workPath][] = __('Incorrect value!');
$result = false;
// "continue" for the $form->fields foreach-loop
continue 2;
}
break;
case 'string':
case 'short_string':
$_POST[$key] = Util::requestString($_POST[$key]);
break;
case 'array':
// eliminate empty values and ensure we have an array
$postValues = is_array($_POST[$key])
? $_POST[$key]
: explode("\n", $_POST[$key]);
$_POST[$key] = [];
$this->fillPostArrayParameters($postValues, $key);
break;
}
// now we have value with proper type
$values[$systemPath] = $_POST[$key];
if ($changeIndex !== false) {
$workPath = str_replace(
'Servers/' . $form->index . '/',
'Servers/' . $changeIndex . '/',
$workPath
);
}
$toSave[$workPath] = $systemPath;
}
}
// save forms
if (! $allowPartialSave && ! empty($this->errors)) {
// don't look for non-critical errors
$this->validate();
return $result;
}
foreach ($toSave as $workPath => $path) {
// TrustedProxies requires changes before saving
if ($path === 'TrustedProxies') {
$proxies = [];
$i = 0;
foreach ($values[$path] as $value) {
$matches = [];
$match = preg_match(
'/^(.+):(?:[ ]?)(\\w+)$/',
$value,
$matches
);
if ($match) {
// correct 'IP: HTTP header' pair
$ip = trim($matches[1]);
$proxies[$ip] = trim($matches[2]);
} else {
// save also incorrect values
$proxies['-' . $i] = $value;
$i++;
}
}
$values[$path] = $proxies;
}
$this->configFile->set($workPath, $values[$path], $path);
}
if ($isSetupScript) {
$this->configFile->set(
'UserprefsDisallow',
array_keys($this->userprefsDisallow)
);
}
// don't look for non-critical errors
$this->validate();
return $result;
}
/**
* Tells whether form validation failed
*
* @return bool
*/
public function hasErrors()
{
return count($this->errors) > 0;
}
/**
* Returns link to documentation
*
* @param string $path Path to documentation
*
* @return string
*/
public function getDocLink($path)
{
$test = mb_substr($path, 0, 6);
if ($test === 'Import' || $test === 'Export') {
return '';
}
return MySQLDocumentation::getDocumentationLink(
'config',
'cfg_' . $this->getOptName($path),
Sanitize::isSetup() ? '../' : './'
);
}
/**
* Changes path so it can be used in URLs
*
* @param string $path Path
*
* @return string
*/
private function getOptName($path)
{
return str_replace(['Servers/1/', '/'], ['Servers/', '_'], $path);
}
/**
* Fills out {@link userprefs_keys} and {@link userprefs_disallow}
*
* @return void
*/
private function loadUserprefsInfo()
{
if ($this->userprefsKeys !== null) {
return;
}
$this->userprefsKeys = array_flip(UserFormList::getFields());
// read real config for user preferences display
$userPrefsDisallow = $GLOBALS['PMA_Config']->get('is_setup')
? $this->configFile->get('UserprefsDisallow', [])
: $GLOBALS['cfg']['UserprefsDisallow'];
$this->userprefsDisallow = array_flip($userPrefsDisallow ?? []);
}
/**
* Sets field comments and warnings based on current environment
*
* @param string $systemPath Path to settings
* @param array $opts Chosen options
*
* @return void
*/
private function setComments($systemPath, array &$opts)
{
// RecodingEngine - mark unavailable types
if ($systemPath === 'RecodingEngine') {
$comment = '';
if (! function_exists('iconv')) {
$opts['values']['iconv'] .= ' (' . __('unavailable') . ')';
$comment = sprintf(
__('"%s" requires %s extension'),
'iconv',
'iconv'
);
}
if (! function_exists('recode_string')) {
$opts['values']['recode'] .= ' (' . __('unavailable') . ')';
$comment .= ($comment ? ', ' : '') . sprintf(
__('"%s" requires %s extension'),
'recode',
'recode'
);
}
/* mbstring is always there thanks to polyfill */
$opts['comment'] = $comment;
$opts['comment_warning'] = true;
}
// ZipDump, GZipDump, BZipDump - check function availability
if ($systemPath === 'ZipDump'
|| $systemPath === 'GZipDump'
|| $systemPath === 'BZipDump'
) {
$comment = '';
$funcs = [
'ZipDump' => [
'zip_open',
'gzcompress',
],
'GZipDump' => [
'gzopen',
'gzencode',
],
'BZipDump' => [
'bzopen',
'bzcompress',
],
];
if (! function_exists($funcs[$systemPath][0])) {
$comment = sprintf(
__(
'Compressed import will not work due to missing function %s.'
),
$funcs[$systemPath][0]
);
}
if (! function_exists($funcs[$systemPath][1])) {
$comment .= ($comment ? '; ' : '') . sprintf(
__(
'Compressed export will not work due to missing function %s.'
),
$funcs[$systemPath][1]
);
}
$opts['comment'] = $comment;
$opts['comment_warning'] = true;
}
if ($GLOBALS['PMA_Config']->get('is_setup')) {
return;
}
if ($systemPath !== 'MaxDbList' && $systemPath !== 'MaxTableList'
&& $systemPath !== 'QueryHistoryMax'
) {
return;
}
$opts['comment'] = sprintf(
__('maximum %s'),
$GLOBALS['cfg'][$systemPath]
);
}
/**
* Copy items of an array to $_POST variable
*
* @param array $postValues List of parameters
* @param string $key Array key
*
* @return void
*/
private function fillPostArrayParameters(array $postValues, $key)
{
foreach ($postValues as $v) {
$v = Util::requestString($v);
if ($v === '') {
continue;
}
$_POST[$key][] = $v;
}
}
}

View file

@ -0,0 +1,491 @@
<?php
/**
* Form templates
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Template;
use function array_flip;
use function array_merge;
use function array_shift;
use function defined;
use function htmlspecialchars;
use function htmlspecialchars_decode;
use function implode;
use function is_bool;
use function mb_strtolower;
use function sprintf;
use function is_string;
/**
* PhpMyAdmin\Config\FormDisplayTemplate class
*/
class FormDisplayTemplate
{
/** @var int */
public $group;
/** @var Config */
protected $config;
/** @var Template */
public $template;
/**
* @param Config $config Config instance
*/
public function __construct(Config $config)
{
$this->config = $config;
$this->template = new Template();
}
/**
* Displays top part of the form
*
* @param string $action default: $_SERVER['REQUEST_URI']
* @param string $method 'post' or 'get'
* @param array|null $hiddenFields array of form hidden fields (key: field name)
*/
public function displayFormTop(
$action = null,
$method = 'post',
$hiddenFields = null
): string {
static $hasCheckPageRefresh = false;
if ($action === null) {
$action = $_SERVER['REQUEST_URI'];
}
if ($method !== 'post') {
$method = 'get';
}
/**
* We do validation on page refresh when browser remembers field values,
* add a field with known value which will be used for checks.
*/
if (! $hasCheckPageRefresh) {
$hasCheckPageRefresh = true;
}
return $this->template->render('config/form_display/form_top', [
'method' => $method,
'action' => $action,
'has_check_page_refresh' => $hasCheckPageRefresh,
'hidden_fields' => (array) $hiddenFields,
]);
}
/**
* Displays form tabs which are given by an array indexed by fieldset id
* ({@link self::displayFieldsetTop}), with values being tab titles.
*
* @param array $tabs tab names
*/
public function displayTabsTop(array $tabs): string
{
return $this->template->render('config/form_display/tabs_top', ['tabs' => $tabs]);
}
/**
* Displays top part of a fieldset
*
* @param string $title title of fieldset
* @param string $description description shown on top of fieldset
* @param array|null $errors error messages to display
* @param array $attributes optional extra attributes of fieldset
*/
public function displayFieldsetTop(
$title = '',
$description = '',
$errors = null,
array $attributes = []
): string {
$this->group = 0;
$attributes = array_merge(['class' => 'optbox'], $attributes);
return $this->template->render('config/form_display/fieldset_top', [
'attributes' => $attributes,
'title' => $title,
'description' => $description,
'errors' => $errors,
]);
}
/**
* Displays input field
*
* $opts keys:
* o doc - (string) documentation link
* o errors - error array
* o setvalue - (string) shows button allowing to set predefined value
* o show_restore_default - (boolean) whether show "restore default" button
* o userprefs_allow - whether user preferences are enabled for this field
* (null - no support, true/false - enabled/disabled)
* o userprefs_comment - (string) field comment
* o values - key - value pairs for <select> fields
* o values_escaped - (boolean) tells whether values array is already escaped
* (defaults to false)
* o values_disabled - (array)list of disabled values (keys from values)
* o comment - (string) tooltip comment
* o comment_warning - (bool) whether this comments warns about something
*
* @param string $path config option path
* @param string $name config option name
* @param string $type type of config option
* @param mixed $value current value
* @param string $description verbose description
* @param bool $valueIsDefault whether value is default
* @param array|null $opts see above description
*/
public function displayInput(
$path,
$name,
$type,
$value,
$description = '',
$valueIsDefault = true,
$opts = null
): string {
static $icons; // An array of IMG tags used further below in the function
if (defined('TESTSUITE')) {
$icons = null;
}
$isSetupScript = $this->config->get('is_setup');
if ($icons === null) { // if the static variables have not been initialised
$icons = [];
// Icon definitions:
// The same indexes will be used in the $icons array.
// The first element contains the filename and the second
// element is used for the "alt" and "title" attributes.
$iconInit = [
'edit' => [
'b_edit',
'',
],
'help' => [
'b_help',
__('Documentation'),
],
'reload' => [
's_reload',
'',
],
'tblops' => [
'b_tblops',
'',
],
];
if ($isSetupScript) {
// When called from the setup script, we don't have access to the
// sprite-aware getImage() function because the PMA_theme class
// has not been loaded, so we generate the img tags manually.
foreach ($iconInit as $k => $v) {
$title = '';
if (! empty($v[1])) {
$title = ' title="' . $v[1] . '"';
}
$icons[$k] = sprintf(
'<img alt="%s" src="%s"%s>',
$v[1],
'../themes/pmahomme/img/' . $v[0] . '.png',
$title
);
}
} else {
// In this case we just use getImage() because it's available
foreach ($iconInit as $k => $v) {
$icons[$k] = Generator::getImage(
$v[0],
$v[1]
);
}
}
}
$hasErrors = isset($opts['errors']) && ! empty($opts['errors']);
$optionIsDisabled = ! $isSetupScript && isset($opts['userprefs_allow'])
&& ! $opts['userprefs_allow'];
$nameId = 'name="' . htmlspecialchars($path) . '" id="'
. htmlspecialchars($path) . '"';
$fieldClass = $type === 'checkbox' ? 'checkbox' : '';
if (! $valueIsDefault) {
$fieldClass .= ($fieldClass == '' ? '' : ' ')
. ($hasErrors ? 'custom field-error' : 'custom');
}
$fieldClass = $fieldClass ? ' class="' . $fieldClass . '"' : '';
$trClass = $this->group > 0
? 'group-field group-field-' . $this->group
: '';
if (isset($opts['setvalue']) && $opts['setvalue'] === ':group') {
unset($opts['setvalue']);
$this->group++;
$trClass = 'group-header-field group-header-' . $this->group;
}
if ($optionIsDisabled) {
$trClass .= ($trClass ? ' ' : '') . 'disabled-field';
}
$trClass = $trClass ? ' class="' . $trClass . '"' : '';
$htmlOutput = '<tr' . $trClass . '>';
$htmlOutput .= '<th>';
$htmlOutput .= '<label for="' . htmlspecialchars($path) . '">' . htmlspecialchars_decode($name)
. '</label>';
if (! empty($opts['doc'])) {
$htmlOutput .= '<span class="doc">';
$htmlOutput .= '<a href="' . $opts['doc']
. '" target="documentation">' . $icons['help'] . '</a>';
$htmlOutput .= "\n";
$htmlOutput .= '</span>';
}
if ($optionIsDisabled) {
$htmlOutput .= '<span class="disabled-notice" title="';
$htmlOutput .= __(
'This setting is disabled, it will not be applied to your configuration.'
);
$htmlOutput .= '">' . __('Disabled') . '</span>';
}
if (! empty($description)) {
$htmlOutput .= '<small>' . $description . '</small>';
}
$htmlOutput .= '</th>';
$htmlOutput .= '<td>';
switch ($type) {
case 'text':
$htmlOutput .= '<input type="text" class="w-75" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars($value) . '">';
break;
case 'password':
$htmlOutput .= '<input type="password" class="w-75" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars($value) . '">';
break;
case 'short_text':
// As seen in the reporting server (#15042) we sometimes receive
// an array here. No clue about its origin nor content, so let's avoid
// a notice on htmlspecialchars().
if (is_string($value)) {
$htmlOutput .= '<input type="text" size="25" ' . $nameId
. $fieldClass . ' value="' . htmlspecialchars($value)
. '">';
}
break;
case 'number_text':
$htmlOutput .= '<input type="number" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars((string) $value) . '">';
break;
case 'checkbox':
$htmlOutput .= '<span' . $fieldClass . '><input type="checkbox" ' . $nameId
. ($value ? ' checked="checked"' : '') . '></span>';
break;
case 'select':
$htmlOutput .= '<select class="w-75" ' . $nameId . $fieldClass . '>';
$escape = ! (isset($opts['values_escaped']) && $opts['values_escaped']);
$valuesDisabled = isset($opts['values_disabled'])
? array_flip($opts['values_disabled']) : [];
foreach ($opts['values'] as $optValueKey => $optValue) {
// set names for boolean values
if (is_bool($optValue)) {
$optValue = mb_strtolower(
$optValue ? __('Yes') : __('No')
);
}
// escape if necessary
if ($escape) {
$display = htmlspecialchars((string) $optValue);
$displayValue = htmlspecialchars((string) $optValueKey);
} else {
$display = $optValue;
$displayValue = $optValueKey;
}
// compare with selected value
// boolean values are cast to integers when used as array keys
$selected = is_bool($value)
? (int) $value === $optValueKey
: $optValueKey === $value;
$htmlOutput .= '<option value="' . $displayValue . '"';
if ($selected) {
$htmlOutput .= ' selected="selected"';
}
if (isset($valuesDisabled[$optValueKey])) {
$htmlOutput .= ' disabled="disabled"';
}
$htmlOutput .= '>' . $display . '</option>';
}
$htmlOutput .= '</select>';
break;
case 'list':
$val = $value;
if (isset($val['wrapper_params'])) {
unset($val['wrapper_params']);
}
$htmlOutput .= '<textarea cols="35" rows="5" ' . $nameId . $fieldClass
. '>' . htmlspecialchars(implode("\n", $val)) . '</textarea>';
break;
}
if ($isSetupScript
&& isset($opts['userprefs_comment'])
&& $opts['userprefs_comment']
) {
$htmlOutput .= '<a class="userprefs-comment" title="'
. htmlspecialchars($opts['userprefs_comment']) . '">'
. $icons['tblops'] . '</a>';
}
if (isset($opts['setvalue']) && $opts['setvalue']) {
$htmlOutput .= '<a class="set-value hide" href="#'
. htmlspecialchars($path . '=' . $opts['setvalue']) . '" title="'
. sprintf(__('Set value: %s'), htmlspecialchars($opts['setvalue']))
. '">' . $icons['edit'] . '</a>';
}
if (isset($opts['show_restore_default']) && $opts['show_restore_default']) {
$htmlOutput .= '<a class="restore-default hide" href="#' . $path . '" title="'
. __('Restore default value') . '">' . $icons['reload'] . '</a>';
}
// this must match with displayErrors() in scripts/config.js
if ($hasErrors) {
$htmlOutput .= "\n <dl class=\"inline_errors\">";
foreach ($opts['errors'] as $error) {
$htmlOutput .= '<dd>' . htmlspecialchars($error) . '</dd>';
}
$htmlOutput .= '</dl>';
}
$htmlOutput .= '</td>';
if ($isSetupScript && isset($opts['userprefs_allow'])) {
$htmlOutput .= '<td class="userprefs-allow" title="' .
__('Allow users to customize this value') . '">';
$htmlOutput .= '<input type="checkbox" name="' . $path
. '-userprefs-allow" ';
if ($opts['userprefs_allow']) {
$htmlOutput .= 'checked="checked"';
}
$htmlOutput .= '>';
$htmlOutput .= '</td>';
} elseif ($isSetupScript) {
$htmlOutput .= '<td>&nbsp;</td>';
}
$htmlOutput .= '</tr>';
return $htmlOutput;
}
/**
* Display group header
*
* @param string $headerText Text of header
*/
public function displayGroupHeader(string $headerText): string
{
$this->group++;
if ($headerText === '') {
return '';
}
$colspan = $this->config->get('is_setup') ? 3 : 2;
return $this->template->render('config/form_display/group_header', [
'group' => $this->group,
'colspan' => $colspan,
'header_text' => $headerText,
]);
}
/**
* Display group footer
*/
public function displayGroupFooter(): void
{
$this->group--;
}
/**
* Displays bottom part of a fieldset
*
* @param bool $showButtons Whether show submit and reset button
*/
public function displayFieldsetBottom(bool $showButtons = true): string
{
return $this->template->render('config/form_display/fieldset_bottom', [
'show_buttons' => $showButtons,
'is_setup' => $this->config->get('is_setup'),
]);
}
/**
* Closes form tabs
*/
public function displayTabsBottom(): string
{
return $this->template->render('config/form_display/tabs_bottom');
}
/**
* Displays bottom part of the form
*/
public function displayFormBottom(): string
{
return $this->template->render('config/form_display/form_bottom');
}
/**
* Appends JS validation code to $js_array
*
* @param string $fieldId ID of field to validate
* @param string|array $validators validators callback
* @param array $jsArray will be updated with javascript code
*/
public function addJsValidate($fieldId, $validators, array &$jsArray): void
{
foreach ((array) $validators as $validator) {
$validator = (array) $validator;
$vName = array_shift($validator);
$vArgs = [];
foreach ($validator as $arg) {
$vArgs[] = Sanitize::escapeJsString($arg);
}
$vArgs = $vArgs ? ", ['" . implode("', '", $vArgs) . "']" : '';
$jsArray[] = "registerFieldValidator('" . $fieldId . "', '" . $vName . "', true" . $vArgs . ')';
}
}
/**
* Displays JavaScript code
*
* @param array $jsArray lines of javascript code
*/
public function displayJavascript(array $jsArray): string
{
if (empty($jsArray)) {
return '';
}
return $this->template->render('javascript/display', ['js_array' => $jsArray]);
}
/**
* Displays error list
*
* @param string $name Name of item with errors
* @param array $errorList List of errors to show
*
* @return string HTML for errors
*/
public function displayErrors($name, array $errorList): string
{
return $this->template->render('config/form_display/errors', [
'name' => $name,
'error_list' => $errorList,
]);
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* Base class for preferences.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\FormDisplay;
use function is_int;
/**
* Base form for user preferences
*/
abstract class BaseForm extends FormDisplay
{
/**
* @param ConfigFile $cf Config file instance
* @param int|null $serverId 0 if new server, validation; >= 1 if editing a server
*/
public function __construct(ConfigFile $cf, $serverId = null)
{
parent::__construct($cf);
foreach (static::getForms() as $formName => $form) {
$this->registerForm($formName, $form, $serverId);
}
}
/**
* List of available forms, each form is described as an array of fields to display.
* Fields MUST have their counterparts in the $cfg array.
*
* To define form field, use the notation below:
* $forms['Form group']['Form name'] = array('Option/path');
*
* You can assign default values set by special button ("set value: ..."), eg.:
* 'Servers/1/pmadb' => 'phpmyadmin'
*
* To group options, use:
* ':group:' . __('group name') // just define a group
* or
* 'option' => ':group' // group starting from this option
* End group blocks with:
* ':group:end'
*
* @return array
*
* @todo This should be abstract, but that does not work in PHP 5
*/
public static function getForms()
{
return [];
}
/**
* Returns list of fields used in the form.
*
* @return string[]
*/
public static function getFields()
{
$names = [];
foreach (static::getForms() as $form) {
foreach ($form as $k => $v) {
$names[] = is_int($k) ? $v : $k;
}
}
return $names;
}
/**
* Returns name of the form
*
* @return string
*
* @todo This should be abstract, but that does not work in PHP 5
*/
public static function getName()
{
return '';
}
}

View file

@ -0,0 +1,149 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms;
use PhpMyAdmin\Config\ConfigFile;
use function array_merge;
use function in_array;
class BaseFormList
{
/**
* List of all forms
*
* @var array
*/
protected static $all = [];
/** @var string */
protected static $ns = 'PhpMyAdmin\\Config\\Forms\\';
/** @var array */
private $forms;
/**
* @return array
*/
public static function getAll()
{
return static::$all;
}
/**
* @param string $name Name
*
* @return bool
*/
public static function isValid($name)
{
return in_array($name, static::$all);
}
/**
* @param string $name Name
*
* @return string|null
*/
public static function get($name)
{
if (static::isValid($name)) {
return static::$ns . $name . 'Form';
}
return null;
}
/**
* @param ConfigFile $cf Config file instance
*/
public function __construct(ConfigFile $cf)
{
$this->forms = [];
foreach (static::$all as $form) {
$class = static::get($form);
$this->forms[] = new $class($cf);
}
}
/**
* Processes forms, returns true on successful save
*
* @param bool $allowPartialSave allows for partial form saving
* on failed validation
* @param bool $checkFormSubmit whether check for $_POST['submit_save']
*
* @return bool whether processing was successful
*/
public function process($allowPartialSave = true, $checkFormSubmit = true)
{
$ret = true;
foreach ($this->forms as $form) {
$ret = $ret && $form->process($allowPartialSave, $checkFormSubmit);
}
return $ret;
}
/**
* Displays errors
*
* @return string HTML for errors
*/
public function displayErrors()
{
$ret = '';
foreach ($this->forms as $form) {
$ret .= $form->displayErrors();
}
return $ret;
}
/**
* Reverts erroneous fields to their default values
*
* @return void
*/
public function fixErrors()
{
foreach ($this->forms as $form) {
$form->fixErrors();
}
}
/**
* Tells whether form validation failed
*
* @return bool
*/
public function hasErrors()
{
$ret = false;
foreach ($this->forms as $form) {
$ret = $ret || $form->hasErrors();
}
return $ret;
}
/**
* Returns list of fields used in the form.
*
* @return string[]
*/
public static function getFields()
{
$names = [];
foreach (static::$all as $form) {
$class = static::get($form);
$names = array_merge($names, $class::getFields());
}
return $names;
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class BrowseForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Browse' => MainForm::getForms()['Browse'],
];
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class DbStructureForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'DbStructure' => MainForm::getForms()['DbStructure'],
];
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\FeaturesForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class EditForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Edit' => MainForm::getForms()['Edit'],
'Text_fields' => FeaturesForm::getForms()['Text_fields'],
];
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class ExportForm extends \PhpMyAdmin\Config\Forms\User\ExportForm
{
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class ImportForm extends \PhpMyAdmin\Config\Forms\User\ImportForm
{
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class NaviForm extends \PhpMyAdmin\Config\Forms\User\NaviForm
{
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Page preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseFormList;
class PageFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Browse',
'DbStructure',
'Edit',
'Export',
'Import',
'Navi',
'Sql',
'TableStructure',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\Page\\';
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class SqlForm extends \PhpMyAdmin\Config\Forms\User\SqlForm
{
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class TableStructureForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'TableStructure' => MainForm::getForms()['TableStructure'],
];
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseForm;
class ConfigForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Config' => [
'DefaultLang',
'ServerDefault',
],
];
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class ExportForm extends \PhpMyAdmin\Config\Forms\User\ExportForm
{
}

View file

@ -0,0 +1,76 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use function array_diff;
class FeaturesForm extends \PhpMyAdmin\Config\Forms\User\FeaturesForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
$result = parent::getForms();
/* Remove only_db/hide_db, we have proper Server form in setup */
$result['Databases'] = array_diff(
$result['Databases'],
[
'Servers/1/only_db',
'Servers/1/hide_db',
]
);
/* Following are not available to user */
$result['Import_export'] = [
'UploadDir',
'SaveDir',
'RecodingEngine' => ':group',
'IconvExtraParams',
':group:end',
'ZipDump',
'GZipDump',
'BZipDump',
'CompressOnFly',
];
$result['Security'] = [
'blowfish_secret',
'CheckConfigurationPermissions',
'TrustedProxies',
'AllowUserDropDatabase',
'AllowArbitraryServer',
'ArbitraryServerRegexp',
'LoginCookieRecall',
'LoginCookieStore',
'LoginCookieDeleteAll',
'CaptchaLoginPublicKey',
'CaptchaLoginPrivateKey',
'CaptchaSiteVerifyURL',
];
$result['Developer'] = [
'UserprefsDeveloperTab',
'DBG/sql',
];
$result['Other_core_settings'] = [
'OBGzip',
'PersistentConnections',
'ExecTimeLimit',
'MemoryLimit',
'UseDbSearch',
'ProxyUrl',
'ProxyUser',
'ProxyPass',
'AllowThirdPartyFraming',
'ZeroConf',
];
return $result;
// phpcs:enable
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class ImportForm extends \PhpMyAdmin\Config\Forms\User\ImportForm
{
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class MainForm extends \PhpMyAdmin\Config\Forms\User\MainForm
{
/**
* @return array
*/
public static function getForms()
{
$result = parent::getForms();
/* Following are not available to user */
$result['Startup'][] = 'ShowPhpInfo';
$result['Startup'][] = 'ShowChgPassword';
return $result;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class NaviForm extends \PhpMyAdmin\Config\Forms\User\NaviForm
{
}

View file

@ -0,0 +1,111 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseForm;
class ServersForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
return [
'Server' => [
'Servers' => [
1 => [
'verbose',
'host',
'port',
'socket',
'ssl',
'compress',
],
],
],
'Server_auth' => [
'Servers' => [
1 => [
'auth_type',
':group:' . __('Config authentication'),
'user',
'password',
':group:end',
':group:' . __('HTTP authentication'),
'auth_http_realm',
':group:end',
':group:' . __('Signon authentication'),
'SignonSession',
'SignonURL',
'LogoutURL',
],
],
],
'Server_config' => [
'Servers' => [
1 => [
'only_db',
'hide_db',
'AllowRoot',
'AllowNoPassword',
'DisableIS',
'AllowDeny/order',
'AllowDeny/rules',
'SessionTimeZone',
],
],
],
'Server_pmadb' => [
'Servers' => [
1 => [
'pmadb' => 'phpmyadmin',
'controlhost',
'controlport',
'controluser',
'controlpass',
'bookmarktable' => 'pma__bookmark',
'relation' => 'pma__relation',
'userconfig' => 'pma__userconfig',
'users' => 'pma__users',
'usergroups' => 'pma__usergroups',
'navigationhiding' => 'pma__navigationhiding',
'table_info' => 'pma__table_info',
'column_info' => 'pma__column_info',
'history' => 'pma__history',
'recent' => 'pma__recent',
'favorite' => 'pma__favorite',
'table_uiprefs' => 'pma__table_uiprefs',
'tracking' => 'pma__tracking',
'table_coords' => 'pma__table_coords',
'pdf_pages' => 'pma__pdf_pages',
'savedsearches' => 'pma__savedsearches',
'central_columns' => 'pma__central_columns',
'designer_settings' => 'pma__designer_settings',
'export_templates' => 'pma__export_templates',
'MaxTableUiprefs' => 100,
],
],
],
'Server_tracking' => [
'Servers' => [
1 => [
'tracking_version_auto_create',
'tracking_default_statements',
'tracking_add_drop_view',
'tracking_add_drop_table',
'tracking_add_drop_database',
],
],
],
];
// phpcs:enable
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Setup preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseFormList;
class SetupFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Config',
'Export',
'Features',
'Import',
'Main',
'Navi',
'Servers',
'Sql',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\Setup\\';
}

View file

@ -0,0 +1,23 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class SqlForm extends \PhpMyAdmin\Config\Forms\User\SqlForm
{
/**
* @return array
*/
public static function getForms()
{
$result = parent::getForms();
/* Following are not available to user */
$result['Sql_queries'][] = 'QueryHistoryDB';
return $result;
}
}

View file

@ -0,0 +1,153 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class ExportForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
return [
'Export_defaults' => [
'Export/method',
':group:' . __('Quick'),
'Export/quick_export_onserver',
'Export/quick_export_onserver_overwrite',
':group:end',
':group:' . __('Custom'),
'Export/format',
'Export/compression',
'Export/charset',
'Export/lock_tables',
'Export/as_separate_files',
'Export/asfile' => ':group',
'Export/onserver',
'Export/onserver_overwrite',
':group:end',
'Export/file_template_table',
'Export/file_template_database',
'Export/file_template_server',
],
'Sql' => [
'Export/sql_include_comments' => ':group',
'Export/sql_dates',
'Export/sql_relation',
'Export/sql_mime',
':group:end',
'Export/sql_use_transaction',
'Export/sql_disable_fk',
'Export/sql_views_as_tables',
'Export/sql_metadata',
'Export/sql_compatibility',
'Export/sql_structure_or_data',
':group:' . __('Structure'),
'Export/sql_drop_database',
'Export/sql_create_database',
'Export/sql_drop_table',
'Export/sql_create_table' => ':group',
'Export/sql_if_not_exists',
'Export/sql_auto_increment',
':group:end',
'Export/sql_create_view' => ':group',
'Export/sql_view_current_user',
'Export/sql_or_replace_view',
':group:end',
'Export/sql_procedure_function',
'Export/sql_create_trigger',
'Export/sql_backquotes',
':group:end',
':group:' . __('Data'),
'Export/sql_delayed',
'Export/sql_ignore',
'Export/sql_type',
'Export/sql_insert_syntax',
'Export/sql_max_query_size',
'Export/sql_hex_for_binary',
'Export/sql_utc_time',
],
'CodeGen' => ['Export/codegen_format'],
'Csv' => [
':group:' . __('CSV'),
'Export/csv_separator',
'Export/csv_enclosed',
'Export/csv_escaped',
'Export/csv_terminated',
'Export/csv_null',
'Export/csv_removeCRLF',
'Export/csv_columns',
':group:end',
':group:' . __('CSV for MS Excel'),
'Export/excel_null',
'Export/excel_removeCRLF',
'Export/excel_columns',
'Export/excel_edition',
],
'Latex' => [
'Export/latex_caption',
'Export/latex_structure_or_data',
':group:' . __('Structure'),
'Export/latex_structure_caption',
'Export/latex_structure_continued_caption',
'Export/latex_structure_label',
'Export/latex_relation',
'Export/latex_comments',
'Export/latex_mime',
':group:end',
':group:' . __('Data'),
'Export/latex_columns',
'Export/latex_data_caption',
'Export/latex_data_continued_caption',
'Export/latex_data_label',
'Export/latex_null',
],
'Microsoft_Office' => [
':group:' . __('Microsoft Word 2000'),
'Export/htmlword_structure_or_data',
'Export/htmlword_null',
'Export/htmlword_columns',
],
'Open_Document' => [
':group:' . __('OpenDocument Spreadsheet'),
'Export/ods_columns',
'Export/ods_null',
':group:end',
':group:' . __('OpenDocument Text'),
'Export/odt_structure_or_data',
':group:' . __('Structure'),
'Export/odt_relation',
'Export/odt_comments',
'Export/odt_mime',
':group:end',
':group:' . __('Data'),
'Export/odt_columns',
'Export/odt_null',
],
'Texy' => [
'Export/texytext_structure_or_data',
':group:' . __('Data'),
'Export/texytext_null',
'Export/texytext_columns',
],
];
// phpcs:enable
}
/**
* @return string
*/
public static function getName()
{
return __('Export');
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class FeaturesForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
$result = [
'General' => [
'VersionCheck',
'NaturalOrder',
'InitialSlidersState',
'LoginCookieValidity',
'SkipLockedTables',
'DisableMultiTableMaintenance',
'ShowHint',
'SendErrorReports',
'ConsoleEnterExecutes',
'DisableShortcutKeys',
'FirstDayOfCalendar',
],
'Databases' => [
'Servers/1/only_db', // saves to Server/only_db
'Servers/1/hide_db', // saves to Server/hide_db
'MaxDbList',
'MaxTableList',
'DefaultConnectionCollation',
],
'Text_fields' => [
'CharEditing',
'MinSizeForInputField',
'MaxSizeForInputField',
'CharTextareaCols',
'CharTextareaRows',
'TextareaCols',
'TextareaRows',
'LongtextDoubleTextarea',
],
'Page_titles' => [
'TitleDefault',
'TitleTable',
'TitleDatabase',
'TitleServer',
],
'Warnings' => [
'PmaNoRelation_DisableWarning',
'SuhosinDisableWarning',
'LoginCookieValidityDisableWarning',
'ReservedWordDisableWarning',
],
'Console' => [
'Console/Mode',
'Console/StartHistory',
'Console/AlwaysExpand',
'Console/CurrentQuery',
'Console/EnterExecutes',
'Console/DarkTheme',
'Console/Height',
'Console/GroupQueries',
'Console/OrderBy',
'Console/Order',
],
];
// skip Developer form if no setting is available
if ($GLOBALS['cfg']['UserprefsDeveloperTab']) {
$result['Developer'] = ['DBG/sql'];
}
return $result;
}
/**
* @return string
*/
public static function getName()
{
return __('Features');
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class ImportForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Import_defaults' => [
'Import/format',
'Import/charset',
'Import/allow_interrupt',
'Import/skip_queries',
'enable_drag_drop_import',
],
'Sql' => [
'Import/sql_compatibility',
'Import/sql_no_auto_value_on_zero',
'Import/sql_read_as_multibytes',
],
'Csv' => [
':group:' . __('CSV'),
'Import/csv_replace',
'Import/csv_ignore',
'Import/csv_terminated',
'Import/csv_enclosed',
'Import/csv_escaped',
'Import/csv_col_names',
':group:end',
':group:' . __('CSV using LOAD DATA'),
'Import/ldi_replace',
'Import/ldi_ignore',
'Import/ldi_terminated',
'Import/ldi_enclosed',
'Import/ldi_escaped',
'Import/ldi_local_option',
],
'Open_Document' => [
':group:' . __('OpenDocument Spreadsheet'),
'Import/ods_col_names',
'Import/ods_empty_rows',
'Import/ods_recognize_percentages',
'Import/ods_recognize_currency',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Import');
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class MainForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Startup' => [
'ShowCreateDb',
'ShowStats',
'ShowServerInfo',
],
'DbStructure' => [
'ShowDbStructureCharset',
'ShowDbStructureComment',
'ShowDbStructureCreation',
'ShowDbStructureLastUpdate',
'ShowDbStructureLastCheck',
],
'TableStructure' => [
'HideStructureActions',
'ShowColumnComments',
':group:' . __('Default transformations'),
'DefaultTransformations/Hex',
'DefaultTransformations/Substring',
'DefaultTransformations/Bool2Text',
'DefaultTransformations/External',
'DefaultTransformations/PreApPend',
'DefaultTransformations/DateFormat',
'DefaultTransformations/Inline',
'DefaultTransformations/TextImageLink',
'DefaultTransformations/TextLink',
':group:end',
],
'Browse' => [
'TableNavigationLinksMode',
'ActionLinksMode',
'ShowAll',
'MaxRows',
'Order',
'BrowsePointerEnable',
'BrowseMarkerEnable',
'GridEditing',
'SaveCellsAtOnce',
'RepeatCells',
'LimitChars',
'RowActionLinks',
'RowActionLinksWithoutUnique',
'TablePrimaryKeyOrder',
'RememberSorting',
'RelationalDisplay',
],
'Edit' => [
'ProtectBinary',
'ShowFunctionFields',
'ShowFieldTypesInDataEditView',
'InsertRows',
'ForeignKeyDropdownOrder',
'ForeignKeyMaxLimit',
],
'Tabs' => [
'TabsMode',
'DefaultTabServer',
'DefaultTabDatabase',
'DefaultTabTable',
],
'DisplayRelationalSchema' => ['PDFDefaultPageSize'],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Main panel');
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class NaviForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Navi_panel' => [
'ShowDatabasesNavigationAsTree',
'NavigationLinkWithMainPanel',
'NavigationDisplayLogo',
'NavigationLogoLink',
'NavigationLogoLinkWindow',
'NavigationTreePointerEnable',
'FirstLevelNavigationItems',
'NavigationTreeDisplayItemFilterMinimum',
'NumRecentTables',
'NumFavoriteTables',
'NavigationWidth',
],
'Navi_tree' => [
'MaxNavigationItems',
'NavigationTreeEnableGrouping',
'NavigationTreeEnableExpansion',
'NavigationTreeShowTables',
'NavigationTreeShowViews',
'NavigationTreeShowFunctions',
'NavigationTreeShowProcedures',
'NavigationTreeShowEvents',
'NavigationTreeAutoexpandSingleDb',
],
'Navi_servers' => [
'NavigationDisplayServers',
'DisplayServersList',
],
'Navi_databases' => [
'NavigationTreeDisplayDbFilterMinimum',
'NavigationTreeDbSeparator',
],
'Navi_tables' => [
'NavigationTreeDefaultTabTable',
'NavigationTreeDefaultTabTable2',
'NavigationTreeTableSeparator',
'NavigationTreeTableLevel',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Navigation panel');
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class SqlForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Sql_queries' => [
'ShowSQL',
'Confirm',
'QueryHistoryMax',
'IgnoreMultiSubmitErrors',
'MaxCharactersInDisplayedSQL',
'RetainQueryBox',
'CodemirrorEnable',
'LintEnable',
'EnableAutocompleteForTablesAndColumns',
'DefaultForeignKeyChecks',
],
'Sql_box' => [
'SQLQuery/Edit',
'SQLQuery/Explain',
'SQLQuery/ShowAsPHP',
'SQLQuery/Refresh',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('SQL queries');
}
}

View file

@ -0,0 +1,25 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseFormList;
class UserFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Features',
'Sql',
'Navi',
'Main',
'Export',
'Import',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\User\\';
}

View file

@ -0,0 +1,199 @@
<?php
/**
* Page-related settings
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config\Forms\Page\PageFormList;
use PhpMyAdmin\Core;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\UserPreferences;
/**
* Page-related settings
*/
class PageSettings
{
/**
* Contains id of the form element
*
* @var string
*/
private $elemId = 'page_settings_modal';
/**
* Name of the group to show
*
* @var string
*/
private $groupName = '';
/**
* Contains HTML of errors
*
* @var string
*/
private $errorHTML = '';
/**
* Contains HTML of settings
*
* @var string
*/
private $HTML = '';
/** @var UserPreferences */
private $userPreferences;
/**
* @param string $formGroupName The name of config form group to display
* @param string $elemId Id of the div containing settings
*/
public function __construct($formGroupName, $elemId = null)
{
$this->userPreferences = new UserPreferences();
$formClass = PageFormList::get($formGroupName);
if ($formClass === null) {
return;
}
if (isset($_REQUEST['printview']) && $_REQUEST['printview'] == '1') {
return;
}
if (! empty($elemId)) {
$this->elemId = $elemId;
}
$this->groupName = $formGroupName;
$cf = new ConfigFile($GLOBALS['PMA_Config']->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new $formClass($cf);
// Process form
$error = null;
if (isset($_POST['submit_save'])
&& $_POST['submit_save'] == $formGroupName
) {
$this->processPageSettings($formDisplay, $cf, $error);
}
// Display forms
$this->HTML = $this->getPageSettingsDisplay($formDisplay, $error);
}
/**
* Process response to form
*
* @param FormDisplay $formDisplay Form
* @param ConfigFile $cf Configuration file
* @param Message|null $error Error message
*
* @return void
*/
private function processPageSettings(&$formDisplay, &$cf, &$error)
{
if (! $formDisplay->process(false) || $formDisplay->hasErrors()) {
return;
}
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
if ($result === true) {
// reload page
$response = Response::getInstance();
Core::sendHeaderLocation(
$response->getFooter()->getSelfUrl()
);
exit;
}
$error = $result;
}
/**
* Store errors in _errorHTML
*
* @param FormDisplay $formDisplay Form
* @param Message|null $error Error message
*
* @return void
*/
private function storeError(&$formDisplay, &$error)
{
$retval = '';
if ($error) {
$retval .= $error->getDisplay();
}
if ($formDisplay->hasErrors()) {
// form has errors
$retval .= '<div class="alert alert-danger config-form" role="alert">'
. '<b>' . __(
'Cannot save settings, submitted configuration form contains '
. 'errors!'
) . '</b>'
. $formDisplay->displayErrors()
. '</div>';
}
$this->errorHTML = $retval;
}
/**
* Display page-related settings
*
* @param FormDisplay $formDisplay Form
* @param Message $error Error message
*
* @return string
*/
private function getPageSettingsDisplay(&$formDisplay, &$error)
{
$response = Response::getInstance();
$retval = '';
$this->storeError($formDisplay, $error);
$retval .= '<div id="' . $this->elemId . '">';
$retval .= '<div class="page_settings">';
$retval .= $formDisplay->getDisplay(
true,
true,
false,
$response->getFooter()->getSelfUrl(),
[
'submit_save' => $this->groupName,
]
);
$retval .= '</div>';
$retval .= '</div>';
return $retval;
}
/**
* Get HTML output
*
* @return string
*/
public function getHTML()
{
return $this->HTML;
}
/**
* Get error HTML output
*
* @return string
*/
public function getErrorHTML()
{
return $this->errorHTML;
}
}

View file

@ -0,0 +1,568 @@
<?php
/**
* Server config checks management
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Setup\Index as SetupIndex;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function function_exists;
use function htmlspecialchars;
use function implode;
use function ini_get;
use function preg_match;
use function sprintf;
use function strlen;
/**
* Performs various compatibility, security and consistency checks on current config
*
* Outputs results to message list, must be called between SetupIndex::messagesBegin()
* and SetupIndex::messagesEnd()
*/
class ServerConfigChecks
{
/** @var ConfigFile configurations being checked */
protected $cfg;
/**
* @param ConfigFile $cfg Configuration
*/
public function __construct(ConfigFile $cfg)
{
$this->cfg = $cfg;
}
/**
* Perform config checks
*
* @return void
*/
public function performConfigChecks()
{
$blowfishSecret = $this->cfg->get('blowfish_secret');
$blowfishSecretSet = false;
$cookieAuthUsed = false;
[$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet]
= $this->performConfigChecksServers(
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet
);
$this->performConfigChecksCookieAuthUsed(
$cookieAuthUsed,
$blowfishSecretSet,
$blowfishSecret
);
// $cfg['AllowArbitraryServer']
// should be disabled
if ($this->cfg->getValue('AllowArbitraryServer')) {
$sAllowArbitraryServerWarn = sprintf(
__(
'This %soption%s should be disabled as it allows attackers to '
. 'bruteforce login to any MySQL server. If you feel this is necessary, '
. 'use %srestrict login to MySQL server%s or %strusted proxies list%s. '
. 'However, IP-based protection with trusted proxies list may not be '
. 'reliable if your IP belongs to an ISP where thousands of users, '
. 'including you, are connected to.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
);
SetupIndex::messagesSet(
'notice',
'AllowArbitraryServer',
Descriptions::get('AllowArbitraryServer'),
Sanitize::sanitizeMessage($sAllowArbitraryServerWarn)
);
}
$this->performConfigChecksLoginCookie();
$sDirectoryNotice = __(
'This value should be double checked to ensure that this directory is '
. 'neither world accessible nor readable or writable by other users on '
. 'your server.'
);
// $cfg['SaveDir']
// should not be world-accessible
if ($this->cfg->getValue('SaveDir') != '') {
SetupIndex::messagesSet(
'notice',
'SaveDir',
Descriptions::get('SaveDir'),
Sanitize::sanitizeMessage($sDirectoryNotice)
);
}
// $cfg['TempDir']
// should not be world-accessible
if ($this->cfg->getValue('TempDir') != '') {
SetupIndex::messagesSet(
'notice',
'TempDir',
Descriptions::get('TempDir'),
Sanitize::sanitizeMessage($sDirectoryNotice)
);
}
$this->performConfigChecksZips();
}
/**
* Check config of servers
*
* @param bool $cookieAuthUsed Cookie auth is used
* @param string $blowfishSecret Blowfish secret
* @param bool $blowfishSecretSet Blowfish secret set
*
* @return array
*/
protected function performConfigChecksServers(
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet
) {
$serverCnt = $this->cfg->getServerCount();
for ($i = 1; $i <= $serverCnt; $i++) {
$cookieAuthServer
= ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'cookie');
$cookieAuthUsed |= $cookieAuthServer;
$serverName = $this->performConfigChecksServersGetServerName(
$this->cfg->getServerName($i),
$i
);
$serverName = htmlspecialchars($serverName);
[$blowfishSecret, $blowfishSecretSet]
= $this->performConfigChecksServersSetBlowfishSecret(
$blowfishSecret,
$cookieAuthServer,
$blowfishSecretSet
);
// $cfg['Servers'][$i]['ssl']
// should be enabled if possible
if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) {
$title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/ssl',
$title,
__(
'You should use SSL connections if your database server '
. 'supports it.'
)
);
}
$sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf(
__(
'If you feel this is necessary, use additional protection settings - '
. '%1$shost authentication%2$s settings and %3$strusted proxies list%4$s. '
. 'However, IP-based protection may not be reliable if your IP belongs '
. 'to an ISP where thousands of users, including you, are connected to.'
),
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
));
// $cfg['Servers'][$i]['auth_type']
// warn about full user credentials if 'auth_type' is 'config'
if ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'config'
&& $this->cfg->getValue('Servers/' . $i . '/user') != ''
&& $this->cfg->getValue('Servers/' . $i . '/password') != ''
) {
$title = Descriptions::get('Servers/1/auth_type')
. ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/auth_type',
$title,
Sanitize::sanitizeMessage(sprintf(
__(
'You set the [kbd]config[/kbd] authentication type and included '
. 'username and password for auto-login, which is not a desirable '
. 'option for live hosts. Anyone who knows or guesses your phpMyAdmin '
. 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication '
. 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].'
),
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]',
'[/a]'
))
. ' ' . $sSecurityInfoMsg
);
}
// $cfg['Servers'][$i]['AllowRoot']
// $cfg['Servers'][$i]['AllowNoPassword']
// serious security flaw
if (! $this->cfg->getValue('Servers/' . $i . '/AllowRoot')
|| ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword')
) {
continue;
}
$title = Descriptions::get('Servers/1/AllowNoPassword')
. ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/AllowNoPassword',
$title,
__('You allow for connecting to the server without a password.')
. ' ' . $sSecurityInfoMsg
);
}
return [
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet,
];
}
/**
* Set blowfish secret
*
* @param string|null $blowfishSecret Blowfish secret
* @param bool $cookieAuthServer Cookie auth is used
* @param bool $blowfishSecretSet Blowfish secret set
*
* @return array
*/
protected function performConfigChecksServersSetBlowfishSecret(
$blowfishSecret,
$cookieAuthServer,
$blowfishSecretSet
): array {
if ($cookieAuthServer && $blowfishSecret === null) {
$blowfishSecretSet = true;
$this->cfg->set('blowfish_secret', Util::generateRandom(32));
}
return [
$blowfishSecret,
$blowfishSecretSet,
];
}
/**
* Define server name
*
* @param string $serverName Server name
* @param int $serverId Server id
*
* @return string Server name
*/
protected function performConfigChecksServersGetServerName(
$serverName,
$serverId
) {
if ($serverName === 'localhost') {
return $serverName . ' [' . $serverId . ']';
}
return $serverName;
}
/**
* Perform config checks for zip part.
*
* @return void
*/
protected function performConfigChecksZips()
{
$this->performConfigChecksServerGZipdump();
$this->performConfigChecksServerBZipdump();
$this->performConfigChecksServersZipdump();
}
/**
* Perform config checks for zip part.
*
* @return void
*/
protected function performConfigChecksServersZipdump()
{
// $cfg['ZipDump']
// requires zip_open in import
if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) {
SetupIndex::messagesSet(
'error',
'ZipDump_import',
Descriptions::get('ZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sZip decompression%s requires functions (%s) which are unavailable '
. 'on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'zip_open'
))
);
}
// $cfg['ZipDump']
// requires gzcompress in export
if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) {
return;
}
SetupIndex::messagesSet(
'error',
'ZipDump_export',
Descriptions::get('ZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sZip compression%s requires functions (%s) which are unavailable on '
. 'this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'gzcompress'
))
);
}
/**
* Check config of servers
*
* @param bool $cookieAuthUsed Cookie auth is used
* @param bool $blowfishSecretSet Blowfish secret set
* @param string $blowfishSecret Blowfish secret
*
* @return void
*/
protected function performConfigChecksCookieAuthUsed(
$cookieAuthUsed,
$blowfishSecretSet,
$blowfishSecret
) {
// $cfg['blowfish_secret']
// it's required for 'cookie' authentication
if (! $cookieAuthUsed) {
return;
}
if ($blowfishSecretSet) {
// 'cookie' auth used, blowfish_secret was generated
SetupIndex::messagesSet(
'notice',
'blowfish_secret_created',
Descriptions::get('blowfish_secret'),
Sanitize::sanitizeMessage(__(
'You didn\'t have blowfish secret set and have enabled '
. '[kbd]cookie[/kbd] authentication, so a key was automatically '
. 'generated for you. It is used to encrypt cookies; you don\'t need to '
. 'remember it.'
))
);
} else {
$blowfishWarnings = [];
// check length
if (strlen($blowfishSecret) < 32) {
// too short key
$blowfishWarnings[] = __(
'Key is too short, it should have at least 32 characters.'
);
}
// check used characters
$hasDigits = (bool) preg_match('/\d/', $blowfishSecret);
$hasChars = (bool) preg_match('/\S/', $blowfishSecret);
$hasNonword = (bool) preg_match('/\W/', $blowfishSecret);
if (! $hasDigits || ! $hasChars || ! $hasNonword) {
$blowfishWarnings[] = Sanitize::sanitizeMessage(
__(
'Key should contain letters, numbers [em]and[/em] '
. 'special characters.'
)
);
}
if (! empty($blowfishWarnings)) {
SetupIndex::messagesSet(
'error',
'blowfish_warnings' . count($blowfishWarnings),
Descriptions::get('blowfish_secret'),
implode('<br>', $blowfishWarnings)
);
}
}
}
/**
* Check configuration for login cookie
*
* @return void
*/
protected function performConfigChecksLoginCookie()
{
// $cfg['LoginCookieValidity']
// value greater than session.gc_maxlifetime will cause
// random session invalidation after that time
$loginCookieValidity = $this->cfg->getValue('LoginCookieValidity');
if ($loginCookieValidity > ini_get('session.gc_maxlifetime')
) {
SetupIndex::messagesSet(
'error',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may '
. 'cause random session invalidation (currently session.gc_maxlifetime '
. 'is %5$d).'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']',
'[/a]',
ini_get('session.gc_maxlifetime')
))
);
}
// $cfg['LoginCookieValidity']
// should be at most 1800 (30 min)
if ($loginCookieValidity > 1800) {
SetupIndex::messagesSet(
'notice',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) '
. 'at most. Values larger than 1800 may pose a security risk such as '
. 'impersonation.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
))
);
}
// $cfg['LoginCookieValidity']
// $cfg['LoginCookieStore']
// LoginCookieValidity must be less or equal to LoginCookieStore
if (($this->cfg->getValue('LoginCookieStore') == 0)
|| ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore'))
) {
return;
}
SetupIndex::messagesSet(
'error',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s '
. 'is not 0, %sLogin cookie validity%s must be set to a value less or '
. 'equal to it.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
))
);
}
/**
* Check GZipDump configuration
*
* @return void
*/
protected function performConfigChecksServerBZipdump()
{
// $cfg['BZipDump']
// requires bzip2 functions
if (! $this->cfg->getValue('BZipDump')
|| ($this->functionExists('bzopen') && $this->functionExists('bzcompress'))
) {
return;
}
$functions = $this->functionExists('bzopen')
? '' :
'bzopen';
$functions .= $this->functionExists('bzcompress')
? ''
: ($functions ? ', ' : '') . 'bzcompress';
SetupIndex::messagesSet(
'error',
'BZipDump',
Descriptions::get('BZipDump'),
Sanitize::sanitizeMessage(
sprintf(
__(
'%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which '
. 'are unavailable on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
$functions
)
)
);
}
/**
* Check GZipDump configuration
*
* @return void
*/
protected function performConfigChecksServerGZipdump()
{
// $cfg['GZipDump']
// requires zlib functions
if (! $this->cfg->getValue('GZipDump')
|| ($this->functionExists('gzopen') && $this->functionExists('gzencode'))
) {
return;
}
SetupIndex::messagesSet(
'error',
'GZipDump',
Descriptions::get('GZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%1$sGZip compression and decompression%2$s requires functions (%3$s) which '
. 'are unavailable on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'gzencode'
))
);
}
/**
* Wrapper around function_exists to allow mock in test
*
* @param string $name Function name
*
* @return bool
*/
protected function functionExists($name)
{
return function_exists($name);
}
}

View file

@ -0,0 +1,489 @@
<?php
/**
* Links configuration for MySQL system tables
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
class SpecialSchemaLinks
{
/**
* This array represent the details for generating links inside
* special schemas like mysql, information_schema etc.
* Major element represent a schema.
* All the strings in this array represented in lower case
*
* Array structure ex:
* array(
* // Database name is the major element
* 'mysql' => array(
* // Table name
* 'db' => array(
* // Column name
* 'user' => array(
* // Main url param (can be an array where represent sql)
* 'link_param' => 'username',
* // Other url params
* 'link_dependancy_params' => array(
* 0 => array(
* // URL parameter name
* // (can be array where url param has static value)
* 'param_info' => 'hostname',
* // Column name related to url param
* 'column_name' => 'host'
* )
* ),
* // Page to link
* 'default_page' => './' . Url::getFromRoute('/server/privileges')
* )
* )
* )
* );
*
* @return array<string,array<string,array<string,array<string,array<int,array<string,string>>|string>>>>
*
* @phpstan-return array<
* string, array<
* string, array<
* string,
* array{
* 'link_param': string,
* 'link_dependancy_params'?: array<
* int,
* array{'param_info': string, 'column_name': string}
* >,
* 'default_page': string
* }>
* >
* >
* }
*/
public static function get(): array
{
global $cfg;
$defaultPage = './' . Util::getScriptNameForOption(
$cfg['DefaultTabTable'],
'table'
);
return [
'mysql' => [
'columns_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
1 => [
'param_info' => 'table',
'column_name' => 'Table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'db' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'event' => [
'name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
],
'default_page' => './' . Url::getFromRoute('/database/events', ['edit_item' => 1]),
],
],
'innodb_index_stats' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
],
'default_page' => $defaultPage,
],
'index_name' => [
'link_param' => 'index',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure'),
],
],
'innodb_table_stats' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
],
'default_page' => $defaultPage,
],
],
'proc' => [
'name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
'specific_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
],
'proc_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'routine_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'Routine_type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
],
'proxies_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'tables_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
],
'default_page' => $defaultPage,
],
],
'user' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
],
'information_schema' => [
'columns' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'key_column_usage' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
'referenced_table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'referenced_table_schema',
],
],
'default_page' => $defaultPage,
],
'referenced_column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'referenced_table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'referenced_table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'partitions' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'processlist' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'referential_constraints' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
'referenced_table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
],
'routines' => [
'routine_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'routine_schema',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'routine_type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines'),
],
],
'schemata' => [
'schema_name' => [
'link_param' => 'db',
'default_page' => $defaultPage,
],
],
'statistics' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'tables' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'table_constraints' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'views' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
],
];
}
}

View file

@ -0,0 +1,608 @@
<?php
/**
* Form validation for configuration editor
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\Util;
use function mysqli_report;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
use const MYSQLI_REPORT_OFF;
use const PHP_INT_MAX;
use function array_map;
use function array_merge;
use function array_shift;
use function call_user_func_array;
use function count;
use function error_clear_last;
use function error_get_last;
use function explode;
use function filter_var;
use function htmlspecialchars;
use function intval;
use function is_array;
use function is_object;
use function mb_strpos;
use function mb_substr;
use function mysqli_close;
use function mysqli_connect;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use function trim;
/**
* Validation class for various validation functions
*
* Validation function takes two argument: id for which it is called
* and array of fields' values (usually values for entire formset).
* The function must always return an array with an error (or error array)
* assigned to a form element (formset name or field path). Even if there are
* no errors, key must be set with an empty value.
*
* Validation functions are assigned in $cfg_db['_validators'] (config.values.php).
*/
class Validator
{
/**
* Returns validator list
*
* @param ConfigFile $cf Config file instance
*
* @return array
*/
public static function getValidators(ConfigFile $cf)
{
static $validators = null;
if ($validators !== null) {
return $validators;
}
$validators = $cf->getDbEntry('_validators', []);
if ($GLOBALS['PMA_Config']->get('is_setup')) {
return $validators;
}
// not in setup script: load additional validators for user
// preferences we need original config values not overwritten
// by user preferences, creating a new PhpMyAdmin\Config instance is a
// better idea than hacking into its code
$uvs = $cf->getDbEntry('_userValidators', []);
foreach ($uvs as $field => $uvList) {
$uvList = (array) $uvList;
foreach ($uvList as &$uv) {
if (! is_array($uv)) {
continue;
}
for ($i = 1, $nb = count($uv); $i < $nb; $i++) {
if (mb_substr($uv[$i], 0, 6) !== 'value:') {
continue;
}
$uv[$i] = Core::arrayRead(
mb_substr($uv[$i], 6),
$GLOBALS['PMA_Config']->baseSettings
);
}
}
$validators[$field] = isset($validators[$field])
? array_merge((array) $validators[$field], $uvList)
: $uvList;
}
return $validators;
}
/**
* Runs validation $validator_id on values $values and returns error list.
*
* Return values:
* o array, keys - field path or formset id, values - array of errors
* when $isPostSource is true values is an empty array to allow for error list
* cleanup in HTML document
* o false - when no validators match name(s) given by $validator_id
*
* @param ConfigFile $cf Config file instance
* @param string|array $validatorId ID of validator(s) to run
* @param array $values Values to validate
* @param bool $isPostSource tells whether $values are directly from
* POST request
*
* @return bool|array
*/
public static function validate(
ConfigFile $cf,
$validatorId,
array &$values,
$isPostSource
) {
// find validators
$validatorId = (array) $validatorId;
$validators = static::getValidators($cf);
$vids = [];
foreach ($validatorId as &$vid) {
$vid = $cf->getCanonicalPath($vid);
if (! isset($validators[$vid])) {
continue;
}
$vids[] = $vid;
}
if (empty($vids)) {
return false;
}
// create argument list with canonical paths and remember path mapping
$arguments = [];
$keyMap = [];
foreach ($values as $k => $v) {
$k2 = $isPostSource ? str_replace('-', '/', $k) : $k;
$k2 = mb_strpos($k2, '/')
? $cf->getCanonicalPath($k2)
: $k2;
$keyMap[$k2] = $k;
$arguments[$k2] = $v;
}
// validate
$result = [];
foreach ($vids as $vid) {
// call appropriate validation functions
foreach ((array) $validators[$vid] as $validator) {
$vdef = (array) $validator;
$vname = array_shift($vdef);
$vname = 'PhpMyAdmin\Config\Validator::' . $vname;
$args = array_merge([$vid, &$arguments], $vdef);
$r = call_user_func_array($vname, $args);
// merge results
if (! is_array($r)) {
continue;
}
foreach ($r as $key => $errorList) {
// skip empty values if $isPostSource is false
if (! $isPostSource && empty($errorList)) {
continue;
}
if (! isset($result[$key])) {
$result[$key] = [];
}
$result[$key] = array_merge(
$result[$key],
(array) $errorList
);
}
}
}
// restore original paths
$newResult = [];
foreach ($result as $k => $v) {
$k2 = $keyMap[$k] ?? $k;
if (is_array($v)) {
$newResult[$k2] = array_map('htmlspecialchars', $v);
} else {
$newResult[$k2] = htmlspecialchars($v);
}
}
return empty($newResult) ? true : $newResult;
}
/**
* Test database connection
*
* @param string $host host name
* @param string $port tcp port to use
* @param string $socket socket to use
* @param string $user username to use
* @param string $pass password to use
* @param string $errorKey key to use in return array
*
* @return bool|array
*/
public static function testDBConnection(
$host,
$port,
$socket,
$user,
$pass = null,
$errorKey = 'Server'
) {
if ($GLOBALS['cfg']['DBG']['demo']) {
// Connection test disabled on the demo server!
return true;
}
$error = null;
$host = Core::sanitizeMySQLHost($host);
error_clear_last();
$socket = empty($socket) ? null : $socket;
$port = empty($port) ? null : $port;
mysqli_report(MYSQLI_REPORT_OFF);
$conn = @mysqli_connect($host, $user, (string) $pass, '', $port, (string) $socket);
if (! $conn) {
$error = __('Could not connect to the database server!');
} else {
mysqli_close($conn);
}
if ($error !== null) {
$lastError = error_get_last();
if ($lastError !== null) {
$error .= ' - ' . $lastError['message'];
}
}
return $error === null ? true : [$errorKey => $error];
}
/**
* Validate server config
*
* @param string $path path to config, not used
* keep this parameter since the method is invoked using
* reflection along with other similar methods
* @param array $values config values
*
* @return array
*/
public static function validateServer($path, array $values)
{
$result = [
'Server' => '',
'Servers/1/user' => '',
'Servers/1/SignonSession' => '',
'Servers/1/SignonURL' => '',
];
$error = false;
if (empty($values['Servers/1/auth_type'])) {
$values['Servers/1/auth_type'] = '';
$result['Servers/1/auth_type'] = __('Invalid authentication type!');
$error = true;
}
if ($values['Servers/1/auth_type'] === 'config'
&& empty($values['Servers/1/user'])
) {
$result['Servers/1/user'] = __(
'Empty username while using [kbd]config[/kbd] authentication method!'
);
$error = true;
}
if ($values['Servers/1/auth_type'] === 'signon'
&& empty($values['Servers/1/SignonSession'])
) {
$result['Servers/1/SignonSession'] = __(
'Empty signon session name '
. 'while using [kbd]signon[/kbd] authentication method!'
);
$error = true;
}
if ($values['Servers/1/auth_type'] === 'signon'
&& empty($values['Servers/1/SignonURL'])
) {
$result['Servers/1/SignonURL'] = __(
'Empty signon URL while using [kbd]signon[/kbd] authentication '
. 'method!'
);
$error = true;
}
if (! $error && $values['Servers/1/auth_type'] === 'config') {
$password = '';
if (! empty($values['Servers/1/password'])) {
$password = $values['Servers/1/password'];
}
$test = static::testDBConnection(
empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
empty($values['Servers/1/user']) ? '' : $values['Servers/1/user'],
$password,
'Server'
);
if ($test !== true) {
$result = array_merge($result, $test);
}
}
return $result;
}
/**
* Validate pmadb config
*
* @param string $path path to config, not used
* keep this parameter since the method is invoked using
* reflection along with other similar methods
* @param array $values config values
*
* @return array
*/
public static function validatePMAStorage($path, array $values)
{
$result = [
'Server_pmadb' => '',
'Servers/1/controluser' => '',
'Servers/1/controlpass' => '',
];
$error = false;
if (empty($values['Servers/1/pmadb'])) {
return $result;
}
$result = [];
if (empty($values['Servers/1/controluser'])) {
$result['Servers/1/controluser'] = __(
'Empty phpMyAdmin control user while using phpMyAdmin configuration '
. 'storage!'
);
$error = true;
}
if (empty($values['Servers/1/controlpass'])) {
$result['Servers/1/controlpass'] = __(
'Empty phpMyAdmin control user password while using phpMyAdmin '
. 'configuration storage!'
);
$error = true;
}
if (! $error) {
$test = static::testDBConnection(
empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
empty($values['Servers/1/controluser']) ? '' : $values['Servers/1/controluser'],
empty($values['Servers/1/controlpass']) ? '' : $values['Servers/1/controlpass'],
'Server_pmadb'
);
if ($test !== true) {
$result = array_merge($result, $test);
}
}
return $result;
}
/**
* Validates regular expression
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateRegex($path, array $values)
{
$result = [$path => ''];
if (empty($values[$path])) {
return $result;
}
error_clear_last();
$matches = [];
// in libraries/ListDatabase.php _checkHideDatabase(),
// a '/' is used as the delimiter for hide_db
@preg_match('/' . Util::requestString($values[$path]) . '/', '', $matches);
$currentError = error_get_last();
if ($currentError !== null) {
$error = preg_replace('/^preg_match\(\): /', '', $currentError['message']);
return [$path => $error];
}
return $result;
}
/**
* Validates TrustedProxies field
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateTrustedProxies($path, array $values)
{
$result = [$path => []];
if (empty($values[$path])) {
return $result;
}
if (is_array($values[$path]) || is_object($values[$path])) {
// value already processed by FormDisplay::save
$lines = [];
foreach ($values[$path] as $ip => $v) {
$v = Util::requestString($v);
$lines[] = preg_match('/^-\d+$/', $ip)
? $v
: $ip . ': ' . $v;
}
} else {
// AJAX validation
$lines = explode("\n", $values[$path]);
}
foreach ($lines as $line) {
$line = trim($line);
$matches = [];
// we catch anything that may (or may not) be an IP
if (! preg_match('/^(.+):(?:[ ]?)\\w+$/', $line, $matches)) {
$result[$path][] = __('Incorrect value:') . ' '
. htmlspecialchars($line);
continue;
}
// now let's check whether we really have an IP address
if (filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false
&& filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false
) {
$ip = htmlspecialchars(trim($matches[1]));
$result[$path][] = sprintf(__('Incorrect IP address: %s'), $ip);
continue;
}
}
return $result;
}
/**
* Tests integer value
*
* @param string $path path to config
* @param array $values config values
* @param bool $allowNegative allow negative values
* @param bool $allowZero allow zero
* @param int $maxValue max allowed value
* @param string $errorString error message string
*
* @return string empty string if test is successful
*/
public static function validateNumber(
$path,
array $values,
$allowNegative,
$allowZero,
$maxValue,
$errorString
) {
if (empty($values[$path])) {
return '';
}
$value = Util::requestString($values[$path]);
if (intval($value) != $value
|| (! $allowNegative && $value < 0)
|| (! $allowZero && $value == 0)
|| $value > $maxValue
) {
return $errorString;
}
return '';
}
/**
* Validates port number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validatePortNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
false,
65535,
__('Not a valid port number!')
),
];
}
/**
* Validates positive number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validatePositiveNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
false,
PHP_INT_MAX,
__('Not a positive number!')
),
];
}
/**
* Validates non-negative number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateNonNegativeNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
true,
PHP_INT_MAX,
__('Not a non-negative number!')
),
];
}
/**
* Validates value according to given regular expression
* Pattern and modifiers must be a valid for PCRE <b>and</b> JavaScript RegExp
*
* @param string $path path to config
* @param array $values config values
* @param string $regex regular expression to match
*
* @return array|string
*/
public static function validateByRegex($path, array $values, $regex)
{
if (! isset($values[$path])) {
return '';
}
$result = preg_match($regex, Util::requestString($values[$path]));
return [$path => $result ? '' : __('Incorrect value!')];
}
/**
* Validates upper bound for numeric inputs
*
* @param string $path path to config
* @param array $values config values
* @param int $maxValue maximal allowed value
*
* @return array
*/
public static function validateUpperBound($path, array $values, $maxValue)
{
$result = $values[$path] <= $maxValue;
return [
$path => $result ? '' : sprintf(
__('Value must be less than or equal to %s!'),
$maxValue
),
];
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* Used to render the console of PMA's pages
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use function count;
use function sprintf;
/**
* Class used to output the console
*/
class Console
{
/**
* Whether to display anything
*
* @access private
* @var bool
*/
private $isEnabled;
/**
* Whether we are servicing an ajax request.
*
* @access private
* @var bool
*/
private $isAjax;
/** @var Relation */
private $relation;
/** @var Template */
public $template;
/**
* Creates a new class instance
*/
public function __construct()
{
global $dbi;
$this->isEnabled = true;
$this->relation = new Relation($dbi);
$this->template = new Template();
}
/**
* Set the ajax flag to indicate whether
* we are servicing an ajax request
*
* @param bool $isAjax Whether we are servicing an ajax request
*/
public function setAjax(bool $isAjax): void
{
$this->isAjax = $isAjax;
}
/**
* Disables the rendering of the footer
*/
public function disable(): void
{
$this->isEnabled = false;
}
/**
* Renders the bookmark content
*
* @access public
*/
public static function getBookmarkContent(): string
{
global $dbi;
$template = new Template();
$cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']);
if ($cfgBookmark) {
$bookmarks = Bookmark::getList(
$dbi,
$GLOBALS['cfg']['Server']['user']
);
$count_bookmarks = count($bookmarks);
if ($count_bookmarks > 0) {
$welcomeMessage = sprintf(
_ngettext(
'Showing %1$d bookmark (both private and shared)',
'Showing %1$d bookmarks (both private and shared)',
$count_bookmarks
),
$count_bookmarks
);
} else {
$welcomeMessage = __('No bookmarks');
}
return $template->render('console/bookmark_content', [
'welcome_message' => $welcomeMessage,
'bookmarks' => $bookmarks,
]);
}
return '';
}
/**
* Returns the list of JS scripts required by console
*
* @return array list of scripts
*/
public function getScripts(): array
{
return ['console.js'];
}
/**
* Renders the console
*
* @access public
*/
public function getDisplay(): string
{
if (! $this->isAjax && $this->isEnabled) {
$cfgBookmark = Bookmark::getParams(
$GLOBALS['cfg']['Server']['user']
);
$image = Html\Generator::getImage('console', __('SQL Query Console'));
$_sql_history = $this->relation->getHistory(
$GLOBALS['cfg']['Server']['user']
);
$bookmarkContent = static::getBookmarkContent();
return $this->template->render('console/display', [
'cfg_bookmark' => $cfgBookmark,
'image' => $image,
'sql_history' => $_sql_history,
'bookmark_content' => $bookmarkContent,
]);
}
return '';
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function strlen;
abstract class AbstractController
{
/** @var Response */
protected $response;
/** @var Template */
protected $template;
/**
* @param Response $response
*/
public function __construct($response, Template $template)
{
$this->response = $response;
$this->template = $template;
}
/**
* @param array<string, mixed> $templateData
*/
protected function render(string $templatePath, array $templateData = []): void
{
$this->response->addHTML($this->template->render($templatePath, $templateData));
}
/**
* @param string[] $files
*/
protected function addScriptFiles(array $files): void
{
$header = $this->response->getHeader();
$scripts = $header->getScripts();
$scripts->addFiles($files);
}
protected function hasDatabase(): bool
{
global $db, $is_db, $errno, $dbi, $message;
if (isset($is_db) && $is_db) {
return true;
}
$is_db = false;
if (strlen($db) > 0) {
$is_db = $dbi->selectDb($db);
// This "Command out of sync" 2014 error may happen, for example
// after calling a MySQL procedure; at this point we can't select
// the db but it's not necessarily wrong
if ($dbi->getError() && $errno == 2014) {
$is_db = true;
unset($errno);
}
}
if (strlen($db) === 0 || ! $is_db) {
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON(
'message',
Message::error(__('No databases selected.'))
);
return false;
}
// Not a valid db name -> back to the welcome page
$params = ['reload' => '1'];
if (isset($message)) {
$params['message'] = $message;
}
$uri = './index.php?route=/' . Url::getCommonRaw($params, '&');
Core::sendHeaderLocation($uri);
return false;
}
return $is_db;
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\BrowseForeigners;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
/**
* Display selection for relational field values
*/
class BrowseForeignersController extends AbstractController
{
/** @var BrowseForeigners */
private $browseForeigners;
/** @var Relation */
private $relation;
/**
* @param Response $response
* @param BrowseForeigners $browseForeigners
* @param Relation $relation
*/
public function __construct($response, Template $template, $browseForeigners, $relation)
{
parent::__construct($response, $template);
$this->browseForeigners = $browseForeigners;
$this->relation = $relation;
}
public function index(): void
{
$params = [
'db' => $_POST['db'] ?? null,
'table' => $_POST['table'] ?? null,
'field' => $_POST['field'] ?? null,
'fieldkey' => $_POST['fieldkey'] ?? null,
'data' => $_POST['data'] ?? null,
'foreign_showAll' => $_POST['foreign_showAll'] ?? null,
'foreign_filter' => $_POST['foreign_filter'] ?? null,
];
if (! isset($params['db'], $params['table'], $params['field'])) {
return;
}
$this->response->getFooter()->setMinimal();
$header = $this->response->getHeader();
$header->disableMenuAndConsole();
$header->setBodyId('body_browse_foreigners');
$foreigners = $this->relation->getForeigners(
$params['db'],
$params['table']
);
$foreignLimit = $this->browseForeigners->getForeignLimit(
$params['foreign_showAll']
);
$foreignData = $this->relation->getForeignData(
$foreigners,
$params['field'],
true,
$params['foreign_filter'] ?? '',
$foreignLimit ?? null,
true
);
$this->response->addHTML($this->browseForeigners->getHtmlForRelationalFieldSelection(
$params['db'],
$params['table'],
$params['field'],
$foreignData,
$params['fieldkey'] ?? '',
$params['data'] ?? ''
));
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Simple script to set correct charset for changelog
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function array_keys;
use function file_get_contents;
use function htmlspecialchars;
use function is_readable;
use function ob_get_clean;
use function ob_start;
use function preg_replace;
use function printf;
use function readgzfile;
use function substr;
class ChangeLogController extends AbstractController
{
public function index(): void
{
$this->response->disable();
$this->response->getHeader()->sendHttpHeaders();
$filename = CHANGELOG_FILE;
/**
* Read changelog.
*/
// Check if the file is available, some distributions remove these.
if (! @is_readable($filename)) {
printf(
__(
'The %s file is not available on this system, please visit ' .
'%s for more information.'
),
$filename,
'<a href="https://www.phpmyadmin.net/">phpmyadmin.net</a>'
);
return;
}
// Test if the if is in a compressed format
if (substr($filename, -3) === '.gz') {
ob_start();
readgzfile($filename);
$changelog = ob_get_clean();
} else {
$changelog = file_get_contents($filename);
}
/**
* Whole changelog in variable.
*/
$changelog = htmlspecialchars((string) $changelog);
$github_url = 'https://github.com/phpmyadmin/phpmyadmin/';
$faq_url = 'https://docs.phpmyadmin.net/en/latest/faq.html';
$replaces = [
'@(https?://[./a-zA-Z0-9.-_-]*[/a-zA-Z0-9_])@'
=> '<a href="url.php?url=\\1">\\1</a>',
// mail address
'/([0-9]{4}-[0-9]{2}-[0-9]{2}) (.+[^ ]) +&lt;(.*@.*)&gt;/i'
=> '\\1 <a href="mailto:\\3">\\2</a>',
// FAQ entries
'/FAQ ([0-9]+)\.([0-9a-z]+)/i'
=> '<a href="url.php?url=' . $faq_url . '#faq\\1-\\2">FAQ \\1.\\2</a>',
// GitHub issues
'/issue\s*#?([0-9]{4,5}) /i'
=> '<a href="url.php?url=' . $github_url . 'issues/\\1">issue #\\1</a> ',
// CVE/CAN entries
'/((CAN|CVE)-[0-9]+-[0-9]+)/'
=> '<a href="url.php?url=https://cve.mitre.org/cgi-bin/cvename.cgi?name=\\1">\\1</a>',
// PMASAentries
'/(PMASA-[0-9]+-[0-9]+)/'
=> '<a href="url.php?url=https://www.phpmyadmin.net/security/\\1/">\\1</a>',
// Highlight releases (with links)
'/([0-9]+)\.([0-9]+)\.([0-9]+)\.0 (\([0-9-]+\))/'
=> '<a id="\\1_\\2_\\3"></a>'
. '<a href="url.php?url=' . $github_url . 'commits/RELEASE_\\1_\\2_\\3">'
. '\\1.\\2.\\3.0 \\4</a>',
'/([0-9]+)\.([0-9]+)\.([0-9]+)\.([1-9][0-9]*) (\([0-9-]+\))/'
=> '<a id="\\1_\\2_\\3_\\4"></a>'
. '<a href="url.php?url=' . $github_url . 'commits/RELEASE_\\1_\\2_\\3_\\4">'
. '\\1.\\2.\\3.\\4 \\5</a>',
// Highlight releases (not linkable)
'/( ### )(.*)/' => '\\1<b>\\2</b>',
// Links target and rel
'/a href="/' => 'a target="_blank" rel="noopener noreferrer" href="',
];
$this->response->header('Content-type: text/html; charset=utf-8');
echo $this->template->render('changelog', [
'changelog' => preg_replace(array_keys($replaces), $replaces, $changelog),
]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* Displays status of phpMyAdmin configuration storage
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
class CheckRelationsController extends AbstractController
{
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Relation $relation)
{
parent::__construct($response, $template);
$this->relation = $relation;
}
public function index(): void
{
global $db;
$params = [
'create_pmadb' => $_POST['create_pmadb'] ?? null,
'fixall_pmadb' => $_POST['fixall_pmadb'] ?? null,
'fix_pmadb' => $_POST['fix_pmadb'] ?? null,
];
// If request for creating the pmadb
if (isset($params['create_pmadb']) && $this->relation->createPmaDatabase()) {
$this->relation->fixPmaTables('phpmyadmin');
}
// If request for creating all PMA tables.
if (isset($params['fixall_pmadb'])) {
$this->relation->fixPmaTables($db);
}
$cfgRelation = $this->relation->getRelationsParam();
// If request for creating missing PMA tables.
if (isset($params['fix_pmadb'])) {
$this->relation->fixPmaTables($cfgRelation['db']);
}
$this->response->addHTML($this->relation->getRelationsParamDiagnostic($cfgRelation));
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
final class ColumnController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $dbi)
{
parent::__construct($response, $template);
$this->dbi = $dbi;
}
public function all(): void
{
if (! isset($_POST['db'], $_POST['table'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$this->response->addJSON(['columns' => $this->dbi->getColumnNames($_POST['db'], $_POST['table'])]);
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Config;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function json_decode;
final class ConfigController extends AbstractController
{
/** @var Config */
private $config;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Config $config)
{
parent::__construct($response, $template);
$this->config = $config;
}
public function get(): void
{
if (! isset($_POST['key'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$this->response->addJSON(['value' => $this->config->get($_POST['key'])]);
}
public function set(): void
{
if (! isset($_POST['key'], $_POST['value'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$result = $this->config->setUserValue(null, $_POST['key'], json_decode($_POST['value']));
if ($result === true) {
return;
}
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => $result]);
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Controllers\AbstractController as Controller;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
abstract class AbstractController extends Controller
{
/** @var string */
protected $db;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct($response, Template $template, $db)
{
parent::__construct($response, $template);
$this->db = $db;
}
}

View file

@ -0,0 +1,302 @@
<?php
/**
* Central Columns view/edit
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Core;
use PhpMyAdmin\Database\CentralColumns;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function is_bool;
use function parse_str;
use function sprintf;
class CentralColumnsController extends AbstractController
{
/** @var CentralColumns */
private $centralColumns;
/**
* @param Response $response
* @param string $db Database name
* @param CentralColumns $centralColumns
*/
public function __construct($response, Template $template, $db, $centralColumns)
{
parent::__construct($response, $template, $db);
$this->centralColumns = $centralColumns;
}
public function index(): void
{
global $cfg, $db, $message, $pos, $num_cols;
if (isset($_POST['edit_save'])) {
echo $this->editSave([
'col_name' => $_POST['col_name'] ?? null,
'orig_col_name' => $_POST['orig_col_name'] ?? null,
'col_default' => $_POST['col_default'] ?? null,
'col_default_sel' => $_POST['col_default_sel'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
'col_isNull' => $_POST['col_isNull'] ?? null,
'col_length' => $_POST['col_length'] ?? null,
'col_attribute' => $_POST['col_attribute'] ?? null,
'col_type' => $_POST['col_type'] ?? null,
'collation' => $_POST['collation'] ?? null,
]);
return;
}
if (isset($_POST['add_new_column'])) {
$tmp_msg = $this->addNewColumn([
'col_name' => $_POST['col_name'] ?? null,
'col_default' => $_POST['col_default'] ?? null,
'col_default_sel' => $_POST['col_default_sel'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
'col_isNull' => $_POST['col_isNull'] ?? null,
'col_length' => $_POST['col_length'] ?? null,
'col_attribute' => $_POST['col_attribute'] ?? null,
'col_type' => $_POST['col_type'] ?? null,
'collation' => $_POST['collation'] ?? null,
]);
}
if (isset($_POST['getColumnList'])) {
$this->response->addJSON('message', $this->getColumnList([
'cur_table' => $_POST['cur_table'] ?? null,
]));
return;
}
if (isset($_POST['add_column'])) {
$tmp_msg = $this->addColumn([
'table-select' => $_POST['table-select'] ?? null,
'column-select' => $_POST['column-select'] ?? null,
]);
}
$this->addScriptFiles([
'vendor/jquery/jquery.uitablefilter.js',
'vendor/jquery/jquery.tablesorter.js',
'database/central_columns.js',
]);
if (isset($_POST['edit_central_columns_page'])) {
$this->editPage([
'selected_fld' => $_POST['selected_fld'] ?? null,
'db' => $_POST['db'] ?? null,
]);
return;
}
if (isset($_POST['multi_edit_central_column_save'])) {
$message = $this->updateMultipleColumn([
'db' => $_POST['db'] ?? null,
'orig_col_name' => $_POST['orig_col_name'] ?? null,
'field_name' => $_POST['field_name'] ?? null,
'field_default_type' => $_POST['field_default_type'] ?? null,
'field_default_value' => $_POST['field_default_value'] ?? null,
'field_length' => $_POST['field_length'] ?? null,
'field_attribute' => $_POST['field_attribute'] ?? null,
'field_type' => $_POST['field_type'] ?? null,
'field_collation' => $_POST['field_collation'] ?? null,
'field_null' => $_POST['field_null'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
]);
if (! is_bool($message)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
}
}
if (isset($_POST['delete_save'])) {
$tmp_msg = $this->deleteSave([
'db' => $_POST['db'] ?? null,
'col_name' => $_POST['col_name'] ?? null,
]);
}
$this->main([
'pos' => $_POST['pos'] ?? null,
'total_rows' => $_POST['total_rows'] ?? null,
]);
$pos = 0;
if (Core::isValid($_POST['pos'], 'integer')) {
$pos = (int) $_POST['pos'];
}
$num_cols = $this->centralColumns->getColumnsCount(
$db,
$pos,
(int) $cfg['MaxRows']
);
$message = Message::success(
sprintf(__('Showing rows %1$s - %2$s.'), $pos + 1, $pos + $num_cols)
);
if (! isset($tmp_msg) || $tmp_msg === true) {
return;
}
$message = $tmp_msg;
}
/**
* @param array $params Request parameters
*/
public function main(array $params): void
{
global $text_dir, $PMA_Theme;
if (! empty($params['total_rows'])
&& Core::isValid($params['total_rows'], 'integer')
) {
$totalRows = (int) $params['total_rows'];
} else {
$totalRows = $this->centralColumns->getCount($this->db);
}
$pos = 0;
if (Core::isValid($params['pos'], 'integer')) {
$pos = (int) $params['pos'];
}
$variables = $this->centralColumns->getTemplateVariablesForMain(
$this->db,
$totalRows,
$pos,
$PMA_Theme->getImgPath(),
$text_dir
);
$this->render('database/central_columns/main', $variables);
}
/**
* @param array $params Request parameters
*
* @return array JSON
*/
public function getColumnList(array $params): array
{
return $this->centralColumns->getListRaw(
$this->db,
$params['cur_table'] ?? ''
);
}
public function populateColumns(): void
{
$columns = $this->centralColumns->getColumnsNotInCentralList($this->db, $_POST['selectedTable']);
$this->render('database/central_columns/populate_columns', ['columns' => $columns]);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function editSave(array $params)
{
$columnDefault = $params['col_default'];
if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') {
$columnDefault = '';
}
return $this->centralColumns->updateOneColumn(
$this->db,
$params['orig_col_name'],
$params['col_name'],
$params['col_type'],
$params['col_attribute'],
$params['col_length'],
isset($params['col_isNull']) ? 1 : 0,
$params['collation'],
$params['col_extra'] ?? '',
$columnDefault
);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function addNewColumn(array $params)
{
$columnDefault = $params['col_default'];
if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') {
$columnDefault = '';
}
return $this->centralColumns->updateOneColumn(
$this->db,
'',
$params['col_name'],
$params['col_type'],
$params['col_attribute'],
$params['col_length'],
isset($params['col_isNull']) ? 1 : 0,
$params['collation'],
$params['col_extra'] ?? '',
$columnDefault
);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function addColumn(array $params)
{
return $this->centralColumns->syncUniqueColumns(
[$params['column-select']],
false,
$params['table-select']
);
}
/**
* @param array $params Request parameters
*/
public function editPage(array $params): void
{
$rows = $this->centralColumns->getHtmlForEditingPage(
$params['selected_fld'],
$params['db']
);
$this->render('database/central_columns/edit', ['rows' => $rows]);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function updateMultipleColumn(array $params)
{
return $this->centralColumns->updateMultipleColumn($params);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function deleteSave(array $params)
{
$name = [];
parse_str($params['col_name'], $name);
return $this->centralColumns->deleteColumnsFromList(
$params['db'],
$name['selected_fld'],
false
);
}
}

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Index;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Util;
use function is_array;
use function str_replace;
class DataDictionaryController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var Transformations */
private $transformations;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param Relation $relation
* @param Transformations $transformations
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $relation, $transformations, $dbi)
{
parent::__construct($response, $template, $db);
$this->relation = $relation;
$this->transformations = $transformations;
$this->dbi = $dbi;
}
public function index(): void
{
Util::checkParameters(['db'], true);
$header = $this->response->getHeader();
$header->enablePrintView();
$cfgRelation = $this->relation->getRelationsParam();
$comment = $this->relation->getDbComment($this->db);
$this->dbi->selectDb($this->db);
$tablesNames = $this->dbi->getTables($this->db);
$tables = [];
foreach ($tablesNames as $tableName) {
$showComment = (string) $this->dbi->getTable(
$this->db,
$tableName
)->getStatusInfo('TABLE_COMMENT');
[, $primaryKeys] = Util::processIndexData(
$this->dbi->getTableIndexes($this->db, $tableName)
);
[$foreigners, $hasRelation] = $this->relation->getRelationsAndStatus(
! empty($cfgRelation['relation']),
$this->db,
$tableName
);
$columnsComments = $this->relation->getComments($this->db, $tableName);
$columns = $this->dbi->getColumns($this->db, $tableName);
$rows = [];
foreach ($columns as $row) {
$extractedColumnSpec = Util::extractColumnSpec($row['Type']);
$relation = '';
if ($hasRelation) {
$foreigner = $this->relation->searchColumnInForeigners(
$foreigners,
$row['Field']
);
if (is_array($foreigner) && isset($foreigner['foreign_table'], $foreigner['foreign_field'])) {
$relation = $foreigner['foreign_table'];
$relation .= ' -> ';
$relation .= $foreigner['foreign_field'];
}
}
$mime = '';
if ($cfgRelation['mimework']) {
$mimeMap = $this->transformations->getMime(
$this->db,
$tableName,
true
);
if (is_array($mimeMap) && isset($mimeMap[$row['Field']]['mimetype'])) {
$mime = str_replace(
'_',
'/',
$mimeMap[$row['Field']]['mimetype']
);
}
}
$rows[$row['Field']] = [
'name' => $row['Field'],
'has_primary_key' => isset($primaryKeys[$row['Field']]),
'type' => $extractedColumnSpec['type'],
'print_type' => $extractedColumnSpec['print_type'],
'is_nullable' => $row['Null'] !== '' && $row['Null'] !== 'NO',
'default' => $row['Default'] ?? null,
'comment' => $columnsComments[$row['Field']] ?? '',
'mime' => $mime,
'relation' => $relation,
];
}
$tables[$tableName] = [
'name' => $tableName,
'comment' => $showComment,
'has_relation' => $hasRelation,
'has_mime' => $cfgRelation['mimework'],
'columns' => $rows,
'indexes' => Index::getFromTable($tableName, $this->db),
];
}
$this->render('database/data_dictionary/index', [
'database' => $this->db,
'comment' => $comment,
'tables' => $tables,
]);
}
}

View file

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Designer;
use PhpMyAdmin\Database\Designer\Common as DesignerCommon;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function htmlspecialchars;
use function in_array;
use function sprintf;
class DesignerController extends AbstractController
{
/** @var Designer */
private $databaseDesigner;
/** @var DesignerCommon */
private $designerCommon;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct(
$response,
Template $template,
$db,
Designer $databaseDesigner,
DesignerCommon $designerCommon
) {
parent::__construct($response, $template, $db);
$this->databaseDesigner = $databaseDesigner;
$this->designerCommon = $designerCommon;
}
public function index(): void
{
global $db, $script_display_field, $tab_column, $tables_all_keys, $tables_pk_or_unique_keys;
global $success, $page, $message, $display_page, $selected_page, $tab_pos, $fullTableNames, $script_tables;
global $script_contr, $params, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos, $classes_side_menu, $cfg, $err_url;
if (isset($_POST['dialog'])) {
if ($_POST['dialog'] === 'edit') {
$html = $this->databaseDesigner->getHtmlForEditOrDeletePages($_POST['db'], 'editPage');
} elseif ($_POST['dialog'] === 'delete') {
$html = $this->databaseDesigner->getHtmlForEditOrDeletePages($_POST['db'], 'deletePage');
} elseif ($_POST['dialog'] === 'save_as') {
$html = $this->databaseDesigner->getHtmlForPageSaveAs($_POST['db']);
} elseif ($_POST['dialog'] === 'export') {
$html = $this->databaseDesigner->getHtmlForSchemaExport(
$_POST['db'],
$_POST['selected_page']
);
} elseif ($_POST['dialog'] === 'add_table') {
// Pass the db and table to the getTablesInfo so we only have the table we asked for
$script_display_field = $this->designerCommon->getTablesInfo($_POST['db'], $_POST['table']);
$tab_column = $this->designerCommon->getColumnsInfo($script_display_field);
$tables_all_keys = $this->designerCommon->getAllKeys($script_display_field);
$tables_pk_or_unique_keys = $this->designerCommon->getPkOrUniqueKeys($script_display_field);
$html = $this->databaseDesigner->getDatabaseTables(
$_POST['db'],
$script_display_field,
[],
-1,
$tab_column,
$tables_all_keys,
$tables_pk_or_unique_keys
);
}
if (! empty($html)) {
$this->response->addHTML($html);
}
return;
}
if (isset($_POST['operation'])) {
if ($_POST['operation'] === 'deletePage') {
$success = $this->designerCommon->deletePage($_POST['selected_page']);
$this->response->setRequestStatus($success);
} elseif ($_POST['operation'] === 'savePage') {
if ($_POST['save_page'] === 'same') {
$page = $_POST['selected_page'];
} elseif ($this->designerCommon->getPageExists($_POST['selected_value'])) {
$this->response->addJSON(
'message',
/* l10n: The user tries to save a page with an existing name in Designer */
__(
sprintf(
'There already exists a page named "%s" please rename it to something else.',
htmlspecialchars($_POST['selected_value'])
)
)
);
$this->response->setRequestStatus(false);
return;
} else {
$page = $this->designerCommon->createNewPage($_POST['selected_value'], $_POST['db']);
$this->response->addJSON('id', $page);
}
$success = $this->designerCommon->saveTablePositions($page);
$this->response->setRequestStatus($success);
} elseif ($_POST['operation'] === 'setDisplayField') {
[
$success,
$message,
] = $this->designerCommon->saveDisplayField(
$_POST['db'],
$_POST['table'],
$_POST['field']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'addNewRelation') {
[$success, $message] = $this->designerCommon->addNewRelation(
$_POST['db'],
$_POST['T1'],
$_POST['F1'],
$_POST['T2'],
$_POST['F2'],
$_POST['on_delete'],
$_POST['on_update'],
$_POST['DB1'],
$_POST['DB2']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'removeRelation') {
[$success, $message] = $this->designerCommon->removeRelation(
$_POST['T1'],
$_POST['F1'],
$_POST['T2'],
$_POST['F2']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'save_setting_value') {
$success = $this->designerCommon->saveSetting($_POST['index'], $_POST['value']);
$this->response->setRequestStatus($success);
}
return;
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$script_display_field = $this->designerCommon->getTablesInfo();
$display_page = -1;
$selected_page = null;
$visualBuilderMode = isset($_GET['query']);
if ($visualBuilderMode) {
$display_page = $this->designerCommon->getDefaultPage($_GET['db']);
} elseif (! empty($_GET['page'])) {
$display_page = $_GET['page'];
} else {
$display_page = $this->designerCommon->getLoadingPage($_GET['db']);
}
if ($display_page != -1) {
$selected_page = $this->designerCommon->getPageName($display_page);
}
$tab_pos = $this->designerCommon->getTablePositions($display_page);
$fullTableNames = [];
foreach ($script_display_field as $designerTable) {
$fullTableNames[] = $designerTable->getDbTableString();
}
foreach ($tab_pos as $position) {
if (in_array($position['dbName'] . '.' . $position['tableName'], $fullTableNames)) {
continue;
}
$designerTables = $this->designerCommon->getTablesInfo($position['dbName'], $position['tableName']);
foreach ($designerTables as $designerTable) {
$script_display_field[] = $designerTable;
}
}
$tab_column = $this->designerCommon->getColumnsInfo($script_display_field);
$script_tables = $this->designerCommon->getScriptTabs($script_display_field);
$tables_pk_or_unique_keys = $this->designerCommon->getPkOrUniqueKeys($script_display_field);
$tables_all_keys = $this->designerCommon->getAllKeys($script_display_field);
$classes_side_menu = $this->databaseDesigner->returnClassNamesFromMenuButtons();
$script_contr = $this->designerCommon->getScriptContr($script_display_field);
$params = ['lang' => $GLOBALS['lang']];
if (isset($_GET['db'])) {
$params['db'] = $_GET['db'];
}
$this->response->getFooter()->setMinimal();
$header = $this->response->getHeader();
$header->setBodyId('designer_body');
$this->addScriptFiles([
'vendor/jquery/jquery.fullscreen.js',
'designer/database.js',
'designer/objects.js',
'designer/page.js',
'designer/history.js',
'designer/move.js',
'designer/init.js',
]);
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
// Embed some data into HTML, later it will be read
// by designer/init.js and converted to JS variables.
$this->response->addHTML(
$this->databaseDesigner->getHtmlForMain(
$db,
$_GET['db'],
$script_display_field,
$script_tables,
$script_contr,
$script_display_field,
$display_page,
$visualBuilderMode,
$selected_page,
$classes_side_menu,
$tab_pos,
$tab_column,
$tables_all_keys,
$tables_pk_or_unique_keys
)
);
$this->response->addHTML('<div id="PMA_disable_floating_menubar"></div>');
}
}

View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Events;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function strlen;
final class EventsController extends AbstractController
{
/** @var Events */
private $events;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Events $events, $dbi)
{
parent::__construct($response, $template, $db);
$this->events = $events;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $tables, $num_tables, $total_num_tables, $sub_part, $errors, $text_dir, $PMA_Theme;
global $tooltip_truename, $tooltip_aliasname, $pos, $cfg, $err_url;
if (! $this->response->isAjax()) {
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$this->events->handleEditor();
$this->events->export();
$items = $this->dbi->getEvents($db);
$this->render('database/events/index', [
'db' => $db,
'items' => $items,
'select_all_arrow_src' => $PMA_Theme->getImgPath() . 'arrow_' . $text_dir . '.png',
'has_privilege' => Util::currentUserHasPrivilege('EVENT', $db),
'scheduler_state' => $this->events->getEventSchedulerStatus(),
'text_dir' => $text_dir,
'theme_image_path' => $PMA_Theme->getImgPath(),
'is_ajax' => $this->response->isAjax() && empty($_REQUEST['ajax_page_request']),
]);
}
}

View file

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Export;
use PhpMyAdmin\Export\Options;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function array_merge;
use function is_array;
final class ExportController extends AbstractController
{
/** @var Export */
private $export;
/** @var Options */
private $exportOptions;
/**
* @param Response $response
* @param string $db Database name.
*/
public function __construct($response, Template $template, $db, Export $export, Options $exportOptions)
{
parent::__construct($response, $template, $db);
$this->export = $export;
$this->exportOptions = $exportOptions;
}
public function index(): void
{
global $db, $table, $sub_part, $url_params, $sql_query;
global $tables, $num_tables, $total_num_tables, $tooltip_truename;
global $tooltip_aliasname, $pos, $table_select, $unlim_num_rows, $cfg, $err_url;
$pageSettings = new PageSettings('Export');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['export.js']);
// $sub_part is used in Util::getDbInfo() to see if we are coming from
// /database/export, in which case we don't obey $cfg['MaxTableList']
$sub_part = '_export';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/export');
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
// exit if no tables in db found
if ($num_tables < 1) {
$this->response->addHTML(
Message::error(__('No tables found in database.'))->getDisplay()
);
return;
}
if (! empty($_POST['selected_tbl']) && empty($table_select)) {
$table_select = $_POST['selected_tbl'];
}
$tablesForMultiValues = [];
foreach ($tables as $each_table) {
if (isset($_POST['table_select']) && is_array($_POST['table_select'])) {
$is_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_select']
);
} elseif (isset($table_select)) {
$is_checked = $this->export->getCheckedClause(
$each_table['Name'],
$table_select
);
} else {
$is_checked = true;
}
if (isset($_POST['table_structure']) && is_array($_POST['table_structure'])) {
$structure_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_structure']
);
} else {
$structure_checked = $is_checked;
}
if (isset($_POST['table_data']) && is_array($_POST['table_data'])) {
$data_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_data']
);
} else {
$data_checked = $is_checked;
}
$tablesForMultiValues[] = [
'name' => $each_table['Name'],
'is_checked_select' => $is_checked,
'is_checked_structure' => $structure_checked,
'is_checked_data' => $data_checked,
];
}
if (! isset($sql_query)) {
$sql_query = '';
}
if (! isset($unlim_num_rows)) {
$unlim_num_rows = 0;
}
$isReturnBackFromRawExport = isset($_POST['export_type']) && $_POST['export_type'] === 'raw';
if (isset($_POST['raw_query']) || $isReturnBackFromRawExport) {
$export_type = 'raw';
} else {
$export_type = 'database';
}
$GLOBALS['single_table'] = $_POST['single_table'] ?? $_GET['single_table'] ?? $GLOBALS['single_table'] ?? null;
$exportList = Plugins::getExport($export_type, isset($GLOBALS['single_table']));
if (empty($exportList)) {
$this->response->addHTML(Message::error(
__('Could not load export plugins, please check your installation!')
)->getDisplay());
return;
}
$options = $this->exportOptions->getOptions(
$export_type,
$db,
$table,
$sql_query,
$num_tables,
$unlim_num_rows,
$exportList
);
$this->render('database/export/index', array_merge($options, [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'structure_or_data_forced' => $_POST['structure_or_data_forced'] ?? 0,
'tables' => $tablesForMultiValues,
]));
}
public function tables(): void
{
if (empty($_POST['selected_tbl'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
$this->index();
}
}

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\Import;
use PhpMyAdmin\Import\Ajax;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function intval;
final class ImportController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $max_upload_size, $table, $tables, $num_tables, $total_num_tables, $cfg;
global $tooltip_truename, $tooltip_aliasname, $pos, $sub_part, $SESSION_KEY, $PMA_Theme, $err_url;
$pageSettings = new PageSettings('Import');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['import.js']);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
[$SESSION_KEY, $uploadId] = Ajax::uploadProgressSetup();
$importList = Plugins::getImport('database');
if (empty($importList)) {
$this->response->addHTML(Message::error(__(
'Could not load import plugins, please check your installation!'
))->getDisplay());
return;
}
$offset = null;
if (Core::isValid($_REQUEST['offset'], 'numeric')) {
$offset = intval($_REQUEST['offset']);
}
$timeoutPassed = $_REQUEST['timeout_passed'] ?? null;
$localImportFile = $_REQUEST['local_import_file'] ?? null;
$compressions = Import::getCompressions();
$allCharsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$charsets = [];
/** @var Charset $charset */
foreach ($allCharsets as $charset) {
$charsets[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
];
}
$idKey = $_SESSION[$SESSION_KEY]['handler']::getIdKey();
$hiddenInputs = [
$idKey => $uploadId,
'import_type' => 'database',
'db' => $db,
];
$this->render('database/import/index', [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'upload_id' => $uploadId,
'handler' => $_SESSION[$SESSION_KEY]['handler'],
'theme_image_path' => $PMA_Theme->getImgPath(),
'hidden_inputs' => $hiddenInputs,
'db' => $db,
'table' => $table,
'max_upload_size' => $max_upload_size,
'import_list' => $importList,
'local_import_file' => $localImportFile,
'is_upload' => $GLOBALS['is_upload'],
'upload_dir' => $cfg['UploadDir'] ?? null,
'timeout_passed_global' => $GLOBALS['timeout_passed'] ?? null,
'compressions' => $compressions,
'is_encoding_supported' => Encoding::isSupported(),
'encodings' => Encoding::listEncodings(),
'import_charset' => $cfg['Import']['charset'] ?? null,
'timeout_passed' => $timeoutPassed,
'offset' => $offset,
'can_convert_kanji' => Encoding::canConvertKanji(),
'charsets' => $charsets,
'is_foreign_key_check' => Util::isForeignKeyCheck(),
'user_upload_dir' => Util::userDir($cfg['UploadDir'] ?? ''),
'local_files' => Import::getLocalFiles($importList),
]);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\MultiTableQuery;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
/**
* Handles database multi-table querying
*/
class MultiTableQueryController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
$this->addScriptFiles([
'vendor/jquery/jquery.md5.js',
'database/multi_table_query.js',
'database/query_generator.js',
]);
$queryInstance = new MultiTableQuery($this->dbi, $this->template, $this->db);
$this->response->addHTML($queryInstance->getFormHtml());
}
public function displayResults(): void
{
global $PMA_Theme;
$params = [
'sql_query' => $_POST['sql_query'],
'db' => $_POST['db'] ?? $_GET['db'] ?? null,
];
$this->response->addHTML(MultiTableQuery::displayResults(
$params['sql_query'],
$params['db'],
$PMA_Theme->getImgPath()
));
}
public function table(): void
{
$params = [
'tables' => $_GET['tables'],
'db' => $_GET['db'] ?? null,
];
$constrains = $this->dbi->getForeignKeyConstrains(
$params['db'],
$params['tables']
);
$this->response->addJSON(['foreignKeyConstrains' => $constrains]);
}
}

View file

@ -0,0 +1,419 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Operations;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\Export\ExportSql;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\Relation;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function mb_strtolower;
use function strlen;
/**
* Handles miscellaneous database operations.
*/
class OperationsController extends AbstractController
{
/** @var Operations */
private $operations;
/** @var CheckUserPrivileges */
private $checkUserPrivileges;
/** @var Relation */
private $relation;
/** @var RelationCleanup */
private $relationCleanup;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct(
$response,
Template $template,
$db,
Operations $operations,
CheckUserPrivileges $checkUserPrivileges,
Relation $relation,
RelationCleanup $relationCleanup,
$dbi
) {
parent::__construct($response, $template, $db);
$this->operations = $operations;
$this->checkUserPrivileges = $checkUserPrivileges;
$this->relation = $relation;
$this->relationCleanup = $relationCleanup;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $server, $sql_query, $move, $message, $tables_full, $err_url;
global $export_sql_plugin, $views, $sqlConstratints, $local_query, $reload, $url_params, $tables;
global $total_num_tables, $sub_part, $tooltip_truename;
global $db_collation, $tooltip_aliasname, $pos, $is_information_schema, $single_table, $num_tables;
$this->checkUserPrivileges->getPrivileges();
$this->addScriptFiles(['database/operations.js']);
$sql_query = '';
/**
* Rename/move or copy database
*/
if (strlen($db) > 0
&& (! empty($_POST['db_rename']) || ! empty($_POST['db_copy']))
) {
if (! empty($_POST['db_rename'])) {
$move = true;
} else {
$move = false;
}
if (! isset($_POST['newname']) || strlen($_POST['newname']) === 0) {
$message = Message::error(__('The database name is empty!'));
} else {
// lower_case_table_names=1 `DB` becomes `db`
if ($this->dbi->getLowerCaseNames() === '1') {
$_POST['newname'] = mb_strtolower(
$_POST['newname']
);
}
if ($_POST['newname'] === $_REQUEST['db']) {
$message = Message::error(
__('Cannot copy database to the same name. Change the name and try again.')
);
} else {
$_error = false;
if ($move || ! empty($_POST['create_database_before_copying'])) {
$this->operations->createDbBeforeCopy();
}
// here I don't use DELIMITER because it's not part of the
// language; I have to send each statement one by one
// to avoid selecting alternatively the current and new db
// we would need to modify the CREATE definitions to qualify
// the db name
$this->operations->runProcedureAndFunctionDefinitions($db);
// go back to current db, just in case
$this->dbi->selectDb($db);
$tables_full = $this->dbi->getTablesFull($db);
// remove all foreign key constraints, otherwise we can get errors
/** @var ExportSql $export_sql_plugin */
$export_sql_plugin = Plugins::getPlugin(
'export',
'sql',
'libraries/classes/Plugins/Export/',
[
'single_table' => isset($single_table),
'export_type' => 'database',
]
);
// create stand-in tables for views
$views = $this->operations->getViewsAndCreateSqlViewStandIn(
$tables_full,
$export_sql_plugin,
$db
);
// copy tables
$sqlConstratints = $this->operations->copyTables(
$tables_full,
$move,
$db
);
// handle the views
if (! $_error) {
$this->operations->handleTheViews($views, $move, $db);
}
unset($views);
// now that all tables exist, create all the accumulated constraints
if (! $_error && count($sqlConstratints) > 0) {
$this->operations->createAllAccumulatedConstraints($sqlConstratints);
}
unset($sqlConstratints);
if ($this->dbi->getVersion() >= 50100) {
// here DELIMITER is not used because it's not part of the
// language; each statement is sent one by one
$this->operations->runEventDefinitionsForDb($db);
}
// go back to current db, just in case
$this->dbi->selectDb($db);
// Duplicate the bookmarks for this db (done once for each db)
$this->operations->duplicateBookmarks($_error, $db);
if (! $_error && $move) {
if (isset($_POST['adjust_privileges'])
&& ! empty($_POST['adjust_privileges'])
) {
$this->operations->adjustPrivilegesMoveDb($db, $_POST['newname']);
}
/**
* cleanup pmadb stuff for this db
*/
$this->relationCleanup->database($db);
// if someday the RENAME DATABASE reappears, do not DROP
$local_query = 'DROP DATABASE '
. Util::backquote($db) . ';';
$sql_query .= "\n" . $local_query;
$this->dbi->query($local_query);
$message = Message::success(
__('Database %1$s has been renamed to %2$s.')
);
$message->addParam($db);
$message->addParam($_POST['newname']);
} elseif (! $_error) {
if (isset($_POST['adjust_privileges'])
&& ! empty($_POST['adjust_privileges'])
) {
$this->operations->adjustPrivilegesCopyDb($db, $_POST['newname']);
}
$message = Message::success(
__('Database %1$s has been copied to %2$s.')
);
$message->addParam($db);
$message->addParam($_POST['newname']);
} else {
$message = Message::error();
}
$reload = true;
/* Change database to be used */
if (! $_error && $move) {
$db = $_POST['newname'];
} elseif (! $_error) {
if (isset($_POST['switch_to_new'])
&& $_POST['switch_to_new'] === 'true'
) {
$_SESSION['pma_switch_to_new'] = true;
$db = $_POST['newname'];
} else {
$_SESSION['pma_switch_to_new'] = false;
}
}
}
}
/**
* Database has been successfully renamed/moved. If in an Ajax request,
* generate the output with {@link Response} and exit
*/
if ($this->response->isAjax()) {
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('newname', $_POST['newname']);
$this->response->addJSON(
'sql_query',
Generator::getMessage('', $sql_query)
);
$this->response->addJSON('db', $db);
return;
}
}
/**
* Settings for relations stuff
*/
$cfgRelation = $this->relation->getRelationsParam();
/**
* Check if comments were updated
* (must be done before displaying the menu tabs)
*/
if (isset($_POST['comment'])) {
$this->relation->setDbComment($db, $_POST['comment']);
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/operations');
// Gets the database structure
$sub_part = '_structure';
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,
$isSystemSchema,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
$oldMessage = '';
if (isset($message)) {
$oldMessage = Generator::getMessage($message, $sql_query);
unset($message);
}
$db_collation = $this->dbi->getDbCollation($db);
$is_information_schema = Utilities::isSystemSchema($db);
if ($is_information_schema) {
return;
}
$databaseComment = '';
if ($cfgRelation['commwork']) {
$databaseComment = $this->relation->getDbComment($db);
}
$hasAdjustPrivileges = $GLOBALS['db_priv'] && $GLOBALS['table_priv']
&& $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] && $GLOBALS['is_reload_priv'];
$isDropDatabaseAllowed = ($this->dbi->isSuperUser() || $cfg['AllowUserDropDatabase'])
&& ! $isSystemSchema && $db !== 'mysql';
$switchToNew = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new'];
$charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
if (! $cfgRelation['allworks']
&& $cfg['PmaNoRelation_DisableWarning'] == false
) {
$message = Message::notice(
__(
'The phpMyAdmin configuration storage has been deactivated. ' .
'%sFind out why%s.'
)
);
$message->addParamHtml(
'<a href="' . Url::getFromRoute('/check-relations')
. '" data-post="' . Url::getCommon(['db' => $db]) . '">'
);
$message->addParamHtml('</a>');
/* Show error if user has configured something, notice elsewhere */
if (! empty($cfg['Servers'][$server]['pmadb'])) {
$message->isError(true);
}
}
$this->render('database/operations/index', [
'message' => $oldMessage,
'db' => $db,
'has_comment' => $cfgRelation['commwork'],
'db_comment' => $databaseComment,
'db_collation' => $db_collation,
'has_adjust_privileges' => $hasAdjustPrivileges,
'is_drop_database_allowed' => $isDropDatabaseAllowed,
'switch_to_new' => $switchToNew,
'charsets' => $charsets,
'collations' => $collations,
]);
}
public function collation(): void
{
global $db, $cfg, $err_url;
if (! $this->response->isAjax()) {
return;
}
if (empty($_POST['db_collation'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No collation provided.')));
return;
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$sql_query = 'ALTER DATABASE ' . Util::backquote($db)
. ' DEFAULT' . Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
$this->dbi->query($sql_query);
$message = Message::success();
/**
* Changes tables charset if requested by the user
*/
if (isset($_POST['change_all_tables_collations']) &&
$_POST['change_all_tables_collations'] === 'on'
) {
[$tables] = Util::getDbInfo($db, null);
foreach ($tables as $tableName => $data) {
if ($this->dbi->getTable($db, $tableName)->isView()) {
// Skip views, we can not change the collation of a view.
// issue #15283
continue;
}
$sql_query = 'ALTER TABLE '
. Util::backquote($db)
. '.'
. Util::backquote($tableName)
. ' DEFAULT '
. Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
$this->dbi->query($sql_query);
/**
* Changes columns charset if requested by the user
*/
if (! isset($_POST['change_all_tables_columns_collations']) ||
$_POST['change_all_tables_columns_collations'] !== 'on'
) {
continue;
}
$this->operations->changeAllColumnsCollation($db, $tableName, $_POST['db_collation']);
}
}
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* Controller for database privileges
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Privileges;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
/**
* Controller for database privileges
*/
class PrivilegesController extends AbstractController
{
/** @var Privileges */
private $privileges;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Privileges $privileges, $dbi)
{
parent::__construct($response, $template, $db);
$this->privileges = $privileges;
$this->dbi = $dbi;
}
/**
* @param array $params Request parameters
*/
public function index(array $params): string
{
global $cfg, $text_dir, $PMA_Theme;
$scriptName = Util::getScriptNameForOption(
$cfg['DefaultTabDatabase'],
'database'
);
$privileges = [];
if ($this->dbi->isSuperUser()) {
$privileges = $this->privileges->getAllPrivileges($params['checkprivsdb']);
}
return $this->template->render('database/privileges/index', [
'is_superuser' => $this->dbi->isSuperUser(),
'db' => $params['checkprivsdb'],
'database_url' => $scriptName,
'theme_image_path' => $PMA_Theme->getImgPath(),
'text_dir' => $text_dir,
'is_createuser' => $this->dbi->isCreateUser(),
'is_grantuser' => $this->dbi->isGrantUser(),
'privileges' => $privileges,
]);
}
}

View file

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Qbe;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Operations;
use PhpMyAdmin\Relation;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\Response;
use PhpMyAdmin\SavedSearches;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function stripos;
class QueryByExampleController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Relation $relation, $dbi)
{
parent::__construct($response, $template, $db);
$this->relation = $relation;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $savedSearchList, $savedSearch, $currentSearchId, $PMA_Theme;
global $sql_query, $goto, $sub_part, $tables, $num_tables, $total_num_tables;
global $tooltip_truename, $tooltip_aliasname, $pos, $url_params, $cfg, $err_url;
// Gets the relation settings
$cfgRelation = $this->relation->getRelationsParam();
$savedSearchList = [];
$savedSearch = null;
$currentSearchId = null;
$this->addScriptFiles(['database/qbe.js']);
if ($cfgRelation['savedsearcheswork']) {
//Get saved search list.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
if (! empty($_POST['searchId'])) {
$savedSearch->setId($_POST['searchId']);
}
//Action field is sent.
if (isset($_POST['action'])) {
$savedSearch->setSearchName($_POST['searchName']);
if ($_POST['action'] === 'create') {
$saveResult = $savedSearch->setId(null)
->setCriterias($_POST)
->save();
} elseif ($_POST['action'] === 'update') {
$saveResult = $savedSearch->setCriterias($_POST)
->save();
} elseif ($_POST['action'] === 'delete') {
$deleteResult = $savedSearch->delete();
//After deletion, reset search.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
$_POST = [];
} elseif ($_POST['action'] === 'load') {
if (empty($_POST['searchId'])) {
//when not loading a search, reset the object.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
$_POST = [];
} else {
$loadResult = $savedSearch->load();
}
}
//Else, it's an "update query"
}
$savedSearchList = $savedSearch->getList();
$currentSearchId = $savedSearch->getId();
}
/**
* A query has been submitted -> (maybe) execute it
*/
$hasMessageToDisplay = false;
if (isset($_POST['submit_sql']) && ! empty($sql_query)) {
if (stripos($sql_query, 'SELECT') !== 0) {
$hasMessageToDisplay = true;
} else {
$goto = Url::getFromRoute('/database/sql');
$sql = new Sql(
$this->dbi,
$this->relation,
new RelationCleanup($this->dbi, $this->relation),
new Operations($this->dbi, $this->relation),
new Transformations(),
$this->template
);
$this->response->addHTML($sql->executeQueryAndSendQueryResponse(
null, // analyzed_sql_results
false, // is_gotofile
$_POST['db'], // db
null, // table
false, // find_real_end
null, // sql_query_for_bookmark
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
$PMA_Theme->getImgPath(),
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
));
}
}
$sub_part = '_qbe';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/qbe');
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
$databaseQbe = new Qbe($this->relation, $this->template, $this->dbi, $db, $savedSearchList, $savedSearch);
$this->render('database/qbe/index', [
'url_params' => $url_params,
'has_message_to_display' => $hasMessageToDisplay,
'selection_form_html' => $databaseQbe->getSelectionForm(),
]);
}
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Core;
use PhpMyAdmin\Database\Routines;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function in_array;
use function strlen;
/**
* Routines management.
*/
class RoutinesController extends AbstractController
{
/** @var CheckUserPrivileges */
private $checkUserPrivileges;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, CheckUserPrivileges $checkUserPrivileges, $dbi)
{
parent::__construct($response, $template, $db);
$this->checkUserPrivileges = $checkUserPrivileges;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
global $errors, $PMA_Theme, $text_dir, $err_url, $url_params, $cfg;
$type = $_REQUEST['type'] ?? null;
$this->checkUserPrivileges->getPrivileges();
if (! $this->response->isAjax()) {
/**
* Displays the header and tabs
*/
if (! empty($table) && in_array($table, $this->dbi->getTables($db))) {
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$err_url .= Url::getCommon($url_params, '&');
DbTableExists::check();
} else {
$table = '';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$routines = new Routines($this->dbi, $this->template, $this->response);
$routines->handleEditor();
$routines->handleExecute();
$routines->export();
if (! Core::isValid($type, ['FUNCTION', 'PROCEDURE'])) {
$type = null;
}
$items = $this->dbi->getRoutines($db, $type);
$isAjax = $this->response->isAjax() && empty($_REQUEST['ajax_page_request']);
$rows = '';
foreach ($items as $item) {
$rows .= $routines->getRow($item, $isAjax ? 'ajaxInsert hide' : '');
}
$this->render('database/routines/index', [
'db' => $db,
'table' => $table,
'items' => $items,
'rows' => $rows,
'select_all_arrow_src' => $PMA_Theme->getImgPath() . 'arrow_' . $text_dir . '.png',
'has_privilege' => Util::currentUserHasPrivilege('CREATE ROUTINE', $db, $table),
]);
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Search;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
class SearchController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $err_url, $url_params, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
$this->addScriptFiles([
'database/search.js',
'vendor/stickyfill.min.js',
'sql.js',
'makegrid.js',
]);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
// If config variable $cfg['UseDbSearch'] is on false : exit.
if (! $cfg['UseDbSearch']) {
Generator::mysqlDie(
__('Access denied!'),
'',
false,
$err_url
);
}
$url_params['goto'] = Url::getFromRoute('/database/search');
// Create a database search instance
$databaseSearch = new Search($this->dbi, $db, $this->template);
// Display top links if we are not in an Ajax request
if (! $this->response->isAjax()) {
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
// Main search form has been submitted, get results
if (isset($_POST['submit_search'])) {
$this->response->addHTML($databaseSearch->getSearchResults());
}
// If we are in an Ajax request, we need to exit after displaying all the HTML
if ($this->response->isAjax() && empty($_REQUEST['ajax_page_request'])) {
return;
}
// Display the search form
$this->response->addHTML($databaseSearch->getMainHtml());
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function json_encode;
/**
* Table/Column autocomplete in SQL editors.
*/
class SqlAutoCompleteController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $sql_autocomplete;
$sql_autocomplete = true;
if ($cfg['EnableAutocompleteForTablesAndColumns']) {
$db = $_POST['db'] ?? $db;
$sql_autocomplete = [];
if ($db) {
$tableNames = $this->dbi->getTables($db);
foreach ($tableNames as $tableName) {
$sql_autocomplete[$tableName] = $this->dbi->getColumns(
$db,
$tableName
);
}
}
}
$this->response->addJSON(['tables' => json_encode($sql_autocomplete)]);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Response;
use PhpMyAdmin\SqlQueryForm;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function htmlspecialchars;
/**
* Database SQL executor
*/
class SqlController extends AbstractController
{
/** @var SqlQueryForm */
private $sqlQueryForm;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct($response, Template $template, $db, SqlQueryForm $sqlQueryForm)
{
parent::__construct($response, $template, $db);
$this->sqlQueryForm = $sqlQueryForm;
}
public function index(): void
{
global $goto, $back, $db, $cfg, $err_url;
$this->addScriptFiles([
'makegrid.js',
'vendor/jquery/jquery.uitablefilter.js',
'vendor/stickyfill.min.js',
'sql.js',
]);
$pageSettings = new PageSettings('Sql');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
/**
* After a syntax error, we return to this script
* with the typed query in the textarea.
*/
$goto = Url::getFromRoute('/database/sql');
$back = $goto;
$this->response->addHTML($this->sqlQueryForm->getHtml(
true,
false,
isset($_POST['delimiter'])
? htmlspecialchars($_POST['delimiter'])
: ';'
));
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\SqlParser\Utils\Formatter;
use function strlen;
/**
* Format SQL for SQL editors.
*/
class SqlFormatController extends AbstractController
{
public function index(): void
{
$params = ['sql' => $_POST['sql'] ?? null];
$query = strlen((string) $params['sql']) > 0 ? $params['sql'] : '';
$this->response->addJSON(['sql' => Formatter::format($query)]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tracker;
use PhpMyAdmin\Tracking;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function htmlspecialchars;
use function sprintf;
/**
* Tracking configuration for database.
*/
class TrackingController extends AbstractController
{
/** @var Tracking */
private $tracking;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Tracking $tracking, $dbi)
{
parent::__construct($response, $template, $db);
$this->tracking = $tracking;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $text_dir, $url_params, $tables, $num_tables, $PMA_Theme;
global $total_num_tables, $sub_part, $pos, $data, $cfg;
global $tooltip_truename, $tooltip_aliasname, $err_url;
$this->addScriptFiles(['vendor/jquery/jquery.tablesorter.js', 'database/tracking.js']);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/table/tracking');
$url_params['back'] = Url::getFromRoute('/database/tracking');
// Get the database structure
$sub_part = '_structure';
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,
$isSystemSchema,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
if (isset($_POST['delete_tracking'], $_POST['table'])) {
Tracker::deleteTracking($db, $_POST['table']);
echo Message::success(
__('Tracking data deleted successfully.')
)->getDisplay();
} elseif (isset($_POST['submit_create_version'])) {
$this->tracking->createTrackingForMultipleTables($_POST['selected']);
echo Message::success(
sprintf(
__(
'Version %1$s was created for selected tables,'
. ' tracking is active for them.'
),
htmlspecialchars($_POST['version'])
)
)->getDisplay();
} elseif (isset($_POST['submit_mult'])) {
if (! empty($_POST['selected_tbl'])) {
if ($_POST['submit_mult'] === 'delete_tracking') {
foreach ($_POST['selected_tbl'] as $table) {
Tracker::deleteTracking($db, $table);
}
echo Message::success(
__('Tracking data deleted successfully.')
)->getDisplay();
} elseif ($_POST['submit_mult'] === 'track') {
echo $this->template->render('create_tracking_version', [
'route' => '/database/tracking',
'url_params' => $url_params,
'last_version' => 0,
'db' => $db,
'selected' => $_POST['selected_tbl'],
'type' => 'both',
'default_statements' => $cfg['Server']['tracking_default_statements'],
]);
return;
}
} else {
echo Message::notice(
__('No tables selected.')
)->getDisplay();
}
}
// Get tracked data about the database
$data = Tracker::getTrackedData($db, '', '1');
// No tables present and no log exist
if ($num_tables == 0 && count($data['ddlog']) === 0) {
echo '<p>' , __('No tables found in database.') , '</p>' , "\n";
if (empty($isSystemSchema)) {
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
echo $this->template->render('database/create_table', ['db' => $db]);
}
return;
}
echo $this->tracking->getHtmlForDbTrackingTables(
$db,
$url_params,
$PMA_Theme->getImgPath(),
$text_dir
);
// If available print out database log
if (count($data['ddlog']) <= 0) {
return;
}
$log = '';
foreach ($data['ddlog'] as $entry) {
$log .= '# ' . $entry['date'] . ' ' . $entry['username'] . "\n"
. $entry['statement'] . "\n";
}
echo Generator::getMessage(__('Database Log'), $log);
}
}

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Triggers;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function in_array;
use function strlen;
/**
* Triggers management.
*/
class TriggersController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
global $errors, $url_params, $err_url, $cfg;
if (! $this->response->isAjax()) {
/**
* Displays the header and tabs
*/
if (! empty($table) && in_array($table, $this->dbi->getTables($db))) {
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$err_url .= Url::getCommon($url_params, '&');
DbTableExists::check();
} else {
$table = '';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$triggers = new Triggers($this->dbi, $this->template, $this->response);
$triggers->main();
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
final class DatabaseController extends AbstractController
{
public function all(): void
{
global $dblist;
$this->response->addJSON(['databases' => $dblist->databases]);
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* Handle error report submission
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\ErrorHandler;
use PhpMyAdmin\ErrorReport;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\UserPreferences;
use function count;
use function in_array;
use function is_string;
use function json_decode;
use function time;
/**
* Handle error report submission
*/
class ErrorReportController extends AbstractController
{
/** @var ErrorReport */
private $errorReport;
/** @var ErrorHandler */
private $errorHandler;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
ErrorReport $errorReport,
ErrorHandler $errorHandler
) {
parent::__construct($response, $template);
$this->errorReport = $errorReport;
$this->errorHandler = $errorHandler;
}
public function index(): void
{
global $cfg;
if (! isset($_POST['exception_type'])
|| ! in_array($_POST['exception_type'], ['js', 'php'])
) {
return;
}
if (isset($_POST['send_error_report'])
&& ($_POST['send_error_report'] == true
|| $_POST['send_error_report'] == '1')
) {
if ($_POST['exception_type'] === 'php') {
/**
* Prevent infinite error submission.
* Happens in case error submissions fails.
* If reporting is done in some time interval,
* just clear them & clear json data too.
*/
if (isset($_SESSION['prev_error_subm_time'], $_SESSION['error_subm_count'])
&& $_SESSION['error_subm_count'] >= 3
&& ($_SESSION['prev_error_subm_time'] - time()) <= 3000
) {
$_SESSION['error_subm_count'] = 0;
$_SESSION['prev_errors'] = '';
$this->response->addJSON('stopErrorReportLoop', '1');
} else {
$_SESSION['prev_error_subm_time'] = time();
$_SESSION['error_subm_count'] = isset($_SESSION['error_subm_count'])
? $_SESSION['error_subm_count'] + 1
: 0;
}
}
$reportData = $this->errorReport->getData($_POST['exception_type']);
// report if and only if there were 'actual' errors.
if (count($reportData) > 0) {
$server_response = $this->errorReport->send($reportData);
if (! is_string($server_response)) {
$success = false;
} else {
$decoded_response = json_decode($server_response, true);
$success = ! empty($decoded_response) ?
$decoded_response['success'] : false;
}
/* Message to show to the user */
if ($success) {
if ((isset($_POST['automatic'])
&& $_POST['automatic'] === 'true')
|| $cfg['SendErrorReports'] === 'always'
) {
$msg = __(
'An error has been detected and an error report has been '
. 'automatically submitted based on your settings.'
);
} else {
$msg = __('Thank you for submitting this report.');
}
} else {
$msg = __(
'An error has been detected and an error report has been '
. 'generated but failed to be sent.'
);
$msg .= ' ';
$msg .= __(
'If you experience any '
. 'problems please submit a bug report manually.'
);
}
$msg .= ' ' . __('You may want to refresh the page.');
/* Create message object */
if ($success) {
$msg = Message::notice($msg);
} else {
$msg = Message::error($msg);
}
/* Add message to response */
if ($this->response->isAjax()) {
if ($_POST['exception_type'] === 'js') {
$this->response->addJSON('message', $msg);
} else {
$this->response->addJSON('errSubmitMsg', $msg);
}
} elseif ($_POST['exception_type'] === 'php') {
$jsCode = 'Functions.ajaxShowMessage(\'<div class="alert alert-danger" role="alert">'
. $msg
. '</div>\', false);';
$this->response->getFooter()->getScripts()->addCode($jsCode);
}
if ($_POST['exception_type'] === 'php') {
// clear previous errors & save new ones.
$this->errorHandler->savePreviousErrors();
}
/* Persist always send settings */
if (isset($_POST['always_send'])
&& $_POST['always_send'] === 'true'
) {
$userPreferences = new UserPreferences();
$userPreferences->persistOption('SendErrorReports', 'always', 'ask');
}
}
} elseif (! empty($_POST['get_settings'])) {
$this->response->addJSON('report_setting', $cfg['SendErrorReports']);
} elseif ($_POST['exception_type'] === 'js') {
$this->response->addHTML($this->errorReport->getForm());
} else {
// clear previous errors & save new ones.
$this->errorHandler->savePreviousErrors();
}
}
}

View file

@ -0,0 +1,704 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Controllers\Database\ExportController as DatabaseExportController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\Exceptions\ExportException;
use PhpMyAdmin\Export;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\ExportPlugin;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
use PhpMyAdmin\SqlParser\Utils\Misc;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use const PHP_EOL;
use function count;
use function function_exists;
use function in_array;
use function ini_set;
use function is_array;
use function ob_end_clean;
use function ob_get_length;
use function ob_get_level;
use function register_shutdown_function;
use function strlen;
use function time;
final class ExportController extends AbstractController
{
/** @var Export */
private $export;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Export $export, Relation $relation)
{
parent::__construct($response, $template);
$this->export = $export;
$this->relation = $relation;
}
public function index(): void
{
global $containerBuilder, $db, $export_type, $filename_template, $sql_query, $err_url, $message;
global $compression, $crlf, $asfile, $buffer_needed, $save_on_server, $file_handle, $separate_files;
global $output_charset_conversion, $output_kanji_conversion, $table, $what, $export_plugin, $single_table;
global $compression_methods, $onserver, $back_button, $refreshButton, $save_filename, $filename;
global $quick_export, $cfg, $tables, $table_select, $aliases, $dump_buffer, $dump_buffer_len;
global $time_start, $charset, $remember_template, $mime_type, $num_tables, $dump_buffer_objects;
global $active_page, $do_relation, $do_comments, $do_mime, $do_dates, $whatStrucOrData, $db_select;
global $table_structure, $table_data, $lock_tables, $allrows, $limit_to, $limit_from;
$this->addScriptFiles(['export_output.js']);
/**
* Sets globals from $_POST
*
* - Please keep the parameters in order of their appearance in the form
* - Some of these parameters are not used, as the code below directly
* verifies from the superglobal $_POST or $_REQUEST
* TODO: this should be removed to avoid passing user input to GLOBALS
* without checking
*/
$post_params = [
'db',
'table',
'what',
'single_table',
'export_type',
'export_method',
'quick_or_custom',
'db_select',
'table_select',
'table_structure',
'table_data',
'limit_to',
'limit_from',
'allrows',
'lock_tables',
'output_format',
'filename_template',
'maxsize',
'remember_template',
'charset',
'compression',
'as_separate_files',
'knjenc',
'xkana',
'htmlword_structure_or_data',
'htmlword_null',
'htmlword_columns',
'mediawiki_headers',
'mediawiki_structure_or_data',
'mediawiki_caption',
'pdf_structure_or_data',
'odt_structure_or_data',
'odt_relation',
'odt_comments',
'odt_mime',
'odt_columns',
'odt_null',
'codegen_structure_or_data',
'codegen_format',
'excel_null',
'excel_removeCRLF',
'excel_columns',
'excel_edition',
'excel_structure_or_data',
'yaml_structure_or_data',
'ods_null',
'ods_structure_or_data',
'ods_columns',
'json_structure_or_data',
'json_pretty_print',
'json_unicode',
'xml_structure_or_data',
'xml_export_events',
'xml_export_functions',
'xml_export_procedures',
'xml_export_tables',
'xml_export_triggers',
'xml_export_views',
'xml_export_contents',
'texytext_structure_or_data',
'texytext_columns',
'texytext_null',
'phparray_structure_or_data',
'sql_include_comments',
'sql_header_comment',
'sql_dates',
'sql_relation',
'sql_mime',
'sql_use_transaction',
'sql_disable_fk',
'sql_compatibility',
'sql_structure_or_data',
'sql_create_database',
'sql_drop_table',
'sql_procedure_function',
'sql_create_table',
'sql_create_view',
'sql_create_trigger',
'sql_view_current_user',
'sql_simple_view_export',
'sql_if_not_exists',
'sql_or_replace_view',
'sql_auto_increment',
'sql_backquotes',
'sql_truncate',
'sql_delayed',
'sql_ignore',
'sql_type',
'sql_insert_syntax',
'sql_max_query_size',
'sql_hex_for_binary',
'sql_utc_time',
'sql_drop_database',
'sql_views_as_tables',
'sql_metadata',
'csv_separator',
'csv_enclosed',
'csv_escaped',
'csv_terminated',
'csv_null',
'csv_removeCRLF',
'csv_columns',
'csv_structure_or_data',
// csv_replace should have been here but we use it directly from $_POST
'latex_caption',
'latex_structure_or_data',
'latex_structure_caption',
'latex_structure_continued_caption',
'latex_structure_label',
'latex_relation',
'latex_comments',
'latex_mime',
'latex_columns',
'latex_data_caption',
'latex_data_continued_caption',
'latex_data_label',
'latex_null',
'aliases',
];
foreach ($post_params as $one_post_param) {
if (! isset($_POST[$one_post_param])) {
continue;
}
$GLOBALS[$one_post_param] = $_POST[$one_post_param];
}
Util::checkParameters(['what', 'export_type']);
// sanitize this parameter which will be used below in a file inclusion
$what = Core::securePath($_POST['what']);
// export class instance, not array of properties, as before
/** @var ExportPlugin $export_plugin */
$export_plugin = Plugins::getPlugin(
'export',
$what,
'libraries/classes/Plugins/Export/',
[
'export_type' => $export_type,
'single_table' => isset($single_table),
]
);
// Check export type
if (empty($export_plugin)) {
Core::fatalError(__('Bad type!'));
}
/**
* valid compression methods
*/
$compression_methods = [];
if ($GLOBALS['cfg']['ZipDump'] && function_exists('gzcompress')) {
$compression_methods[] = 'zip';
}
if ($GLOBALS['cfg']['GZipDump'] && function_exists('gzencode')) {
$compression_methods[] = 'gzip';
}
/**
* init and variable checking
*/
$compression = '';
$onserver = false;
$save_on_server = false;
$buffer_needed = false;
$back_button = '';
$refreshButton = '';
$save_filename = '';
$file_handle = '';
$err_url = '';
$filename = '';
$separate_files = '';
// Is it a quick or custom export?
if (isset($_POST['quick_or_custom'])
&& $_POST['quick_or_custom'] === 'quick'
) {
$quick_export = true;
} else {
$quick_export = false;
}
if ($_POST['output_format'] === 'astext') {
$asfile = false;
} else {
$asfile = true;
$selectedCompression = $_POST['compression'] ?? '';
if (isset($_POST['as_separate_files'])
&& ! empty($_POST['as_separate_files'])
) {
if (! empty($selectedCompression)
&& $selectedCompression === 'zip'
) {
$separate_files = $_POST['as_separate_files'];
}
}
if (in_array($selectedCompression, $compression_methods)) {
$compression = $selectedCompression;
$buffer_needed = true;
}
if (($quick_export && ! empty($_POST['quick_export_onserver']))
|| (! $quick_export && ! empty($_POST['onserver']))
) {
if ($quick_export) {
$onserver = $_POST['quick_export_onserver'];
} else {
$onserver = $_POST['onserver'];
}
// Will we save dump on server?
$save_on_server = ! empty($cfg['SaveDir']) && $onserver;
}
}
/**
* If we are sending the export file (as opposed to just displaying it
* as text), we have to bypass the usual PhpMyAdmin\Response mechanism
*/
if (isset($_POST['output_format']) && $_POST['output_format'] === 'sendit' && ! $save_on_server) {
$this->response->disable();
//Disable all active buffers (see: ob_get_status(true) at this point)
do {
if (ob_get_length() > 0 || ob_get_level() > 0) {
$hasBuffer = ob_end_clean();
} else {
$hasBuffer = false;
}
} while ($hasBuffer);
}
$tables = [];
// Generate error url and check for needed variables
if ($export_type === 'server') {
$err_url = Url::getFromRoute('/server/export');
} elseif ($export_type === 'database' && strlen($db) > 0) {
$err_url = Url::getFromRoute('/database/export', ['db' => $db]);
// Check if we have something to export
if (isset($table_select)) {
$tables = $table_select;
} else {
$tables = [];
}
} elseif ($export_type === 'table' && strlen($db) > 0 && strlen($table) > 0) {
$err_url = Url::getFromRoute('/table/export', [
'db' => $db,
'table' => $table,
]);
} elseif ($export_type === 'raw') {
$err_url = Url::getFromRoute('/server/export', ['sql_query' => $sql_query]);
} else {
Core::fatalError(__('Bad parameters!'));
}
// Merge SQL Query aliases with Export aliases from
// export page, Export page aliases are given more
// preference over SQL Query aliases.
$parser = new Parser($sql_query);
$aliases = [];
if (! empty($parser->statements[0])
&& ($parser->statements[0] instanceof SelectStatement)
) {
$aliases = Misc::getAliases($parser->statements[0], $db);
}
if (! empty($_POST['aliases'])) {
$aliases = $this->export->mergeAliases($aliases, $_POST['aliases']);
$_SESSION['tmpval']['aliases'] = $_POST['aliases'];
}
/**
* Increase time limit for script execution and initializes some variables
*/
Util::setTimeLimit();
if (! empty($cfg['MemoryLimit'])) {
ini_set('memory_limit', $cfg['MemoryLimit']);
}
register_shutdown_function([$this->export, 'shutdown']);
// Start with empty buffer
$dump_buffer = '';
$dump_buffer_len = 0;
// Array of dump_buffers - used in separate file exports
$dump_buffer_objects = [];
// We send fake headers to avoid browser timeout when buffering
$time_start = time();
// Defines the default <CR><LF> format.
// For SQL always use \n as MySQL wants this on all platforms.
if ($what === 'sql') {
$crlf = "\n";
} else {
$crlf = PHP_EOL;
}
$output_kanji_conversion = Encoding::canConvertKanji();
// Do we need to convert charset?
$output_charset_conversion = $asfile
&& Encoding::isSupported()
&& isset($charset) && $charset !== 'utf-8';
// Use on the fly compression?
$GLOBALS['onfly_compression'] = $GLOBALS['cfg']['CompressOnFly']
&& $compression === 'gzip';
if ($GLOBALS['onfly_compression']) {
$GLOBALS['memory_limit'] = $this->export->getMemoryLimit();
}
// Generate filename and mime type if needed
if ($asfile) {
if (empty($remember_template)) {
$remember_template = '';
}
[$filename, $mime_type] = $this->export->getFilenameAndMimetype(
$export_type,
$remember_template,
$export_plugin,
$compression,
$filename_template
);
} else {
$mime_type = '';
}
// For raw query export, filename will be export.extension
if ($export_type === 'raw') {
[$filename] = $this->export->getFinalFilenameAndMimetypeForFilename(
$export_plugin,
$compression,
'export'
);
}
// Open file on server if needed
if ($save_on_server) {
[$save_filename, $message, $file_handle] = $this->export->openFile(
$filename,
$quick_export
);
// problem opening export file on server?
if (! empty($message)) {
$this->export->showPage($export_type);
return;
}
} else {
/**
* Send headers depending on whether the user chose to download a dump file
* or not
*/
if ($asfile) {
// Download
// (avoid rewriting data containing HTML with anchors and forms;
// this was reported to happen under Plesk)
ini_set('url_rewriter.tags', '');
$filename = Sanitize::sanitizeFilename($filename);
Core::downloadHeader($filename, $mime_type);
} else {
// HTML
if ($export_type === 'database') {
$num_tables = count($tables);
if ($num_tables === 0) {
$message = Message::error(
__('No tables found in database.')
);
$active_page = Url::getFromRoute('/database/export');
/** @var DatabaseExportController $controller */
$controller = $containerBuilder->get(DatabaseExportController::class);
$controller->index();
exit;
}
}
[$html, $back_button, $refreshButton] = $this->export->getHtmlForDisplayedExportHeader(
$export_type,
$db,
$table
);
echo $html;
unset($html);
}
}
try {
// Re - initialize
$dump_buffer = '';
$dump_buffer_len = 0;
// Add possibly some comments to export
if (! $export_plugin->exportHeader()) {
throw new ExportException('Failure during header export.');
}
// Will we need relation & co. setup?
$do_relation = isset($GLOBALS[$what . '_relation']);
$do_comments = isset($GLOBALS[$what . '_include_comments'])
|| isset($GLOBALS[$what . '_comments']);
$do_mime = isset($GLOBALS[$what . '_mime']);
if ($do_relation || $do_comments || $do_mime) {
$this->relation->getRelationsParam();
}
// Include dates in export?
$do_dates = isset($GLOBALS[$what . '_dates']);
$whatStrucOrData = $GLOBALS[$what . '_structure_or_data'];
if ($export_type === 'raw') {
$whatStrucOrData = 'raw';
}
/**
* Builds the dump
*/
if ($export_type === 'server') {
if (! isset($db_select)) {
$db_select = '';
}
$this->export->exportServer(
$db_select,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
} elseif ($export_type === 'database') {
if (! isset($table_structure) || ! is_array($table_structure)) {
$table_structure = [];
}
if (! isset($table_data) || ! is_array($table_data)) {
$table_data = [];
}
if (! empty($_POST['structure_or_data_forced'])) {
$table_structure = $tables;
$table_data = $tables;
}
if (isset($lock_tables)) {
$this->export->lockTables($db, $tables, 'READ');
try {
$this->export->exportDatabase(
$db,
$tables,
$whatStrucOrData,
$table_structure,
$table_data,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
} finally {
$this->export->unlockTables();
}
} else {
$this->export->exportDatabase(
$db,
$tables,
$whatStrucOrData,
$table_structure,
$table_data,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
}
} elseif ($export_type === 'raw') {
Export::exportRaw(
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$sql_query,
$export_type
);
} else {
// We export just one table
// $allrows comes from the form when "Dump all rows" has been selected
if (! isset($allrows)) {
$allrows = '';
}
if (! isset($limit_to)) {
$limit_to = '0';
}
if (! isset($limit_from)) {
$limit_from = '0';
}
if (isset($lock_tables)) {
try {
$this->export->lockTables($db, [$table], 'READ');
$this->export->exportTable(
$db,
$table,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$allrows,
$limit_to,
$limit_from,
$sql_query,
$aliases
);
} finally {
$this->export->unlockTables();
}
} else {
$this->export->exportTable(
$db,
$table,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$allrows,
$limit_to,
$limit_from,
$sql_query,
$aliases
);
}
}
if (! $export_plugin->exportFooter()) {
throw new ExportException('Failure during footer export.');
}
} catch (ExportException $e) {
null; // Avoid phpcs error...
}
if ($save_on_server && ! empty($message)) {
$this->export->showPage($export_type);
return;
}
/**
* Send the dump as a file...
*/
if (empty($asfile)) {
echo $this->export->getHtmlForDisplayedExportFooter($back_button, $refreshButton);
return;
}
// Convert the charset if required.
if ($output_charset_conversion) {
$dump_buffer = Encoding::convertString(
'utf-8',
$GLOBALS['charset'],
$dump_buffer
);
}
// Compression needed?
if ($compression) {
if (! empty($separate_files)) {
$dump_buffer = $this->export->compress(
$dump_buffer_objects,
$compression,
$filename
);
} else {
$dump_buffer = $this->export->compress($dump_buffer, $compression, $filename);
}
}
/* If we saved on server, we have to close file now */
if ($save_on_server) {
$message = $this->export->closeFile(
$file_handle,
$dump_buffer,
$save_filename
);
$this->export->showPage($export_type);
return;
}
echo $dump_buffer;
}
public function checkTimeOut(): void
{
$this->response->setAjax(true);
if (isset($_SESSION['pma_export_error'])) {
unset($_SESSION['pma_export_error']);
$this->response->addJSON('message', 'timeout');
return;
}
$this->response->addJSON('message', 'success');
}
}

View file

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Export\Template as ExportTemplate;
use PhpMyAdmin\Export\TemplateModel;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function is_array;
use function is_string;
final class ExportTemplateController extends AbstractController
{
/** @var TemplateModel */
private $model;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
TemplateModel $model,
Relation $relation
) {
parent::__construct($response, $template);
$this->model = $model;
$this->relation = $relation;
}
public function create(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = ExportTemplate::fromArray([
'username' => $cfg['Server']['user'],
'exportType' => $_POST['exportType'] ?? '',
'name' => $_POST['templateName'] ?? '',
'data' => $_POST['templateData'] ?? '',
]);
$result = $this->model->create($cfgRelation['db'], $cfgRelation['export_templates'], $template);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$templates = $this->model->getAll(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$template->getUsername(),
$template->getExportType()
);
$this->response->setRequestStatus(true);
$this->response->addJSON(
'data',
$this->template->render('export/template_options', [
'templates' => is_array($templates) ? $templates : [],
'selected_template' => $_POST['template_id'] ?? null,
])
);
}
public function delete(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$result = $this->model->delete(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$cfg['Server']['user'],
(int) $_POST['templateId']
);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$this->response->setRequestStatus(true);
}
public function load(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = $this->model->load(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$cfg['Server']['user'],
(int) $_POST['templateId']
);
if (! $template instanceof ExportTemplate) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $template);
return;
}
$this->response->setRequestStatus(true);
$this->response->addJSON('data', $template->getData());
}
public function update(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = ExportTemplate::fromArray([
'id' => (int) $_POST['templateId'],
'username' => $cfg['Server']['user'],
'data' => $_POST['templateData'],
]);
$result = $this->model->update($cfgRelation['db'], $cfgRelation['export_templates'], $template);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$this->response->setRequestStatus(true);
}
}

View file

@ -0,0 +1,151 @@
<?php
/**
* Editor for Geometry data types.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Gis\GisFactory;
use PhpMyAdmin\Gis\GisVisualization;
use function array_merge;
use function in_array;
use function intval;
use function mb_strpos;
use function mb_strtoupper;
use function mb_substr;
use function substr;
use function trim;
/**
* Editor for Geometry data types.
*/
class GisDataEditorController extends AbstractController
{
public function index(): void
{
global $gis_data, $gis_types, $start, $geom_type, $gis_obj, $srid, $wkt, $wkt_with_zero, $PMA_Theme;
global $result, $visualizationSettings, $data, $visualization, $open_layers, $geom_count, $dbi;
if (! isset($_POST['field'])) {
return;
}
// Get data if any posted
$gis_data = [];
if (Core::isValid($_POST['gis_data'], 'array')) {
$gis_data = $_POST['gis_data'];
}
$gis_types = [
'POINT',
'MULTIPOINT',
'LINESTRING',
'MULTILINESTRING',
'POLYGON',
'MULTIPOLYGON',
'GEOMETRYCOLLECTION',
];
// Extract type from the initial call and make sure that it's a valid one.
// Extract from field's values if available, if not use the column type passed.
if (! isset($gis_data['gis_type'])) {
if (isset($_POST['type']) && $_POST['type'] != '') {
$gis_data['gis_type'] = mb_strtoupper($_POST['type']);
}
if (isset($_POST['value']) && trim($_POST['value']) != '') {
$start = substr($_POST['value'], 0, 1) == "'" ? 1 : 0;
$gis_data['gis_type'] = mb_substr(
$_POST['value'],
$start,
mb_strpos($_POST['value'], '(') - $start
);
}
if (! isset($gis_data['gis_type'])
|| (! in_array($gis_data['gis_type'], $gis_types))
) {
$gis_data['gis_type'] = $gis_types[0];
}
}
$geom_type = $gis_data['gis_type'];
// Generate parameters from value passed.
$gis_obj = GisFactory::factory($geom_type);
if ($gis_obj === false) {
return;
}
if (isset($_POST['value'])) {
$gis_data = array_merge(
$gis_data,
$gis_obj->generateParams($_POST['value'])
);
}
// Generate Well Known Text
$srid = isset($gis_data['srid']) && $gis_data['srid'] != '' ? $gis_data['srid'] : 0;
$wkt = $gis_obj->generateWkt($gis_data, 0);
$wkt_with_zero = $gis_obj->generateWkt($gis_data, 0, '0');
$result = "'" . $wkt . "'," . $srid;
// Generate SVG based visualization
$visualizationSettings = [
'width' => 450,
'height' => 300,
'spatialColumn' => 'wkt',
'mysqlVersion' => $dbi->getVersion(),
'isMariaDB' => $dbi->isMariaDB(),
];
$data = [
[
'wkt' => $wkt_with_zero,
'srid' => $srid,
],
];
$visualization = GisVisualization::getByData($data, $visualizationSettings)
->toImage('svg');
$open_layers = GisVisualization::getByData($data, $visualizationSettings)
->asOl();
// If the call is to update the WKT and visualization make an AJAX response
if (isset($_POST['generate']) && $_POST['generate'] == true) {
$this->response->addJSON([
'result' => $result,
'visualization' => $visualization,
'openLayers' => $open_layers,
]);
return;
}
$geom_count = 1;
if ($geom_type === 'GEOMETRYCOLLECTION') {
$geom_count = isset($gis_data[$geom_type]['geom_count'])
? intval($gis_data[$geom_type]['geom_count']) : 1;
if (isset($gis_data[$geom_type]['add_geom'])) {
$geom_count++;
}
}
$templateOutput = $this->template->render('gis_data_editor_form', [
'width' => $visualizationSettings['width'],
'height' => $visualizationSettings['height'],
'theme_image_path' => $PMA_Theme->getImgPath(),
'field' => $_POST['field'],
'input_name' => $_POST['input_name'],
'srid' => $srid,
'visualization' => $visualization,
'open_layers' => $open_layers,
'gis_types' => $gis_types,
'geom_type' => $geom_type,
'geom_count' => $geom_count,
'gis_data' => $gis_data,
'result' => $result,
]);
$this->response->addJSON(['gis_editor' => $templateOutput]);
}
}

View file

@ -0,0 +1,500 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Git;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Message;
use PhpMyAdmin\RecentFavoriteTable;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Select;
use PhpMyAdmin\Template;
use PhpMyAdmin\ThemeManager;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use PhpMyAdmin\Util;
use const E_USER_NOTICE;
use const E_USER_WARNING;
use const PHP_VERSION;
use function count;
use function extension_loaded;
use function file_exists;
use function ini_get;
use function preg_match;
use function sprintf;
use function strlen;
use function strtotime;
use function trigger_error;
class HomeController extends AbstractController
{
/** @var Config */
private $config;
/** @var ThemeManager */
private $themeManager;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Config $config
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $config, ThemeManager $themeManager, $dbi)
{
parent::__construct($response, $template);
$this->config = $config;
$this->themeManager = $themeManager;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $server, $collation_connection, $message, $show_query, $db, $table, $err_url;
if ($this->response->isAjax() && ! empty($_REQUEST['access_time'])) {
return;
}
// This is for $cfg['ShowDatabasesNavigationAsTree'] = false;
// See: https://github.com/phpmyadmin/phpmyadmin/issues/16520
// The DB is defined here and sent to the JS front-end to refresh the DB tree
$db = $_POST['db'] ?? '';
$table = '';
$show_query = '1';
$err_url = Url::getFromRoute('/');
if ($server > 0 && $this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$languageManager = LanguageManager::getInstance();
if (! empty($message)) {
$displayMessage = Generator::getMessage($message);
unset($message);
}
if (isset($_SESSION['partial_logout'])) {
$partialLogout = Message::success(__(
'You were logged out from one server, to logout completely '
. 'from phpMyAdmin, you need to logout from all servers.'
))->getDisplay();
unset($_SESSION['partial_logout']);
}
$syncFavoriteTables = RecentFavoriteTable::getInstance('favorite')
->getHtmlSyncFavoriteTables();
$hasServer = $server > 0 || count($cfg['Servers']) > 1;
if ($hasServer) {
$hasServerSelection = $cfg['ServerDefault'] == 0
|| (! $cfg['NavigationDisplayServers']
&& (count($cfg['Servers']) > 1
|| ($server == 0 && count($cfg['Servers']) === 1)));
if ($hasServerSelection) {
$serverSelection = Select::render(true, true);
}
if ($server > 0) {
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
$charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']);
$charsetsList = [];
/** @var Charset $charset */
foreach ($charsets as $charset) {
$collationsList = [];
/** @var Collation $collation */
foreach ($collations[$charset->getName()] as $collation) {
$collationsList[] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
'is_selected' => $collation_connection === $collation->getName(),
];
}
$charsetsList[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
'collations' => $collationsList,
];
}
}
}
$languageSelector = '';
if (empty($cfg['Lang']) && $languageManager->hasChoice()) {
$languageSelector = $languageManager->getSelectorDisplay($this->template);
}
$themeSelection = '';
if ($cfg['ThemeManager']) {
$themeSelection = $this->themeManager->getHtmlSelectBox();
}
$databaseServer = [];
if ($server > 0 && $cfg['ShowServerInfo']) {
$hostInfo = '';
if (! empty($cfg['Server']['verbose'])) {
$hostInfo .= $cfg['Server']['verbose'];
if ($cfg['ShowServerInfo']) {
$hostInfo .= ' (';
}
}
if ($cfg['ShowServerInfo'] || empty($cfg['Server']['verbose'])) {
$hostInfo .= $this->dbi->getHostInfo();
}
if (! empty($cfg['Server']['verbose']) && $cfg['ShowServerInfo']) {
$hostInfo .= ')';
}
$serverCharset = Charsets::getServerCharset($this->dbi, $cfg['Server']['DisableIS']);
$databaseServer = [
'host' => $hostInfo,
'type' => Util::getServerType(),
'connection' => Generator::getServerSSL(),
'version' => $this->dbi->getVersionString() . ' - ' . $this->dbi->getVersionComment(),
'protocol' => $this->dbi->getProtoInfo(),
'user' => $this->dbi->fetchValue('SELECT USER();'),
'charset' => $serverCharset->getDescription() . ' (' . $serverCharset->getName() . ')',
];
}
$webServer = [];
if ($cfg['ShowServerInfo']) {
$webServer['software'] = $_SERVER['SERVER_SOFTWARE'] ?? null;
if ($server > 0) {
$clientVersion = $this->dbi->getClientInfo();
if (preg_match('#\d+\.\d+\.\d+#', $clientVersion)) {
$clientVersion = 'libmysql - ' . $clientVersion;
}
$webServer['database'] = $clientVersion;
$webServer['php_extensions'] = Util::listPHPExtensions();
$webServer['php_version'] = PHP_VERSION;
}
}
$relation = new Relation($this->dbi);
if ($server > 0) {
$cfgRelation = $relation->getRelationsParam();
if (! $cfgRelation['allworks']
&& $cfg['PmaNoRelation_DisableWarning'] == false
) {
$messageText = __(
'The phpMyAdmin configuration storage is not completely '
. 'configured, some extended features have been deactivated. '
. '%sFind out why%s. '
);
if ($cfg['ZeroConf'] == true) {
$messageText .= '<br>' .
__(
'Or alternately go to \'Operations\' tab of any database '
. 'to set it up there.'
);
}
$messageInstance = Message::notice($messageText);
$messageInstance->addParamHtml(
'<a href="' . Url::getFromRoute('/check-relations')
. '" data-post="' . Url::getCommon() . '">'
);
$messageInstance->addParamHtml('</a>');
/* Show error if user has configured something, notice elsewhere */
if (! empty($cfg['Servers'][$server]['pmadb'])) {
$messageInstance->isError(true);
}
$configStorageMessage = $messageInstance->getDisplay();
}
}
$this->checkRequirements();
$git = new Git($this->config);
$this->render('home/index', [
'message' => $displayMessage ?? '',
'partial_logout' => $partialLogout ?? '',
'is_git_revision' => $git->isGitRevision(),
'server' => $server,
'sync_favorite_tables' => $syncFavoriteTables,
'has_server' => $hasServer,
'is_demo' => $cfg['DBG']['demo'],
'has_server_selection' => $hasServerSelection ?? false,
'server_selection' => $serverSelection ?? '',
'has_change_password_link' => $cfg['Server']['auth_type'] !== 'config' && $cfg['ShowChgPassword'],
'charsets' => $charsetsList ?? [],
'language_selector' => $languageSelector,
'theme_selection' => $themeSelection,
'database_server' => $databaseServer,
'web_server' => $webServer,
'show_php_info' => $cfg['ShowPhpInfo'],
'is_version_checked' => $cfg['VersionCheck'],
'phpmyadmin_version' => PMA_VERSION,
'config_storage_message' => $configStorageMessage ?? '',
]);
}
public function setTheme(): void
{
$this->themeManager->setActiveTheme($_POST['set_theme']);
$this->themeManager->setThemeCookie();
$userPreferences = new UserPreferences();
$preferences = $userPreferences->load();
$preferences['config_data']['ThemeDefault'] = $_POST['set_theme'];
$userPreferences->save($preferences['config_data']);
$this->response->header('Location: index.php?route=/' . Url::getCommonRaw([], '&'));
}
public function setCollationConnection(): void
{
$this->config->setUserValue(
null,
'DefaultConnectionCollation',
$_POST['collation_connection'],
'utf8mb4_unicode_ci'
);
$this->response->header('Location: index.php?route=/' . Url::getCommonRaw([], '&'));
}
public function reloadRecentTablesList(): void
{
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'list' => RecentFavoriteTable::getInstance('recent')->getHtmlList(),
]);
}
public function gitRevision(): void
{
if (! $this->response->isAjax()) {
return;
}
$git = new Git($this->config);
if (! $git->isGitRevision()) {
return;
}
$commit = $git->checkGitRevision();
if (! $this->config->get('PMA_VERSION_GIT') || $commit === null) {
$this->response->setRequestStatus(false);
return;
}
$commit['author']['date'] = Util::localisedDate(strtotime($commit['author']['date']));
$commit['committer']['date'] = Util::localisedDate(strtotime($commit['committer']['date']));
$this->render('home/git_info', $commit);
}
private function checkRequirements(): void
{
global $cfg, $server, $lang;
/**
* mbstring is used for handling multibytes inside parser, so it is good
* to tell user something might be broken without it, see bug #1063149.
*/
if (! extension_loaded('mbstring')) {
trigger_error(
__(
'The mbstring PHP extension was not found and you seem to be using'
. ' a multibyte charset. Without the mbstring extension phpMyAdmin'
. ' is unable to split strings correctly and it may result in'
. ' unexpected results.'
),
E_USER_WARNING
);
}
/**
* Missing functionality
*/
if (! extension_loaded('curl') && ! ini_get('allow_url_fopen')) {
trigger_error(
__(
'The curl extension was not found and allow_url_fopen is '
. 'disabled. Due to this some features such as error reporting '
. 'or version check are disabled.'
)
);
}
if ($cfg['LoginCookieValidityDisableWarning'] == false) {
/**
* Check whether session.gc_maxlifetime limits session validity.
*/
$gc_time = (int) ini_get('session.gc_maxlifetime');
if ($gc_time < $cfg['LoginCookieValidity']) {
trigger_error(
__(
'Your PHP parameter [a@https://www.php.net/manual/en/session.' .
'configuration.php#ini.session.gc-maxlifetime@_blank]session.' .
'gc_maxlifetime[/a] is lower than cookie validity configured ' .
'in phpMyAdmin, because of this, your login might expire sooner ' .
'than configured in phpMyAdmin.'
),
E_USER_WARNING
);
}
}
/**
* Check whether LoginCookieValidity is limited by LoginCookieStore.
*/
if ($cfg['LoginCookieStore'] != 0
&& $cfg['LoginCookieStore'] < $cfg['LoginCookieValidity']
) {
trigger_error(
__(
'Login cookie store is lower than cookie validity configured in ' .
'phpMyAdmin, because of this, your login will expire sooner than ' .
'configured in phpMyAdmin.'
),
E_USER_WARNING
);
}
/**
* Warning if using the default MySQL controluser account
*/
if (isset($cfg['Server']['controluser'], $cfg['Server']['controlpass'])
&& $server != 0
&& $cfg['Server']['controluser'] === 'pma'
&& $cfg['Server']['controlpass'] === 'pmapass'
) {
trigger_error(
__(
'Your server is running with default values for the ' .
'controluser and password (controlpass) and is open to ' .
'intrusion; you really should fix this security weakness' .
' by changing the password for controluser \'pma\'.'
),
E_USER_WARNING
);
}
/**
* Check if user does not have defined blowfish secret and it is being used.
*/
if (! empty($_SESSION['encryption_key'])) {
if (empty($cfg['blowfish_secret'])) {
trigger_error(
__(
'The configuration file now needs a secret passphrase (blowfish_secret).'
),
E_USER_WARNING
);
} elseif (strlen($cfg['blowfish_secret']) < 32) {
trigger_error(
__(
'The secret passphrase in configuration (blowfish_secret) is too short.'
),
E_USER_WARNING
);
}
}
/**
* Check for existence of config directory which should not exist in
* production environment.
*/
if (@file_exists(ROOT_PATH . 'config')) {
trigger_error(
__(
'Directory [code]config[/code], which is used by the setup script, ' .
'still exists in your phpMyAdmin directory. It is strongly ' .
'recommended to remove it once phpMyAdmin has been configured. ' .
'Otherwise the security of your server may be compromised by ' .
'unauthorized people downloading your configuration.'
),
E_USER_WARNING
);
}
/**
* Warning about Suhosin only if its simulation mode is not enabled
*/
if ($cfg['SuhosinDisableWarning'] == false
&& ini_get('suhosin.request.max_value_length')
&& ini_get('suhosin.simulation') == '0'
) {
trigger_error(
sprintf(
__(
'Server running with Suhosin. Please refer ' .
'to %sdocumentation%s for possible issues.'
),
'[doc@faq1-38]',
'[/doc]'
),
E_USER_WARNING
);
}
/* Missing template cache */
if ($this->config->getTempDir('twig') === null) {
trigger_error(
sprintf(
__(
'The $cfg[\'TempDir\'] (%s) is not accessible. ' .
'phpMyAdmin is not able to cache templates and will ' .
'be slow because of this.'
),
$this->config->get('TempDir')
),
E_USER_WARNING
);
}
/**
* Warning about incomplete translations.
*
* The data file is created while creating release by ./scripts/remove-incomplete-mo
*/
if (! @file_exists(ROOT_PATH . 'libraries/language_stats.inc.php')) {
return;
}
include ROOT_PATH . 'libraries/language_stats.inc.php';
/*
* This message is intentionally not translated, because we're
* handling incomplete translations here and focus on english
* speaking users.
*/
if (! isset($GLOBALS['language_stats'][$lang])
|| $GLOBALS['language_stats'][$lang] >= $cfg['TranslationWarningThreshold']
) {
return;
}
trigger_error(
'You are using an incomplete translation, please help to make it '
. 'better by [a@https://www.phpmyadmin.net/translate/'
. '@_blank]contributing[/a].',
E_USER_NOTICE
);
}
}

View file

@ -0,0 +1,884 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Bookmark;
use PhpMyAdmin\Console;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\File;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Import;
use PhpMyAdmin\Message;
use PhpMyAdmin\ParseAnalyze;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\ImportPlugin;
use PhpMyAdmin\Response;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use Throwable;
use function define;
use function htmlspecialchars;
use function in_array;
use function ini_get;
use function ini_set;
use function intval;
use function is_array;
use function is_link;
use function is_uploaded_file;
use function mb_strlen;
use function mb_strtolower;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function strlen;
use function substr;
use function time;
use function trim;
final class ImportController extends AbstractController
{
/** @var Import */
private $import;
/** @var Sql */
private $sql;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Import $import, Sql $sql, $dbi)
{
parent::__construct($response, $template);
$this->import = $import;
$this->sql = $sql;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $collation_connection, $db, $import_type, $table, $goto, $display_query, $PMA_Theme;
global $format, $local_import_file, $ajax_reload, $import_text, $sql_query, $message, $err_url, $url_params;
global $memory_limit, $read_limit, $finished, $offset, $charset_conversion, $charset_of_file;
global $timestamp, $maximum_time, $timeout_passed, $import_file, $go_sql, $sql_file, $error, $max_sql_len, $msg;
global $sql_query_disabled, $executed_queries, $run_query, $reset_charset, $bookmark_created;
global $result, $import_file_name, $sql_data, $import_notice, $read_multiply, $my_die, $active_page;
global $show_as_php, $reload, $charset_connection, $is_js_confirmed, $MAX_FILE_SIZE, $message_to_show;
global $noplugin, $skip_queries;
$charset_of_file = $_POST['charset_of_file'] ?? null;
$format = $_POST['format'] ?? '';
$import_type = $_POST['import_type'] ?? null;
$is_js_confirmed = $_POST['is_js_confirmed'] ?? null;
$MAX_FILE_SIZE = $_POST['MAX_FILE_SIZE'] ?? null;
$message_to_show = $_POST['message_to_show'] ?? null;
$noplugin = $_POST['noplugin'] ?? null;
$skip_queries = $_POST['skip_queries'] ?? null;
$local_import_file = $_POST['local_import_file'] ?? null;
$show_as_php = $_POST['show_as_php'] ?? null;
/* Enable LOAD DATA LOCAL INFILE for LDI plugin */
if ($format === 'ldi') {
define('PMA_ENABLE_LDI', 1);
}
// If there is a request to 'Simulate DML'.
if (isset($_POST['simulate_dml'])) {
$this->import->handleSimulateDmlRequest();
return;
}
// If it's a refresh console bookmarks request
if (isset($_GET['console_bookmark_refresh'])) {
$this->response->addJSON(
'console_message_bookmark',
Console::getBookmarkContent()
);
return;
}
// If it's a console bookmark add request
if (isset($_POST['console_bookmark_add'])) {
if (! isset($_POST['label'], $_POST['db'], $_POST['bookmark_query'], $_POST['shared'])) {
$this->response->addJSON('message', __('Incomplete params'));
return;
}
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$bookmarkFields = [
'bkm_database' => $_POST['db'],
'bkm_user' => $cfgBookmark['user'],
'bkm_sql_query' => $_POST['bookmark_query'],
'bkm_label' => $_POST['label'],
];
$isShared = ($_POST['shared'] === 'true');
$bookmark = Bookmark::createBookmark(
$this->dbi,
$cfg['Server']['user'],
$bookmarkFields,
$isShared
);
if ($bookmark !== false && $bookmark->save()) {
$this->response->addJSON('message', __('Succeeded'));
$this->response->addJSON('data', $bookmarkFields);
$this->response->addJSON('isShared', $isShared);
} else {
$this->response->addJSON('message', __('Failed'));
}
return;
}
// reset import messages for ajax request
$_SESSION['Import_message']['message'] = null;
$_SESSION['Import_message']['go_back_url'] = null;
// default values
$reload = false;
// Use to identify current cycle is executing
// a multiquery statement or stored routine
if (! isset($_SESSION['is_multi_query'])) {
$_SESSION['is_multi_query'] = false;
}
$ajax_reload = [];
$import_text = '';
// Are we just executing plain query or sql file?
// (eg. non import, but query box/window run)
if (! empty($sql_query)) {
// apply values for parameters
if (! empty($_POST['parameterized'])
&& ! empty($_POST['parameters'])
&& is_array($_POST['parameters'])
) {
$parameters = $_POST['parameters'];
foreach ($parameters as $parameter => $replacement) {
$quoted = preg_quote($parameter, '/');
// making sure that :param does not apply values to :param1
$sql_query = preg_replace(
'/' . $quoted . '([^a-zA-Z0-9_])/',
$this->dbi->escapeString($replacement) . '${1}',
$sql_query
);
// for parameters the appear at the end of the string
$sql_query = preg_replace(
'/' . $quoted . '$/',
$this->dbi->escapeString($replacement),
$sql_query
);
}
}
// run SQL query
$import_text = $sql_query;
$import_type = 'query';
$format = 'sql';
$_SESSION['sql_from_query_box'] = true;
// If there is a request to ROLLBACK when finished.
if (isset($_POST['rollback_query'])) {
$this->import->handleRollbackRequest($import_text);
}
// refresh navigation and main panels
if (preg_match('/^(DROP)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i', $sql_query)) {
$reload = true;
$ajax_reload['reload'] = true;
}
// refresh navigation panel only
if (preg_match(
'/^(CREATE|ALTER)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$sql_query
)) {
$ajax_reload['reload'] = true;
}
// do a dynamic reload if table is RENAMED
// (by sending the instruction to the AJAX response handler)
if (preg_match(
'/^RENAME\s+TABLE\s+(.*?)\s+TO\s+(.*?)($|;|\s)/i',
$sql_query,
$rename_table_names
)) {
$ajax_reload['reload'] = true;
$ajax_reload['table_name'] = Util::unQuote(
$rename_table_names[2]
);
}
$sql_query = '';
} elseif (! empty($sql_file)) {
// run uploaded SQL file
$import_file = $sql_file;
$import_type = 'queryfile';
$format = 'sql';
unset($sql_file);
} elseif (! empty($_POST['id_bookmark'])) {
// run bookmark
$import_type = 'query';
$format = 'sql';
}
// If we didn't get any parameters, either user called this directly, or
// upload limit has been reached, let's assume the second possibility.
if ($_POST == [] && $_GET == []) {
$message = Message::error(
__(
'You probably tried to upload a file that is too large. Please refer ' .
'to %sdocumentation%s for a workaround for this limit.'
)
);
$message->addParam('[doc@faq1-16]');
$message->addParam('[/doc]');
// so we can obtain the message
$_SESSION['Import_message']['message'] = $message->getDisplay();
$_SESSION['Import_message']['go_back_url'] = $goto;
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
return; // the footer is displayed automatically
}
// Add console message id to response output
if (isset($_POST['console_message_id'])) {
$this->response->addJSON('console_message_id', $_POST['console_message_id']);
}
/**
* Sets globals from $_POST patterns, for import plugins
* We only need to load the selected plugin
*/
if (! in_array(
$format,
[
'csv',
'ldi',
'mediawiki',
'ods',
'shp',
'sql',
'xml',
]
)
) {
// this should not happen for a normal user
// but only during an attack
Core::fatalError('Incorrect format parameter');
}
$post_patterns = [
'/^force_file_/',
'/^' . $format . '_/',
];
Core::setPostAsGlobal($post_patterns);
// Check needed parameters
Util::checkParameters(['import_type', 'format']);
// We don't want anything special in format
$format = Core::securePath($format);
if (strlen($table) > 0 && strlen($db) > 0) {
$url_params = [
'db' => $db,
'table' => $table,
];
} elseif (strlen($db) > 0) {
$url_params = ['db' => $db];
} else {
$url_params = [];
}
// Create error and goto url
if ($import_type === 'table') {
$goto = Url::getFromRoute('/table/import');
} elseif ($import_type === 'database') {
$goto = Url::getFromRoute('/database/import');
} elseif ($import_type === 'server') {
$goto = Url::getFromRoute('/server/import');
} elseif (empty($goto) || ! preg_match('@^index\.php$@i', $goto)) {
if (strlen($table) > 0 && strlen($db) > 0) {
$goto = Url::getFromRoute('/table/structure');
} elseif (strlen($db) > 0) {
$goto = Url::getFromRoute('/database/structure');
} else {
$goto = Url::getFromRoute('/server/sql');
}
}
$err_url = $goto . Url::getCommon($url_params, '&');
$_SESSION['Import_message']['go_back_url'] = $err_url;
if (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
Util::setTimeLimit();
if (! empty($cfg['MemoryLimit'])) {
ini_set('memory_limit', $cfg['MemoryLimit']);
}
$timestamp = time();
if (isset($_POST['allow_interrupt'])) {
$maximum_time = ini_get('max_execution_time');
} else {
$maximum_time = 0;
}
// set default values
$timeout_passed = false;
$error = false;
$read_multiply = 1;
$finished = false;
$offset = 0;
$max_sql_len = 0;
$sql_query = '';
$sql_query_disabled = false;
$go_sql = false;
$executed_queries = 0;
$run_query = true;
$charset_conversion = false;
$reset_charset = false;
$bookmark_created = false;
$msg = 'Sorry an unexpected error happened!';
/** @var mixed|bool $result */
$result = false;
// Bookmark Support: get a query back from bookmark if required
if (! empty($_POST['id_bookmark'])) {
$id_bookmark = (int) $_POST['id_bookmark'];
switch ($_POST['action_bookmark']) {
case 0: // bookmarked query that have to be run
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark,
'id',
isset($_POST['action_bookmark_all'])
);
if (! $bookmark instanceof Bookmark) {
break;
}
if (! empty($_POST['bookmark_variable'])) {
$import_text = $bookmark->applyVariables(
$_POST['bookmark_variable']
);
} else {
$import_text = $bookmark->getQuery();
}
// refresh navigation and main panels
if (preg_match(
'/^(DROP)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$import_text
)) {
$reload = true;
$ajax_reload['reload'] = true;
}
// refresh navigation panel only
if (preg_match(
'/^(CREATE|ALTER)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$import_text
)
) {
$ajax_reload['reload'] = true;
}
break;
case 1: // bookmarked query that have to be displayed
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark
);
if (! $bookmark instanceof Bookmark) {
break;
}
$import_text = $bookmark->getQuery();
if ($this->response->isAjax()) {
$message = Message::success(__('Showing bookmark'));
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('sql_query', $import_text);
$this->response->addJSON('action_bookmark', $_POST['action_bookmark']);
return;
} else {
$run_query = false;
}
break;
case 2: // bookmarked query that have to be deleted
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark
);
if (! $bookmark instanceof Bookmark) {
break;
}
$bookmark->delete();
if ($this->response->isAjax()) {
$message = Message::success(
__('The bookmark has been deleted.')
);
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('action_bookmark', $_POST['action_bookmark']);
$this->response->addJSON('id_bookmark', $id_bookmark);
return;
} else {
$run_query = false;
$error = true; // this is kind of hack to skip processing the query
}
break;
}
}
// Do no run query if we show PHP code
if (isset($show_as_php)) {
$run_query = false;
$go_sql = true;
}
// We can not read all at once, otherwise we can run out of memory
$memory_limit = trim((string) ini_get('memory_limit'));
// 2 MB as default
if (empty($memory_limit)) {
$memory_limit = 2 * 1024 * 1024;
}
// In case no memory limit we work on 10MB chunks
if ($memory_limit == -1) {
$memory_limit = 10 * 1024 * 1024;
}
// Calculate value of the limit
$memoryUnit = mb_strtolower(substr((string) $memory_limit, -1));
if ($memoryUnit === 'm') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024 * 1024;
} elseif ($memoryUnit === 'k') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024;
} elseif ($memoryUnit === 'g') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024 * 1024 * 1024;
} else {
$memory_limit = (int) $memory_limit;
}
// Just to be sure, there might be lot of memory needed for uncompression
$read_limit = $memory_limit / 8;
// handle filenames
if (isset($_FILES['import_file'])) {
$import_file = $_FILES['import_file']['tmp_name'];
$import_file_name = $_FILES['import_file']['name'];
}
if (! empty($local_import_file) && ! empty($cfg['UploadDir'])) {
// sanitize $local_import_file as it comes from a POST
$local_import_file = Core::securePath($local_import_file);
$import_file = Util::userDir($cfg['UploadDir'])
. $local_import_file;
/*
* Do not allow symlinks to avoid security issues
* (user can create symlink to file they can not access,
* but phpMyAdmin can).
*/
if (@is_link($import_file)) {
$import_file = 'none';
}
} elseif (empty($import_file) || ! is_uploaded_file($import_file)) {
$import_file = 'none';
}
// Do we have file to import?
if ($import_file !== 'none' && ! $error) {
/**
* Handle file compression
*/
$importHandle = new File($import_file);
$importHandle->checkUploadedFile();
if ($importHandle->isError()) {
/** @var Message $errorMessage */
$errorMessage = $importHandle->getError();
$importHandle->close();
$_SESSION['Import_message']['message'] = $errorMessage->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $errorMessage->getDisplay());
$this->response->addHTML($errorMessage->getDisplay());
return;
}
$importHandle->setDecompressContent(true);
$importHandle->open();
if ($importHandle->isError()) {
/** @var Message $errorMessage */
$errorMessage = $importHandle->getError();
$importHandle->close();
$_SESSION['Import_message']['message'] = $errorMessage->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $errorMessage->getDisplay());
$this->response->addHTML($errorMessage->getDisplay());
return;
}
} elseif (! $error && (! isset($import_text) || empty($import_text))) {
$message = Message::error(
__(
'No data was received to import. Either no file name was ' .
'submitted, or the file size exceeded the maximum size permitted ' .
'by your PHP configuration. See [doc@faq1-16]FAQ 1.16[/doc].'
)
);
$_SESSION['Import_message']['message'] = $message->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
$this->response->addHTML($message->getDisplay());
return;
}
// Convert the file's charset if necessary
if (Encoding::isSupported() && isset($charset_of_file)) {
if ($charset_of_file !== 'utf-8') {
$charset_conversion = true;
}
} elseif (isset($charset_of_file) && $charset_of_file !== 'utf-8') {
$this->dbi->query('SET NAMES \'' . $charset_of_file . '\'');
// We can not show query in this case, it is in different charset
$sql_query_disabled = true;
$reset_charset = true;
}
// Something to skip? (because timeout has passed)
if (! $error && isset($_POST['skip'])) {
$original_skip = $skip = intval($_POST['skip']);
while ($skip > 0 && ! $finished) {
$this->import->getNextChunk($importHandle ?? null, $skip < $read_limit ? $skip : $read_limit);
// Disable read progressivity, otherwise we eat all memory!
$read_multiply = 1;
$skip -= $read_limit;
}
unset($skip);
}
// This array contain the data like number of valid sql queries in the statement
// and complete valid sql statement (which affected for rows)
$sql_data = [
'valid_sql' => [],
'valid_queries' => 0,
];
if (! $error) {
/**
* @var ImportPlugin $import_plugin
*/
$import_plugin = Plugins::getPlugin(
'import',
$format,
'libraries/classes/Plugins/Import/',
$import_type
);
if ($import_plugin == null) {
$message = Message::error(
__('Could not load import plugins, please check your installation!')
);
$_SESSION['Import_message']['message'] = $message->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
$this->response->addHTML($message->getDisplay());
return;
}
// Do the real import
$default_fk_check = Util::handleDisableFKCheckInit();
try {
$import_plugin->doImport($importHandle ?? null, $sql_data);
Util::handleDisableFKCheckCleanup($default_fk_check);
} catch (Throwable $e) {
Util::handleDisableFKCheckCleanup($default_fk_check);
throw $e;
}
}
if (isset($importHandle)) {
$importHandle->close();
}
// Reset charset back, if we did some changes
if ($reset_charset) {
$this->dbi->query('SET CHARACTER SET ' . $charset_connection);
$this->dbi->setCollation($collation_connection);
}
// Show correct message
if (! empty($id_bookmark) && $_POST['action_bookmark'] == 2) {
$message = Message::success(__('The bookmark has been deleted.'));
$display_query = $import_text;
$error = false; // unset error marker, it was used just to skip processing
} elseif (! empty($id_bookmark) && $_POST['action_bookmark'] == 1) {
$message = Message::notice(__('Showing bookmark'));
} elseif ($bookmark_created) {
$special_message = '[br]' . sprintf(
__('Bookmark %s has been created.'),
htmlspecialchars($_POST['bkm_label'])
);
} elseif ($finished && ! $error) {
// Do not display the query with message, we do it separately
$display_query = ';';
if ($import_type !== 'query') {
$message = Message::success(
'<em>'
. _ngettext(
'Import has been successfully finished, %d query executed.',
'Import has been successfully finished, %d queries executed.',
$executed_queries
)
. '</em>'
);
$message->addParam($executed_queries);
if (! empty($import_notice)) {
$message->addHtml($import_notice);
}
if (! empty($local_import_file)) {
$message->addText('(' . $local_import_file . ')');
} else {
$message->addText('(' . $_FILES['import_file']['name'] . ')');
}
}
}
// Did we hit timeout? Tell it user.
if ($timeout_passed) {
$url_params['timeout_passed'] = '1';
$url_params['offset'] = $offset;
if (isset($local_import_file)) {
$url_params['local_import_file'] = $local_import_file;
}
$importUrl = $err_url = $goto . Url::getCommon($url_params, '&');
$message = Message::error(
__(
'Script timeout passed, if you want to finish import,'
. ' please %sresubmit the same file%s and import will resume.'
)
);
$message->addParamHtml('<a href="' . $importUrl . '">');
$message->addParamHtml('</a>');
if ($offset == 0 || (isset($original_skip) && $original_skip == $offset)) {
$message->addText(
__(
'However on last run no data has been parsed,'
. ' this usually means phpMyAdmin won\'t be able to'
. ' finish this import unless you increase php time limits.'
)
);
}
}
// if there is any message, copy it into $_SESSION as well,
// so we can obtain it by AJAX call
if (isset($message)) {
$_SESSION['Import_message']['message'] = $message->getDisplay();
}
// Parse and analyze the query, for correct db and table name
// in case of a query typed in the query window
// (but if the query is too large, in case of an imported file, the parser
// can choke on it so avoid parsing)
$sqlLength = mb_strlen($sql_query);
if ($sqlLength <= $cfg['MaxCharactersInDisplayedSQL']) {
[
$analyzed_sql_results,
$db,
$table_from_sql,
] = ParseAnalyze::sqlQuery($sql_query, $db);
$reload = $analyzed_sql_results['reload'];
$offset = $analyzed_sql_results['offset'];
if ($table != $table_from_sql && ! empty($table_from_sql)) {
$table = $table_from_sql;
}
}
// There was an error?
if (isset($my_die)) {
foreach ($my_die as $key => $die) {
Generator::mysqlDie(
$die['error'],
$die['sql'],
false,
$err_url,
$error
);
}
}
if ($go_sql) {
if (! empty($sql_data) && ($sql_data['valid_queries'] > 1)) {
$_SESSION['is_multi_query'] = true;
$sql_queries = $sql_data['valid_sql'];
} else {
$sql_queries = [$sql_query];
}
$html_output = '';
foreach ($sql_queries as $sql_query) {
// parse sql query
[
$analyzed_sql_results,
$db,
$table_from_sql,
] = ParseAnalyze::sqlQuery($sql_query, $db);
$offset = $analyzed_sql_results['offset'];
$reload = $analyzed_sql_results['reload'];
// Check if User is allowed to issue a 'DROP DATABASE' Statement
if ($this->sql->hasNoRightsToDropDatabase(
$analyzed_sql_results,
$cfg['AllowUserDropDatabase'],
$this->dbi->isSuperUser()
)) {
Generator::mysqlDie(
__('"DROP DATABASE" statements are disabled.'),
'',
false,
$_SESSION['Import_message']['go_back_url']
);
return;
}
if ($table != $table_from_sql && ! empty($table_from_sql)) {
$table = $table_from_sql;
}
$html_output .= $this->sql->executeQueryAndGetQueryResponse(
$analyzed_sql_results, // analyzed_sql_results
false, // is_gotofile
$db, // db
$table, // table
null, // find_real_end
null, // sql_query_for_bookmark - see below
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
$PMA_Theme->getImgPath(),
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
);
}
// sql_query_for_bookmark is not included in Sql::executeQueryAndGetQueryResponse
// since only one bookmark has to be added for all the queries submitted through
// the SQL tab
if (! empty($_POST['bkm_label']) && ! empty($import_text)) {
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$this->sql->storeTheQueryAsBookmark(
$db,
$cfgBookmark['user'],
$_POST['sql_query'],
$_POST['bkm_label'],
isset($_POST['bkm_replace'])
);
}
$this->response->addJSON('ajax_reload', $ajax_reload);
$this->response->addHTML($html_output);
return;
}
if ($result) {
// Save a Bookmark with more than one queries (if Bookmark label given).
if (! empty($_POST['bkm_label']) && ! empty($import_text)) {
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$this->sql->storeTheQueryAsBookmark(
$db,
$cfgBookmark['user'],
$_POST['sql_query'],
$_POST['bkm_label'],
isset($_POST['bkm_replace'])
);
}
$this->response->setRequestStatus(true);
$this->response->addJSON('message', Message::success($msg));
$this->response->addJSON(
'sql_query',
Generator::getMessage($msg, $sql_query, 'success')
);
} elseif ($result === false) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error($msg));
} else {
$active_page = $goto;
include ROOT_PATH . $goto;
}
// If there is request for ROLLBACK in the end.
if (! isset($_POST['rollback_query'])) {
return;
}
$this->dbi->query('ROLLBACK');
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Import\Ajax;
use PhpMyAdmin\Message;
use PhpMyAdmin\Template;
use function header;
use function ini_get;
use function session_start;
use function session_write_close;
use function time;
use function usleep;
/**
* Import progress bar backend
*/
class ImportStatusController
{
/** @var Template */
private $template;
/**
* @param Template $template Template object
*/
public function __construct(Template $template)
{
$this->template = $template;
}
public function index(): void
{
global $SESSION_KEY, $upload_id, $plugins, $timestamp;
[
$SESSION_KEY,
$upload_id,
$plugins,
] = Ajax::uploadProgressSetup();
// $_GET["message"] is used for asking for an import message
if (isset($_GET['message']) && $_GET['message']) {
// AJAX requests can't be cached!
Core::noCacheHeader();
header('Content-type: text/html');
// wait 0.3 sec before we check for $_SESSION variable
usleep(300000);
$maximumTime = ini_get('max_execution_time');
$timestamp = time();
// wait until message is available
while (($_SESSION['Import_message']['message'] ?? null) == null) {
// close session before sleeping
session_write_close();
// sleep
usleep(250000); // 0.25 sec
// reopen session
session_start();
if (time() - $timestamp > $maximumTime) {
$_SESSION['Import_message']['message'] = Message::error(
__('Could not load the progress of the import.')
)->getDisplay();
break;
}
}
echo $_SESSION['Import_message']['message'] ?? '';
if (isset($_SESSION['Import_message']['go_back_url'])) {
echo $this->template->render('import_status', [
'go_back_url' => $_SESSION['Import_message']['go_back_url'],
]);
}
} else {
Ajax::status($_GET['id']);
}
}
}

View file

@ -0,0 +1,738 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function json_encode;
/**
* Exporting of translated messages from PHP to JavaScript.
*/
final class JavaScriptMessagesController
{
/** @var array<string, string> */
private $messages = [];
public function __construct()
{
$this->setMessages();
}
public function index(): void
{
echo 'var Messages = ' . json_encode($this->messages) . ';';
}
private function setMessages(): void
{
global $cfg, $PMA_Theme;
$ajaxClockSmallGifPath = $PMA_Theme !== null ? $PMA_Theme->getImgPath('ajax_clock_small.gif') : '';
$this->messages = [
/* For confirmations */
'strConfirm' => __('Confirm'),
'strDoYouReally' => __('Do you really want to execute "%s"?'),
'strDropDatabaseStrongWarning' => __('You are about to DESTROY a complete database!'),
'strDatabaseRenameToSameName' => __(
'Cannot rename database to the same name. Change the name and try again'
),
'strDropTableStrongWarning' => __('You are about to DESTROY a complete table!'),
'strTruncateTableStrongWarning' => __('You are about to TRUNCATE a complete table!'),
'strDeleteTrackingData' => __('Delete tracking data for this table?'),
'strDeleteTrackingDataMultiple' => __('Delete tracking data for these tables?'),
'strDeleteTrackingVersion' => __('Delete tracking data for this version?'),
'strDeleteTrackingVersionMultiple' => __('Delete tracking data for these versions?'),
'strDeletingTrackingEntry' => __('Delete entry from tracking report?'),
'strDeletingTrackingData' => __('Deleting tracking data'),
'strDroppingPrimaryKeyIndex' => __('Dropping Primary Key/Index'),
'strDroppingForeignKey' => __('Dropping Foreign key.'),
'strOperationTakesLongTime' => __('This operation could take a long time. Proceed anyway?'),
'strDropUserGroupWarning' => __('Do you really want to delete user group "%s"?'),
'strConfirmDeleteQBESearch' => __('Do you really want to delete the search "%s"?'),
'strConfirmNavigation' => __('You have unsaved changes; are you sure you want to leave this page?'),
'strConfirmRowChange' => __(
'You are trying to reduce the number of rows, but have already entered'
. ' data in those rows which will be lost. Do you wish to continue?'
),
'strDropUserWarning' => __('Do you really want to revoke the selected user(s) ?'),
'strDeleteCentralColumnWarning' => __('Do you really want to delete this central column?'),
'strDropRTEitems' => __('Do you really want to delete the selected items?'),
'strDropPartitionWarning' => __(
'Do you really want to DROP the selected partition(s)? This will also DELETE ' .
'the data related to the selected partition(s)!'
),
'strTruncatePartitionWarning' => __('Do you really want to TRUNCATE the selected partition(s)?'),
'strRemovePartitioningWarning' => __('Do you really want to remove partitioning?'),
'strResetSlaveWarning' => __('Do you really want to RESET SLAVE?'),
'strChangeColumnCollation' => __(
'This operation will attempt to convert your data to the new collation. In '
. 'rare cases, especially where a character doesn\'t exist in the new '
. 'collation, this process could cause the data to appear incorrectly under '
. 'the new collation; in this case we suggest you revert to the original '
. 'collation and refer to the tips at '
)
. '<a href="%s" target="garbled_data_wiki">' . __('Garbled Data') . '</a>.'
. '<br><br>'
. __('Are you sure you wish to change the collation and convert the data?'),
'strChangeAllColumnCollationsWarning' => __(
'Through this operation, MySQL attempts to map the data values between '
. 'collations. If the character sets are incompatible, there may be data loss '
. 'and this lost data may <b>NOT</b> be recoverable simply by changing back the '
. 'column collation(s). <b>To convert existing data, it is suggested to use the '
. 'column(s) editing feature (the "Change" Link) on the table structure page. '
. '</b>'
)
. '<br><br>'
. __(
'Are you sure you wish to change all the column collations and convert the data?'
),
/* For modal dialog buttons */
'strSaveAndClose' => __('Save & close'),
'strReset' => __('Reset'),
'strResetAll' => __('Reset all'),
/* For indexes */
'strFormEmpty' => __('Missing value in the form!'),
'strRadioUnchecked' => __('Select at least one of the options!'),
'strEnterValidNumber' => __('Please enter a valid number!'),
'strEnterValidLength' => __('Please enter a valid length!'),
'strAddIndex' => __('Add index'),
'strEditIndex' => __('Edit index'),
/* l10n: Rename a table Index */
'strRenameIndex' => __('Rename index'),
'strAddToIndex' => __('Add %s column(s) to index'),
'strCreateSingleColumnIndex' => __('Create single-column index'),
'strCreateCompositeIndex' => __('Create composite index'),
'strCompositeWith' => __('Composite with:'),
'strMissingColumn' => __('Please select column(s) for the index.'),
/* For Preview SQL*/
'strPreviewSQL' => __('Preview SQL'),
/* For Simulate DML*/
'strSimulateDML' => __('Simulate query'),
'strMatchedRows' => __('Matched rows:'),
'strSQLQuery' => __('SQL query:'),
/* Charts */
/* l10n: Default label for the y-Axis of Charts */
'strYValues' => __('Y values'),
/* Database multi-table query */
'strEmptyQuery' => __('Please enter the SQL query first.'),
/* For server/privileges.js */
'strHostEmpty' => __('The host name is empty!'),
'strUserEmpty' => __('The user name is empty!'),
'strPasswordEmpty' => __('The password is empty!'),
'strPasswordNotSame' => __('The passwords aren\'t the same!'),
'strRemovingSelectedUsers' => __('Removing Selected Users'),
'strClose' => __('Close'),
/* For export.js */
'strTemplateCreated' => __('Template was created.'),
'strTemplateLoaded' => __('Template was loaded.'),
'strTemplateUpdated' => __('Template was updated.'),
'strTemplateDeleted' => __('Template was deleted.'),
/* l10n: Other, small valued, queries */
'strOther' => __('Other'),
/* l10n: Thousands separator */
'strThousandsSeparator' => __(','),
/* l10n: Decimal separator */
'strDecimalSeparator' => __('.'),
'strChartConnectionsTitle' => __('Connections / Processes'),
/* server status monitor */
'strIncompatibleMonitorConfig' => __('Local monitor configuration incompatible!'),
'strIncompatibleMonitorConfigDescription' => __(
'The chart arrangement configuration in your browsers local storage is not '
. 'compatible anymore to the newer version of the monitor dialog. It is very '
. 'likely that your current configuration will not work anymore. Please reset '
. 'your configuration to default in the <i>Settings</i> menu.'
),
'strQueryCacheEfficiency' => __('Query cache efficiency'),
'strQueryCacheUsage' => __('Query cache usage'),
'strQueryCacheUsed' => __('Query cache used'),
'strSystemCPUUsage' => __('System CPU usage'),
'strSystemMemory' => __('System memory'),
'strSystemSwap' => __('System swap'),
'strAverageLoad' => __('Average load'),
'strTotalMemory' => __('Total memory'),
'strCachedMemory' => __('Cached memory'),
'strBufferedMemory' => __('Buffered memory'),
'strFreeMemory' => __('Free memory'),
'strUsedMemory' => __('Used memory'),
'strTotalSwap' => __('Total swap'),
'strCachedSwap' => __('Cached swap'),
'strUsedSwap' => __('Used swap'),
'strFreeSwap' => __('Free swap'),
'strBytesSent' => __('Bytes sent'),
'strBytesReceived' => __('Bytes received'),
'strConnections' => __('Connections'),
'strProcesses' => __('Processes'),
/* summary row */
'strB' => __('B'),
'strKiB' => __('KiB'),
'strMiB' => __('MiB'),
'strGiB' => __('GiB'),
'strTiB' => __('TiB'),
'strPiB' => __('PiB'),
'strEiB' => __('EiB'),
'strNTables' => __('%d table(s)'),
/* l10n: Questions is the name of a MySQL Status variable */
'strQuestions' => __('Questions'),
'strTraffic' => __('Traffic'),
'strSettings' => __('Settings'),
'strAddChart' => __('Add chart to grid'),
'strAddOneSeriesWarning' => __('Please add at least one variable to the series!'),
'strNone' => __('None'),
/* l10n: SQL Query on modal to show exported query */
'strQuery' => __('SQL Query'),
'strResumeMonitor' => __('Resume monitor'),
'strPauseMonitor' => __('Pause monitor'),
'strStartRefresh' => __('Start auto refresh'),
'strStopRefresh' => __('Stop auto refresh'),
/* Monitor: Instructions Dialog */
'strBothLogOn' => __('general_log and slow_query_log are enabled.'),
'strGenLogOn' => __('general_log is enabled.'),
'strSlowLogOn' => __('slow_query_log is enabled.'),
'strBothLogOff' => __('slow_query_log and general_log are disabled.'),
'strLogOutNotTable' => __('log_output is not set to TABLE.'),
'strLogOutIsTable' => __('log_output is set to TABLE.'),
'strSmallerLongQueryTimeAdvice' => __(
'slow_query_log is enabled, but the server logs only queries that take longer '
. 'than %d seconds. It is advisable to set this long_query_time 0-2 seconds, '
. 'depending on your system.'
),
'strLongQueryTimeSet' => __('long_query_time is set to %d second(s).'),
'strSettingsAppliedGlobal' => __(
'Following settings will be applied globally and reset to default on server '
. 'restart:'
),
/* l10n: %s is FILE or TABLE */
'strSetLogOutput' => __('Set log_output to %s'),
/* l10n: Enable in this context means setting a status variable to ON */
'strEnableVar' => __('Enable %s'),
/* l10n: Disable in this context means setting a status variable to OFF */
'strDisableVar' => __('Disable %s'),
/* l10n: %d seconds */
'setSetLongQueryTime' => __('Set long_query_time to %d seconds.'),
'strNoSuperUser' => __(
'You can\'t change these variables. Please log in as root or contact'
. ' your database administrator.'
),
'strChangeSettings' => __('Change settings'),
'strCurrentSettings' => __('Current settings'),
'strChartTitle' => __('Chart title'),
/* l10n: As in differential values */
'strDifferential' => __('Differential'),
'strDividedBy' => __('Divided by %s'),
'strUnit' => __('Unit'),
'strFromSlowLog' => __('From slow log'),
'strFromGeneralLog' => __('From general log'),
'strServerLogError' => __(
'The database name is not known for this query in the server\'s logs.'
),
'strAnalysingLogsTitle' => __('Analysing logs'),
'strAnalysingLogs' => __('Analysing & loading logs. This may take a while.'),
'strCancelRequest' => __('Cancel request'),
'strCountColumnExplanation' => __(
'This column shows the amount of identical queries that are grouped together. '
. 'However only the SQL query itself has been used as a grouping criteria, so '
. 'the other attributes of queries, such as start time, may differ.'
),
'strMoreCountColumnExplanation' => __(
'Since grouping of INSERTs queries has been selected, INSERT queries into the '
. 'same table are also being grouped together, disregarding of the inserted '
. 'data.'
),
'strLogDataLoaded' => __('Log data loaded. Queries executed in this time span:'),
'strJumpToTable' => __('Jump to Log table'),
'strNoDataFoundTitle' => __('No data found'),
'strNoDataFound' => __('Log analysed, but no data found in this time span.'),
'strAnalyzing' => __('Analyzing…'),
'strExplainOutput' => __('Explain output'),
'strStatus' => __('Status'),
'strTime' => __('Time'),
'strTotalTime' => __('Total time:'),
'strProfilingResults' => __('Profiling results'),
'strTable' => _pgettext('Display format', 'Table'),
'strChart' => __('Chart'),
'strAliasDatabase' => _pgettext('Alias', 'Database'),
'strAliasTable' => _pgettext('Alias', 'Table'),
'strAliasColumn' => _pgettext('Alias', 'Column'),
/* l10n: A collection of available filters */
'strFiltersForLogTable' => __('Log table filter options'),
/* l10n: Filter as in "Start Filtering" */
'strFilter' => __('Filter'),
'strFilterByWordRegexp' => __('Filter queries by word/regexp:'),
'strIgnoreWhereAndGroup' => __('Group queries, ignoring variable data in WHERE clauses'),
'strSumRows' => __('Sum of grouped rows:'),
'strTotal' => __('Total:'),
'strLoadingLogs' => __('Loading logs'),
'strRefreshFailed' => __('Monitor refresh failed'),
'strInvalidResponseExplanation' => __(
'While requesting new chart data the server returned an invalid response. This '
. 'is most likely because your session expired. Reloading the page and '
. 'reentering your credentials should help.'
),
'strReloadPage' => __('Reload page'),
'strAffectedRows' => __('Affected rows:'),
'strFailedParsingConfig' => __(
'Failed parsing config file. It doesn\'t seem to be valid JSON code.'
),
'strFailedBuildingGrid' => __(
'Failed building chart grid with imported config. Resetting to default config…'
),
'strImport' => __('Import'),
'strImportDialogTitle' => __('Import monitor configuration'),
'strImportDialogMessage' => __('Please select the file you want to import.'),
'strTableNameDialogMessage' => __('Please enter a valid table name.'),
'strDBNameDialogMessage' => __('Please enter a valid database name.'),
'strNoImportFile' => __('No files available on server for import!'),
'strAnalyzeQuery' => __('Analyse query'),
/* For query editor */
'strFormatting' => __('Formatting SQL…'),
'strNoParam' => __('No parameters found!'),
/* For inline query editing */
'strGo' => __('Go'),
'strCancel' => __('Cancel'),
/* For page-related settings */
'strPageSettings' => __('Page-related settings'),
'strApply' => __('Apply'),
/* For Ajax Notifications */
'strLoading' => __('Loading…'),
'strAbortedRequest' => __('Request aborted!!'),
'strProcessingRequest' => __('Processing request'),
'strRequestFailed' => __('Request failed!!'),
'strErrorProcessingRequest' => __('Error in processing request'),
'strErrorCode' => __('Error code: %s'),
'strErrorText' => __('Error text: %s'),
'strErrorConnection' => __(
'It seems that the connection to server has been lost. Please check your ' .
'network connectivity and server status.'
),
'strNoDatabasesSelected' => __('No databases selected.'),
'strNoAccountSelected' => __('No accounts selected.'),
'strDroppingColumn' => __('Dropping column'),
'strAddingPrimaryKey' => __('Adding primary key'),
'strOK' => __('OK'),
'strDismiss' => __('Click to dismiss this notification'),
/* For database/operations.js */
'strRenamingDatabases' => __('Renaming databases'),
'strCopyingDatabase' => __('Copying database'),
'strChangingCharset' => __('Changing charset'),
'strNo' => __('No'),
/* For Foreign key checks */
'strForeignKeyCheck' => __('Enable foreign key checks'),
/* For database/structure.js */
'strErrorRealRowCount' => __('Failed to get real row count.'),
/* For database/search.js */
'strSearching' => __('Searching'),
'strHideSearchResults' => __('Hide search results'),
'strShowSearchResults' => __('Show search results'),
'strBrowsing' => __('Browsing'),
'strDeleting' => __('Deleting'),
'strConfirmDeleteResults' => __('Delete the matches for the %s table?'),
/* For rte.js */
'MissingReturn' => __('The definition of a stored function must contain a RETURN statement!'),
'strExport' => __('Export'),
'NoExportable' => __('No routine is exportable. Required privileges may be lacking.'),
/* For ENUM/SET editor*/
'enum_editor' => __('ENUM/SET editor'),
'enum_columnVals' => __('Values for column %s'),
'enum_newColumnVals' => __('Values for a new column'),
'enum_hint' => __('Enter each value in a separate field.'),
'enum_addValue' => __('Add %d value(s)'),
/* For import.js */
'strImportCSV' => __(
'Note: If the file contains multiple tables, they will be combined into one.'
),
/* For sql.js */
'strHideQueryBox' => __('Hide query box'),
'strShowQueryBox' => __('Show query box'),
'strEdit' => __('Edit'),
'strDelete' => __('Delete'),
'strNotValidRowNumber' => __('%d is not valid row number.'),
'strBrowseForeignValues' => __('Browse foreign values'),
'strNoAutoSavedQuery' => __('No previously auto-saved query is available. Loading default query.'),
'strPreviousSaveQuery' => __(
'You have a previously saved query. Click Get auto-saved query to load the query.'
),
'strBookmarkVariable' => __('Variable %d:'),
/* For Central list of columns */
'pickColumn' => __('Pick'),
'pickColumnTitle' => __('Column selector'),
'searchList' => __('Search this list'),
'strEmptyCentralList' => __(
'No columns in the central list. Make sure the Central columns list for '
. 'database %s has columns that are not present in the current table.'
),
'seeMore' => __('See more'),
'confirmTitle' => __('Are you sure?'),
'makeConsistentMessage' => __(
'This action may change some of the columns definition.<br>Are you sure you '
. 'want to continue?'
),
'strContinue' => __('Continue'),
/** For normalization */
'strAddPrimaryKey' => __('Add primary key'),
'strPrimaryKeyAdded' => __('Primary key added.'),
'strToNextStep' => __('Taking you to next step…'),
'strFinishMsg' => __("The first step of normalization is complete for table '%s'."),
'strEndStep' => __('End of step'),
'str2NFNormalization' => __('Second step of normalization (2NF)'),
'strDone' => __('Done'),
'strConfirmPd' => __('Confirm partial dependencies'),
'strSelectedPd' => __('Selected partial dependencies are as follows:'),
'strPdHintNote' => __(
'Note: a, b -> d,f implies values of columns a and b combined together can '
. 'determine values of column d and column f.'
),
'strNoPdSelected' => __('No partial dependencies selected!'),
'strBack' => __('Back'),
'strShowPossiblePd' => __('Show me the possible partial dependencies based on data in the table'),
'strHidePd' => __('Hide partial dependencies list'),
'strWaitForPd' => __(
'Sit tight! It may take few seconds depending on data size and column count of '
. 'the table.'
),
'strStep' => __('Step'),
'strMoveRepeatingGroup' => '<ol><b>' . __('The following actions will be performed:') . '</b>'
. '<li>' . __('DROP columns %s from the table %s') . '</li>'
. '<li>' . __('Create the following table') . '</li>',
'strNewTablePlaceholder' => 'Enter new table name',
'strNewColumnPlaceholder' => 'Enter column name',
'str3NFNormalization' => __('Third step of normalization (3NF)'),
'strConfirmTd' => __('Confirm transitive dependencies'),
'strSelectedTd' => __('Selected dependencies are as follows:'),
'strNoTdSelected' => __('No dependencies selected!'),
/* For server/variables.js */
'strSave' => __('Save'),
/* For table/select.js */
'strHideSearchCriteria' => __('Hide search criteria'),
'strShowSearchCriteria' => __('Show search criteria'),
'strRangeSearch' => __('Range search'),
'strColumnMax' => __('Column maximum:'),
'strColumnMin' => __('Column minimum:'),
'strMinValue' => __('Minimum value:'),
'strMaxValue' => __('Maximum value:'),
/* For table/find_replace.js */
'strHideFindNReplaceCriteria' => __('Hide find and replace criteria'),
'strShowFindNReplaceCriteria' => __('Show find and replace criteria'),
/* For table/zoom_plot_jqplot.js */
'strDisplayHelp' => '<ul><li>'
. __('Each point represents a data row.')
. '</li><li>'
. __('Hovering over a point will show its label.')
. '</li><li>'
. __('To zoom in, select a section of the plot with the mouse.')
. '</li><li>'
. __('Click reset zoom button to come back to original state.')
. '</li><li>'
. __('Click a data point to view and possibly edit the data row.')
. '</li><li>'
. __('The plot can be resized by dragging it along the bottom right corner.')
. '</li></ul>',
'strHelpTitle' => 'Zoom search instructions',
'strInputNull' => '<strong>' . __('Select two columns') . '</strong>',
'strSameInputs' => '<strong>'
. __('Select two different columns')
. '</strong>',
'strDataPointContent' => __('Data point content'),
/* For table/change.js */
'strIgnore' => __('Ignore'),
'strCopy' => __('Copy'),
'strX' => __('X'),
'strY' => __('Y'),
'strPoint' => __('Point'),
'strPointN' => __('Point %d'),
'strLineString' => __('Linestring'),
'strPolygon' => __('Polygon'),
'strGeometry' => __('Geometry'),
'strInnerRing' => __('Inner ring'),
'strOuterRing' => __('Outer ring'),
'strAddPoint' => __('Add a point'),
'strAddInnerRing' => __('Add an inner ring'),
'strYes' => __('Yes'),
'strCopyEncryptionKey' => __('Do you want to copy encryption key?'),
'strEncryptionKey' => __('Encryption key'),
/* l10n: Tip for HEX conversion of Integers */
'HexConversionInfo' => __(
'The HEX function will treat the integer as a string while calculating the hexadecimal value'
),
/* For Tip to be shown on Time field */
'strMysqlAllowedValuesTipTime' => __(
'MySQL accepts additional values not selectable by the slider;'
. ' key in those values directly if desired'
),
/* For Tip to be shown on Date field */
'strMysqlAllowedValuesTipDate' => __(
'MySQL accepts additional values not selectable by the datepicker;'
. ' key in those values directly if desired'
),
/* For Lock symbol Tooltip */
'strLockToolTip' => __(
'Indicates that you have made changes to this page;'
. ' you will be prompted for confirmation before abandoning changes'
),
/* Designer (js/designer/move.js) */
'strSelectReferencedKey' => __('Select referenced key'),
'strSelectForeignKey' => __('Select Foreign Key'),
'strPleaseSelectPrimaryOrUniqueKey' => __('Please select the primary key or a unique key!'),
'strChangeDisplay' => __('Choose column to display'),
'strLeavingDesigner' => __(
'You haven\'t saved the changes in the layout. They will be lost if you'
. ' don\'t save them. Do you want to continue?'
),
'strQueryEmpty' => __('value/subQuery is empty'),
'strAddTables' => __('Add tables from other databases'),
'strPageName' => __('Page name'),
'strSavePage' => __('Save page'),
'strSavePageAs' => __('Save page as'),
'strOpenPage' => __('Open page'),
'strDeletePage' => __('Delete page'),
/* l10n: When the user opens a page saved in the Designer */
'strSavedPageTableMissing' => __('Some tables saved in this page might have been renamed or deleted.'),
'strUntitled' => __('Untitled'),
'strSelectPage' => __('Please select a page to continue'),
'strEnterValidPageName' => __('Please enter a valid page name'),
'strLeavingPage' => __('Do you want to save the changes to the current page?'),
'strSuccessfulPageDelete' => __('Successfully deleted the page'),
'strExportRelationalSchema' => __('Export relational schema'),
'strModificationSaved' => __('Modifications have been saved'),
/* Visual query builder (js/designer/move.js) */
'strObjectsCreated' => __('%d object(s) created.'),
'strColumnName' => __('Column name'),
'strSubmit' => __('Submit'),
/* For makegrid.js (column reordering, show/hide column, grid editing) */
'strCellEditHint' => __('Press escape to cancel editing.'),
'strSaveCellWarning' => __(
'You have edited some data and they have not been saved. Are you sure you want '
. 'to leave this page before saving the data?'
),
'strColOrderHint' => __('Drag to reorder.'),
'strSortHint' => __('Click to sort results by this column.'),
'strMultiSortHint' => __(
'Shift+Click to add this column to ORDER BY clause or to toggle ASC/DESC.'
. '<br>- Ctrl+Click or Alt+Click (Mac: Shift+Option+Click) to remove column '
. 'from ORDER BY clause'
),
'strColMarkHint' => __('Click to mark/unmark.'),
'strColNameCopyHint' => __('Double-click to copy column name.'),
'strColVisibHint' => __(
'Click the drop-down arrow<br>to toggle column\'s visibility.'
),
'strShowAllCol' => __('Show all'),
'strAlertNonUnique' => __(
'This table does not contain a unique column. Features related to the grid '
. 'edit, checkbox, Edit, Copy and Delete links may not work after saving.'
),
'strEnterValidHex' => __('Please enter a valid hexadecimal string. Valid characters are 0-9, A-F.'),
'strShowAllRowsWarning' => __(
'Do you really want to see all of the rows? For a big table this could crash '
. 'the browser.'
),
'strOriginalLength' => __('Original length'),
/** Drag & Drop sql import messages */
'dropImportMessageCancel' => __('cancel'),
'dropImportMessageAborted' => __('Aborted'),
'dropImportMessageFailed' => __('Failed'),
'dropImportMessageSuccess' => __('Success'),
'dropImportImportResultHeader' => __('Import status'),
'dropImportDropFiles' => __('Drop files here'),
'dropImportSelectDB' => __('Select database first'),
/* For Print view */
'print' => __('Print'),
'back' => __('Back'),
// this approach does not work when the parameter is changed via user prefs
'strGridEditFeatureHint' => $cfg['GridEditing'] === 'double-click'
? __('You can also edit most values<br>by double-clicking directly on them.')
: ($cfg['GridEditing'] === 'click'
? __('You can also edit most values<br>by clicking directly on them.')
: ''),
'strGoToLink' => __('Go to link:'),
'strColNameCopyTitle' => __('Copy column name.'),
'strColNameCopyText' => __('Right-click the column name to copy it to your clipboard.'),
/* password generation */
'strGeneratePassword' => __('Generate password'),
'strGenerate' => __('Generate'),
'strChangePassword' => __('Change password'),
/* navigation tabs */
'strMore' => __('More'),
/* navigation panel */
'strShowPanel' => __('Show panel'),
'strHidePanel' => __('Hide panel'),
'strUnhideNavItem' => __('Show hidden navigation tree items.'),
'linkWithMain' => __('Link with main panel'),
'unlinkWithMain' => __('Unlink from main panel'),
/* microhistory */
'strInvalidPage' => __('The requested page was not found in the history, it may have expired.'),
/* update */
'strNewerVersion' => __(
'A newer version of phpMyAdmin is available and you should consider upgrading. '
. 'The newest version is %s, released on %s.'
),
/* l10n: Latest available phpMyAdmin version */
'strLatestAvailable' => __(', latest stable version:'),
'strUpToDate' => __('up to date'),
'strCreateView' => __('Create view'),
/* Error Reporting */
'strSendErrorReport' => __('Send error report'),
'strSubmitErrorReport' => __('Submit error report'),
'strErrorOccurred' => __(
'A fatal JavaScript error has occurred. Would you like to send an error report?'
),
'strChangeReportSettings' => __('Change report settings'),
'strShowReportDetails' => __('Show report details'),
'strTimeOutError' => __(
'Your export is incomplete, due to a low execution time limit at the PHP level!'
),
'strTooManyInputs' => __(
'Warning: a form on this page has more than %d fields. On submission, '
. "some of the fields might be ignored, due to PHP's "
. 'max_input_vars configuration.'
),
'phpErrorsFound' => '<div class="alert alert-danger" role="alert">'
. __('Some errors have been detected on the server!')
. '<br>'
. __('Please look at the bottom of this window.')
. '<div>'
. '<input id="pma_ignore_errors_popup" type="submit" value="'
. __('Ignore')
. '" class="btn btn-secondary floatright message_errors_found">'
. '<input id="pma_ignore_all_errors_popup" type="submit" value="'
. __('Ignore All')
. '" class="btn btn-secondary floatright message_errors_found">'
. '</div></div>',
'phpErrorsBeingSubmitted' => '<div class="alert alert-danger" role="alert">'
. __('Some errors have been detected on the server!')
. '<br>'
. __(
'As per your settings, they are being submitted currently, please be '
. 'patient.'
)
. '<br>'
. '<img src="'
. $ajaxClockSmallGifPath
. '" width="16" height="16" alt="ajax clock">'
. '</div>',
'strCopyQueryButtonSuccess' => __('Successfully copied!'),
'strCopyQueryButtonFailure' => __('Copying failed!'),
// For console
'strConsoleRequeryConfirm' => __('Execute this query again?'),
'strConsoleDeleteBookmarkConfirm' => __('Do you really want to delete this bookmark?'),
'strConsoleDebugError' => __('Some error occurred while getting SQL debug info.'),
'strConsoleDebugSummary' => __('%s queries executed %s times in %s seconds.'),
'strConsoleDebugArgsSummary' => __('%s argument(s) passed'),
'strConsoleDebugShowArgs' => __('Show arguments'),
'strConsoleDebugHideArgs' => __('Hide arguments'),
'strConsoleDebugTimeTaken' => __('Time taken:'),
'strNoLocalStorage' => __(
'There was a problem accessing your browser storage, some features may not'
. ' work properly for you. It is likely that the browser doesn\'t support storage'
. ' or the quota limit has been reached. In Firefox, corrupted storage can also'
. ' cause such a problem, clearing your "Offline Website Data" might help. In Safari,'
. ' such problem is commonly caused by "Private Mode Browsing".'
),
// For modals in /database/structure
'strCopyTablesTo' => __('Copy tables to'),
'strAddPrefix' => __('Add table prefix'),
'strReplacePrefix' => __('Replace table with prefix'),
'strCopyPrefix' => __('Copy table with prefix'),
/* For password strength simulation */
'strExtrWeak' => __('Extremely weak'),
'strVeryWeak' => __('Very weak'),
'strWeak' => __('Weak'),
'strGood' => __('Good'),
'strStrong' => __('Strong'),
/* U2F errors */
// l10n: error code 5 (from U2F API)
'strU2FTimeout' => _pgettext('U2F error', 'Timed out waiting for security key activation.'),
// l10n: error code 2 (from U2F API)
'strU2FBadRequest' => _pgettext('U2F error', 'Invalid request sent to security key.'),
// l10n: unknown error code (from U2F API)
'strU2FUnknown' => _pgettext('U2F error', 'Unknown security key error.'),
// l10n: error code 3 (from U2F API)
'strU2FInvalidClient' => _pgettext('U2F error', 'Client does not support security key.'),
// l10n: error code 4 (from U2F API) on register
'strU2FErrorRegister' => _pgettext('U2F error', 'Failed security key activation.'),
// l10n: error code 4 (from U2F API) on authanticate
'strU2FErrorAuthenticate' => _pgettext('U2F error', 'Invalid security key.'),
/* Designer */
'strTableAlreadyExists' => _pgettext(
'The table already exists in the designer and can not be added once more.',
'Table %s already exists!'
),
'strHide' => __('Hide'),
'strShow' => __('Show'),
'strStructure' => __('Structure'),
];
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* Simple script to set correct charset for the license
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function is_readable;
use function printf;
use function readfile;
/**
* Simple script to set correct charset for the license
*/
class LicenseController extends AbstractController
{
public function index(): void
{
$this->response->disable();
$this->response->header('Content-type: text/plain; charset=utf-8');
$filename = LICENSE_FILE;
// Check if the file is available, some distributions remove these.
if (@is_readable($filename)) {
readfile($filename);
} else {
printf(
__(
'The %s file is not available on this system, please visit ' .
'%s for more information.'
),
$filename,
'https://www.phpmyadmin.net/'
);
}
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* Represents the interface between the linter and the query editor.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Linter;
use function json_encode;
/**
* Represents the interface between the linter and the query editor.
*/
class LintController extends AbstractController
{
public function index(): void
{
$params = [
'sql_query' => $_POST['sql_query'] ?? null,
'options' => $_POST['options'] ?? null,
];
/**
* The SQL query to be analyzed.
*
* This does not need to be checked again XSS or MySQL injections because it is
* never executed, just parsed.
*
* The client, which will receive the JSON response will decode the message and
* and any HTML fragments that are displayed to the user will be encoded anyway.
*
* @var string
*/
$sqlQuery = ! empty($params['sql_query']) ? $params['sql_query'] : '';
$this->response->setAjax(true);
// Disabling standard response.
$this->response->disable();
Core::headerJSON();
if (! empty($params['options'])) {
$options = $params['options'];
if (! empty($options['routine_editor'])) {
$sqlQuery = 'CREATE PROCEDURE `a`() ' . $sqlQuery;
} elseif (! empty($options['trigger_editor'])) {
$sqlQuery = 'CREATE TRIGGER `a` AFTER INSERT ON `b` FOR EACH ROW ' . $sqlQuery;
} elseif (! empty($options['event_editor'])) {
$sqlQuery = 'CREATE EVENT `a` ON SCHEDULE EVERY MINUTE DO ' . $sqlQuery;
}
}
echo json_encode(Linter::lint($sqlQuery));
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
class LogoutController
{
public function index(): void
{
global $auth_plugin, $token_mismatch;
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || $token_mismatch) {
Core::sendHeaderLocation('./index.php?route=/');
return;
}
$auth_plugin->logOut();
}
}

View file

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Message;
use PhpMyAdmin\Navigation\Navigation;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Utils\SessionCache;
/**
* The navigation panel
*
* Displays server, database and table selection tree.
*/
class NavigationController extends AbstractController
{
/** @var Navigation */
private $navigation;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
Navigation $navigation,
Relation $relation
) {
parent::__construct($response, $template);
$this->navigation = $navigation;
$this->relation = $relation;
}
public function index(): void
{
if (! $this->response->isAjax()) {
$this->response->addHTML(
Message::error(
__('Fatal error: The navigation can only be accessed via AJAX')
)->getDisplay()
);
return;
}
if (isset($_POST['getNaviSettings']) && $_POST['getNaviSettings']) {
$pageSettings = new PageSettings('Navi', 'pma_navigation_settings');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addJSON('message', $pageSettings->getHTML());
return;
}
if (isset($_POST['reload'])) {
SessionCache::set('dbs_to_test', false);// Empty database list cache, see #14252
}
$cfgRelation = $this->relation->getRelationsParam();
if ($cfgRelation['navwork']) {
if (isset($_POST['hideNavItem'])) {
if (! empty($_POST['itemName'])
&& ! empty($_POST['itemType'])
&& ! empty($_POST['dbName'])
) {
$this->navigation->hideNavigationItem(
$_POST['itemName'],
$_POST['itemType'],
$_POST['dbName'],
(! empty($_POST['tableName']) ? $_POST['tableName'] : null)
);
}
return;
}
if (isset($_POST['unhideNavItem'])) {
if (! empty($_POST['itemName'])
&& ! empty($_POST['itemType'])
&& ! empty($_POST['dbName'])
) {
$this->navigation->unhideNavigationItem(
$_POST['itemName'],
$_POST['itemType'],
$_POST['dbName'],
(! empty($_POST['tableName']) ? $_POST['tableName'] : null)
);
}
return;
}
if (isset($_POST['showUnhideDialog'])) {
if (! empty($_POST['dbName'])) {
$this->response->addJSON(
'message',
$this->navigation->getItemUnhideDialog($_POST['dbName'])
);
}
return;
}
}
$this->response->addJSON('message', $this->navigation->getDisplay());
}
}

View file

@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Normalization;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function intval;
use function json_decode;
use function json_encode;
use function min;
/**
* Normalization process (temporarily specific to 1NF).
*/
class NormalizationController extends AbstractController
{
/** @var Normalization */
private $normalization;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Normalization $normalization)
{
parent::__construct($response, $template);
$this->normalization = $normalization;
}
public function index(): void
{
global $db, $table;
if (isset($_POST['getColumns'])) {
$html = '<option selected disabled>' . __('Select one…') . '</option>'
. '<option value="no_such_col">' . __('No such column') . '</option>';
//get column whose datatype falls under string category
$html .= $this->normalization->getHtmlForColumnsList(
$db,
$table,
_pgettext('string types', 'String')
);
echo $html;
return;
}
if (isset($_POST['splitColumn'])) {
$num_fields = min(4096, intval($_POST['numFields']));
$html = $this->normalization->getHtmlForCreateNewColumn($num_fields, $db, $table);
$html .= Url::getHiddenInputs($db, $table);
echo $html;
return;
}
if (isset($_POST['addNewPrimary'])) {
$num_fields = 1;
$columnMeta = [
'Field' => $table . '_id',
'Extra' => 'auto_increment',
];
$html = $this->normalization->getHtmlForCreateNewColumn(
$num_fields,
$db,
$table,
$columnMeta
);
$html .= Url::getHiddenInputs($db, $table);
echo $html;
return;
}
if (isset($_POST['findPdl'])) {
$html = $this->normalization->findPartialDependencies($table, $db);
echo $html;
return;
}
if (isset($_POST['getNewTables2NF'])) {
$partialDependencies = json_decode($_POST['pd'], true);
$html = $this->normalization->getHtmlForNewTables2NF($partialDependencies, $table);
echo $html;
return;
}
if (isset($_POST['getNewTables3NF'])) {
$dependencies = json_decode($_POST['pd']);
$tables = json_decode($_POST['tables'], true);
$newTables = $this->normalization->getHtmlForNewTables3NF($dependencies, $tables, $db);
$this->response->disable();
Core::headerJSON();
echo json_encode($newTables);
return;
}
$this->addScriptFiles(['normalization.js', 'vendor/jquery/jquery.uitablefilter.js']);
$normalForm = '1nf';
if (Core::isValid($_POST['normalizeTo'], ['1nf', '2nf', '3nf'])) {
$normalForm = $_POST['normalizeTo'];
}
if (isset($_POST['createNewTables2NF'])) {
$partialDependencies = json_decode($_POST['pd'], true);
$tablesName = json_decode($_POST['newTablesName']);
$res = $this->normalization->createNewTablesFor2NF($partialDependencies, $tablesName, $table, $db);
$this->response->addJSON($res);
return;
}
if (isset($_POST['createNewTables3NF'])) {
$newtables = json_decode($_POST['newTables'], true);
$res = $this->normalization->createNewTablesFor3NF($newtables, $db);
$this->response->addJSON($res);
return;
}
if (isset($_POST['repeatingColumns'])) {
$repeatingColumns = $_POST['repeatingColumns'];
$newTable = $_POST['newTable'];
$newColumn = $_POST['newColumn'];
$primary_columns = $_POST['primary_columns'];
$res = $this->normalization->moveRepeatingGroup(
$repeatingColumns,
$primary_columns,
$newTable,
$newColumn,
$table,
$db
);
$this->response->addJSON($res);
return;
}
if (isset($_POST['step1'])) {
$html = $this->normalization->getHtmlFor1NFStep1($db, $table, $normalForm);
$this->response->addHTML($html);
} elseif (isset($_POST['step2'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep2($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step3'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep3($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step4'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep4($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step']) && $_POST['step'] == '2.1') {
$res = $this->normalization->getHtmlFor2NFstep1($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step']) && $_POST['step'] == '3.1') {
$tables = $_POST['tables'];
$res = $this->normalization->getHtmlFor3NFstep1($db, $tables);
$this->response->addJSON($res);
} else {
$this->response->addHTML($this->normalization->getHtmlForNormalizeTable());
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* phpinfo() wrapper to allow displaying only when configured to do so.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use const INFO_CONFIGURATION;
use const INFO_GENERAL;
use const INFO_MODULES;
use function phpinfo;
/**
* phpinfo() wrapper to allow displaying only when configured to do so.
*/
class PhpInfoController extends AbstractController
{
public function index(): void
{
global $cfg;
$this->response->disable();
$this->response->getHeader()->sendHttpHeaders();
if (! $cfg['ShowPhpInfo']) {
return;
}
phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES);
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\ExportForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class ExportController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new ExportForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/export');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/export',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/export'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\FeaturesForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class FeaturesController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new FeaturesForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/features');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/features',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/features'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\ImportForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class ImportController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new ImportForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/import');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/import',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/import'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\MainForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class MainPanelController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new MainForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/main-panel');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/main-panel',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/main-panel'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\UserFormList;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\File;
use PhpMyAdmin\Message;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\ThemeManager;
use PhpMyAdmin\UserPreferences;
use PhpMyAdmin\Util;
use const JSON_PRETTY_PRINT;
use const PHP_URL_PATH;
use const UPLOAD_ERR_OK;
use function array_merge;
use function define;
use function file_exists;
use function is_array;
use function is_uploaded_file;
use function json_decode;
use function json_encode;
use function mb_strpos;
use function mb_substr;
use function parse_url;
use function str_replace;
use function urlencode;
use function var_export;
/**
* User preferences management page.
*/
class ManageController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cf, $error, $filename, $json, $PMA_Config, $lang, $max_upload_size;
global $new_config, $config, $return_url, $form_display, $all_ok, $params, $query, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$error = '';
if (isset($_POST['submit_export'], $_POST['export_type']) && $_POST['export_type'] === 'text_file') {
// export to JSON file
$this->response->disable();
$filename = 'phpMyAdmin-config-' . urlencode(Core::getenv('HTTP_HOST')) . '.json';
Core::downloadHeader($filename, 'application/json');
$settings = $this->userPreferences->load();
echo json_encode($settings['config_data'], JSON_PRETTY_PRINT);
return;
}
if (isset($_POST['submit_export'], $_POST['export_type']) && $_POST['export_type'] === 'php_file') {
// export to JSON file
$this->response->disable();
$filename = 'phpMyAdmin-config-' . urlencode(Core::getenv('HTTP_HOST')) . '.php';
Core::downloadHeader($filename, 'application/php');
$settings = $this->userPreferences->load();
echo '/* ' . __('phpMyAdmin configuration snippet') . " */\n\n";
echo '/* ' . __('Paste it to your config.inc.php') . " */\n\n";
foreach ($settings['config_data'] as $key => $val) {
echo '$cfg[\'' . str_replace('/', '\'][\'', $key) . '\'] = ';
echo var_export($val, true) . ";\n";
}
return;
}
if (isset($_POST['submit_get_json'])) {
$settings = $this->userPreferences->load();
$this->response->addJSON('prefs', json_encode($settings['config_data']));
$this->response->addJSON('mtime', $settings['mtime']);
return;
}
if (isset($_POST['submit_import'])) {
// load from JSON file
$json = '';
if (isset($_POST['import_type'], $_FILES['import_file'])
&& $_POST['import_type'] === 'text_file'
&& $_FILES['import_file']['error'] == UPLOAD_ERR_OK
&& is_uploaded_file($_FILES['import_file']['tmp_name'])
) {
$importHandle = new File($_FILES['import_file']['tmp_name']);
$importHandle->checkUploadedFile();
if ($importHandle->isError()) {
$error = $importHandle->getError();
} else {
// read JSON from uploaded file
$json = $importHandle->getRawContent();
}
} else {
// read from POST value (json)
$json = $_POST['json'] ?? null;
}
// hide header message
$_SESSION['userprefs_autoload'] = true;
$config = json_decode($json, true);
$return_url = $_POST['return_url'] ?? null;
if (! is_array($config)) {
if (! isset($error)) {
$error = __('Could not import configuration');
}
} else {
// sanitize input values: treat them as though
// they came from HTTP POST request
$form_display = new UserFormList($cf);
$new_config = $cf->getFlatDefaultConfig();
if (! empty($_POST['import_merge'])) {
$new_config = array_merge($new_config, $cf->getConfigArray());
}
$new_config = array_merge($new_config, $config);
$_POST_bak = $_POST;
foreach ($new_config as $k => $v) {
$_POST[str_replace('/', '-', (string) $k)] = $v;
}
$cf->resetConfigData();
$all_ok = $form_display->process(true, false);
$all_ok = $all_ok && ! $form_display->hasErrors();
$_POST = $_POST_bak;
if (! $all_ok && isset($_POST['fix_errors'])) {
$form_display->fixErrors();
$all_ok = true;
}
if (! $all_ok) {
// mimic original form and post json in a hidden field
$cfgRelation = $this->relation->getRelationsParam();
echo $this->template->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
echo $this->template->render('preferences/manage/error', [
'form_errors' => $form_display->displayErrors(),
'json' => $json,
'import_merge' => $_POST['import_merge'] ?? null,
'return_url' => $return_url,
]);
return;
}
// check for ThemeDefault
$params = [];
$tmanager = ThemeManager::getInstance();
if (isset($config['ThemeDefault'])
&& $tmanager->theme->getId() != $config['ThemeDefault']
&& $tmanager->checkTheme($config['ThemeDefault'])
) {
$tmanager->setActiveTheme($config['ThemeDefault']);
$tmanager->setThemeCookie();
}
if (isset($config['lang'])
&& $config['lang'] != $lang
) {
$params['lang'] = $config['lang'];
}
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
if ($result === true) {
if ($return_url) {
$query = Util::splitURLQuery($return_url);
$return_url = parse_url($return_url, PHP_URL_PATH);
foreach ($query as $q) {
$pos = mb_strpos($q, '=');
$k = mb_substr($q, 0, (int) $pos);
if ($k === 'token') {
continue;
}
$params[$k] = mb_substr($q, $pos + 1);
}
} else {
$return_url = 'index.php?route=/preferences/manage';
}
// reload config
$PMA_Config->loadUserPreferences();
$this->userPreferences->redirect($return_url ?? '', $params);
return;
}
$error = $result;
}
} elseif (isset($_POST['submit_clear'])) {
$result = $this->userPreferences->save([]);
if ($result === true) {
$params = [];
$PMA_Config->removeCookie('pma_collaction_connection');
$PMA_Config->removeCookie('pma_lang');
$this->userPreferences->redirect('index.php?route=/preferences/manage', $params);
return;
} else {
$error = $result;
}
return;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
echo $this->template->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($error) {
if (! $error instanceof Message) {
$error = Message::error($error);
}
$error->getDisplay();
}
echo $this->template->render('preferences/manage/main', [
'error' => $error,
'max_upload_size' => $max_upload_size,
'exists_setup_and_not_exists_config' => @file_exists(ROOT_PATH . 'setup/index.php')
&& ! @file_exists(CONFIG_FILE),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\NaviForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class NavigationController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new NaviForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/navigation');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/navigation',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/navigation'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\SqlForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class SqlController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new SqlForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/sql');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/sql',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/sql'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Message;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use function count;
class TwoFactorController extends AbstractController
{
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Relation $relation)
{
parent::__construct($response, $template);
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $route;
$cfgRelation = $this->relation->getRelationsParam();
echo $this->template->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
$twoFactor = new TwoFactor($cfg['Server']['user']);
if (isset($_POST['2fa_remove'])) {
if (! $twoFactor->check(true)) {
echo $this->template->render('preferences/two_factor/confirm', [
'form' => $twoFactor->render(),
]);
return;
}
$twoFactor->configure('');
echo Message::rawNotice(__('Two-factor authentication has been removed.'))->getDisplay();
} elseif (isset($_POST['2fa_configure'])) {
if (! $twoFactor->configure($_POST['2fa_configure'])) {
echo $this->template->render('preferences/two_factor/configure', [
'form' => $twoFactor->setup(),
'configure' => $_POST['2fa_configure'],
]);
return;
}
echo Message::rawNotice(__('Two-factor authentication has been configured.'))->getDisplay();
}
$backend = $twoFactor->getBackend();
echo $this->template->render('preferences/two_factor/main', [
'enabled' => $twoFactor->isWritable(),
'num_backends' => count($twoFactor->getAvailable()),
'backend_id' => $backend::$id,
'backend_name' => $backend::getName(),
'backend_description' => $backend::getDescription(),
'backends' => $twoFactor->getAllBackends(),
'missing' => $twoFactor->getMissingDeps(),
]);
}
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Export;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Util;
/**
* Schema export handler
*/
class SchemaExportController
{
/** @var Export */
private $export;
/** @var Relation */
private $relation;
/**
* @param Export $export A Export instance.
* @param Relation $relation A Relation instance.
*/
public function __construct(Export $export, Relation $relation)
{
$this->export = $export;
$this->relation = $relation;
}
public function index(): void
{
global $cfgRelation;
/**
* get all variables needed for exporting relational schema
* in $cfgRelation
*/
$cfgRelation = $this->relation->getRelationsParam();
if (! isset($_POST['export_type'])) {
Util::checkParameters(['export_type']);
}
/**
* Include the appropriate Schema Class depending on $export_type
* default is PDF
*/
$this->export->processExportSchema($_POST['export_type']);
}
}

View file

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function array_key_exists;
/**
* Handles viewing binary logs
*/
class BinlogController extends AbstractController
{
/**
* binary log files
*
* @var array
*/
protected $binaryLogs;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $dbi)
{
parent::__construct($response, $template);
$this->dbi = $dbi;
$this->binaryLogs = $this->dbi->fetchResult(
'SHOW MASTER LOGS',
'Log_name',
null,
DatabaseInterface::CONNECT_USER,
DatabaseInterface::QUERY_STORE
);
}
public function index(): void
{
global $cfg, $PMA_Theme, $err_url;
$params = [
'log' => $_POST['log'] ?? null,
'pos' => $_POST['pos'] ?? null,
'is_full_query' => $_POST['is_full_query'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$position = ! empty($params['pos']) ? (int) $params['pos'] : 0;
$urlParams = [];
if (isset($params['log'])
&& array_key_exists($params['log'], $this->binaryLogs)
) {
$urlParams['log'] = $params['log'];
}
$isFullQuery = false;
if (! empty($params['is_full_query'])) {
$isFullQuery = true;
$urlParams['is_full_query'] = 1;
}
$sqlQuery = $this->getSqlQuery(
$params['log'] ?? '',
$position,
(int) $cfg['MaxRows']
);
$result = $this->dbi->query($sqlQuery);
$numRows = 0;
if (isset($result) && $result) {
$numRows = $this->dbi->numRows($result);
}
$previousParams = $urlParams;
$fullQueriesParams = $urlParams;
$nextParams = $urlParams;
if ($position > 0) {
$fullQueriesParams['pos'] = $position;
if ($position > $cfg['MaxRows']) {
$previousParams['pos'] = $position - $cfg['MaxRows'];
}
}
$fullQueriesParams['is_full_query'] = 1;
if ($isFullQuery) {
unset($fullQueriesParams['is_full_query']);
}
if ($numRows >= $cfg['MaxRows']) {
$nextParams['pos'] = $position + $cfg['MaxRows'];
}
$values = [];
while ($value = $this->dbi->fetchAssoc($result)) {
$values[] = $value;
}
$this->render('server/binlog/index', [
'url_params' => $urlParams,
'binary_logs' => $this->binaryLogs,
'log' => $params['log'],
'sql_message' => Generator::getMessage(Message::success(), $sqlQuery),
'values' => $values,
'has_previous' => $position > 0,
'has_next' => $numRows >= $cfg['MaxRows'],
'previous_params' => $previousParams,
'full_queries_params' => $fullQueriesParams,
'next_params' => $nextParams,
'has_icons' => Util::showIcons('TableNavigationLinksMode'),
'is_full_query' => $isFullQuery,
'image_path' => $PMA_Theme->getImgPath(),
]);
}
/**
* @param string $log Binary log file name
* @param int $position Position to display
* @param int $maxRows Maximum number of rows
*/
private function getSqlQuery(
string $log,
int $position,
int $maxRows
): string {
$sqlQuery = 'SHOW BINLOG EVENTS';
if (! empty($log)) {
$sqlQuery .= ' IN \'' . $log . '\'';
}
$sqlQuery .= ' LIMIT ' . $position . ', ' . $maxRows;
return $sqlQuery;
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
/**
* Handles viewing character sets and collations
*/
class CollationsController extends AbstractController
{
/** @var array|null */
private $charsets;
/** @var array|null */
private $collations;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
* @param array|null $charsets Array of charsets
* @param array|null $collations Array of collations
*/
public function __construct(
$response,
Template $template,
$dbi,
?array $charsets = null,
?array $collations = null
) {
global $cfg;
parent::__construct($response, $template);
$this->dbi = $dbi;
$this->charsets = $charsets ?? Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$this->collations = $collations ?? Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']);
}
public function index(): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$charsets = [];
/** @var Charset $charset */
foreach ($this->charsets as $charset) {
$charsetCollations = [];
/** @var Collation $collation */
foreach ($this->collations[$charset->getName()] as $collation) {
$charsetCollations[] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
'is_default' => $collation->isDefault(),
];
}
$charsets[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
'collations' => $charsetCollations,
];
}
$this->render('server/collations/index', ['charsets' => $charsets]);
}
}

View file

@ -0,0 +1,509 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\ReplicationInfo;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function array_key_exists;
use function array_keys;
use function array_search;
use function count;
use function explode;
use function in_array;
use function mb_strlen;
use function mb_strtolower;
use function strlen;
use function strpos;
/**
* Handles viewing and creating and deleting databases
*/
class DatabasesController extends AbstractController
{
/** @var array array of database details */
private $databases = [];
/** @var int number of databases */
private $databaseCount = 0;
/** @var string sort by column */
private $sortBy;
/** @var string sort order of databases */
private $sortOrder;
/** @var bool whether to show database statistics */
private $hasStatistics;
/** @var int position in list navigation */
private $position;
/** @var Transformations */
private $transformations;
/** @var RelationCleanup */
private $relationCleanup;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct(
$response,
Template $template,
Transformations $transformations,
RelationCleanup $relationCleanup,
$dbi
) {
parent::__construct($response, $template);
$this->transformations = $transformations;
$this->relationCleanup = $relationCleanup;
$this->dbi = $dbi;
$checkUserPrivileges = new CheckUserPrivileges($dbi);
$checkUserPrivileges->getPrivileges();
}
public function index(): void
{
global $cfg, $server, $dblist, $is_create_db_priv;
global $db_to_create, $text_dir, $PMA_Theme, $err_url;
$params = [
'statistics' => $_REQUEST['statistics'] ?? null,
'pos' => $_REQUEST['pos'] ?? null,
'sort_by' => $_REQUEST['sort_by'] ?? null,
'sort_order' => $_REQUEST['sort_order'] ?? null,
];
$this->addScriptFiles(['server/databases.js']);
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$replicationInfo = new ReplicationInfo($this->dbi);
$replicationInfo->load($_POST['master_connection'] ?? null);
$primaryInfo = $replicationInfo->getPrimaryInfo();
$replicaInfo = $replicationInfo->getReplicaInfo();
$this->setSortDetails($params['sort_by'], $params['sort_order']);
$this->hasStatistics = ! empty($params['statistics']);
$this->position = ! empty($params['pos']) ? (int) $params['pos'] : 0;
/**
* Gets the databases list
*/
if ($server > 0) {
$this->databases = $this->dbi->getDatabasesFull(
null,
$this->hasStatistics,
DatabaseInterface::CONNECT_USER,
$this->sortBy,
$this->sortOrder,
$this->position,
true
);
$this->databaseCount = count($dblist->databases);
}
$urlParams = [
'statistics' => $this->hasStatistics,
'pos' => $this->position,
'sort_by' => $this->sortBy,
'sort_order' => $this->sortOrder,
];
$databases = $this->getDatabases($primaryInfo, $replicaInfo);
$charsetsList = [];
if ($cfg['ShowCreateDb'] && $is_create_db_priv) {
$charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']);
$serverCollation = $this->dbi->getServerCollation();
/** @var Charset $charset */
foreach ($charsets as $charset) {
$collationsList = [];
/** @var Collation $collation */
foreach ($collations[$charset->getName()] as $collation) {
$collationsList[] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
'is_selected' => $serverCollation === $collation->getName(),
];
}
$charsetsList[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
'collations' => $collationsList,
];
}
}
$headerStatistics = $this->getStatisticsColumns();
$this->render('server/databases/index', [
'is_create_database_shown' => $cfg['ShowCreateDb'],
'has_create_database_privileges' => $is_create_db_priv,
'has_statistics' => $this->hasStatistics,
'database_to_create' => $db_to_create,
'databases' => $databases['databases'],
'total_statistics' => $databases['total_statistics'],
'header_statistics' => $headerStatistics,
'charsets' => $charsetsList,
'database_count' => $this->databaseCount,
'pos' => $this->position,
'url_params' => $urlParams,
'max_db_list' => $cfg['MaxDbList'],
'has_master_replication' => $primaryInfo['status'],
'has_slave_replication' => $replicaInfo['status'],
'is_drop_allowed' => $this->dbi->isSuperUser() || $cfg['AllowUserDropDatabase'],
'theme_image_path' => $PMA_Theme->getImgPath(),
'text_dir' => $text_dir,
]);
}
public function create(): void
{
global $cfg, $db;
$params = [
'new_db' => $_POST['new_db'] ?? null,
'db_collation' => $_POST['db_collation'] ?? null,
];
if (! isset($params['new_db']) || mb_strlen($params['new_db']) === 0 || ! $this->response->isAjax()) {
$this->response->addJSON(['message' => Message::error()]);
return;
}
// lower_case_table_names=1 `DB` becomes `db`
if ($this->dbi->getLowerCaseNames() === '1') {
$params['new_db'] = mb_strtolower(
$params['new_db']
);
}
/**
* Builds and executes the db creation sql query
*/
$sqlQuery = 'CREATE DATABASE ' . Util::backquote($params['new_db']);
if (! empty($params['db_collation'])) {
[$databaseCharset] = explode('_', $params['db_collation']);
$charsets = Charsets::getCharsets(
$this->dbi,
$cfg['Server']['DisableIS']
);
$collations = Charsets::getCollations(
$this->dbi,
$cfg['Server']['DisableIS']
);
if (array_key_exists($databaseCharset, $charsets)
&& array_key_exists($params['db_collation'], $collations[$databaseCharset])
) {
$sqlQuery .= ' DEFAULT'
. Util::getCharsetQueryPart($params['db_collation']);
}
}
$sqlQuery .= ';';
$result = $this->dbi->tryQuery($sqlQuery);
if (! $result) {
// avoid displaying the not-created db name in header or navi panel
$db = '';
$message = Message::rawError((string) $this->dbi->getError());
$json = ['message' => $message];
$this->response->setRequestStatus(false);
} else {
$db = $params['new_db'];
$message = Message::success(__('Database %1$s has been created.'));
$message->addParam($params['new_db']);
$scriptName = Util::getScriptNameForOption(
$cfg['DefaultTabDatabase'],
'database'
);
$json = [
'message' => $message,
'sql_query' => Generator::getMessage('', $sqlQuery, 'success'),
'url' => $scriptName . Url::getCommon(
['db' => $params['new_db']],
strpos($scriptName, '?') === false ? '?' : '&'
),
];
}
$this->response->addJSON($json);
}
/**
* Handles dropping multiple databases
*/
public function destroy(): void
{
global $selected, $err_url, $cfg, $dblist, $reload;
$params = [
'drop_selected_dbs' => $_POST['drop_selected_dbs'] ?? null,
'selected_dbs' => $_POST['selected_dbs'] ?? null,
];
/** @var Message|int $message */
$message = -1;
if (! isset($params['drop_selected_dbs'])
|| ! $this->response->isAjax()
|| (! $this->dbi->isSuperUser() && ! $cfg['AllowUserDropDatabase'])
) {
$message = Message::error();
$json = ['message' => $message];
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON($json);
return;
}
if (! isset($params['selected_dbs'])) {
$message = Message::error(__('No databases selected.'));
$json = ['message' => $message];
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON($json);
return;
}
$err_url = Url::getFromRoute('/server/databases');
$selected = $_POST['selected_dbs'];
$rebuildDatabaseList = false;
$sqlQuery = '';
$numberOfDatabases = count($selected);
for ($i = 0; $i < $numberOfDatabases; $i++) {
$this->relationCleanup->database($selected[$i]);
$aQuery = 'DROP DATABASE ' . Util::backquote($selected[$i]);
$reload = true;
$rebuildDatabaseList = true;
$sqlQuery .= $aQuery . ';' . "\n";
$this->dbi->query($aQuery);
$this->transformations->clear($selected[$i]);
}
if ($rebuildDatabaseList) {
$dblist->databases->build();
}
if ($message === -1) { // no error message
$message = Message::success(
_ngettext(
'%1$d database has been dropped successfully.',
'%1$d databases have been dropped successfully.',
$numberOfDatabases
)
);
$message->addParam($numberOfDatabases);
}
$json = [];
if ($message instanceof Message) {
$json = ['message' => $message];
$this->response->setRequestStatus($message->isSuccess());
}
$this->response->addJSON($json);
}
/**
* Extracts parameters sort order and sort by
*
* @param string|null $sortBy sort by
* @param string|null $sortOrder sort order
*/
private function setSortDetails(?string $sortBy, ?string $sortOrder): void
{
if (empty($sortBy)) {
$this->sortBy = 'SCHEMA_NAME';
} else {
$sortByAllowList = [
'SCHEMA_NAME',
'DEFAULT_COLLATION_NAME',
'SCHEMA_TABLES',
'SCHEMA_TABLE_ROWS',
'SCHEMA_DATA_LENGTH',
'SCHEMA_INDEX_LENGTH',
'SCHEMA_LENGTH',
'SCHEMA_DATA_FREE',
];
$this->sortBy = 'SCHEMA_NAME';
if (in_array($sortBy, $sortByAllowList)) {
$this->sortBy = $sortBy;
}
}
$this->sortOrder = 'asc';
if (! isset($sortOrder)
|| mb_strtolower($sortOrder) !== 'desc'
) {
return;
}
$this->sortOrder = 'desc';
}
/**
* @param array $primaryInfo
* @param array $replicaInfo
*
* @return array
*/
private function getDatabases($primaryInfo, $replicaInfo): array
{
global $cfg;
$databases = [];
$totalStatistics = $this->getStatisticsColumns();
foreach ($this->databases as $database) {
$replication = [
'master' => ['status' => $primaryInfo['status']],
'slave' => ['status' => $replicaInfo['status']],
];
if ($primaryInfo['status']) {
$key = array_search($database['SCHEMA_NAME'], $primaryInfo['Ignore_DB']);
$replication['master']['is_replicated'] = false;
if (strlen((string) $key) === 0) {
$key = array_search($database['SCHEMA_NAME'], $primaryInfo['Do_DB']);
if (strlen((string) $key) > 0 || count($primaryInfo['Do_DB']) === 0) {
$replication['master']['is_replicated'] = true;
}
}
}
if ($replicaInfo['status']) {
$key = array_search($database['SCHEMA_NAME'], $replicaInfo['Ignore_DB']);
$replication['slave']['is_replicated'] = false;
if (strlen((string) $key) === 0) {
$key = array_search($database['SCHEMA_NAME'], $replicaInfo['Do_DB']);
if (strlen((string) $key) > 0 || count($replicaInfo['Do_DB']) === 0) {
$replication['slave']['is_replicated'] = true;
}
}
}
$statistics = $this->getStatisticsColumns();
if ($this->hasStatistics) {
foreach (array_keys($statistics) as $key) {
$statistics[$key]['raw'] = $database[$key] ?? null;
$totalStatistics[$key]['raw'] += (int) $database[$key] ?? 0;
}
}
$url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$url .= Url::getCommonRaw(
['db' => $database['SCHEMA_NAME']],
strpos($url, '?') === false ? '?' : '&'
);
$databases[$database['SCHEMA_NAME']] = [
'name' => $database['SCHEMA_NAME'],
'collation' => [],
'statistics' => $statistics,
'replication' => $replication,
'is_system_schema' => Utilities::isSystemSchema(
$database['SCHEMA_NAME'],
true
),
'is_pmadb' => $database['SCHEMA_NAME'] === ($cfg['Server']['pmadb'] ?? ''),
'url' => $url,
];
$collation = Charsets::findCollationByName(
$this->dbi,
$cfg['Server']['DisableIS'],
$database['DEFAULT_COLLATION_NAME']
);
if ($collation === null) {
continue;
}
$databases[$database['SCHEMA_NAME']]['collation'] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
];
}
return [
'databases' => $databases,
'total_statistics' => $totalStatistics,
];
}
/**
* Prepares the statistics columns
*
* @return array
*/
private function getStatisticsColumns(): array
{
return [
'SCHEMA_TABLES' => [
'title' => __('Tables'),
'format' => 'number',
'raw' => 0,
],
'SCHEMA_TABLE_ROWS' => [
'title' => __('Rows'),
'format' => 'number',
'raw' => 0,
],
'SCHEMA_DATA_LENGTH' => [
'title' => __('Data'),
'format' => 'byte',
'raw' => 0,
],
'SCHEMA_INDEX_LENGTH' => [
'title' => __('Indexes'),
'format' => 'byte',
'raw' => 0,
],
'SCHEMA_LENGTH' => [
'title' => __('Total'),
'format' => 'byte',
'raw' => 0,
],
'SCHEMA_DATA_FREE' => [
'title' => __('Overhead'),
'format' => 'byte',
'raw' => 0,
],
];
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\StorageEngine;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
/**
* Handles viewing storage engine details
*/
class EnginesController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $dbi)
{
parent::__construct($response, $template);
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->render('server/engines/index', [
'engines' => StorageEngine::getStorageEngines(),
]);
}
/**
* Displays details about a given Storage Engine
*
* @param array $params Request params
*/
public function show(array $params): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$page = $params['page'] ?? '';
$engine = [];
if (StorageEngine::isValid($params['engine'])) {
$storageEngine = StorageEngine::getEngine($params['engine']);
$engine = [
'engine' => $params['engine'],
'title' => $storageEngine->getTitle(),
'help_page' => $storageEngine->getMysqlHelpPage(),
'comment' => $storageEngine->getComment(),
'info_pages' => $storageEngine->getInfoPages(),
'support' => $storageEngine->getSupportInformationMessage(),
'variables' => $storageEngine->getHtmlVariables(),
'page' => ! empty($page) ? $storageEngine->getPage($page) : '',
];
}
$this->render('server/engines/show', [
'engine' => $engine,
'page' => $page,
]);
}
}

Some files were not shown because too many files have changed in this diff Show more