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,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,
]);
}
}

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Export\Options;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function array_merge;
final class ExportController extends AbstractController
{
/** @var Options */
private $export;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Options $export, $dbi)
{
parent::__construct($response, $template);
$this->export = $export;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $sql_query, $num_tables, $unlim_num_rows;
global $tmp_select, $select_item, $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$pageSettings = new PageSettings('Export');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['export.js']);
$select_item = $tmp_select ?? '';
$databases = $this->export->getDatabasesForSelectOptions($select_item);
if (! isset($sql_query)) {
$sql_query = '';
}
if (! isset($num_tables)) {
$num_tables = 0;
}
if (! isset($unlim_num_rows)) {
$unlim_num_rows = 0;
}
$GLOBALS['single_table'] = $_POST['single_table'] ?? $_GET['single_table'] ?? $GLOBALS['single_table'] ?? null;
$exportList = Plugins::getExport('server', 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->export->getOptions(
'server',
$db,
$table,
$sql_query,
$num_tables,
$unlim_num_rows,
$exportList
);
$this->render('server/export/index', array_merge($options, [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'databases' => $databases,
]));
}
}

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Controllers\AbstractController;
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 DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $dbi)
{
parent::__construct($response, $template);
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $max_upload_size, $table, $SESSION_KEY, $cfg, $PMA_Theme, $err_url;
$pageSettings = new PageSettings('Import');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['import.js']);
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
[$SESSION_KEY, $uploadId] = Ajax::uploadProgressSetup();
$importList = Plugins::getImport('server');
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' => 'server',
];
$this->render('server/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,73 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Plugins;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function array_keys;
use function ksort;
use function mb_strtolower;
use function preg_replace;
/**
* Handles viewing server plugin details
*/
class PluginsController extends AbstractController
{
/** @var Plugins */
private $plugins;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Plugins $plugins, $dbi)
{
parent::__construct($response, $template);
$this->plugins = $plugins;
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->addScriptFiles(['vendor/jquery/jquery.tablesorter.js', 'server/plugins.js']);
$plugins = [];
$serverPlugins = $this->plugins->getAll();
foreach ($serverPlugins as $plugin) {
$plugins[$plugin->getType()][] = $plugin->toArray();
}
ksort($plugins);
$cleanTypes = [];
foreach (array_keys($plugins) as $type) {
$cleanTypes[$type] = preg_replace(
'/[^a-z]/',
'',
mb_strtolower($type)
);
}
$this->render('server/plugins/index', [
'plugins' => $plugins,
'clean_types' => $cleanTypes,
]);
}
}

View file

@ -0,0 +1,486 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Controllers\Database\PrivilegesController as DatabaseController;
use PhpMyAdmin\Controllers\Table\PrivilegesController as TableController;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Relation;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Privileges;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function header;
use function implode;
use function is_array;
use function ob_get_clean;
use function ob_start;
use function str_replace;
use function urlencode;
/**
* Server privileges and users manipulations.
*/
class PrivilegesController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Relation $relation, $dbi)
{
parent::__construct($response, $template);
$this->relation = $relation;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $err_url, $message, $text_dir, $post_patterns, $PMA_Theme;
global $username, $hostname, $dbname, $tablename, $routinename, $db_and_table, $dbname_is_wildcard;
global $queries, $password, $ret_message, $ret_queries, $queries_for_display, $sql_query, $_add_user_error;
global $itemType, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos, $title, $export, $grants, $one_grant, $url_dbname;
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
$cfgRelation = $this->relation->getRelationsParam();
$this->addScriptFiles(['server/privileges.js', 'vendor/zxcvbn.js']);
$relationCleanup = new RelationCleanup($this->dbi, $this->relation);
$serverPrivileges = new Privileges($this->template, $this->dbi, $this->relation, $relationCleanup);
$databaseController = new DatabaseController(
$this->response,
$this->template,
$db,
$serverPrivileges,
$this->dbi
);
$tableController = new TableController(
$this->response,
$this->template,
$db,
$table,
$serverPrivileges,
$this->dbi
);
if ((isset($_GET['viewing_mode'])
&& $_GET['viewing_mode'] === 'server')
&& $GLOBALS['cfgRelation']['menuswork']
) {
$this->response->addHTML('<div class="container-fluid">');
$this->render('server/privileges/subnav', [
'active' => 'privileges',
'is_super_user' => $this->dbi->isSuperUser(),
]);
}
/**
* Sets globals from $_POST patterns, for privileges and max_* vars
*/
$post_patterns = [
'/_priv$/i',
'/^max_/i',
];
Core::setPostAsGlobal($post_patterns);
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$_add_user_error = false;
/**
* Get DB information: username, hostname, dbname,
* tablename, db_and_table, dbname_is_wildcard
*/
[
$username,
$hostname,
$dbname,
$tablename,
$routinename,
$db_and_table,
$dbname_is_wildcard,
] = $serverPrivileges->getDataForDBInfo();
/**
* Checks if the user is allowed to do what they try to...
*/
$isGrantUser = $this->dbi->isGrantUser();
$isCreateUser = $this->dbi->isCreateUser();
if (! $this->dbi->isSuperUser() && ! $isGrantUser && ! $isCreateUser) {
$this->render('server/sub_page_header', [
'type' => 'privileges',
'is_image' => false,
]);
$this->response->addHTML(
Message::error(__('No Privileges'))
->getDisplay()
);
return;
}
if (! $isGrantUser && ! $isCreateUser) {
$this->response->addHTML(Message::notice(
__('You do not have the privileges to administrate the users!')
)->getDisplay());
}
/**
* Checks if the user is using "Change Login Information / Copy User" dialog
* only to update the password
*/
if (isset($_POST['change_copy']) && $username == $_POST['old_username']
&& $hostname == $_POST['old_hostname']
) {
$this->response->addHTML(
Message::error(
__(
"Username and hostname didn't change. "
. 'If you only want to change the password, '
. "'Change password' tab should be used."
)
)->getDisplay()
);
$this->response->setRequestStatus(false);
return;
}
/**
* Changes / copies a user, part I
*/
[$queries, $password] = $serverPrivileges->getDataForChangeOrCopyUser();
/**
* Adds a user
* (Changes / copies a user, part II)
*/
[
$ret_message,
$ret_queries,
$queries_for_display,
$sql_query,
$_add_user_error,
] = $serverPrivileges->addUser(
$dbname ?? null,
$username ?? null,
$hostname ?? null,
$password ?? null,
(bool) $cfgRelation['menuswork']
);
//update the old variables
if (isset($ret_queries)) {
$queries = $ret_queries;
unset($ret_queries);
}
if (isset($ret_message)) {
$message = $ret_message;
unset($ret_message);
}
/**
* Changes / copies a user, part III
*/
if (isset($_POST['change_copy'])) {
$queries = $serverPrivileges->getDbSpecificPrivsQueriesForChangeOrCopyUser(
$queries,
$username,
$hostname
);
}
$itemType = '';
if (! empty($routinename)) {
$itemType = $serverPrivileges->getRoutineType($dbname, $routinename);
}
/**
* Updates privileges
*/
if (! empty($_POST['update_privs'])) {
if (is_array($dbname)) {
foreach ($dbname as $key => $db_name) {
[$sql_query[$key], $message] = $serverPrivileges->updatePrivileges(
($username ?? ''),
($hostname ?? ''),
($tablename ?? ($routinename ?? '')),
($db_name ?? ''),
$itemType
);
}
$sql_query = implode("\n", $sql_query);
} else {
[$sql_query, $message] = $serverPrivileges->updatePrivileges(
($username ?? ''),
($hostname ?? ''),
($tablename ?? ($routinename ?? '')),
($dbname ?? ''),
$itemType
);
}
}
/**
* Assign users to user groups
*/
if (! empty($_POST['changeUserGroup']) && $cfgRelation['menuswork']
&& $this->dbi->isSuperUser() && $this->dbi->isCreateUser()
) {
$serverPrivileges->setUserGroup($username, $_POST['userGroup']);
$message = Message::success();
}
/**
* Revokes Privileges
*/
if (isset($_POST['revokeall'])) {
[$message, $sql_query] = $serverPrivileges->getMessageAndSqlQueryForPrivilegesRevoke(
($dbname ?? ''),
($tablename ?? ($routinename ?? '')),
$username,
$hostname,
$itemType
);
}
/**
* Updates the password
*/
if (isset($_POST['change_pw'])) {
$message = $serverPrivileges->updatePassword(
$err_url,
$username,
$hostname
);
}
/**
* Deletes users
* (Changes / copies a user, part IV)
*/
if (isset($_POST['delete'])
|| (isset($_POST['change_copy']) && $_POST['mode'] < 4)
) {
$queries = $serverPrivileges->getDataForDeleteUsers($queries);
if (empty($_POST['change_copy'])) {
[$sql_query, $message] = $serverPrivileges->deleteUser($queries);
}
}
/**
* Changes / copies a user, part V
*/
if (isset($_POST['change_copy'])) {
$queries = $serverPrivileges->getDataForQueries($queries, $queries_for_display);
$message = Message::success();
$sql_query = implode("\n", $queries);
}
/**
* Reloads the privilege tables into memory
*/
$message_ret = $serverPrivileges->updateMessageForReload();
if ($message_ret !== null) {
$message = $message_ret;
unset($message_ret);
}
/**
* If we are in an Ajax request for Create User/Edit User/Revoke User/
* Flush Privileges, show $message and return.
*/
if ($this->response->isAjax()
&& empty($_REQUEST['ajax_page_request'])
&& ! isset($_GET['export'])
&& (! isset($_POST['submit_mult']) || $_POST['submit_mult'] !== 'export')
&& ((! isset($_GET['initial']) || $_GET['initial'] === null
|| $_GET['initial'] === '')
|| (isset($_POST['delete']) && $_POST['delete'] === __('Go')))
&& ! isset($_GET['showall'])
&& ! isset($_GET['edit_user_group_dialog'])
) {
$extra_data = $serverPrivileges->getExtraDataForAjaxBehavior(
($password ?? ''),
($sql_query ?? ''),
($hostname ?? ''),
($username ?? '')
);
if (! empty($message) && $message instanceof Message) {
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON($extra_data);
return;
}
}
/**
* Displays the links
*/
if (isset($_GET['viewing_mode']) && $_GET['viewing_mode'] === 'db') {
$db = $_REQUEST['db'] = $_GET['checkprivsdb'];
// Gets the database structure
$sub_part = '_structure';
ob_start();
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
$content = ob_get_clean();
$this->response->addHTML($content . "\n");
} elseif (! empty($GLOBALS['message'])) {
$this->response->addHTML(Generator::getMessage($GLOBALS['message']));
unset($GLOBALS['message']);
}
if (! empty($_GET['edit_user_group_dialog']) && $cfgRelation['menuswork']) {
$dialog = $serverPrivileges->getHtmlToChooseUserGroup($username ?? null);
if ($this->response->isAjax()) {
$this->response->addJSON('message', $dialog);
return;
}
$this->response->addHTML($dialog);
}
// export user definition
if (isset($_GET['export'])
|| (isset($_POST['submit_mult']) && $_POST['submit_mult'] === 'export')
) {
[$title, $export] = $serverPrivileges->getListForExportUserDefinition(
$username ?? '',
$hostname ?? ''
);
unset($username, $hostname, $grants, $one_grant);
if ($this->response->isAjax()) {
$this->response->addJSON('message', $export);
$this->response->addJSON('title', $title);
return;
}
$this->response->addHTML('<h2>' . $title . '</h2>' . $export);
}
// Show back the form if an error occurred
if (isset($_GET['adduser']) || $_add_user_error === true) {
// Add user
$this->response->addHTML(
$serverPrivileges->getHtmlForAddUser(($dbname ?? ''))
);
} elseif (isset($_GET['checkprivsdb'])) {
if (isset($_GET['checkprivstable'])) {
$this->response->addHTML($tableController->index([
'checkprivsdb' => $_GET['checkprivsdb'],
'checkprivstable' => $_GET['checkprivstable'],
]));
} elseif ($this->response->isAjax() === true && empty($_REQUEST['ajax_page_request'])) {
$message = Message::success(__('User has been added.'));
$this->response->addJSON('message', $message);
return;
} else {
$this->response->addHTML($databaseController->index([
'checkprivsdb' => $_GET['checkprivsdb'],
]));
}
} else {
if (isset($dbname) && ! is_array($dbname)) {
$url_dbname = urlencode(
str_replace(
[
'\_',
'\%',
],
[
'_',
'%',
],
$dbname
)
);
}
if (! isset($username)) {
// No username is given --> display the overview
$this->response->addHTML(
$serverPrivileges->getHtmlForUserOverview($PMA_Theme->getImgPath(), $text_dir)
);
} elseif (! empty($routinename)) {
$this->response->addHTML(
$serverPrivileges->getHtmlForRoutineSpecificPrivileges(
$username,
$hostname ?? '',
$dbname,
$routinename,
$url_dbname ?? ''
)
);
} else {
// A user was selected -> display the user's properties
// In an Ajax request, prevent cached values from showing
if ($this->response->isAjax()) {
header('Cache-Control: no-cache');
}
$this->response->addHTML(
$serverPrivileges->getHtmlForUserProperties(
$dbname_is_wildcard,
$url_dbname ?? '',
$username,
$hostname ?? '',
$dbname ?? '',
$tablename ?? ''
)
);
}
}
if ((! isset($_GET['viewing_mode']) || $_GET['viewing_mode'] !== 'server')
|| ! $cfgRelation['menuswork']
) {
return;
}
$this->response->addHTML('</div>');
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* Server replications
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\ReplicationGui;
use PhpMyAdmin\ReplicationInfo;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function is_array;
/**
* Server replications
*/
class ReplicationController extends AbstractController
{
/** @var ReplicationGui */
private $replicationGui;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, ReplicationGui $replicationGui, $dbi)
{
parent::__construct($response, $template);
$this->replicationGui = $replicationGui;
$this->dbi = $dbi;
}
public function index(): void
{
global $url_params, $err_url;
$params = [
'url_params' => $_POST['url_params'] ?? null,
'mr_configure' => $_POST['mr_configure'] ?? null,
'sl_configure' => $_POST['sl_configure'] ?? null,
'repl_clear_scr' => $_POST['repl_clear_scr'] ?? null,
];
$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->addScriptFiles(['server/privileges.js', 'replication.js', 'vendor/zxcvbn.js']);
if (isset($params['url_params']) && is_array($params['url_params'])) {
$url_params = $params['url_params'];
}
if ($this->dbi->isSuperUser()) {
$this->replicationGui->handleControlRequest();
}
$errorMessages = $this->replicationGui->getHtmlForErrorMessage();
if ($primaryInfo['status']) {
$masterReplicationHtml = $this->replicationGui->getHtmlForMasterReplication();
}
if (isset($params['mr_configure'])) {
$masterConfigurationHtml = $this->replicationGui->getHtmlForMasterConfiguration();
} else {
if (! isset($params['repl_clear_scr'])) {
$slaveConfigurationHtml = $this->replicationGui->getHtmlForSlaveConfiguration(
$replicaInfo['status'],
$replicationInfo->getReplicaStatus()
);
}
if (isset($params['sl_configure'])) {
$changeMasterHtml = $this->replicationGui->getHtmlForReplicationChangeMaster('slave_changemaster');
}
}
$this->render('server/replication/index', [
'url_params' => $url_params,
'is_super_user' => $this->dbi->isSuperUser(),
'error_messages' => $errorMessages,
'is_master' => $primaryInfo['status'],
'master_configure' => $params['mr_configure'],
'slave_configure' => $params['sl_configure'],
'clear_screen' => $params['repl_clear_scr'],
'master_replication_html' => $masterReplicationHtml ?? '',
'master_configuration_html' => $masterConfigurationHtml ?? '',
'slave_configuration_html' => $slaveConfigurationHtml ?? '',
'change_master_html' => $changeMasterHtml ?? '',
]);
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\SqlQueryForm;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
/**
* Server SQL executor
*/
class SqlController extends AbstractController
{
/** @var SqlQueryForm */
private $sqlQueryForm;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, SqlQueryForm $sqlQueryForm, $dbi)
{
parent::__construct($response, $template);
$this->sqlQueryForm = $sqlQueryForm;
$this->dbi = $dbi;
}
public function index(): void
{
global $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());
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->response->addHTML($this->sqlQueryForm->getHtml());
}
}

View file

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

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\Advisor;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Template;
/**
* Displays the advisor feature
*/
class AdvisorController extends AbstractController
{
/** @var Advisor */
private $advisor;
/**
* @param Response $response
* @param Data $data
*/
public function __construct($response, Template $template, $data, Advisor $advisor)
{
parent::__construct($response, $template, $data);
$this->advisor = $advisor;
}
public function index(): void
{
$data = [];
if ($this->data->dataLoaded) {
$data = $this->advisor->run();
}
$this->render('server/status/advisor/index', ['data' => $data]);
}
}

View file

@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Server\Status\Monitor;
use PhpMyAdmin\Server\SysInfo\SysInfo;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function is_numeric;
use function microtime;
class MonitorController extends AbstractController
{
/** @var Monitor */
private $monitor;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Data $data
* @param Monitor $monitor
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $data, $monitor, $dbi)
{
parent::__construct($response, $template, $data);
$this->monitor = $monitor;
$this->dbi = $dbi;
}
public function index(): void
{
global $PMA_Theme, $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->addScriptFiles([
'vendor/jquery/jquery.tablesorter.js',
'jquery.sortable-table.js',
'vendor/jqplot/jquery.jqplot.js',
'vendor/jqplot/plugins/jqplot.pieRenderer.js',
'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js',
'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js',
'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js',
'vendor/jqplot/plugins/jqplot.highlighter.js',
'vendor/jqplot/plugins/jqplot.cursor.js',
'jqplot/plugins/jqplot.byteFormatter.js',
'server/status/monitor.js',
'server/status/sorter.js',
]);
$form = [
'server_time' => (int) (microtime(true) * 1000),
'server_os' => SysInfo::getOs(),
'is_superuser' => $this->dbi->isSuperUser(),
'server_db_isLocal' => $this->data->dbIsLocal,
];
$javascriptVariableNames = [];
foreach ($this->data->status as $name => $value) {
if (! is_numeric($value)) {
continue;
}
$javascriptVariableNames[] = $name;
}
$this->render('server/status/monitor/index', [
'image_path' => $PMA_Theme->getImgPath(),
'javascript_variable_names' => $javascriptVariableNames,
'form' => $form,
]);
}
public function chartingData(): void
{
global $err_url;
$params = ['requiredData' => $_POST['requiredData'] ?? null];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'message' => $this->monitor->getJsonForChartingData(
$params['requiredData'] ?? ''
),
]);
}
public function logDataTypeSlow(): void
{
global $err_url;
$params = [
'time_start' => $_POST['time_start'] ?? null,
'time_end' => $_POST['time_end'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'message' => $this->monitor->getJsonForLogDataTypeSlow(
(int) $params['time_start'],
(int) $params['time_end']
),
]);
}
public function logDataTypeGeneral(): void
{
global $err_url;
$params = [
'time_start' => $_POST['time_start'] ?? null,
'time_end' => $_POST['time_end'] ?? null,
'limitTypes' => $_POST['limitTypes'] ?? null,
'removeVariables' => $_POST['removeVariables'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'message' => $this->monitor->getJsonForLogDataTypeGeneral(
(int) $params['time_start'],
(int) $params['time_end'],
(bool) $params['limitTypes'],
(bool) $params['removeVariables']
),
]);
}
public function loggingVars(): void
{
global $err_url;
$params = [
'varName' => $_POST['varName'] ?? null,
'varValue' => $_POST['varValue'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'message' => $this->monitor->getJsonForLoggingVars(
$params['varName'],
$params['varValue']
),
]);
}
public function queryAnalyzer(): void
{
global $err_url;
$params = [
'database' => $_POST['database'] ?? null,
'query' => $_POST['query'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'message' => $this->monitor->getJsonForQueryAnalyzer(
$params['database'] ?? '',
$params['query'] ?? ''
),
]);
}
}

View file

@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function array_keys;
use function count;
use function mb_strtolower;
use function strlen;
use function ucfirst;
class ProcessesController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Data $data
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $data, $dbi)
{
parent::__construct($response, $template, $data);
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$params = [
'showExecuting' => $_POST['showExecuting'] ?? null,
'full' => $_POST['full'] ?? null,
'column_name' => $_POST['column_name'] ?? null,
'order_by_field' => $_POST['order_by_field'] ?? null,
'sort_order' => $_POST['sort_order'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->addScriptFiles(['server/status/processes.js']);
$isChecked = false;
if (! empty($params['showExecuting'])) {
$isChecked = true;
}
$urlParams = [
'ajax_request' => true,
'full' => $params['full'] ?? '',
'column_name' => $params['column_name'] ?? '',
'order_by_field' => $params['order_by_field'] ?? '',
'sort_order' => $params['sort_order'] ?? '',
];
$serverProcessList = $this->getList($params);
$this->render('server/status/processes/index', [
'url_params' => $urlParams,
'is_checked' => $isChecked,
'server_process_list' => $serverProcessList,
]);
}
/**
* Only sends the process list table
*/
public function refresh(): void
{
$params = [
'showExecuting' => $_POST['showExecuting'] ?? null,
'full' => $_POST['full'] ?? null,
'column_name' => $_POST['column_name'] ?? null,
'order_by_field' => $_POST['order_by_field'] ?? null,
'sort_order' => $_POST['sort_order'] ?? null,
];
if (! $this->response->isAjax()) {
return;
}
$this->response->addHTML($this->getList($params));
}
/**
* @param array $params Request parameters
*/
public function kill(array $params): void
{
if (! $this->response->isAjax()) {
return;
}
$kill = (int) $params['id'];
$query = $this->dbi->getKillQuery($kill);
if ($this->dbi->tryQuery($query)) {
$message = Message::success(
__('Thread %s was successfully killed.')
);
$this->response->setRequestStatus(true);
} else {
$message = Message::error(
__(
'phpMyAdmin was unable to kill thread %s.'
. ' It probably has already been closed.'
)
);
$this->response->setRequestStatus(false);
}
$message->addParam($kill);
$this->response->addJSON(['message' => $message]);
}
/**
* @param array $params Request parameters
*/
private function getList(array $params): string
{
$urlParams = [];
$showFullSql = ! empty($params['full']);
if ($showFullSql) {
$urlParams['full'] = '';
} else {
$urlParams['full'] = 1;
}
// This array contains display name and real column name of each
// sortable column in the table
$sortableColumns = [
[
'column_name' => __('ID'),
'order_by_field' => 'Id',
],
[
'column_name' => __('User'),
'order_by_field' => 'User',
],
[
'column_name' => __('Host'),
'order_by_field' => 'Host',
],
[
'column_name' => __('Database'),
'order_by_field' => 'db',
],
[
'column_name' => __('Command'),
'order_by_field' => 'Command',
],
[
'column_name' => __('Time'),
'order_by_field' => 'Time',
],
[
'column_name' => __('Status'),
'order_by_field' => 'State',
],
[
'column_name' => __('Progress'),
'order_by_field' => 'Progress',
],
[
'column_name' => __('SQL query'),
'order_by_field' => 'Info',
],
];
$sortableColCount = count($sortableColumns);
$sqlQuery = $showFullSql
? 'SHOW FULL PROCESSLIST'
: 'SHOW PROCESSLIST';
if ((! empty($params['order_by_field'])
&& ! empty($params['sort_order']))
|| ! empty($params['showExecuting'])
) {
$urlParams['order_by_field'] = $params['order_by_field'];
$urlParams['sort_order'] = $params['sort_order'];
$urlParams['showExecuting'] = $params['showExecuting'];
$sqlQuery = 'SELECT * FROM `INFORMATION_SCHEMA`.`PROCESSLIST` ';
}
if (! empty($params['showExecuting'])) {
$sqlQuery .= ' WHERE state != "" ';
}
if (! empty($params['order_by_field']) && ! empty($params['sort_order'])) {
$sqlQuery .= ' ORDER BY '
. Util::backquote($params['order_by_field'])
. ' ' . $params['sort_order'];
}
$result = $this->dbi->query($sqlQuery);
$columns = [];
foreach ($sortableColumns as $columnKey => $column) {
$is_sorted = ! empty($params['order_by_field'])
&& ! empty($params['sort_order'])
&& ($params['order_by_field'] == $column['order_by_field']);
$column['sort_order'] = 'ASC';
if ($is_sorted && $params['sort_order'] === 'ASC') {
$column['sort_order'] = 'DESC';
}
if (isset($params['showExecuting'])) {
$column['showExecuting'] = 'on';
}
$columns[$columnKey] = [
'name' => $column['column_name'],
'params' => $column,
'is_sorted' => $is_sorted,
'sort_order' => $column['sort_order'],
'has_full_query' => false,
'is_full' => false,
];
if (0 !== --$sortableColCount) {
continue;
}
$columns[$columnKey]['has_full_query'] = true;
if (! $showFullSql) {
continue;
}
$columns[$columnKey]['is_full'] = true;
}
$rows = [];
while ($process = $this->dbi->fetchAssoc($result)) {
// Array keys need to modify due to the way it has used
// to display column values
if ((! empty($params['order_by_field']) && ! empty($params['sort_order']))
|| ! empty($params['showExecuting'])
) {
foreach (array_keys($process) as $key) {
$newKey = ucfirst(mb_strtolower($key));
if ($newKey === $key) {
continue;
}
$process[$newKey] = $process[$key];
unset($process[$key]);
}
}
$rows[] = [
'id' => $process['Id'],
'user' => $process['User'],
'host' => $process['Host'],
'db' => ! isset($process['db']) || strlen($process['db']) === 0 ? '' : $process['db'],
'command' => $process['Command'],
'time' => $process['Time'],
'state' => ! empty($process['State']) ? $process['State'] : '---',
'progress' => ! empty($process['Progress']) ? $process['Progress'] : '---',
'info' => ! empty($process['Info']) ? Generator::formatSql(
$process['Info'],
! $showFullSql
) : '---',
];
}
return $this->template->render('server/status/processes/list', [
'columns' => $columns,
'rows' => $rows,
'refresh_params' => $urlParams,
]);
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* Displays query statistics for the server
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function array_sum;
use function arsort;
use function count;
use function str_replace;
class QueriesController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Data $data
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $data, $dbi)
{
parent::__construct($response, $template, $data);
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->addScriptFiles([
'chart.js',
'vendor/jqplot/jquery.jqplot.js',
'vendor/jqplot/plugins/jqplot.pieRenderer.js',
'vendor/jqplot/plugins/jqplot.highlighter.js',
'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js',
'vendor/jquery/jquery.tablesorter.js',
'server/status/sorter.js',
'server/status/queries.js',
]);
if ($this->data->dataLoaded) {
$hourFactor = 3600 / $this->data->status['Uptime'];
$usedQueries = $this->data->usedQueries;
$totalQueries = array_sum($usedQueries);
$stats = [
'total' => $totalQueries,
'per_hour' => $totalQueries * $hourFactor,
'per_minute' => $totalQueries * 60 / $this->data->status['Uptime'],
'per_second' => $totalQueries / $this->data->status['Uptime'],
];
// reverse sort by value to show most used statements first
arsort($usedQueries);
$chart = [];
$querySum = array_sum($usedQueries);
$otherSum = 0;
$queries = [];
foreach ($usedQueries as $key => $value) {
// For the percentage column, use Questions - Connections, because
// the number of connections is not an item of the Query types
// but is included in Questions. Then the total of the percentages is 100.
$name = str_replace(['Com_', '_'], ['', ' '], $key);
// Group together values that make out less than 2% into "Other", but only
// if we have more than 6 fractions already
if ($value < $querySum * 0.02 && count($chart) > 6) {
$otherSum += $value;
} else {
$chart[$name] = $value;
}
$queries[$key] = [
'name' => $name,
'value' => $value,
'per_hour' => $value * $hourFactor,
'percentage' => $value * 100 / $totalQueries,
];
}
if ($otherSum > 0) {
$chart[__('Other')] = $otherSum;
}
}
$this->render('server/status/queries/index', [
'is_data_loaded' => $this->data->dataLoaded,
'stats' => $stats ?? null,
'queries' => $queries ?? [],
'chart' => $chart ?? [],
]);
}
}

View file

@ -0,0 +1,256 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\ReplicationGui;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function implode;
/**
* Object the server status page: processes, connections and traffic.
*/
class StatusController extends AbstractController
{
/** @var ReplicationGui */
private $replicationGui;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Data $data
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $data, ReplicationGui $replicationGui, $dbi)
{
parent::__construct($response, $template, $data);
$this->replicationGui = $replicationGui;
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$replicationInfo = $this->data->getReplicationInfo();
$primaryInfo = $replicationInfo->getPrimaryInfo();
$replicaInfo = $replicationInfo->getReplicaInfo();
$traffic = [];
$connections = [];
$replication = '';
if ($this->data->dataLoaded) {
// In some case the data was reported not to exist, check it for all keys
if (isset($this->data->status['Bytes_received'], $this->data->status['Bytes_sent'])) {
$networkTraffic = implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'],
3,
1
)
);
}
if (isset($this->data->status['Uptime'])) {
$uptime = Util::timespanFormat($this->data->status['Uptime']);
}
$startTime = Util::localisedDate($this->getStartTime());
$traffic = $this->getTrafficInfo();
$connections = $this->getConnectionsInfo();
if ($primaryInfo['status']) {
$replication .= $this->replicationGui->getHtmlForReplicationStatusTable('master');
}
if ($replicaInfo['status']) {
$replication .= $this->replicationGui->getHtmlForReplicationStatusTable('slave');
}
}
$this->render('server/status/status/index', [
'is_data_loaded' => $this->data->dataLoaded,
'network_traffic' => $networkTraffic ?? null,
'uptime' => $uptime ?? null,
'start_time' => $startTime ?? null,
'traffic' => $traffic,
'connections' => $connections,
'is_master' => $primaryInfo['status'],
'is_slave' => $replicaInfo['status'],
'replication' => $replication,
]);
}
private function getStartTime(): int
{
return (int) $this->dbi->fetchValue(
'SELECT UNIX_TIMESTAMP() - ' . $this->data->status['Uptime']
);
}
/**
* @return array
*/
private function getTrafficInfo(): array
{
$hourFactor = 3600 / $this->data->status['Uptime'];
return [
[
'name' => __('Received'),
'number' => implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_received'],
3,
1
)
),
'per_hour' => implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_received'] * $hourFactor,
3,
1
)
),
],
[
'name' => __('Sent'),
'number' => implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_sent'],
3,
1
)
),
'per_hour' => implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_sent'] * $hourFactor,
3,
1
)
),
],
[
'name' => __('Total'),
'number' => implode(
' ',
Util::formatByteDown(
$this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'],
3,
1
)
),
'per_hour' => implode(
' ',
Util::formatByteDown(
($this->data->status['Bytes_received'] + $this->data->status['Bytes_sent']) * $hourFactor,
3,
1
)
),
],
];
}
/**
* @return array
*/
private function getConnectionsInfo(): array
{
$hourFactor = 3600 / $this->data->status['Uptime'];
$failedAttemptsPercentage = '---';
$abortedPercentage = '---';
if ($this->data->status['Connections'] > 0) {
$failedAttemptsPercentage = Util::formatNumber(
$this->data->status['Aborted_connects'] * 100 / $this->data->status['Connections'],
0,
2,
true
) . '%';
$abortedPercentage = Util::formatNumber(
$this->data->status['Aborted_clients'] * 100 / $this->data->status['Connections'],
0,
2,
true
) . '%';
}
return [
[
'name' => __('Max. concurrent connections'),
'number' => Util::formatNumber(
$this->data->status['Max_used_connections'],
0
),
'per_hour' => '---',
'percentage' => '---',
],
[
'name' => __('Failed attempts'),
'number' => Util::formatNumber(
$this->data->status['Aborted_connects'],
4,
1,
true
),
'per_hour' => Util::formatNumber(
$this->data->status['Aborted_connects'] * $hourFactor,
4,
2,
true
),
'percentage' => $failedAttemptsPercentage,
],
[
'name' => __('Aborted'),
'number' => Util::formatNumber(
$this->data->status['Aborted_clients'],
4,
1,
true
),
'per_hour' => Util::formatNumber(
$this->data->status['Aborted_clients'] * $hourFactor,
4,
2,
true
),
'percentage' => $abortedPercentage,
],
[
'name' => __('Total'),
'number' => Util::formatNumber(
$this->data->status['Connections'],
4,
0
),
'per_hour' => Util::formatNumber(
$this->data->status['Connections'] * $hourFactor,
4,
2
),
'percentage' => Util::formatNumber(100, 0, 2) . '%',
],
];
}
}

View file

@ -0,0 +1,679 @@
<?php
/**
* Displays a list of server status variables
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server\Status;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Status\Data;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function in_array;
use function is_numeric;
use function mb_strpos;
class VariablesController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Data $data
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $data, $dbi)
{
parent::__construct($response, $template, $data);
$this->dbi = $dbi;
}
public function index(): void
{
global $err_url;
$params = [
'flush' => $_POST['flush'] ?? null,
'filterAlert' => $_POST['filterAlert'] ?? null,
'filterText' => $_POST['filterText'] ?? null,
'filterCategory' => $_POST['filterCategory'] ?? null,
'dontFormat' => $_POST['dontFormat'] ?? null,
];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$this->addScriptFiles([
'server/status/variables.js',
'vendor/jquery/jquery.tablesorter.js',
'server/status/sorter.js',
]);
if (isset($params['flush'])) {
$this->flush($params['flush']);
}
if ($this->data->dataLoaded) {
$categories = [];
foreach ($this->data->sections as $sectionId => $sectionName) {
if (! isset($this->data->sectionUsed[$sectionId])) {
continue;
}
$categories[$sectionId] = [
'id' => $sectionId,
'name' => $sectionName,
'is_selected' => false,
];
if (empty($params['filterCategory'])
|| $params['filterCategory'] !== $sectionId
) {
continue;
}
$categories[$sectionId]['is_selected'] = true;
}
$links = [];
foreach ($this->data->links as $sectionName => $sectionLinks) {
$links[$sectionName] = [
'name' => 'status_' . $sectionName,
'links' => $sectionLinks,
];
}
$descriptions = $this->getDescriptions();
$alerts = $this->getAlerts();
$variables = [];
foreach ($this->data->status as $name => $value) {
$variables[$name] = [
'name' => $name,
'value' => $value,
'is_numeric' => is_numeric($value),
'class' => $this->data->allocationMap[$name] ?? null,
'doc' => '',
'has_alert' => false,
'is_alert' => false,
'description' => $descriptions[$name] ?? '',
'description_doc' => [],
];
// Fields containing % are calculated,
// they can not be described in MySQL documentation
if (mb_strpos($name, '%') === false) {
$variables[$name]['doc'] = Generator::linkToVarDocumentation(
$name,
$this->dbi->isMariaDB()
);
}
if (isset($alerts[$name])) {
$variables[$name]['has_alert'] = true;
if ($value > $alerts[$name]) {
$variables[$name]['is_alert'] = true;
}
}
if (! isset($this->data->links[$name])) {
continue;
}
foreach ($this->data->links[$name] as $linkName => $linkUrl) {
$variables[$name]['description_doc'][] = [
'name' => $linkName,
'url' => $linkUrl,
];
}
}
}
$this->render('server/status/variables/index', [
'is_data_loaded' => $this->data->dataLoaded,
'filter_text' => ! empty($params['filterText']) ? $params['filterText'] : '',
'is_only_alerts' => ! empty($params['filterAlert']),
'is_not_formatted' => ! empty($params['dontFormat']),
'categories' => $categories ?? [],
'links' => $links ?? [],
'variables' => $variables ?? [],
]);
}
/**
* Flush status variables if requested
*
* @param string $flush Variable name
*/
private function flush(string $flush): void
{
$flushCommands = [
'STATUS',
'TABLES',
'QUERY CACHE',
];
if (! in_array($flush, $flushCommands)) {
return;
}
$this->dbi->query('FLUSH ' . $flush . ';');
}
/**
* @return array
*/
private function getAlerts(): array
{
// name => max value before alert
return [
// lower is better
// variable => max value
'Aborted_clients' => 0,
'Aborted_connects' => 0,
'Binlog_cache_disk_use' => 0,
'Created_tmp_disk_tables' => 0,
'Handler_read_rnd' => 0,
'Handler_read_rnd_next' => 0,
'Innodb_buffer_pool_pages_dirty' => 0,
'Innodb_buffer_pool_reads' => 0,
'Innodb_buffer_pool_wait_free' => 0,
'Innodb_log_waits' => 0,
'Innodb_row_lock_time_avg' => 10, // ms
'Innodb_row_lock_time_max' => 50, // ms
'Innodb_row_lock_waits' => 0,
'Slow_queries' => 0,
'Delayed_errors' => 0,
'Select_full_join' => 0,
'Select_range_check' => 0,
'Sort_merge_passes' => 0,
'Opened_tables' => 0,
'Table_locks_waited' => 0,
'Qcache_lowmem_prunes' => 0,
'Qcache_free_blocks' =>
isset($this->data->status['Qcache_total_blocks'])
? $this->data->status['Qcache_total_blocks'] / 5
: 0,
'Slow_launch_threads' => 0,
// depends on Key_read_requests
// normally lower then 1:0.01
'Key_reads' => isset($this->data->status['Key_read_requests'])
? 0.01 * $this->data->status['Key_read_requests'] : 0,
// depends on Key_write_requests
// normally nearly 1:1
'Key_writes' => isset($this->data->status['Key_write_requests'])
? 0.9 * $this->data->status['Key_write_requests'] : 0,
'Key_buffer_fraction' => 0.5,
// alert if more than 95% of thread cache is in use
'Threads_cached' => isset($this->data->variables['thread_cache_size'])
? 0.95 * $this->data->variables['thread_cache_size'] : 0,
// higher is better
// variable => min value
//'Handler read key' => '> ',
];
}
/**
* Returns a list of variable descriptions
*
* @return array
*/
private function getDescriptions(): array
{
/**
* Messages are built using the message name
*/
return [
'Aborted_clients' => __(
'The number of connections that were aborted because the client died'
. ' without closing the connection properly.'
),
'Aborted_connects' => __(
'The number of failed attempts to connect to the MySQL server.'
),
'Binlog_cache_disk_use' => __(
'The number of transactions that used the temporary binary log cache'
. ' but that exceeded the value of binlog_cache_size and used a'
. ' temporary file to store statements from the transaction.'
),
'Binlog_cache_use' => __(
'The number of transactions that used the temporary binary log cache.'
),
'Connections' => __(
'The number of connection attempts (successful or not)'
. ' to the MySQL server.'
),
'Created_tmp_disk_tables' => __(
'The number of temporary tables on disk created automatically by'
. ' the server while executing statements. If'
. ' Created_tmp_disk_tables is big, you may want to increase the'
. ' tmp_table_size value to cause temporary tables to be'
. ' memory-based instead of disk-based.'
),
'Created_tmp_files' => __(
'How many temporary files mysqld has created.'
),
'Created_tmp_tables' => __(
'The number of in-memory temporary tables created automatically'
. ' by the server while executing statements.'
),
'Delayed_errors' => __(
'The number of rows written with INSERT DELAYED for which some'
. ' error occurred (probably duplicate key).'
),
'Delayed_insert_threads' => __(
'The number of INSERT DELAYED handler threads in use. Every'
. ' different table on which one uses INSERT DELAYED gets'
. ' its own thread.'
),
'Delayed_writes' => __(
'The number of INSERT DELAYED rows written.'
),
'Flush_commands' => __(
'The number of executed FLUSH statements.'
),
'Handler_commit' => __(
'The number of internal COMMIT statements.'
),
'Handler_delete' => __(
'The number of times a row was deleted from a table.'
),
'Handler_discover' => __(
'The MySQL server can ask the NDB Cluster storage engine if it'
. ' knows about a table with a given name. This is called discovery.'
. ' Handler_discover indicates the number of time tables have been'
. ' discovered.'
),
'Handler_read_first' => __(
'The number of times the first entry was read from an index. If this'
. ' is high, it suggests that the server is doing a lot of full'
. ' index scans; for example, SELECT col1 FROM foo, assuming that'
. ' col1 is indexed.'
),
'Handler_read_key' => __(
'The number of requests to read a row based on a key. If this is'
. ' high, it is a good indication that your queries and tables'
. ' are properly indexed.'
),
'Handler_read_next' => __(
'The number of requests to read the next row in key order. This is'
. ' incremented if you are querying an index column with a range'
. ' constraint or if you are doing an index scan.'
),
'Handler_read_prev' => __(
'The number of requests to read the previous row in key order.'
. ' This read method is mainly used to optimize ORDER BY … DESC.'
),
'Handler_read_rnd' => __(
'The number of requests to read a row based on a fixed position.'
. ' This is high if you are doing a lot of queries that require'
. ' sorting of the result. You probably have a lot of queries that'
. ' require MySQL to scan whole tables or you have joins that'
. ' don\'t use keys properly.'
),
'Handler_read_rnd_next' => __(
'The number of requests to read the next row in the data file.'
. ' This is high if you are doing a lot of table scans. Generally'
. ' this suggests that your tables are not properly indexed or that'
. ' your queries are not written to take advantage of the indexes'
. ' you have.'
),
'Handler_rollback' => __(
'The number of internal ROLLBACK statements.'
),
'Handler_update' => __(
'The number of requests to update a row in a table.'
),
'Handler_write' => __(
'The number of requests to insert a row in a table.'
),
'Innodb_buffer_pool_pages_data' => __(
'The number of pages containing data (dirty or clean).'
),
'Innodb_buffer_pool_pages_dirty' => __(
'The number of pages currently dirty.'
),
'Innodb_buffer_pool_pages_flushed' => __(
'The number of buffer pool pages that have been requested'
. ' to be flushed.'
),
'Innodb_buffer_pool_pages_free' => __(
'The number of free pages.'
),
'Innodb_buffer_pool_pages_latched' => __(
'The number of latched pages in InnoDB buffer pool. These are pages'
. ' currently being read or written or that can\'t be flushed or'
. ' removed for some other reason.'
),
'Innodb_buffer_pool_pages_misc' => __(
'The number of pages busy because they have been allocated for'
. ' administrative overhead such as row locks or the adaptive'
. ' hash index. This value can also be calculated as'
. ' Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free'
. ' - Innodb_buffer_pool_pages_data.'
),
'Innodb_buffer_pool_pages_total' => __(
'Total size of buffer pool, in pages.'
),
'Innodb_buffer_pool_read_ahead_rnd' => __(
'The number of "random" read-aheads InnoDB initiated. This happens'
. ' when a query is to scan a large portion of a table but in'
. ' random order.'
),
'Innodb_buffer_pool_read_ahead_seq' => __(
'The number of sequential read-aheads InnoDB initiated. This'
. ' happens when InnoDB does a sequential full table scan.'
),
'Innodb_buffer_pool_read_requests' => __(
'The number of logical read requests InnoDB has done.'
),
'Innodb_buffer_pool_reads' => __(
'The number of logical reads that InnoDB could not satisfy'
. ' from buffer pool and had to do a single-page read.'
),
'Innodb_buffer_pool_wait_free' => __(
'Normally, writes to the InnoDB buffer pool happen in the'
. ' background. However, if it\'s necessary to read or create a page'
. ' and no clean pages are available, it\'s necessary to wait for'
. ' pages to be flushed first. This counter counts instances of'
. ' these waits. If the buffer pool size was set properly, this'
. ' value should be small.'
),
'Innodb_buffer_pool_write_requests' => __(
'The number writes done to the InnoDB buffer pool.'
),
'Innodb_data_fsyncs' => __(
'The number of fsync() operations so far.'
),
'Innodb_data_pending_fsyncs' => __(
'The current number of pending fsync() operations.'
),
'Innodb_data_pending_reads' => __(
'The current number of pending reads.'
),
'Innodb_data_pending_writes' => __(
'The current number of pending writes.'
),
'Innodb_data_read' => __(
'The amount of data read so far, in bytes.'
),
'Innodb_data_reads' => __(
'The total number of data reads.'
),
'Innodb_data_writes' => __(
'The total number of data writes.'
),
'Innodb_data_written' => __(
'The amount of data written so far, in bytes.'
),
'Innodb_dblwr_pages_written' => __(
'The number of pages that have been written for'
. ' doublewrite operations.'
),
'Innodb_dblwr_writes' => __(
'The number of doublewrite operations that have been performed.'
),
'Innodb_log_waits' => __(
'The number of waits we had because log buffer was too small and'
. ' we had to wait for it to be flushed before continuing.'
),
'Innodb_log_write_requests' => __(
'The number of log write requests.'
),
'Innodb_log_writes' => __(
'The number of physical writes to the log file.'
),
'Innodb_os_log_fsyncs' => __(
'The number of fsync() writes done to the log file.'
),
'Innodb_os_log_pending_fsyncs' => __(
'The number of pending log file fsyncs.'
),
'Innodb_os_log_pending_writes' => __(
'Pending log file writes.'
),
'Innodb_os_log_written' => __(
'The number of bytes written to the log file.'
),
'Innodb_pages_created' => __(
'The number of pages created.'
),
'Innodb_page_size' => __(
'The compiled-in InnoDB page size (default 16KB). Many values are'
. ' counted in pages; the page size allows them to be easily'
. ' converted to bytes.'
),
'Innodb_pages_read' => __(
'The number of pages read.'
),
'Innodb_pages_written' => __(
'The number of pages written.'
),
'Innodb_row_lock_current_waits' => __(
'The number of row locks currently being waited for.'
),
'Innodb_row_lock_time_avg' => __(
'The average time to acquire a row lock, in milliseconds.'
),
'Innodb_row_lock_time' => __(
'The total time spent in acquiring row locks, in milliseconds.'
),
'Innodb_row_lock_time_max' => __(
'The maximum time to acquire a row lock, in milliseconds.'
),
'Innodb_row_lock_waits' => __(
'The number of times a row lock had to be waited for.'
),
'Innodb_rows_deleted' => __(
'The number of rows deleted from InnoDB tables.'
),
'Innodb_rows_inserted' => __(
'The number of rows inserted in InnoDB tables.'
),
'Innodb_rows_read' => __(
'The number of rows read from InnoDB tables.'
),
'Innodb_rows_updated' => __(
'The number of rows updated in InnoDB tables.'
),
'Key_blocks_not_flushed' => __(
'The number of key blocks in the key cache that have changed but'
. ' haven\'t yet been flushed to disk. It used to be known as'
. ' Not_flushed_key_blocks.'
),
'Key_blocks_unused' => __(
'The number of unused blocks in the key cache. You can use this'
. ' value to determine how much of the key cache is in use.'
),
'Key_blocks_used' => __(
'The number of used blocks in the key cache. This value is a'
. ' high-water mark that indicates the maximum number of blocks'
. ' that have ever been in use at one time.'
),
'Key_buffer_fraction_%' => __(
'Percentage of used key cache (calculated value)'
),
'Key_read_requests' => __(
'The number of requests to read a key block from the cache.'
),
'Key_reads' => __(
'The number of physical reads of a key block from disk. If Key_reads'
. ' is big, then your key_buffer_size value is probably too small.'
. ' The cache miss rate can be calculated as'
. ' Key_reads/Key_read_requests.'
),
'Key_read_ratio_%' => __(
'Key cache miss calculated as rate of physical reads compared'
. ' to read requests (calculated value)'
),
'Key_write_requests' => __(
'The number of requests to write a key block to the cache.'
),
'Key_writes' => __(
'The number of physical writes of a key block to disk.'
),
'Key_write_ratio_%' => __(
'Percentage of physical writes compared'
. ' to write requests (calculated value)'
),
'Last_query_cost' => __(
'The total cost of the last compiled query as computed by the query'
. ' optimizer. Useful for comparing the cost of different query'
. ' plans for the same query. The default value of 0 means that'
. ' no query has been compiled yet.'
),
'Max_used_connections' => __(
'The maximum number of connections that have been in use'
. ' simultaneously since the server started.'
),
'Not_flushed_delayed_rows' => __(
'The number of rows waiting to be written in INSERT DELAYED queues.'
),
'Opened_tables' => __(
'The number of tables that have been opened. If opened tables is'
. ' big, your table_open_cache value is probably too small.'
),
'Open_files' => __(
'The number of files that are open.'
),
'Open_streams' => __(
'The number of streams that are open (used mainly for logging).'
),
'Open_tables' => __(
'The number of tables that are open.'
),
'Qcache_free_blocks' => __(
'The number of free memory blocks in query cache. High numbers can'
. ' indicate fragmentation issues, which may be solved by issuing'
. ' a FLUSH QUERY CACHE statement.'
),
'Qcache_free_memory' => __(
'The amount of free memory for query cache.'
),
'Qcache_hits' => __(
'The number of cache hits.'
),
'Qcache_inserts' => __(
'The number of queries added to the cache.'
),
'Qcache_lowmem_prunes' => __(
'The number of queries that have been removed from the cache to'
. ' free up memory for caching new queries. This information can'
. ' help you tune the query cache size. The query cache uses a'
. ' least recently used (LRU) strategy to decide which queries'
. ' to remove from the cache.'
),
'Qcache_not_cached' => __(
'The number of non-cached queries (not cachable, or not cached'
. ' due to the query_cache_type setting).'
),
'Qcache_queries_in_cache' => __(
'The number of queries registered in the cache.'
),
'Qcache_total_blocks' => __(
'The total number of blocks in the query cache.'
),
'Rpl_status' => __(
'The status of failsafe replication (not yet implemented).'
),
'Select_full_join' => __(
'The number of joins that do not use indexes. If this value is'
. ' not 0, you should carefully check the indexes of your tables.'
),
'Select_full_range_join' => __(
'The number of joins that used a range search on a reference table.'
),
'Select_range_check' => __(
'The number of joins without keys that check for key usage after'
. ' each row. (If this is not 0, you should carefully check the'
. ' indexes of your tables.)'
),
'Select_range' => __(
'The number of joins that used ranges on the first table. (It\'s'
. ' normally not critical even if this is big.)'
),
'Select_scan' => __(
'The number of joins that did a full scan of the first table.'
),
'Slave_open_temp_tables' => __(
'The number of temporary tables currently'
. ' open by the slave SQL thread.'
),
'Slave_retried_transactions' => __(
'Total (since startup) number of times the replication slave SQL'
. ' thread has retried transactions.'
),
'Slave_running' => __(
'This is ON if this server is a slave that is connected to a master.'
),
'Slow_launch_threads' => __(
'The number of threads that have taken more than slow_launch_time'
. ' seconds to create.'
),
'Slow_queries' => __(
'The number of queries that have taken more than long_query_time'
. ' seconds.'
),
'Sort_merge_passes' => __(
'The number of merge passes the sort algorithm has had to do.'
. ' If this value is large, you should consider increasing the'
. ' value of the sort_buffer_size system variable.'
),
'Sort_range' => __(
'The number of sorts that were done with ranges.'
),
'Sort_rows' => __(
'The number of sorted rows.'
),
'Sort_scan' => __(
'The number of sorts that were done by scanning the table.'
),
'Table_locks_immediate' => __(
'The number of times that a table lock was acquired immediately.'
),
'Table_locks_waited' => __(
'The number of times that a table lock could not be acquired'
. ' immediately and a wait was needed. If this is high, and you have'
. ' performance problems, you should first optimize your queries,'
. ' and then either split your table or tables or use replication.'
),
'Threads_cached' => __(
'The number of threads in the thread cache. The cache hit rate can'
. ' be calculated as Threads_created/Connections. If this value is'
. ' red you should raise your thread_cache_size.'
),
'Threads_connected' => __(
'The number of currently open connections.'
),
'Threads_created' => __(
'The number of threads created to handle connections. If'
. ' Threads_created is big, you may want to increase the'
. ' thread_cache_size value. (Normally this doesn\'t give a notable'
. ' performance improvement if you have a good thread'
. ' implementation.)'
),
'Threads_cache_hitrate_%' => __(
'Thread cache hit rate (calculated value)'
),
'Threads_running' => __(
'The number of threads that are not sleeping.'
),
];
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\UserGroups;
use PhpMyAdmin\Template;
/**
* Displays the 'User groups' sub page under 'Users' page.
*/
class UserGroupsController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Relation $relation, $dbi)
{
parent::__construct($response, $template);
$this->relation = $relation;
$this->dbi = $dbi;
}
public function index(): void
{
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['menuswork']) {
return;
}
$this->addScriptFiles(['server/user_groups.js']);
/**
* Only allowed to superuser
*/
if (! $this->dbi->isSuperUser()) {
$this->response->addHTML(
Message::error(__('No Privileges'))->getDisplay()
);
return;
}
$this->response->addHTML('<div class="container-fluid">');
$this->render('server/privileges/subnav', [
'active' => 'user-groups',
'is_super_user' => $this->dbi->isSuperUser(),
]);
/**
* Delete user group
*/
if (! empty($_POST['deleteUserGroup'])) {
UserGroups::delete($_POST['userGroup']);
}
/**
* Add a new user group
*/
if (! empty($_POST['addUserGroupSubmit'])) {
UserGroups::edit($_POST['userGroup'], true);
}
/**
* Update a user group
*/
if (! empty($_POST['editUserGroupSubmit'])) {
UserGroups::edit($_POST['userGroup']);
}
if (isset($_POST['viewUsers'])) {
// Display users belonging to a user group
$this->response->addHTML(UserGroups::getHtmlForListingUsersofAGroup($_POST['userGroup']));
}
if (isset($_GET['addUserGroup'])) {
// Display add user group dialog
$this->response->addHTML(UserGroups::getHtmlToEditUserGroup());
} elseif (isset($_POST['editUserGroup'])) {
// Display edit user group dialog
$this->response->addHTML(UserGroups::getHtmlToEditUserGroup($_POST['userGroup']));
} else {
// Display user groups table
$this->response->addHTML(UserGroups::getHtmlForUserGroupsTable());
}
$this->response->addHTML('</div>');
}
}

View file

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Server;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Providers\ServerVariables\ServerVariablesProvider;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function header;
use function htmlspecialchars;
use function implode;
use function in_array;
use function is_numeric;
use function mb_strtolower;
use function pow;
use function preg_match;
use function str_replace;
use function strtolower;
use function trim;
/**
* Handles viewing and editing server variables
*/
class VariablesController 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;
$params = ['filter' => $_GET['filter'] ?? null];
$err_url = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$filterValue = ! empty($params['filter']) ? $params['filter'] : '';
$this->addScriptFiles(['server/variables.js']);
$variables = [];
$serverVarsResult = $this->dbi->tryQuery('SHOW SESSION VARIABLES;');
if ($serverVarsResult !== false) {
$serverVarsSession = [];
while ($arr = $this->dbi->fetchRow($serverVarsResult)) {
$serverVarsSession[$arr[0]] = $arr[1];
}
$this->dbi->freeResult($serverVarsResult);
$serverVars = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES;', 0, 1);
// list of static (i.e. non-editable) system variables
$staticVariables = ServerVariablesProvider::getImplementation()->getStaticVariables();
foreach ($serverVars as $name => $value) {
$hasSessionValue = isset($serverVarsSession[$name])
&& $serverVarsSession[$name] !== $value;
$docLink = Generator::linkToVarDocumentation(
$name,
$this->dbi->isMariaDB(),
str_replace('_', '&nbsp;', $name)
);
[$formattedValue, $isEscaped] = $this->formatVariable($name, $value);
if ($hasSessionValue) {
[$sessionFormattedValue] = $this->formatVariable(
$name,
$serverVarsSession[$name]
);
}
$variables[] = [
'name' => $name,
'is_editable' => ! in_array(strtolower($name), $staticVariables),
'doc_link' => $docLink,
'value' => $formattedValue,
'is_escaped' => $isEscaped,
'has_session_value' => $hasSessionValue,
'session_value' => $sessionFormattedValue ?? null,
];
}
}
$this->render('server/variables/index', [
'variables' => $variables,
'filter_value' => $filterValue,
'is_superuser' => $this->dbi->isSuperUser(),
'is_mariadb' => $this->dbi->isMariaDB(),
]);
}
/**
* Handle the AJAX request for a single variable value
*
* @param array $params Request parameters
*/
public function getValue(array $params): void
{
if (! $this->response->isAjax()) {
return;
}
// Send with correct charset
header('Content-Type: text/html; charset=UTF-8');
// Do not use double quotes inside the query to avoid a problem
// when server is running in ANSI_QUOTES sql_mode
$varValue = $this->dbi->fetchSingleRow(
'SHOW GLOBAL VARIABLES WHERE Variable_name=\''
. $this->dbi->escapeString($params['name']) . '\';',
'NUM'
);
$json = [
'message' => $varValue[1],
];
$variableType = ServerVariablesProvider::getImplementation()->getVariableType($params['name']);
if ($variableType === 'byte') {
$json['message'] = implode(
' ',
Util::formatByteDown($varValue[1], 3, 3)
);
}
$this->response->addJSON($json);
}
/**
* Handle the AJAX request for setting value for a single variable
*
* @param array $vars Request parameters
*/
public function setValue(array $vars): void
{
$params = [
'varName' => $vars['name'],
'varValue' => $_POST['varValue'] ?? null,
];
if (! $this->response->isAjax()) {
return;
}
$value = (string) $params['varValue'];
$variableName = (string) $params['varName'];
$matches = [];
$variableType = ServerVariablesProvider::getImplementation()->getVariableType($variableName);
if ($variableType === 'byte' && preg_match(
'/^\s*(\d+(\.\d+)?)\s*(mb|kb|mib|kib|gb|gib)\s*$/i',
$value,
$matches
)) {
$exp = [
'kb' => 1,
'kib' => 1,
'mb' => 2,
'mib' => 2,
'gb' => 3,
'gib' => 3,
];
$value = (float) $matches[1] * pow(
1024,
$exp[mb_strtolower($matches[3])]
);
} else {
$value = $this->dbi->escapeString($value);
}
if (! is_numeric($value)) {
$value = "'" . $value . "'";
}
$json = [];
if (! preg_match('/[^a-zA-Z0-9_]+/', $params['varName'])
&& $this->dbi->query(
'SET GLOBAL ' . $params['varName'] . ' = ' . $value
)
) {
// Some values are rounded down etc.
$varValue = $this->dbi->fetchSingleRow(
'SHOW GLOBAL VARIABLES WHERE Variable_name="'
. $this->dbi->escapeString($params['varName'])
. '";',
'NUM'
);
[$formattedValue, $isHtmlFormatted] = $this->formatVariable(
$params['varName'],
$varValue[1]
);
if ($isHtmlFormatted === false) {
$json['variable'] = htmlspecialchars($formattedValue);
} else {
$json['variable'] = $formattedValue;
}
} else {
$this->response->setRequestStatus(false);
$json['error'] = __('Setting variable failed');
}
$this->response->addJSON($json);
}
/**
* Format Variable
*
* @param string $name variable name
* @param int|string $value variable value
*
* @return array formatted string and bool if string is HTML formatted
*/
private function formatVariable($name, $value): array
{
$isHtmlFormatted = false;
$formattedValue = $value;
if (is_numeric($value)) {
$variableType = ServerVariablesProvider::getImplementation()->getVariableType($name);
if ($variableType === 'byte') {
$isHtmlFormatted = true;
$formattedValue = trim(
$this->template->render(
'server/variables/format_variable',
[
'valueTitle' => Util::formatNumber($value, 0),
'value' => implode(' ', Util::formatByteDown($value, 3, 3)),
]
)
);
} else {
$formattedValue = Util::formatNumber($value, 0);
}
}
return [
$formattedValue,
$isHtmlFormatted,
];
}
}