Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 08:02:04 +01:00
parent 4413528994
commit 1d90fbf296
6865 changed files with 1091082 additions and 0 deletions

View file

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

View file

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Config;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Core;
use PhpMyAdmin\CreateAddField;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\ColumnsDefinition;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function intval;
use function is_array;
use function min;
use function strlen;
/**
* Displays add field form and handles it.
*/
class AddFieldController extends AbstractController
{
/** @var Transformations */
private $transformations;
/** @var Config */
private $config;
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Transformations $transformations,
Config $config,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->transformations = $transformations;
$this->config = $config;
$this->relation = $relation;
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $errorUrl, $message, $action, $active_page, $sql_query;
global $num_fields, $regenerate, $result, $db, $table;
$this->addScriptFiles(['table/structure.js']);
// Check parameters
Util::checkParameters(['db', 'table']);
$cfg = $this->config->settings;
/**
* Defines the url to return to in case of error in a sql statement
*/
$errorUrl = Url::getFromRoute('/table/sql', [
'db' => $db,
'table' => $table,
]);
// check number of fields to be created
if (isset($_POST['submit_num_fields'])) {
if (isset($_POST['orig_after_field'])) {
$_POST['after_field'] = $_POST['orig_after_field'];
}
if (isset($_POST['orig_field_where'])) {
$_POST['field_where'] = $_POST['orig_field_where'];
}
$num_fields = min(
intval($_POST['orig_num_fields']) + intval($_POST['added_fields']),
4096
);
$regenerate = true;
} elseif (isset($_POST['num_fields']) && intval($_POST['num_fields']) > 0) {
$num_fields = min(4096, intval($_POST['num_fields']));
} else {
$num_fields = 1;
}
if (isset($_POST['do_save_data'])) {
// avoid an incorrect calling of PMA_updateColumns() via
// /table/structure below
unset($_POST['do_save_data']);
$createAddField = new CreateAddField($this->dbi);
$sql_query = $createAddField->getColumnCreationQuery($table);
// If there is a request for SQL previewing.
if (isset($_POST['preview_sql'])) {
Core::previewSQL($sql_query);
return;
}
$result = $createAddField->tryColumnCreationQuery($db, $sql_query, $errorUrl);
if ($result !== true) {
$error_message_html = Generator::mysqlDie('', '', false, $errorUrl, false);
$this->response->addHTML($error_message_html ?? '');
$this->response->setRequestStatus(false);
return;
}
// Update comment table for mime types [MIME]
if (isset($_POST['field_mimetype']) && is_array($_POST['field_mimetype']) && $cfg['BrowseMIME']) {
foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
if (! isset($_POST['field_name'][$fieldindex]) || strlen($_POST['field_name'][$fieldindex]) <= 0) {
continue;
}
$this->transformations->setMime(
$db,
$table,
$_POST['field_name'][$fieldindex],
$mimetype,
$_POST['field_transformation'][$fieldindex],
$_POST['field_transformation_options'][$fieldindex],
$_POST['field_input_transformation'][$fieldindex],
$_POST['field_input_transformation_options'][$fieldindex]
);
}
}
// Go back to the structure sub-page
$message = Message::success(
__('Table %1$s has been altered successfully.')
);
$message->addParam($table);
$this->response->addJSON(
'message',
Generator::getMessage($message, $sql_query, 'success')
);
// Give an URL to call and use to appends the structure after the success message
$this->response->addJSON(
'structure_refresh_route',
Url::getFromRoute('/table/structure', [
'db' => $db,
'table' => $table,
'ajax_request' => '1',
])
);
return;
}
$url_params = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($url_params, '&');
DbTableExists::check();
$active_page = Url::getFromRoute('/table/structure');
/**
* Display the form
*/
$action = Url::getFromRoute('/table/add-field');
$this->addScriptFiles(['vendor/jquery/jquery.uitablefilter.js', 'indexes.js']);
$templateData = ColumnsDefinition::displayForm(
$this->transformations,
$this->relation,
$this->dbi,
$action,
$num_fields,
$regenerate
);
$this->render('columns_definitions/column_definitions_form', $templateData);
}
}

View file

@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\InsertEdit;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function __;
use function array_fill;
use function count;
use function is_array;
use function str_contains;
use function strlen;
use function strpos;
/**
* Displays form for editing and inserting new table rows.
*/
class ChangeController extends AbstractController
{
/** @var InsertEdit */
private $insertEdit;
/** @var Relation */
private $relation;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
InsertEdit $insertEdit,
Relation $relation
) {
parent::__construct($response, $template, $db, $table);
$this->insertEdit = $insertEdit;
$this->relation = $relation;
}
public function __invoke(): void
{
global $cfg, $db, $table, $text_dir, $disp_message, $urlParams;
global $errorUrl, $where_clause, $unsaved_values, $insert_mode, $where_clause_array, $where_clauses;
global $result, $rows, $found_unique_key, $after_insert, $comments_map, $table_columns;
global $chg_evt_handler, $timestamp_seen, $columns_cnt, $tabindex;
global $tabindex_for_value, $o_rows, $biggest_max_file_size, $has_blob_field;
global $jsvkey, $vkey, $current_result, $repopulate, $checked;
$pageSettings = new PageSettings('Edit');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
DbTableExists::check();
/**
* Determine whether Insert or Edit and set global variables
*/
[
$insert_mode,
$where_clause,
$where_clause_array,
$where_clauses,
$result,
$rows,
$found_unique_key,
$after_insert,
] = $this->insertEdit->determineInsertOrEdit($where_clause ?? null, $db, $table);
// Increase number of rows if unsaved rows are more
if (! empty($unsaved_values) && count($rows) < count($unsaved_values)) {
$rows = array_fill(0, count($unsaved_values), false);
}
/**
* Defines the url to return to in case of error in a sql statement
* (at this point, $GLOBALS['goto'] will be set but could be empty)
*/
if (empty($GLOBALS['goto'])) {
if (strlen($table) > 0) {
// avoid a problem (see bug #2202709)
$GLOBALS['goto'] = Url::getFromRoute('/table/sql');
} else {
$GLOBALS['goto'] = Url::getFromRoute('/database/sql');
}
}
$urlParams = [
'db' => $db,
'sql_query' => $_POST['sql_query'] ?? '',
];
if (strpos($GLOBALS['goto'] ?? '', 'index.php?route=/table') === 0) {
$urlParams['table'] = $table;
}
$errorUrl = $GLOBALS['goto'] . Url::getCommon(
$urlParams,
! str_contains($GLOBALS['goto'], '?') ? '?' : '&'
);
unset($urlParams);
$comments_map = $this->insertEdit->getCommentsMap($db, $table);
/**
* START REGULAR OUTPUT
*/
$this->addScriptFiles([
'makegrid.js',
'sql.js',
'table/change.js',
'vendor/jquery/additional-methods.js',
'gis_data_editor.js',
]);
/**
* Displays the query submitted and its result
*
* $disp_message come from /table/replace
*/
if (! empty($disp_message)) {
$this->response->addHTML(Generator::getMessage($disp_message, null));
}
$table_columns = $this->insertEdit->getTableColumns($db, $table);
// retrieve keys into foreign fields, if any
$foreigners = $this->relation->getForeigners($db, $table);
// Retrieve form parameters for insert/edit form
$_form_params = $this->insertEdit->getFormParametersForInsertForm(
$db,
$table,
$where_clauses,
$where_clause_array,
$errorUrl
);
/**
* Displays the form
*/
// autocomplete feature of IE kills the "onchange" event handler and it
// must be replaced by the "onpropertychange" one in this case
$chg_evt_handler = 'onchange';
// Had to put the URI because when hosted on an https server,
// some browsers send wrongly this form to the http server.
$html_output = '';
// Set if we passed the first timestamp field
$timestamp_seen = false;
$columns_cnt = count($table_columns);
$tabindex = 0;
$tabindex_for_value = 0;
$o_rows = 0;
$biggest_max_file_size = 0;
$urlParams['db'] = $db;
$urlParams['table'] = $table;
$urlParams = $this->insertEdit->urlParamsInEditMode($urlParams, $where_clause_array);
$has_blob_field = false;
foreach ($table_columns as $column) {
if ($this->insertEdit->isColumn($column, ['blob', 'tinyblob', 'mediumblob', 'longblob'])) {
$has_blob_field = true;
break;
}
}
//Insert/Edit form
//If table has blob fields we have to disable ajax.
$isUpload = $GLOBALS['config']->get('enable_upload');
$html_output .= $this->insertEdit->getHtmlForInsertEditFormHeader($has_blob_field, $isUpload);
$html_output .= Url::getHiddenInputs($_form_params);
// user can toggle the display of Function column and column types
// (currently does not work for multi-edits)
if (! $cfg['ShowFunctionFields'] || ! $cfg['ShowFieldTypesInDataEditView']) {
$html_output .= __('Show');
}
if (! $cfg['ShowFunctionFields']) {
$html_output .= $this->insertEdit->showTypeOrFunction('function', $urlParams, false);
}
if (! $cfg['ShowFieldTypesInDataEditView']) {
$html_output .= $this->insertEdit->showTypeOrFunction('type', $urlParams, false);
}
$GLOBALS['plugin_scripts'] = [];
foreach ($rows as $row_id => $current_row) {
if (empty($current_row)) {
$current_row = [];
}
$jsvkey = $row_id;
$vkey = '[multi_edit][' . $jsvkey . ']';
$current_result = (isset($result) && is_array($result) && isset($result[$row_id])
? $result[$row_id]
: $result);
$repopulate = [];
$checked = true;
if (isset($unsaved_values[$row_id])) {
$repopulate = $unsaved_values[$row_id];
$checked = false;
}
if ($insert_mode && $row_id > 0) {
$html_output .= $this->insertEdit->getHtmlForIgnoreOption($row_id, $checked);
}
$html_output .= $this->insertEdit->getHtmlForInsertEditRow(
$urlParams,
$table_columns,
$comments_map,
$timestamp_seen,
$current_result,
$chg_evt_handler,
$jsvkey,
$vkey,
$insert_mode,
$current_row,
$o_rows,
$tabindex,
$columns_cnt,
$isUpload,
$foreigners,
$tabindex_for_value,
$table,
$db,
$row_id,
$biggest_max_file_size,
$text_dir,
$repopulate,
$where_clause_array
);
}
$this->addScriptFiles($GLOBALS['plugin_scripts']);
unset($unsaved_values, $checked, $repopulate, $GLOBALS['plugin_scripts']);
if (! isset($after_insert)) {
$after_insert = 'back';
}
$isNumeric = InsertEdit::isWhereClauseNumeric($where_clause);
$html_output .= $this->template->render('table/insert/actions_panel', [
'where_clause' => $where_clause,
'after_insert' => $after_insert,
'found_unique_key' => $found_unique_key,
'is_numeric' => $isNumeric,
]);
if ($biggest_max_file_size > 0) {
$html_output .= '<input type="hidden" name="MAX_FILE_SIZE" value="' . $biggest_max_file_size . '">' . "\n";
}
$html_output .= '</form>';
$html_output .= $this->insertEdit->getHtmlForGisEditor();
// end Insert/Edit form
if ($insert_mode) {
//Continue insertion form
$html_output .= $this->insertEdit->getContinueInsertionForm($table, $db, $where_clause_array, $errorUrl);
}
$this->response->addHTML($html_output);
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function __;
use function is_array;
final class ChangeRowsController extends AbstractController
{
/** @var ChangeController */
private $changeController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
ChangeController $changeController
) {
parent::__construct($response, $template, $db, $table);
$this->changeController = $changeController;
}
public function __invoke(): void
{
global $active_page, $where_clause;
if (isset($_POST['goto']) && (! isset($_POST['rows_to_delete']) || ! is_array($_POST['rows_to_delete']))) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No row selected.'));
return;
}
// As we got the rows to be edited from the
// 'rows_to_delete' checkbox, we use the index of it as the
// indicating WHERE clause. Then we build the array which is used
// for the /table/change script.
$where_clause = [];
if (isset($_POST['rows_to_delete']) && is_array($_POST['rows_to_delete'])) {
foreach ($_POST['rows_to_delete'] as $i_where_clause) {
$where_clause[] = $i_where_clause;
}
}
$active_page = Url::getFromRoute('/table/change');
($this->changeController)();
}
}

View file

@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\FieldMetadata;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\SqlParser\Components\Limit;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function array_keys;
use function htmlspecialchars;
use function json_encode;
use function min;
use function strlen;
/**
* Handles creation of the chart.
*/
class ChartController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $db, $table, $cfg, $sql_query, $errorUrl;
if (isset($_REQUEST['pos'], $_REQUEST['session_max_rows']) && $this->response->isAjax()) {
$this->ajax();
return;
}
// Throw error if no sql query is set
if (! isset($sql_query) || $sql_query == '') {
$this->response->setRequestStatus(false);
$this->response->addHTML(
Message::error(__('No SQL query was set to fetch data.'))->getDisplay()
);
return;
}
$this->addScriptFiles([
'chart.js',
'table/chart.js',
'vendor/jqplot/jquery.jqplot.js',
'vendor/jqplot/plugins/jqplot.barRenderer.js',
'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js',
'vendor/jqplot/plugins/jqplot.categoryAxisRenderer.js',
'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js',
'vendor/jqplot/plugins/jqplot.pointLabels.js',
'vendor/jqplot/plugins/jqplot.pieRenderer.js',
'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js',
'vendor/jqplot/plugins/jqplot.highlighter.js',
]);
$url_params = [];
/**
* Runs common work
*/
if (strlen($table) > 0) {
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($url_params, '&');
DbTableExists::check();
$url_params['goto'] = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$url_params['back'] = Url::getFromRoute('/table/sql');
$this->dbi->selectDb($db);
} elseif (strlen($db) > 0) {
$url_params['goto'] = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$url_params['back'] = Url::getFromRoute('/sql');
Util::checkParameters(['db']);
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$errorUrl .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
} else {
$url_params['goto'] = Util::getScriptNameForOption($cfg['DefaultTabServer'], 'server');
$url_params['back'] = Url::getFromRoute('/sql');
$errorUrl = Url::getFromRoute('/');
if ($this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
}
$result = $this->dbi->tryQuery($sql_query);
$fields_meta = $row = [];
if ($result !== false) {
$fields_meta = $this->dbi->getFieldsMeta($result);
$row = $result->fetchAssoc();
}
$keys = array_keys($row);
$numericColumnFound = false;
foreach (array_keys($keys) as $idx) {
if (
isset($fields_meta[$idx]) && (
$fields_meta[$idx]->isType(FieldMetadata::TYPE_INT)
|| $fields_meta[$idx]->isType(FieldMetadata::TYPE_REAL)
)
) {
$numericColumnFound = true;
break;
}
}
if (! $numericColumnFound) {
$this->response->setRequestStatus(false);
$this->response->addJSON(
'message',
__('No numeric columns present in the table to plot.')
);
return;
}
$url_params['db'] = $db;
$url_params['reload'] = 1;
$startAndNumberOfRowsFieldset = Generator::getStartAndNumberOfRowsFieldsetData($sql_query);
/**
* Displays the page
*/
$this->render('table/chart/tbl_chart', [
'url_params' => $url_params,
'keys' => $keys,
'fields_meta' => $fields_meta,
'table_has_a_numeric_column' => $numericColumnFound,
'start_and_number_of_rows_fieldset' => $startAndNumberOfRowsFieldset,
]);
}
/**
* Handle ajax request
*/
public function ajax(): void
{
global $db, $table, $sql_query, $urlParams, $errorUrl, $cfg;
if (strlen($table) > 0 && strlen($db) > 0) {
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
}
$parser = new Parser($sql_query);
/**
* @var SelectStatement $statement
*/
$statement = $parser->statements[0];
if (empty($statement->limit)) {
$statement->limit = new Limit($_REQUEST['session_max_rows'], $_REQUEST['pos']);
} else {
$start = $statement->limit->offset + $_REQUEST['pos'];
$rows = min($_REQUEST['session_max_rows'], $statement->limit->rowCount - $_REQUEST['pos']);
$statement->limit = new Limit($rows, $start);
}
$sql_with_limit = $statement->build();
$result = $this->dbi->tryQuery($sql_with_limit);
$data = [];
if ($result !== false) {
$data = $result->fetchAllAssoc();
}
if ($data === []) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No data to display'));
return;
}
$sanitized_data = [];
foreach ($data as $data_row) {
$tmp_row = [];
foreach ($data_row as $data_column => $data_value) {
$escaped_value = $data_value === null ? null : htmlspecialchars($data_value);
$tmp_row[htmlspecialchars($data_column)] = $escaped_value;
}
$sanitized_data[] = $tmp_row;
}
$this->response->setRequestStatus(true);
$this->response->addJSON('message', null);
$this->response->addJSON('chartData', json_encode($sanitized_data));
}
}

View file

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Config;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Core;
use PhpMyAdmin\CreateAddField;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\ColumnsDefinition;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function htmlspecialchars;
use function is_array;
use function mb_strtolower;
use function sprintf;
use function strlen;
/**
* Displays table create form and handles it.
*/
class CreateController extends AbstractController
{
/** @var Transformations */
private $transformations;
/** @var Config */
private $config;
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Transformations $transformations,
Config $config,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->transformations = $transformations;
$this->config = $config;
$this->relation = $relation;
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $num_fields, $action, $sql_query, $result, $db, $table;
Util::checkParameters(['db']);
$cfg = $this->config->settings;
/* Check if database name is empty */
if (strlen($db) === 0) {
Generator::mysqlDie(
__('The database name is empty!'),
'',
false,
'index.php'
);
}
/**
* Selects the database to work with
*/
if (! $this->dbi->selectDb($db)) {
Generator::mysqlDie(
sprintf(__('\'%s\' database does not exist.'), htmlspecialchars($db)),
'',
false,
'index.php'
);
}
if ($this->dbi->getColumns($db, $table)) {
// table exists already
Generator::mysqlDie(
sprintf(__('Table %s already exists!'), htmlspecialchars($table)),
'',
false,
Url::getFromRoute('/database/structure', ['db' => $db])
);
}
$createAddField = new CreateAddField($this->dbi);
$num_fields = $createAddField->getNumberOfFieldsFromRequest();
$action = Url::getFromRoute('/table/create');
/**
* The form used to define the structure of the table has been submitted
*/
if (isset($_POST['do_save_data'])) {
// lower_case_table_names=1 `DB` becomes `db`
if ($this->dbi->getLowerCaseNames() === '1') {
$db = mb_strtolower($db);
$table = mb_strtolower($table);
}
$sql_query = $createAddField->getTableCreationQuery($db, $table);
// If there is a request for SQL previewing.
if (isset($_POST['preview_sql'])) {
Core::previewSQL($sql_query);
return;
}
// Executes the query
$result = $this->dbi->tryQuery($sql_query);
if ($result) {
// Update comment table for mime types [MIME]
if (isset($_POST['field_mimetype']) && is_array($_POST['field_mimetype']) && $cfg['BrowseMIME']) {
foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
if (
! isset($_POST['field_name'][$fieldindex])
|| strlen($_POST['field_name'][$fieldindex]) <= 0
) {
continue;
}
$this->transformations->setMime(
$db,
$table,
$_POST['field_name'][$fieldindex],
$mimetype,
$_POST['field_transformation'][$fieldindex],
$_POST['field_transformation_options'][$fieldindex],
$_POST['field_input_transformation'][$fieldindex],
$_POST['field_input_transformation_options'][$fieldindex]
);
}
}
} else {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $this->dbi->getError());
}
return;
}
// Do not display the table in the header since it hasn't been created yet
$this->response->getHeader()->getMenu()->setTable('');
$this->addScriptFiles(['vendor/jquery/jquery.uitablefilter.js', 'indexes.js']);
$templateData = ColumnsDefinition::displayForm(
$this->transformations,
$this->relation,
$this->dbi,
$action,
$num_fields
);
$this->render('columns_definitions/column_definitions_form', $templateData);
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use function __;
use function is_array;
final class DeleteConfirmController extends AbstractController
{
public function __invoke(): void
{
global $db, $table, $sql_query, $urlParams, $errorUrl, $cfg;
$selected = $_POST['rows_to_delete'] ?? null;
if (! isset($selected) || ! is_array($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No row selected.'));
return;
}
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$this->render('table/delete/confirm', [
'db' => $db,
'table' => $table,
'selected' => $selected,
'sql_query' => $sql_query,
'is_foreign_key_check' => ForeignKey::isCheckEnabled(),
]);
}
}

View file

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Operations;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use function __;
use function sprintf;
final class DeleteRowsController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $db, $goto, $sql_query, $table, $disp_message, $disp_query, $active_page;
$mult_btn = $_POST['mult_btn'] ?? '';
$original_sql_query = $_POST['original_sql_query'] ?? '';
$selected = $_POST['selected'] ?? [];
$relation = new Relation($this->dbi);
$sql = new Sql(
$this->dbi,
$relation,
new RelationCleanup($this->dbi, $relation),
new Operations($this->dbi, $relation),
new Transformations(),
$this->template
);
if ($mult_btn === __('Yes')) {
$default_fk_check_value = ForeignKey::handleDisableCheckInit();
$sql_query = '';
foreach ($selected as $row) {
$query = sprintf(
'DELETE FROM %s WHERE %s LIMIT 1;',
Util::backquote($table),
$row
);
$sql_query .= $query . "\n";
$this->dbi->selectDb($db);
$this->dbi->query($query);
}
if (! empty($_REQUEST['pos'])) {
$_REQUEST['pos'] = $sql->calculatePosForLastPage($db, $table, $_REQUEST['pos']);
}
ForeignKey::handleDisableCheckCleanup($default_fk_check_value);
$disp_message = __('Your SQL query has been executed successfully.');
$disp_query = $sql_query;
}
$_url_params = $GLOBALS['urlParams'];
$_url_params['goto'] = Url::getFromRoute('/table/sql');
if (isset($original_sql_query)) {
$sql_query = $original_sql_query;
}
$active_page = Url::getFromRoute('/sql');
$this->response->addHTML($sql->executeQueryAndSendQueryResponse(
null,
false,
$db,
$table,
null,
null,
null,
null,
null,
$goto,
null,
null,
$sql_query,
null
));
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
final class DropColumnConfirmationController extends AbstractController
{
public function __invoke(): void
{
global $urlParams, $errorUrl, $cfg;
$selected = $_POST['selected_fld'] ?? null;
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $this->db, 'table' => $this->table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$this->render('table/structure/drop_confirm', [
'db' => $this->db,
'table' => $this->table,
'fields' => $selected,
]);
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\FlashMessages;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function _ngettext;
use function count;
final class DropColumnController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var FlashMessages */
private $flash;
/** @var RelationCleanup */
private $relationCleanup;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
FlashMessages $flash,
RelationCleanup $relationCleanup
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->flash = $flash;
$this->relationCleanup = $relationCleanup;
}
public function __invoke(): void
{
$selected = $_POST['selected'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$selectedCount = count($selected);
if (($_POST['mult_btn'] ?? '') === __('Yes')) {
$i = 1;
$statement = 'ALTER TABLE ' . Util::backquote($this->table);
foreach ($selected as $field) {
$this->relationCleanup->column($this->db, $this->table, $field);
$statement .= ' DROP ' . Util::backquote($field);
$statement .= $i++ === $selectedCount ? ';' : ',';
}
$this->dbi->selectDb($this->db);
$result = $this->dbi->tryQuery($statement);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
} else {
$message = Message::success(__('No change'));
}
if (empty($message)) {
$message = Message::success(
_ngettext(
'%1$d column has been dropped successfully.',
'%1$d columns have been dropped successfully.',
$selectedCount
)
);
$message->addParam($selectedCount);
}
$this->flash->addMessage($message->isError() ? 'danger' : 'success', $message->getMessage());
$this->redirect('/table/structure', ['db' => $this->db, 'table' => $this->table]);
}
}

View file

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Export\Options;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
use PhpMyAdmin\SqlParser\Utils\Query;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function array_merge;
use function implode;
use function is_array;
class ExportController extends AbstractController
{
/** @var Options */
private $export;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Options $export
) {
parent::__construct($response, $template, $db, $table);
$this->export = $export;
}
public function __invoke(): void
{
global $db, $urlParams, $table, $replaces, $cfg, $errorUrl;
global $sql_query, $where_clause, $num_tables, $unlim_num_rows;
$pageSettings = new PageSettings('Export');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['export.js']);
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
$urlParams['goto'] = Url::getFromRoute('/table/export');
$urlParams['back'] = Url::getFromRoute('/table/export');
// When we have some query, we need to remove LIMIT from that and possibly
// generate WHERE clause (if we are asked to export specific rows)
if (! empty($sql_query)) {
$parser = new Parser($sql_query);
if (! empty($parser->statements[0]) && ($parser->statements[0] instanceof SelectStatement)) {
// Checking if the WHERE clause has to be replaced.
if (! empty($where_clause) && is_array($where_clause)) {
$replaces[] = [
'WHERE',
'WHERE (' . implode(') OR (', $where_clause) . ')',
];
}
// Preparing to remove the LIMIT clause.
$replaces[] = [
'LIMIT',
'',
];
// Replacing the clauses.
$sql_query = Query::replaceClauses($parser->statements[0], $parser->list, $replaces);
}
}
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('table', isset($GLOBALS['single_table']));
if (empty($exportList)) {
$this->response->addHTML(Message::error(
__('Could not load export plugins, please check your installation!')
)->getDisplay());
return;
}
$exportType = 'table';
$isReturnBackFromRawExport = isset($_POST['export_type']) && $_POST['export_type'] === 'raw';
if (isset($_POST['raw_query']) || $isReturnBackFromRawExport) {
$exportType = 'raw';
}
$options = $this->export->getOptions(
$exportType,
$db,
$table,
$sql_query,
$num_tables,
$unlim_num_rows,
$exportList
);
$this->render('table/export/index', array_merge($options, [
'export_type' => $exportType,
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
]));
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function __;
use function is_array;
final class ExportRowsController extends AbstractController
{
/** @var ExportController */
private $exportController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
ExportController $exportController
) {
parent::__construct($response, $template, $db, $table);
$this->exportController = $exportController;
}
public function __invoke(): void
{
global $active_page, $single_table, $where_clause;
if (isset($_POST['goto']) && (! isset($_POST['rows_to_delete']) || ! is_array($_POST['rows_to_delete']))) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No row selected.'));
return;
}
// Needed to allow SQL export
$single_table = true;
// As we got the rows to be exported from the
// 'rows_to_delete' checkbox, we use the index of it as the
// indicating WHERE clause. Then we build the array which is used
// for the /table/change script.
$where_clause = [];
if (isset($_POST['rows_to_delete']) && is_array($_POST['rows_to_delete'])) {
foreach ($_POST['rows_to_delete'] as $i_where_clause) {
$where_clause[] = $i_where_clause;
}
}
$active_page = Url::getFromRoute('/table/export');
($this->exportController)();
}
}

View file

@ -0,0 +1,367 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function array_key_exists;
use function count;
use function is_array;
use function mb_strtolower;
use function preg_match;
use function preg_replace;
use function str_contains;
use function str_ireplace;
use function str_replace;
use function strncasecmp;
/**
* Handles find and replace tab.
*
* Displays find and replace form, allows previewing and do the replacing.
*/
class FindReplaceController extends AbstractController
{
/** @var array */
private $columnNames;
/** @var array */
private $columnTypes;
/** @var string */
private $connectionCharSet;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->columnNames = [];
$this->columnTypes = [];
$this->loadTableInfo();
$this->connectionCharSet = (string) $this->dbi->fetchValue('SELECT @@character_set_connection');
}
public function __invoke(): void
{
global $db, $table, $urlParams, $cfg, $errorUrl;
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
if (isset($_POST['find'])) {
$this->findAction();
return;
}
$this->addScriptFiles(['table/find_replace.js']);
if (isset($_POST['replace'])) {
$this->replaceAction();
}
// Displays the find and replace form
$this->displaySelectionFormAction();
}
/**
* Gets all the columns of a table along with their types.
*/
private function loadTableInfo(): void
{
// Gets the list and number of columns
$columns = $this->dbi->getColumns($this->db, $this->table, true);
foreach ($columns as $row) {
// set column name
$this->columnNames[] = $row['Field'];
$type = (string) $row['Type'];
// reformat mysql query output
if (strncasecmp($type, 'set', 3) == 0 || strncasecmp($type, 'enum', 4) == 0) {
$type = str_replace(',', ', ', $type);
} else {
// strip the "BINARY" attribute, except if we find "BINARY(" because
// this would be a BINARY or VARBINARY column type
if (! preg_match('@BINARY[\(]@i', $type)) {
$type = str_ireplace('BINARY', '', $type);
}
$type = str_ireplace('ZEROFILL', '', $type);
$type = str_ireplace('UNSIGNED', '', $type);
$type = mb_strtolower($type);
}
if (empty($type)) {
$type = '&nbsp;';
}
$this->columnTypes[] = $type;
}
}
/**
* Display selection form action
*/
public function displaySelectionFormAction(): void
{
global $goto;
if (! isset($goto)) {
$goto = Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabTable'], 'table');
}
$column_names = $this->columnNames;
$column_types = $this->columnTypes;
$types = [];
$num_cols = count($column_names);
for ($i = 0; $i < $num_cols; $i++) {
$types[$column_names[$i]] = preg_replace('@\\(.*@s', '', $column_types[$i]);
}
$this->render('table/find_replace/index', [
'db' => $this->db,
'table' => $this->table,
'goto' => $goto,
'column_names' => $column_names,
'types' => $types,
'sql_types' => $this->dbi->types,
]);
}
public function findAction(): void
{
$useRegex = array_key_exists('useRegex', $_POST)
&& $_POST['useRegex'] === 'on';
$preview = $this->getReplacePreview(
$_POST['columnIndex'],
$_POST['find'],
$_POST['replaceWith'],
$useRegex,
$this->connectionCharSet
);
$this->response->addJSON('preview', $preview);
}
public function replaceAction(): void
{
$this->replace(
$_POST['columnIndex'],
$_POST['findString'],
$_POST['replaceWith'],
$_POST['useRegex'],
$this->connectionCharSet
);
$this->response->addHTML(
Generator::getMessage(
__('Your SQL query has been executed successfully.'),
null,
'success'
)
);
}
/**
* Returns HTML for previewing strings found and their replacements
*
* @param int $columnIndex index of the column
* @param string $find string to find in the column
* @param string $replaceWith string to replace with
* @param bool $useRegex to use Regex replace or not
* @param string $charSet character set of the connection
*
* @return string HTML for previewing strings found and their replacements
*/
public function getReplacePreview(
$columnIndex,
$find,
$replaceWith,
$useRegex,
$charSet
) {
$column = $this->columnNames[$columnIndex];
if ($useRegex) {
$result = $this->getRegexReplaceRows($columnIndex, $find, $replaceWith, $charSet);
} else {
$sql_query = 'SELECT '
. Util::backquote($column) . ','
. ' REPLACE('
. Util::backquote($column) . ", '" . $find . "', '"
. $replaceWith
. "'),"
. ' COUNT(*)'
. ' FROM ' . Util::backquote($this->db)
. '.' . Util::backquote($this->table)
. ' WHERE ' . Util::backquote($column)
. " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we
// change the collation of the 2nd operand to a case sensitive
// binary collation to make sure that the comparison
// is case sensitive
$sql_query .= ' GROUP BY ' . Util::backquote($column)
. ' ORDER BY ' . Util::backquote($column) . ' ASC';
$result = $this->dbi->fetchResult($sql_query, 0);
}
return $this->template->render('table/find_replace/replace_preview', [
'db' => $this->db,
'table' => $this->table,
'column_index' => $columnIndex,
'find' => $find,
'replace_with' => $replaceWith,
'use_regex' => $useRegex,
'result' => $result,
]);
}
/**
* Finds and returns Regex pattern and their replacements
*
* @param int $columnIndex index of the column
* @param string $find string to find in the column
* @param string $replaceWith string to replace with
* @param string $charSet character set of the connection
*
* @return array|bool Array containing original values, replaced values and count
*/
private function getRegexReplaceRows(
$columnIndex,
$find,
$replaceWith,
$charSet
) {
$column = $this->columnNames[$columnIndex];
$sql_query = 'SELECT '
. Util::backquote($column) . ','
. ' 1,' // to add an extra column that will have replaced value
. ' COUNT(*)'
. ' FROM ' . Util::backquote($this->db)
. '.' . Util::backquote($this->table)
. ' WHERE ' . Util::backquote($column)
. " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
. $charSet . '_bin'; // here we
// change the collation of the 2nd operand to a case sensitive
// binary collation to make sure that the comparison is case sensitive
$sql_query .= ' GROUP BY ' . Util::backquote($column)
. ' ORDER BY ' . Util::backquote($column) . ' ASC';
$result = $this->dbi->fetchResult($sql_query, 0);
/* Iterate over possible delimiters to get one */
$delimiters = [
'/',
'@',
'#',
'~',
'!',
'$',
'%',
'^',
'&',
'_',
];
$found = false;
for ($i = 0, $l = count($delimiters); $i < $l; $i++) {
if (! str_contains($find, $delimiters[$i])) {
$found = true;
break;
}
}
if (! $found) {
return false;
}
$find = $delimiters[$i] . $find . $delimiters[$i];
foreach ($result as $index => $row) {
$result[$index][1] = preg_replace($find, $replaceWith, $row[0]);
}
return $result;
}
/**
* Replaces a given string in a column with a give replacement
*
* @param int $columnIndex index of the column
* @param string $find string to find in the column
* @param string $replaceWith string to replace with
* @param bool $useRegex to use Regex replace or not
* @param string $charSet character set of the connection
*/
public function replace(
$columnIndex,
$find,
$replaceWith,
$useRegex,
$charSet
): void {
$column = $this->columnNames[$columnIndex];
if ($useRegex) {
$toReplace = $this->getRegexReplaceRows($columnIndex, $find, $replaceWith, $charSet);
$sql_query = 'UPDATE ' . Util::backquote($this->table)
. ' SET ' . Util::backquote($column);
if (is_array($toReplace)) {
if (count($toReplace) > 0) {
$sql_query .= ' = CASE';
foreach ($toReplace as $row) {
$sql_query .= "\n WHEN " . Util::backquote($column)
. " = '" . $this->dbi->escapeString($row[0])
. "' THEN '" . $this->dbi->escapeString($row[1]) . "'";
}
$sql_query .= ' END';
} else {
$sql_query .= ' = ' . Util::backquote($column);
}
}
$sql_query .= ' WHERE ' . Util::backquote($column)
. " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
. $charSet . '_bin'; // here we
// change the collation of the 2nd operand to a case sensitive
// binary collation to make sure that the comparison
// is case sensitive
} else {
$sql_query = 'UPDATE ' . Util::backquote($this->table)
. ' SET ' . Util::backquote($column) . ' ='
. ' REPLACE('
. Util::backquote($column) . ", '" . $find . "', '"
. $replaceWith
. "')"
. ' WHERE ' . Util::backquote($column)
. " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we
// change the collation of the 2nd operand to a case sensitive
// binary collation to make sure that the comparison
// is case sensitive
}
$this->dbi->query($sql_query);
$GLOBALS['sql_query'] = $sql_query;
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Mime;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function htmlspecialchars;
use function ini_set;
use function sprintf;
use function strlen;
/**
* Provides download to a given field defined in parameters.
*/
class GetFieldController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $db, $table;
$this->response->disable();
/* Check parameters */
Util::checkParameters([
'db',
'table',
]);
/* Select database */
if (! $this->dbi->selectDb($db)) {
Generator::mysqlDie(
sprintf(__('\'%s\' database does not exist.'), htmlspecialchars($db)),
'',
false
);
}
/* Check if table exists */
if (! $this->dbi->getColumns($db, $table)) {
Generator::mysqlDie(__('Invalid table name'));
}
if (
! isset($_GET['where_clause'])
|| ! isset($_GET['where_clause_sign'])
|| ! Core::checkSqlQuerySignature($_GET['where_clause'], $_GET['where_clause_sign'])
) {
/* l10n: In case a SQL query did not pass a security check */
Core::fatalError(__('There is an issue with your request.'));
return;
}
/* Grab data */
$sql = 'SELECT ' . Util::backquote($_GET['transform_key'])
. ' FROM ' . Util::backquote($table)
. ' WHERE ' . $_GET['where_clause'] . ';';
$result = $this->dbi->fetchValue($sql);
/* Check return code */
if ($result === false) {
Generator::mysqlDie(
__('MySQL returned an empty result set (i.e. zero rows).'),
$sql
);
return;
}
/* Avoid corrupting data */
ini_set('url_rewriter.tags', '');
Core::downloadHeader(
$table . '-' . $_GET['transform_key'] . '.bin',
Mime::detect($result),
strlen($result)
);
echo $result;
}
}

View file

@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Gis\GisVisualization;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function array_merge;
use function is_array;
/**
* Handles creation of the GIS visualizations.
*/
final class GisVisualizationController extends AbstractController
{
/** @var GisVisualization */
private $visualization;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $cfg, $urlParams, $db, $errorUrl;
Util::checkParameters(['db']);
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$errorUrl .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
// SQL query for retrieving GIS data
$sqlQuery = '';
if (isset($_GET['sql_query'], $_GET['sql_signature'])) {
if (Core::checkSqlQuerySignature($_GET['sql_query'], $_GET['sql_signature'])) {
$sqlQuery = $_GET['sql_query'];
}
} elseif (isset($_POST['sql_query'])) {
$sqlQuery = $_POST['sql_query'];
}
// Throw error if no sql query is set
if ($sqlQuery == '') {
$this->response->setRequestStatus(false);
$this->response->addHTML(
Message::error(__('No SQL query was set to fetch data.'))->getDisplay()
);
return;
}
// Execute the query and return the result
$result = $this->dbi->tryQuery($sqlQuery);
// Get the meta data of results
$meta = [];
if ($result !== false) {
$meta = $this->dbi->getFieldsMeta($result);
}
// Find the candidate fields for label column and spatial column
$labelCandidates = [];
$spatialCandidates = [];
foreach ($meta as $column_meta) {
if ($column_meta->isMappedTypeGeometry) {
$spatialCandidates[] = $column_meta->name;
} else {
$labelCandidates[] = $column_meta->name;
}
}
// Get settings if any posted
$visualizationSettings = [];
// Download as PNG/SVG/PDF use _GET and the normal form uses _POST
if (isset($_POST['visualizationSettings']) && is_array($_POST['visualizationSettings'])) {
$visualizationSettings = $_POST['visualizationSettings'];
} elseif (isset($_GET['visualizationSettings']) && is_array($_GET['visualizationSettings'])) {
$visualizationSettings = $_GET['visualizationSettings'];
}
// Check mysql version
$visualizationSettings['mysqlVersion'] = $this->dbi->getVersion();
$visualizationSettings['isMariaDB'] = $this->dbi->isMariaDB();
if (! isset($visualizationSettings['labelColumn']) && isset($labelCandidates[0])) {
$visualizationSettings['labelColumn'] = '';
}
// If spatial column is not set, use first geometric column as spatial column
if (! isset($visualizationSettings['spatialColumn'])) {
$visualizationSettings['spatialColumn'] = $spatialCandidates[0];
}
// Download as PNG/SVG/PDF use _GET and the normal form uses _POST
// Convert geometric columns from bytes to text.
$pos = (int) ($_POST['pos'] ?? $_GET['pos'] ?? $_SESSION['tmpval']['pos']);
if (isset($_POST['session_max_rows']) || isset($_GET['session_max_rows'])) {
$rows = (int) ($_POST['session_max_rows'] ?? $_GET['session_max_rows']);
} else {
if ($_SESSION['tmpval']['max_rows'] !== 'all') {
$rows = (int) $_SESSION['tmpval']['max_rows'];
} else {
$rows = (int) $GLOBALS['cfg']['MaxRows'];
}
}
$this->visualization = GisVisualization::get($sqlQuery, $visualizationSettings, $rows, $pos);
if (isset($_GET['saveToFile'])) {
$this->saveToFile($visualizationSettings['spatialColumn'], $_GET['fileFormat']);
return;
}
$this->addScriptFiles(['vendor/openlayers/OpenLayers.js', 'table/gis_visualization.js']);
// If all the rows contain SRID, use OpenStreetMaps on the initial loading.
if (! isset($_POST['displayVisualization'])) {
if ($this->visualization->hasSrid()) {
$visualizationSettings['choice'] = 'useBaseLayer';
} else {
unset($visualizationSettings['choice']);
}
}
$this->visualization->setUserSpecifiedSettings($visualizationSettings);
if ($visualizationSettings != null) {
foreach ($this->visualization->getSettings() as $setting => $val) {
if (isset($visualizationSettings[$setting])) {
continue;
}
$visualizationSettings[$setting] = $val;
}
}
/**
* Displays the page
*/
$urlParams['goto'] = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$urlParams['back'] = Url::getFromRoute('/sql');
$urlParams['sql_query'] = $sqlQuery;
$urlParams['sql_signature'] = Core::signSqlQuery($sqlQuery);
$downloadUrl = Url::getFromRoute('/table/gis-visualization', array_merge(
$urlParams,
[
'saveToFile' => true,
'session_max_rows' => $rows,
'pos' => $pos,
'visualizationSettings[spatialColumn]' => $visualizationSettings['spatialColumn'],
'visualizationSettings[labelColumn]' => $visualizationSettings['labelColumn'] ?? null,
]
));
$startAndNumberOfRowsFieldset = Generator::getStartAndNumberOfRowsFieldsetData($sqlQuery);
$html = $this->template->render('table/gis_visualization/gis_visualization', [
'url_params' => $urlParams,
'download_url' => $downloadUrl,
'label_candidates' => $labelCandidates,
'spatial_candidates' => $spatialCandidates,
'visualization_settings' => $visualizationSettings,
'start_and_number_of_rows_fieldset' => $startAndNumberOfRowsFieldset,
'visualization' => $this->visualization->toImage('svg'),
'draw_ol' => $this->visualization->asOl(),
]);
$this->response->addHTML($html);
}
/**
* @param string $filename File name
* @param string $format Save format
*/
private function saveToFile(string $filename, string $format): void
{
$this->response->disable();
$this->visualization->toFile($filename, $format);
}
}

View file

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\Import;
use PhpMyAdmin\Import\Ajax;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use function __;
use function intval;
use function is_numeric;
final class ImportController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $db, $table, $urlParams, $SESSION_KEY, $cfg, $errorUrl;
$pageSettings = new PageSettings('Import');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['import.js']);
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$urlParams['goto'] = Url::getFromRoute('/table/import');
$urlParams['back'] = Url::getFromRoute('/table/import');
[$SESSION_KEY, $uploadId] = Ajax::uploadProgressSetup();
$importList = Plugins::getImport('table');
if (empty($importList)) {
$this->response->addHTML(Message::error(__(
'Could not load import plugins, please check your installation!'
))->getDisplay());
return;
}
$offset = null;
if (isset($_REQUEST['offset']) && is_numeric($_REQUEST['offset'])) {
$offset = intval($_REQUEST['offset']);
}
$timeoutPassed = $_REQUEST['timeout_passed'] ?? null;
$localImportFile = $_REQUEST['local_import_file'] ?? null;
$compressions = Import::getCompressions();
$charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$idKey = $_SESSION[$SESSION_KEY]['handler']::getIdKey();
$hiddenInputs = [
$idKey => $uploadId,
'import_type' => 'table',
'db' => $db,
'table' => $table,
];
$default = isset($_GET['format']) ? (string) $_GET['format'] : Plugins::getDefault('Import', 'format');
$choice = Plugins::getChoice($importList, $default);
$options = Plugins::getOptions('Import', $importList);
$skipQueriesDefault = Plugins::getDefault('Import', 'skip_queries');
$isAllowInterruptChecked = Plugins::checkboxCheck('Import', 'allow_interrupt');
$maxUploadSize = (int) $GLOBALS['config']->get('max_upload_size');
$this->render('table/import/index', [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'upload_id' => $uploadId,
'handler' => $_SESSION[$SESSION_KEY]['handler'],
'hidden_inputs' => $hiddenInputs,
'db' => $db,
'table' => $table,
'max_upload_size' => $maxUploadSize,
'formatted_maximum_upload_size' => Util::getFormattedMaximumUploadSize($maxUploadSize),
'plugins_choice' => $choice,
'options' => $options,
'skip_queries_default' => $skipQueriesDefault,
'is_allow_interrupt_checked' => $isAllowInterruptChecked,
'local_import_file' => $localImportFile,
'is_upload' => $GLOBALS['config']->get('enable_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' => ForeignKey::isCheckEnabled(),
'user_upload_dir' => Util::userDir((string) ($cfg['UploadDir'] ?? '')),
'local_files' => Import::getLocalFiles($importList),
]);
}
}

View file

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Index;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Indexes;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function is_array;
final class IndexRenameController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var Indexes */
private $indexes;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
Indexes $indexes
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->indexes = $indexes;
}
public function __invoke(): void
{
global $db, $table, $urlParams, $cfg, $errorUrl;
if (! isset($_POST['create_edit_table'])) {
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
}
if (isset($_POST['index'])) {
if (is_array($_POST['index'])) {
// coming already from form
$index = new Index($_POST['index']);
} else {
$index = $this->dbi->getTable($this->db, $this->table)->getIndex($_POST['index']);
}
} else {
$index = new Index();
}
if (isset($_POST['do_save_data'])) {
$this->indexes->doSaveData($index, true, $this->db, $this->table);
return;
}
$this->displayRenameForm($index);
}
/**
* Display the rename form to rename an index
*
* @param Index $index An Index instance.
*/
private function displayRenameForm(Index $index): void
{
$this->dbi->selectDb($GLOBALS['db']);
$formParams = [
'db' => $this->db,
'table' => $this->table,
];
if (isset($_POST['old_index'])) {
$formParams['old_index'] = $_POST['old_index'];
} elseif (isset($_POST['index'])) {
$formParams['old_index'] = $_POST['index'];
}
$this->addScriptFiles(['indexes.js']);
$this->render('table/index_rename_form', [
'index' => $index,
'form_params' => $formParams,
]);
}
}

View file

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Index;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Indexes;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function is_array;
use function is_numeric;
use function json_decode;
use function min;
/**
* Displays index edit/creation form and handles it.
*/
class IndexesController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var Indexes */
private $indexes;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
Indexes $indexes
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->indexes = $indexes;
}
public function __invoke(): void
{
global $db, $table, $urlParams, $cfg, $errorUrl;
if (! isset($_POST['create_edit_table'])) {
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
}
if (isset($_POST['index'])) {
if (is_array($_POST['index'])) {
// coming already from form
$index = new Index($_POST['index']);
} else {
$index = $this->dbi->getTable($this->db, $this->table)->getIndex($_POST['index']);
}
} else {
$index = new Index();
}
if (isset($_POST['do_save_data'])) {
$this->indexes->doSaveData($index, false, $this->db, $this->table);
return;
}
$this->displayForm($index);
}
/**
* Display the form to edit/create an index
*
* @param Index $index An Index instance.
*/
private function displayForm(Index $index): void
{
$this->dbi->selectDb($GLOBALS['db']);
$add_fields = 0;
if (isset($_POST['index']) && is_array($_POST['index'])) {
// coming already from form
if (isset($_POST['index']['columns']['names'])) {
$add_fields = count($_POST['index']['columns']['names'])
- $index->getColumnCount();
}
if (isset($_POST['add_fields'])) {
$add_fields += $_POST['added_fields'];
}
} elseif (isset($_POST['create_index'])) {
/**
* In most cases, an index may consist of up to 16 columns, so add an initial limit.
* More columns could be added later if necessary.
*
* @see https://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html "up to 16 columns"
* @see https://mariadb.com/kb/en/innodb-limitations/#limitations-on-schema "maximum of 16 columns"
* @see https://mariadb.com/kb/en/myisam-overview/#myisam-features "Maximum of 32 columns per index"
*/
$add_fields = 1;
if (is_numeric($_POST['added_fields']) && $_POST['added_fields'] >= 2) {
$add_fields = min((int) $_POST['added_fields'], 16);
}
}
// Get fields and stores their name/type
if (isset($_POST['create_edit_table'])) {
$fields = json_decode($_POST['columns'], true);
$index_params = [
'Non_unique' => $_POST['index']['Index_choice'] === 'UNIQUE'
? '0' : '1',
];
$index->set($index_params);
$add_fields = count($fields);
} else {
$fields = $this->dbi->getTable($this->db, $this->table)
->getNameAndTypeOfTheColumns();
}
$form_params = [
'db' => $this->db,
'table' => $this->table,
];
if (isset($_POST['create_index'])) {
$form_params['create_index'] = 1;
} elseif (isset($_POST['old_index'])) {
$form_params['old_index'] = $_POST['old_index'];
} elseif (isset($_POST['index'])) {
$form_params['old_index'] = $_POST['index'];
}
$this->addScriptFiles(['indexes.js']);
$this->render('table/index_form', [
'fields' => $fields,
'index' => $index,
'form_params' => $form_params,
'add_fields' => $add_fields,
'create_edit_table' => isset($_POST['create_edit_table']),
'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
'is_from_nav' => isset($_POST['is_from_nav']),
]);
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Maintenance;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Maintenance;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
use function count;
final class AnalyzeController extends AbstractController
{
/** @var Maintenance */
private $model;
/** @var Config */
private $config;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $model,
Config $config
) {
parent::__construct($response, $template, $db, $table);
$this->model = $model;
$this->config = $config;
}
public function __invoke(ServerRequest $request): void
{
$selectedTablesParam = $request->getParsedBodyParam('selected_tbl');
try {
Assert::isArray($selectedTablesParam);
Assert::notEmpty($selectedTablesParam);
Assert::allStringNotEmpty($selectedTablesParam);
} catch (InvalidArgumentException $exception) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
try {
$database = DatabaseName::fromValue($request->getParam('db'));
$selectedTables = [];
foreach ($selectedTablesParam as $table) {
$selectedTables[] = TableName::fromValue($table);
}
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
return;
}
if ($this->config->get('DisableMultiTableMaintenance') && count($selectedTables) > 1) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('Maintenance operations on multiple tables are disabled.'));
return;
}
[$rows, $query] = $this->model->getAnalyzeTableRows($database, $selectedTables);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/maintenance/analyze', [
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Maintenance;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Maintenance;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
use function count;
final class CheckController extends AbstractController
{
/** @var Maintenance */
private $model;
/** @var Config */
private $config;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $model,
Config $config
) {
parent::__construct($response, $template, $db, $table);
$this->model = $model;
$this->config = $config;
}
public function __invoke(ServerRequest $request): void
{
$selectedTablesParam = $request->getParsedBodyParam('selected_tbl');
try {
Assert::isArray($selectedTablesParam);
Assert::notEmpty($selectedTablesParam);
Assert::allStringNotEmpty($selectedTablesParam);
} catch (InvalidArgumentException $exception) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
try {
$database = DatabaseName::fromValue($request->getParam('db'));
$selectedTables = [];
foreach ($selectedTablesParam as $table) {
$selectedTables[] = TableName::fromValue($table);
}
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
return;
}
if ($this->config->get('DisableMultiTableMaintenance') && count($selectedTables) > 1) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('Maintenance operations on multiple tables are disabled.'));
return;
}
[$rows, $query] = $this->model->getCheckTableRows($database, $selectedTables);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$indexesProblems = $this->model->getIndexesProblems($database, $selectedTables);
$this->render('table/maintenance/check', [
'message' => $message,
'rows' => $rows,
'indexes_problems' => $indexesProblems,
]);
}
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Maintenance;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Maintenance;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
use function count;
final class ChecksumController extends AbstractController
{
/** @var Maintenance */
private $model;
/** @var Config */
private $config;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $model,
Config $config
) {
parent::__construct($response, $template, $db, $table);
$this->model = $model;
$this->config = $config;
}
public function __invoke(ServerRequest $request): void
{
$selectedTablesParam = $request->getParsedBodyParam('selected_tbl');
try {
Assert::isArray($selectedTablesParam);
Assert::notEmpty($selectedTablesParam);
Assert::allStringNotEmpty($selectedTablesParam);
} catch (InvalidArgumentException $exception) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
try {
$database = DatabaseName::fromValue($request->getParam('db'));
$selectedTables = [];
foreach ($selectedTablesParam as $table) {
$selectedTables[] = TableName::fromValue($table);
}
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
return;
}
if ($this->config->get('DisableMultiTableMaintenance') && count($selectedTables) > 1) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('Maintenance operations on multiple tables are disabled.'));
return;
}
[$rows, $query, $warnings] = $this->model->getChecksumTableRows($database, $selectedTables);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/maintenance/checksum', [
'message' => $message,
'rows' => $rows,
'warnings' => $warnings,
]);
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Maintenance;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Maintenance;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
use function count;
final class OptimizeController extends AbstractController
{
/** @var Maintenance */
private $model;
/** @var Config */
private $config;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $model,
Config $config
) {
parent::__construct($response, $template, $db, $table);
$this->model = $model;
$this->config = $config;
}
public function __invoke(ServerRequest $request): void
{
$selectedTablesParam = $request->getParsedBodyParam('selected_tbl');
try {
Assert::isArray($selectedTablesParam);
Assert::notEmpty($selectedTablesParam);
Assert::allStringNotEmpty($selectedTablesParam);
} catch (InvalidArgumentException $exception) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
try {
$database = DatabaseName::fromValue($request->getParam('db'));
$selectedTables = [];
foreach ($selectedTablesParam as $table) {
$selectedTables[] = TableName::fromValue($table);
}
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
return;
}
if ($this->config->get('DisableMultiTableMaintenance') && count($selectedTables) > 1) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('Maintenance operations on multiple tables are disabled.'));
return;
}
[$rows, $query] = $this->model->getOptimizeTableRows($database, $selectedTables);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/maintenance/optimize', [
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Maintenance;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Maintenance;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
use function count;
final class RepairController extends AbstractController
{
/** @var Maintenance */
private $model;
/** @var Config */
private $config;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $model,
Config $config
) {
parent::__construct($response, $template, $db, $table);
$this->model = $model;
$this->config = $config;
}
public function __invoke(ServerRequest $request): void
{
$selectedTablesParam = $request->getParsedBodyParam('selected_tbl');
try {
Assert::isArray($selectedTablesParam);
Assert::notEmpty($selectedTablesParam);
Assert::allStringNotEmpty($selectedTablesParam);
} catch (InvalidArgumentException $exception) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
try {
$database = DatabaseName::fromValue($request->getParam('db'));
$selectedTables = [];
foreach ($selectedTablesParam as $table) {
$selectedTables[] = TableName::fromValue($table);
}
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
return;
}
if ($this->config->get('DisableMultiTableMaintenance') && count($selectedTables) > 1) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('Maintenance operations on multiple tables are disabled.'));
return;
}
[$rows, $query] = $this->model->getRepairTableRows($database, $selectedTables);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/maintenance/repair', [
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,499 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Index;
use PhpMyAdmin\Message;
use PhpMyAdmin\Operations;
use PhpMyAdmin\Partitioning\Partition;
use PhpMyAdmin\Query\Generator as QueryGenerator;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\StorageEngine;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function count;
use function implode;
use function mb_strstr;
use function mb_strtolower;
use function mb_strtoupper;
use function preg_replace;
use function strlen;
use function urldecode;
class OperationsController extends AbstractController
{
/** @var Operations */
private $operations;
/** @var CheckUserPrivileges */
private $checkUserPrivileges;
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Operations $operations,
CheckUserPrivileges $checkUserPrivileges,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->operations = $operations;
$this->checkUserPrivileges = $checkUserPrivileges;
$this->relation = $relation;
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $urlParams, $reread_info, $tbl_is_view, $tbl_storage_engine;
global $show_comment, $tbl_collation, $table_info_num_rows, $row_format, $auto_increment, $create_options;
global $table_alters, $warning_messages, $lowerCaseNames, $db, $table, $reload, $result;
global $new_tbl_storage_engine, $sql_query, $message_to_show, $columns, $hideOrderTable, $indexes;
global $notNull, $comment, $errorUrl, $cfg;
$this->checkUserPrivileges->getPrivileges();
// lower_case_table_names=1 `DB` becomes `db`
$lowerCaseNames = $this->dbi->getLowerCaseNames() === '1';
if ($lowerCaseNames) {
$table = mb_strtolower($table);
}
$pma_table = $this->dbi->getTable($db, $table);
$this->addScriptFiles(['table/operations.js']);
Util::checkParameters(['db', 'table']);
$isSystemSchema = Utilities::isSystemSchema($db);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$urlParams['goto'] = $urlParams['back'] = Url::getFromRoute('/table/operations');
$relationParameters = $this->relation->getRelationParameters();
/**
* Reselect current db (needed in some cases probably due to the calling of {@link Relation})
*/
$this->dbi->selectDb($db);
$reread_info = $pma_table->getStatusInfo(null, false);
$GLOBALS['showtable'] = $pma_table->getStatusInfo(null, (isset($reread_info) && $reread_info));
if ($pma_table->isView()) {
$tbl_is_view = true;
$tbl_storage_engine = __('View');
$show_comment = null;
} else {
$tbl_is_view = false;
$tbl_storage_engine = $pma_table->getStorageEngine();
$show_comment = $pma_table->getComment();
}
$tbl_collation = $pma_table->getCollation();
$table_info_num_rows = $pma_table->getNumRows();
$row_format = $pma_table->getRowFormat();
$auto_increment = $pma_table->getAutoIncrement();
$create_options = $pma_table->getCreateOptions();
// set initial value of these variables, based on the current table engine
if ($pma_table->isEngine('ARIA')) {
// the value for transactional can be implicit
// (no create option found, in this case it means 1)
// or explicit (option found with a value of 0 or 1)
// ($create_options['transactional'] may have been set by Table class,
// from the $create_options)
$create_options['transactional'] = ($create_options['transactional'] ?? '') == '0'
? '0'
: '1';
$create_options['page_checksum'] = $create_options['page_checksum'] ?? '';
}
$pma_table = $this->dbi->getTable($db, $table);
$reread_info = false;
$table_alters = [];
/**
* If the table has to be moved to some other database
*/
if (isset($_POST['submit_move']) || isset($_POST['submit_copy'])) {
$message = $this->operations->moveOrCopyTable($db, $table);
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON('message', $message);
if ($message->isSuccess()) {
if (isset($_POST['submit_move'], $_POST['target_db'])) {
$db = $_POST['target_db'];// Used in Header::getJsParams()
}
$this->response->addJSON('db', $db);
return;
}
$this->response->setRequestStatus(false);
return;
}
/**
* Updates table comment, type and options if required
*/
if (isset($_POST['submitoptions'])) {
$_message = '';
$warning_messages = [];
if (isset($_POST['new_name'])) {
// lower_case_table_names=1 `DB` becomes `db`
if ($lowerCaseNames) {
$_POST['new_name'] = mb_strtolower($_POST['new_name']);
}
// Get original names before rename operation
$oldTable = $pma_table->getName();
$oldDb = $pma_table->getDbName();
if ($pma_table->rename($_POST['new_name'])) {
if (isset($_POST['adjust_privileges']) && ! empty($_POST['adjust_privileges'])) {
$this->operations->adjustPrivilegesRenameOrMoveTable(
$oldDb,
$oldTable,
$_POST['db'],
$_POST['new_name']
);
}
// Reselect the original DB
$db = $oldDb;
$this->dbi->selectDb($oldDb);
$_message .= $pma_table->getLastMessage();
$result = true;
$table = $pma_table->getName();
$reread_info = true;
$reload = true;
} else {
$_message .= $pma_table->getLastError();
$result = false;
}
}
if (
! empty($_POST['new_tbl_storage_engine'])
&& mb_strtoupper($_POST['new_tbl_storage_engine']) !== $tbl_storage_engine
) {
$new_tbl_storage_engine = mb_strtoupper($_POST['new_tbl_storage_engine']);
if ($pma_table->isEngine('ARIA')) {
$create_options['transactional'] = ($create_options['transactional'] ?? '') == '0'
? '0'
: '1';
$create_options['page_checksum'] = $create_options['page_checksum'] ?? '';
}
} else {
$new_tbl_storage_engine = '';
}
$row_format = $create_options['row_format'] ?? $pma_table->getRowFormat();
$table_alters = $this->operations->getTableAltersArray(
$pma_table,
$create_options['pack_keys'],
(empty($create_options['checksum']) ? '0' : '1'),
($create_options['page_checksum'] ?? ''),
(empty($create_options['delay_key_write']) ? '0' : '1'),
$row_format,
$new_tbl_storage_engine,
(isset($create_options['transactional']) && $create_options['transactional'] == '0' ? '0' : '1'),
$tbl_collation
);
if (count($table_alters) > 0) {
$sql_query = 'ALTER TABLE '
. Util::backquote($table);
$sql_query .= "\r\n" . implode("\r\n", $table_alters);
$sql_query .= ';';
$result = (bool) $this->dbi->query($sql_query);
$reread_info = true;
unset($table_alters);
$warning_messages = $this->operations->getWarningMessagesArray();
}
if (! empty($_POST['tbl_collation']) && ! empty($_POST['change_all_collations'])) {
$this->operations->changeAllColumnsCollation($db, $table, $_POST['tbl_collation']);
}
if (isset($_POST['tbl_collation']) && empty($_POST['tbl_collation'])) {
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON(
'message',
Message::error(__('No collation provided.'))
);
return;
}
}
}
/**
* Reordering the table has been requested by the user
*/
if (isset($_POST['submitorderby']) && ! empty($_POST['order_field'])) {
$sql_query = QueryGenerator::getQueryForReorderingTable(
$table,
urldecode($_POST['order_field']),
$_POST['order_order'] ?? null
);
$result = $this->dbi->query($sql_query);
}
/**
* A partition operation has been requested by the user
*/
if (isset($_POST['submit_partition']) && ! empty($_POST['partition_operation'])) {
$sql_query = QueryGenerator::getQueryForPartitioningTable(
$table,
$_POST['partition_operation'],
$_POST['partition_name']
);
$result = $this->dbi->query($sql_query);
}
if ($reread_info) {
// to avoid showing the old value (for example the AUTO_INCREMENT) after
// a change, clear the cache
$this->dbi->getCache()->clearTableCache();
$this->dbi->selectDb($db);
$GLOBALS['showtable'] = $pma_table->getStatusInfo(null, true);
if ($pma_table->isView()) {
$tbl_is_view = true;
$tbl_storage_engine = __('View');
$show_comment = null;
} else {
$tbl_is_view = false;
$tbl_storage_engine = $pma_table->getStorageEngine();
$show_comment = $pma_table->getComment();
}
$tbl_collation = $pma_table->getCollation();
$table_info_num_rows = $pma_table->getNumRows();
$row_format = $pma_table->getRowFormat();
$auto_increment = $pma_table->getAutoIncrement();
$create_options = $pma_table->getCreateOptions();
}
unset($reread_info);
if (isset($result) && empty($message_to_show)) {
if (empty($_message)) {
if (empty($sql_query)) {
$_message = Message::success(__('No change'));
} else {
$_message = $result
? Message::success()
: Message::error();
}
if ($this->response->isAjax()) {
$this->response->setRequestStatus($_message->isSuccess());
$this->response->addJSON('message', $_message);
if (! empty($sql_query)) {
$this->response->addJSON(
'sql_query',
Generator::getMessage('', $sql_query)
);
}
return;
}
} else {
$_message = $result
? Message::success($_message)
: Message::error($_message);
}
if (! empty($warning_messages)) {
$_message = new Message();
$_message->addMessagesString($warning_messages);
$_message->isError(true);
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $_message);
if (! empty($sql_query)) {
$this->response->addJSON(
'sql_query',
Generator::getMessage('', $sql_query)
);
}
return;
}
unset($warning_messages);
}
if (empty($sql_query)) {
$this->response->addHTML(
$_message->getDisplay()
);
} else {
$this->response->addHTML(
Generator::getMessage($_message, $sql_query)
);
}
unset($_message);
}
$urlParams['goto'] = $urlParams['back'] = Url::getFromRoute('/table/operations');
$columns = $this->dbi->getColumns($db, $table);
$hideOrderTable = false;
// `ALTER TABLE ORDER BY` does not make sense for InnoDB tables that contain
// a user-defined clustered index (PRIMARY KEY or NOT NULL UNIQUE index).
// InnoDB always orders table rows according to such an index if one is present.
if ($tbl_storage_engine === 'INNODB') {
$indexes = Index::getFromTable($table, $db);
foreach ($indexes as $name => $idx) {
if ($name === 'PRIMARY') {
$hideOrderTable = true;
break;
}
if ($idx->getNonUnique()) {
continue;
}
$notNull = true;
foreach ($idx->getColumns() as $column) {
if ($column->getNull()) {
$notNull = false;
break;
}
}
if ($notNull) {
$hideOrderTable = true;
break;
}
}
}
$comment = '';
if (mb_strstr((string) $show_comment, '; InnoDB free') === false) {
if (mb_strstr((string) $show_comment, 'InnoDB free') === false) {
// only user entered comment
$comment = (string) $show_comment;
} else {
// here we have just InnoDB generated part
$comment = '';
}
} else {
// remove InnoDB comment from end, just the minimal part (*? is non greedy)
$comment = preg_replace('@; InnoDB free:.*?$@', '', (string) $show_comment);
}
$storageEngines = StorageEngine::getArray();
$charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
$hasPackKeys = isset($create_options['pack_keys'])
&& $pma_table->isEngine(['MYISAM', 'ARIA', 'ISAM']);
$hasChecksumAndDelayKeyWrite = $pma_table->isEngine(['MYISAM', 'ARIA']);
$hasTransactionalAndPageChecksum = $pma_table->isEngine('ARIA');
$hasAutoIncrement = strlen((string) $auto_increment) > 0
&& $pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB']);
$possibleRowFormats = $this->operations->getPossibleRowFormat();
$databaseList = [];
if (count($GLOBALS['dblist']->databases) <= $GLOBALS['cfg']['MaxDbList']) {
$databaseList = $GLOBALS['dblist']->databases->getList();
}
$hasForeignKeys = ! empty($this->relation->getForeigners($db, $table, '', 'foreign'));
$hasPrivileges = $GLOBALS['table_priv'] && $GLOBALS['col_priv'] && $GLOBALS['is_reload_priv'];
$switchToNew = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new'];
$partitions = [];
$partitionsChoices = [];
if (Partition::havePartitioning()) {
$partitionNames = Partition::getPartitionNames($db, $table);
if (isset($partitionNames[0])) {
$partitions = $partitionNames;
$partitionsChoices = $this->operations->getPartitionMaintenanceChoices();
}
}
$foreigners = $this->operations->getForeignersForReferentialIntegrityCheck(
$urlParams,
$relationParameters->relationFeature !== null
);
$this->render('table/operations/index', [
'db' => $db,
'table' => $table,
'url_params' => $urlParams,
'columns' => $columns,
'hide_order_table' => $hideOrderTable,
'table_comment' => $comment,
'storage_engine' => $tbl_storage_engine,
'storage_engines' => $storageEngines,
'charsets' => $charsets,
'collations' => $collations,
'tbl_collation' => $tbl_collation,
'row_formats' => $possibleRowFormats[$tbl_storage_engine] ?? [],
'row_format_current' => $GLOBALS['showtable']['Row_format'],
'has_auto_increment' => $hasAutoIncrement,
'auto_increment' => $auto_increment,
'has_pack_keys' => $hasPackKeys,
'pack_keys' => $create_options['pack_keys'] ?? '',
'has_transactional_and_page_checksum' => $hasTransactionalAndPageChecksum,
'has_checksum_and_delay_key_write' => $hasChecksumAndDelayKeyWrite,
'delay_key_write' => empty($create_options['delay_key_write']) ? '0' : '1',
'transactional' => ($create_options['transactional'] ?? '') == '0' ? '0' : '1',
'page_checksum' => $create_options['page_checksum'] ?? '',
'checksum' => empty($create_options['checksum']) ? '0' : '1',
'database_list' => $databaseList,
'has_foreign_keys' => $hasForeignKeys,
'has_privileges' => $hasPrivileges,
'switch_to_new' => $switchToNew,
'is_system_schema' => $isSystemSchema,
'is_view' => $tbl_is_view,
'partitions' => $partitions,
'partitions_choices' => $partitionsChoices,
'foreigners' => $foreigners,
]);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class AnalyzeController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$rows, $query] = $this->model->analyze($database, $table, $partitionName);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/partition/analyze', [
'partition_name' => $partitionName,
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class CheckController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$rows, $query] = $this->model->check($database, $table, $partitionName);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/partition/check', [
'partition_name' => $partitionName,
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class DropController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$result, $query] = $this->model->drop($database, $table, $partitionName);
if ($result) {
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
} else {
$message = Generator::getMessage(
__('Error'),
$query,
'error'
);
}
$this->render('table/partition/drop', [
'partition_name' => $partitionName,
'message' => $message,
]);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class OptimizeController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$rows, $query] = $this->model->optimize($database, $table, $partitionName);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/partition/optimize', [
'partition_name' => $partitionName,
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class RebuildController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$result, $query] = $this->model->rebuild($database, $table, $partitionName);
if ($result) {
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
} else {
$message = Generator::getMessage(
__('Error'),
$query,
'error'
);
}
$this->render('table/partition/rebuild', [
'partition_name' => $partitionName,
'message' => $message,
]);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class RepairController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$rows, $query] = $this->model->repair($database, $table, $partitionName);
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
$this->render('table/partition/repair', [
'partition_name' => $partitionName,
'message' => $message,
'rows' => $rows,
]);
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Partition;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Dbal\DatabaseName;
use PhpMyAdmin\Dbal\TableName;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\Maintenance;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
use function __;
final class TruncateController extends AbstractController
{
/** @var Maintenance */
private $model;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Maintenance $maintenance
) {
parent::__construct($response, $template, $db, $table);
$this->model = $maintenance;
}
public function __invoke(ServerRequest $request): void
{
$partitionName = $request->getParsedBodyParam('partition_name');
try {
Assert::stringNotEmpty($partitionName);
$database = DatabaseName::fromValue($request->getParam('db'));
$table = TableName::fromValue($request->getParam('table'));
} catch (InvalidArgumentException $exception) {
$message = Message::error($exception->getMessage());
$this->response->addHTML($message->getDisplay());
return;
}
[$result, $query] = $this->model->truncate($database, $table, $partitionName);
if ($result) {
$message = Generator::getMessage(
__('Your SQL query has been executed successfully.'),
$query,
'success'
);
} else {
$message = Generator::getMessage(
__('Error'),
$query,
'error'
);
}
$this->render('table/partition/truncate', [
'partition_name' => $partitionName,
'message' => $message,
]);
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* Controller for table privileges
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Server\Privileges;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function mb_strtolower;
/**
* Controller for table privileges
*/
class PrivilegesController extends AbstractController
{
/** @var Privileges */
private $privileges;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Privileges $privileges,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->privileges = $privileges;
$this->dbi = $dbi;
}
/**
* @param string[] $params Request parameters
* @psalm-param array{checkprivsdb: string, checkprivstable: string} $params
*/
public function __invoke(array $params): string
{
global $cfg, $text_dir;
$scriptName = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$db = $params['checkprivsdb'];
$table = $params['checkprivstable'];
if ($this->dbi->getLowerCaseNames() === '1') {
$db = mb_strtolower($params['checkprivsdb']);
$table = mb_strtolower($params['checkprivstable']);
}
$privileges = [];
if ($this->dbi->isSuperUser()) {
$privileges = $this->privileges->getAllPrivileges($db, $table);
}
return $this->template->render('table/privileges/index', [
'db' => $db,
'table' => $table,
'is_superuser' => $this->dbi->isSuperUser(),
'table_url' => $scriptName,
'text_dir' => $text_dir,
'is_createuser' => $this->dbi->isCreateUser(),
'is_grantuser' => $this->dbi->isGrantUser(),
'privileges' => $privileges,
]);
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Controllers\Sql\SqlController;
use PhpMyAdmin\RecentFavoriteTable;
/**
* Browse recent and favorite tables chosen from navigation.
*/
class RecentFavoriteController extends AbstractController
{
public function __invoke(): void
{
global $containerBuilder;
RecentFavoriteTable::getInstance('recent')->removeIfInvalid($_REQUEST['db'], $_REQUEST['table']);
RecentFavoriteTable::getInstance('favorite')->removeIfInvalid($_REQUEST['db'], $_REQUEST['table']);
/** @var SqlController $controller */
$controller = $containerBuilder->get(SqlController::class);
$controller();
}
}

View file

@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\Features\DisplayFeature;
use PhpMyAdmin\ConfigStorage\Features\RelationFeature;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Index;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use function __;
use function array_key_exists;
use function array_keys;
use function array_values;
use function mb_strtoupper;
use function md5;
use function strtoupper;
use function uksort;
use function usort;
/**
* Display table relations for viewing and editing.
*
* Includes phpMyAdmin relations and InnoDB relations.
*/
final class RelationController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->relation = $relation;
$this->dbi = $dbi;
}
/**
* Index
*/
public function __invoke(): void
{
global $route;
$options = [
'CASCADE' => 'CASCADE',
'SET_NULL' => 'SET NULL',
'NO_ACTION' => 'NO ACTION',
'RESTRICT' => 'RESTRICT',
];
$table = $this->dbi->getTable($this->db, $this->table);
$storageEngine = mb_strtoupper((string) $table->getStatusInfo('Engine'));
$relationParameters = $this->relation->getRelationParameters();
$relations = [];
if ($relationParameters->relationFeature !== null) {
$relations = $this->relation->getForeigners($this->db, $this->table, '', 'internal');
}
$relationsForeign = [];
if (ForeignKey::isSupported($storageEngine)) {
$relationsForeign = $this->relation->getForeigners($this->db, $this->table, '', 'foreign');
}
// Send table of column names to populate corresponding dropdowns depending
// on the current selection
if (isset($_POST['getDropdownValues']) && $_POST['getDropdownValues'] === 'true') {
// if both db and table are selected
if (isset($_POST['foreignTable'])) {
$this->getDropdownValueForTable();
} else { // if only the db is selected
$this->getDropdownValueForDatabase($storageEngine);
}
return;
}
$this->addScriptFiles(['table/relation.js', 'indexes.js']);
// Set the database
$this->dbi->selectDb($this->db);
// updates for Internal relations
if (isset($_POST['destination_db']) && $relationParameters->relationFeature !== null) {
$this->updateForInternalRelation($table, $relationParameters->relationFeature, $relations);
}
// updates for foreign keys
$this->updateForForeignKeys($table, $options, $relationsForeign);
// Updates for display field
if ($relationParameters->displayFeature !== null && isset($_POST['display_field'])) {
$this->updateForDisplayField($table, $relationParameters->displayFeature);
}
// If we did an update, refresh our data
if (isset($_POST['destination_db']) && $relationParameters->relationFeature !== null) {
$relations = $this->relation->getForeigners($this->db, $this->table, '', 'internal');
}
if (isset($_POST['destination_foreign_db']) && ForeignKey::isSupported($storageEngine)) {
$relationsForeign = $this->relation->getForeigners($this->db, $this->table, '', 'foreign');
}
/**
* Dialog
*/
// Now find out the columns of our $table
// need to use DatabaseInterface::QUERY_BUFFERED with $this->dbi->numRows()
// in mysqli
$columns = $this->dbi->getColumns($this->db, $this->table);
$column_array = [];
$column_hash_array = [];
$column_array[''] = '';
foreach ($columns as $column) {
if (strtoupper($storageEngine) !== 'INNODB' && empty($column['Key'])) {
continue;
}
$column_array[$column['Field']] = $column['Field'];
$column_hash_array[$column['Field']] = md5($column['Field']);
}
if ($GLOBALS['cfg']['NaturalOrder']) {
uksort($column_array, 'strnatcasecmp');
}
// common form
$engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine();
$this->render('table/relation/common_form', [
'is_foreign_key_supported' => ForeignKey::isSupported($engine),
'db' => $this->db,
'table' => $this->table,
'relation_parameters' => $relationParameters,
'tbl_storage_engine' => $storageEngine,
'existrel' => $relations,
'existrel_foreign' => array_key_exists('foreign_keys_data', $relationsForeign)
? $relationsForeign['foreign_keys_data']
: [],
'options_array' => $options,
'column_array' => $column_array,
'column_hash_array' => $column_hash_array,
'save_row' => array_values($columns),
'url_params' => $GLOBALS['urlParams'],
'databases' => $GLOBALS['dblist']->databases,
'dbi' => $this->dbi,
'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
'route' => $route,
]);
}
/**
* Update for display field
*/
private function updateForDisplayField(Table $table, DisplayFeature $displayFeature): void
{
$table->updateDisplayField($_POST['display_field'], $displayFeature);
$this->response->addHTML(
Generator::getMessage(
__('Display column was successfully updated.'),
'',
'success'
)
);
}
/**
* Update for FK
*
* @param Table $table Table
* @param array $options Options
* @param array $relationsForeign External relations
*/
private function updateForForeignKeys(Table $table, array $options, array $relationsForeign): void
{
$multi_edit_columns_name = $_POST['foreign_key_fields_name'] ?? null;
$preview_sql_data = '';
$seen_error = false;
// (for now, one index name only; we keep the definitions if the
// foreign db is not the same)
if (
isset($_POST['destination_foreign_db'], $_POST['destination_foreign_table'])
&& isset($_POST['destination_foreign_column'])
) {
[
$html,
$preview_sql_data,
$display_query,
$seen_error,
] = $table->updateForeignKeys(
$_POST['destination_foreign_db'],
$multi_edit_columns_name,
$_POST['destination_foreign_table'],
$_POST['destination_foreign_column'],
$options,
$this->table,
array_key_exists('foreign_keys_data', $relationsForeign)
? $relationsForeign['foreign_keys_data']
: []
);
$this->response->addHTML($html);
}
// If there is a request for SQL previewing.
if (isset($_POST['preview_sql'])) {
Core::previewSQL($preview_sql_data);
exit;
}
if (empty($display_query) || $seen_error) {
return;
}
$GLOBALS['display_query'] = $display_query;
$this->response->addHTML(
Generator::getMessage(
__('Your SQL query has been executed successfully.'),
null,
'success'
)
);
}
/**
* Update for internal relation
*
* @param array $relations Relations
*/
private function updateForInternalRelation(
Table $table,
RelationFeature $relationFeature,
array $relations
): void {
$multi_edit_columns_name = $_POST['fields_name'] ?? null;
if (
! $table->updateInternalRelations(
$multi_edit_columns_name,
$_POST['destination_db'],
$_POST['destination_table'],
$_POST['destination_column'],
$relationFeature,
$relations
)
) {
return;
}
$this->response->addHTML(
Generator::getMessage(
__('Internal relationships were successfully updated.'),
'',
'success'
)
);
}
/**
* Send table columns for foreign table dropdown
*/
public function getDropdownValueForTable(): void
{
$foreignTable = $_POST['foreignTable'];
$table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable);
// Since views do not have keys defined on them provide the full list of
// columns
if ($table_obj->isView()) {
$columnList = $table_obj->getColumns(false, false);
} else {
$columnList = $table_obj->getIndexedColumns(false, false);
}
if ($GLOBALS['cfg']['NaturalOrder']) {
usort($columnList, 'strnatcasecmp');
}
$this->response->addJSON('columns', $columnList);
// @todo should be: $server->db($db)->table($table)->primary()
$primary = Index::getPrimary($foreignTable, $_POST['foreignDb']);
if ($primary === false) {
return;
}
$this->response->addJSON('primary', array_keys($primary->getColumns()));
}
/**
* Send database selection values for dropdown
*
* @param string $storageEngine Storage engine.
*/
public function getDropdownValueForDatabase(string $storageEngine): void
{
$tables = [];
$foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true';
if ($foreign) {
$query = 'SHOW TABLE STATUS FROM '
. Util::backquote($_POST['foreignDb']);
$tables_rs = $this->dbi->query($query);
foreach ($tables_rs as $row) {
if (! isset($row['Engine']) || mb_strtoupper($row['Engine']) != $storageEngine) {
continue;
}
$tables[] = $row['Name'];
}
} else {
$query = 'SHOW TABLES FROM '
. Util::backquote($_POST['foreignDb']);
$tables_rs = $this->dbi->query($query);
$tables = $tables_rs->fetchAllColumn();
}
if ($GLOBALS['cfg']['NaturalOrder']) {
usort($tables, 'strnatcasecmp');
}
$this->response->addJSON('tables', $tables);
}
}

View file

@ -0,0 +1,669 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Controllers\Database\SqlController as DatabaseSqlController;
use PhpMyAdmin\Controllers\Sql\SqlController;
use PhpMyAdmin\Controllers\Table\SqlController as TableSqlController;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\File;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\InsertEdit;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins\IOTransformationsPlugin;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Util;
use function __;
use function array_keys;
use function array_values;
use function class_exists;
use function count;
use function implode;
use function in_array;
use function is_file;
use function is_numeric;
use function method_exists;
use function parse_str;
use function sprintf;
/**
* Manipulation of table data like inserting, replacing and updating.
*/
final class ReplaceController extends AbstractController
{
/** @var InsertEdit */
private $insertEdit;
/** @var Transformations */
private $transformations;
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
InsertEdit $insertEdit,
Transformations $transformations,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->insertEdit = $insertEdit;
$this->transformations = $transformations;
$this->relation = $relation;
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $containerBuilder, $db, $table, $urlParams, $message;
global $errorUrl, $mime_map, $unsaved_values, $active_page, $disp_query, $disp_message;
global $goto_include, $loop_array, $using_key, $is_insert, $is_insertignore, $query;
global $value_sets, $func_no_param, $func_optional_param, $gis_from_text_functions, $gis_from_wkb_functions;
global $query_fields, $insert_errors, $row_skipped, $query_values;
global $total_affected_rows, $last_messages, $warning_messages, $error_messages, $return_to_sql_query;
Util::checkParameters(['db', 'table', 'goto']);
$this->dbi->selectDb($db);
/**
* Initializes some variables
*/
$goto_include = false;
$this->addScriptFiles(['makegrid.js', 'sql.js', 'indexes.js', 'gis_data_editor.js']);
$insertRows = $_POST['insert_rows'] ?? null;
if (is_numeric($insertRows) && $insertRows != $GLOBALS['cfg']['InsertRows']) {
// check whether insert row mode, if so include /table/change
$this->addScriptFiles([
'vendor/jquery/additional-methods.js',
'table/change.js',
]);
$GLOBALS['cfg']['InsertRows'] = $_POST['insert_rows'];
/** @var ChangeController $controller */
$controller = $containerBuilder->get(ChangeController::class);
$controller();
return;
}
$after_insert_actions = [
'new_insert',
'same_insert',
'edit_next',
];
if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $after_insert_actions)) {
$urlParams['after_insert'] = $_POST['after_insert'];
if (isset($_POST['where_clause'])) {
foreach ($_POST['where_clause'] as $one_where_clause) {
if ($_POST['after_insert'] === 'same_insert') {
$urlParams['where_clause'][] = $one_where_clause;
} elseif ($_POST['after_insert'] === 'edit_next') {
$this->insertEdit->setSessionForEditNext($one_where_clause);
}
}
}
}
//get $goto_include for different cases
$goto_include = $this->insertEdit->getGotoInclude($goto_include);
// Defines the url to return in case of failure of the query
$errorUrl = $this->insertEdit->getErrorUrl($urlParams);
/**
* Prepares the update/insert of a row
*/
[
$loop_array,
$using_key,
$is_insert,
$is_insertignore,
] = $this->insertEdit->getParamsForUpdateOrInsert();
$query = [];
$value_sets = [];
$func_no_param = [
'CONNECTION_ID',
'CURRENT_USER',
'CURDATE',
'CURTIME',
'CURRENT_DATE',
'CURRENT_TIME',
'DATABASE',
'LAST_INSERT_ID',
'NOW',
'PI',
'RAND',
'SYSDATE',
'UNIX_TIMESTAMP',
'USER',
'UTC_DATE',
'UTC_TIME',
'UTC_TIMESTAMP',
'UUID',
'UUID_SHORT',
'VERSION',
];
$func_optional_param = [
'RAND',
'UNIX_TIMESTAMP',
];
$gis_from_text_functions = [
'GeomFromText',
'GeomCollFromText',
'LineFromText',
'MLineFromText',
'PointFromText',
'MPointFromText',
'PolyFromText',
'MPolyFromText',
];
$gis_from_wkb_functions = [
'GeomFromWKB',
'GeomCollFromWKB',
'LineFromWKB',
'MLineFromWKB',
'PointFromWKB',
'MPointFromWKB',
'PolyFromWKB',
'MPolyFromWKB',
];
if ($this->dbi->getVersion() >= 50600) {
$gis_from_text_functions = [
'ST_GeomFromText',
'ST_GeomCollFromText',
'ST_LineFromText',
'ST_MLineFromText',
'ST_PointFromText',
'ST_MPointFromText',
'ST_PolyFromText',
'ST_MPolyFromText',
];
$gis_from_wkb_functions = [
'ST_GeomFromWKB',
'ST_GeomCollFromWKB',
'ST_LineFromWKB',
'ST_MLineFromWKB',
'ST_PointFromWKB',
'ST_MPointFromWKB',
'ST_PolyFromWKB',
'ST_MPolyFromWKB',
];
}
$mime_map = $this->transformations->getMime($db, $table);
if ($mime_map === null) {
$mime_map = [];
}
$query_fields = [];
$insert_errors = [];
$row_skipped = false;
$unsaved_values = [];
foreach ($loop_array as $rownumber => $where_clause) {
// skip fields to be ignored
if (! $using_key && isset($_POST['insert_ignore_' . $where_clause])) {
continue;
}
// Defines the SET part of the sql query
$query_values = [];
// Map multi-edit keys to single-level arrays, dependent on how we got the fields
$multi_edit_columns = $_POST['fields']['multi_edit'][$rownumber] ?? [];
$multi_edit_columns_name = $_POST['fields_name']['multi_edit'][$rownumber] ?? [];
$multi_edit_columns_prev = $_POST['fields_prev']['multi_edit'][$rownumber] ?? null;
$multi_edit_funcs = $_POST['funcs']['multi_edit'][$rownumber] ?? null;
$multi_edit_salt = $_POST['salt']['multi_edit'][$rownumber] ?? null;
$multi_edit_columns_type = $_POST['fields_type']['multi_edit'][$rownumber] ?? null;
$multi_edit_columns_null = $_POST['fields_null']['multi_edit'][$rownumber] ?? null;
$multi_edit_columns_null_prev = $_POST['fields_null_prev']['multi_edit'][$rownumber] ?? null;
$multi_edit_auto_increment = $_POST['auto_increment']['multi_edit'][$rownumber] ?? null;
$multi_edit_virtual = $_POST['virtual']['multi_edit'][$rownumber] ?? null;
// When a select field is nullified, it's not present in $_POST
// so initialize it; this way, the foreach($multi_edit_columns) will process it
foreach (array_keys($multi_edit_columns_name) as $key) {
if (isset($multi_edit_columns[$key])) {
continue;
}
$multi_edit_columns[$key] = '';
}
// Iterate in the order of $multi_edit_columns_name,
// not $multi_edit_columns, to avoid problems
// when inserting multiple entries
$insert_fail = false;
foreach ($multi_edit_columns_name as $key => $column_name) {
$current_value = $multi_edit_columns[$key];
// Note: $key is an md5 of the fieldname. The actual fieldname is
// available in $multi_edit_columns_name[$key]
$file_to_insert = new File();
$file_to_insert->checkTblChangeForm((string) $key, (string) $rownumber);
$possibly_uploaded_val = $file_to_insert->getContent();
if ($possibly_uploaded_val !== false) {
$current_value = $possibly_uploaded_val;
}
// Apply Input Transformation if defined
if (! empty($mime_map[$column_name]) && ! empty($mime_map[$column_name]['input_transformation'])) {
$filename = 'libraries/classes/Plugins/Transformations/'
. $mime_map[$column_name]['input_transformation'];
if (is_file(ROOT_PATH . $filename)) {
$classname = $this->transformations->getClassName($filename);
if (class_exists($classname)) {
/** @var IOTransformationsPlugin $transformation_plugin */
$transformation_plugin = new $classname();
$transformation_options = $this->transformations->getOptions(
$mime_map[$column_name]['input_transformation_options']
);
$current_value = $transformation_plugin->applyTransformation(
$current_value,
$transformation_options
);
// check if transformation was successful or not
// and accordingly set error messages & insert_fail
if (
method_exists($transformation_plugin, 'isSuccess')
&& ! $transformation_plugin->isSuccess()
) {
$insert_fail = true;
$row_skipped = true;
$insert_errors[] = sprintf(
__('Row: %1$s, Column: %2$s, Error: %3$s'),
$rownumber,
$column_name,
$transformation_plugin->getError()
);
}
}
}
}
if ($file_to_insert->isError()) {
$insert_errors[] = $file_to_insert->getError();
}
// delete $file_to_insert temporary variable
$file_to_insert->cleanUp();
if (empty($multi_edit_funcs[$key])) {
$current_value_as_an_array = $this->insertEdit->getCurrentValueForDifferentTypes(
$possibly_uploaded_val,
$key,
$multi_edit_columns_type,
$current_value,
$multi_edit_auto_increment,
$rownumber,
$multi_edit_columns_name,
$multi_edit_columns_null,
$multi_edit_columns_null_prev,
$is_insert,
$using_key,
$where_clause,
$table,
$multi_edit_funcs
);
} else {
$current_value_as_an_array = $this->insertEdit->getCurrentValueAsAnArrayForMultipleEdit(
$multi_edit_funcs,
$multi_edit_salt,
$gis_from_text_functions,
$current_value,
$gis_from_wkb_functions,
$func_optional_param,
$func_no_param,
$key
);
}
if (! isset($multi_edit_virtual, $multi_edit_virtual[$key])) {
[
$query_values,
$query_fields,
] = $this->insertEdit->getQueryValuesForInsertAndUpdateInMultipleEdit(
$multi_edit_columns_name,
$multi_edit_columns_null,
$current_value,
$multi_edit_columns_prev,
$multi_edit_funcs,
$is_insert,
$query_values,
$query_fields,
$current_value_as_an_array,
$value_sets,
$key,
$multi_edit_columns_null_prev
);
}
if (! isset($multi_edit_columns_null[$key])) {
continue;
}
$multi_edit_columns[$key] = null;
}
// temporarily store rows not inserted
// so that they can be populated again.
if ($insert_fail) {
$unsaved_values[$rownumber] = $multi_edit_columns;
}
if ($insert_fail || count($query_values) <= 0) {
continue;
}
if ($is_insert) {
$value_sets[] = implode(', ', $query_values);
} else {
// build update query
$clauseIsUnique = $_POST['clause_is_unique'] ?? '';// Should contain 0 or 1
$query[] = 'UPDATE ' . Util::backquote($table)
. ' SET ' . implode(', ', $query_values)
. ' WHERE ' . $where_clause
. ($clauseIsUnique ? '' : ' LIMIT 1');
}
}
unset(
$multi_edit_columns_name,
$multi_edit_columns_prev,
$multi_edit_funcs,
$multi_edit_columns_type,
$multi_edit_columns_null,
$func_no_param,
$multi_edit_auto_increment,
$current_value_as_an_array,
$key,
$current_value,
$loop_array,
$where_clause,
$using_key,
$multi_edit_columns_null_prev,
$insert_fail
);
// Builds the sql query
if ($is_insert && count($value_sets) > 0) {
$query = $this->insertEdit->buildSqlQuery($is_insertignore, $query_fields, $value_sets);
} elseif (empty($query) && ! isset($_POST['preview_sql']) && ! $row_skipped) {
// No change -> move back to the calling script
//
// Note: logic passes here for inline edit
$message = Message::success(__('No change'));
// Avoid infinite recursion
if ($goto_include === '/table/replace') {
$goto_include = '/table/change';
}
$active_page = $goto_include;
if ($goto_include === '/sql') {
/** @var SqlController $controller */
$controller = $containerBuilder->get(SqlController::class);
$controller();
return;
}
if ($goto_include === '/database/sql') {
/** @var DatabaseSqlController $controller */
$controller = $containerBuilder->get(DatabaseSqlController::class);
$controller();
return;
}
if ($goto_include === '/table/change') {
/** @var ChangeController $controller */
$controller = $containerBuilder->get(ChangeController::class);
$controller();
return;
}
if ($goto_include === '/table/sql') {
/** @var TableSqlController $controller */
$controller = $containerBuilder->get(TableSqlController::class);
$controller();
return;
}
/** @psalm-suppress UnresolvableInclude */
include ROOT_PATH . Core::securePath($goto_include);
return;
}
unset($multi_edit_columns, $is_insertignore);
// If there is a request for SQL previewing.
if (isset($_POST['preview_sql'])) {
Core::previewSQL($query);
return;
}
/**
* Executes the sql query and get the result, then move back to the calling
* page
*/
[
$urlParams,
$total_affected_rows,
$last_messages,
$warning_messages,
$error_messages,
$return_to_sql_query,
] = $this->insertEdit->executeSqlQuery($urlParams, $query);
if ($is_insert && (count($value_sets) > 0 || $row_skipped)) {
$message = Message::getMessageForInsertedRows($total_affected_rows);
$unsaved_values = array_values($unsaved_values);
} else {
$message = Message::getMessageForAffectedRows($total_affected_rows);
}
if ($row_skipped) {
$goto_include = '/table/change';
$message->addMessagesString($insert_errors, '<br>');
$message->isError(true);
}
$message->addMessages($last_messages, '<br>');
if (! empty($warning_messages)) {
$message->addMessagesString($warning_messages, '<br>');
$message->isError(true);
}
if (! empty($error_messages)) {
$message->addMessagesString($error_messages);
$message->isError(true);
}
unset(
$error_messages,
$warning_messages,
$total_affected_rows,
$last_messages,
$row_skipped,
$insert_errors
);
/**
* The following section only applies to grid editing.
* However, verifying isAjax() is not enough to ensure we are coming from
* grid editing. If we are coming from the Edit or Copy link in Browse mode,
* ajax_page_request is present in the POST parameters.
*/
if ($this->response->isAjax() && ! isset($_POST['ajax_page_request'])) {
/**
* If we are in grid editing, we need to process the relational and
* transformed fields, if they were edited. After that, output the correct
* link/transformed value and exit
*/
if (isset($_POST['rel_fields_list']) && $_POST['rel_fields_list'] != '') {
$map = $this->relation->getForeigners($db, $table, '', 'both');
/** @var array<int,array> $relation_fields */
$relation_fields = [];
parse_str($_POST['rel_fields_list'], $relation_fields);
// loop for each relation cell
foreach ($relation_fields as $cell_index => $curr_rel_field) {
foreach ($curr_rel_field as $relation_field => $relation_field_value) {
$where_comparison = "='" . $relation_field_value . "'";
$dispval = $this->insertEdit->getDisplayValueForForeignTableColumn(
$where_comparison,
$map,
$relation_field
);
$extra_data['relations'][$cell_index] = $this->insertEdit->getLinkForRelationalDisplayField(
$map,
$relation_field,
$where_comparison,
$dispval,
$relation_field_value
);
}
}
}
if (isset($_POST['do_transformations']) && $_POST['do_transformations'] == true) {
$edited_values = [];
parse_str($_POST['transform_fields_list'], $edited_values);
if (! isset($extra_data)) {
$extra_data = [];
}
$transformation_types = [
'input_transformation',
'transformation',
];
foreach ($mime_map as $transformation) {
$column_name = $transformation['column_name'];
foreach ($transformation_types as $type) {
$file = Core::securePath($transformation[$type]);
$extra_data = $this->insertEdit->transformEditedValues(
$db,
$table,
$transformation,
$edited_values,
$file,
$column_name,
$extra_data,
$type
);
}
}
}
// Need to check the inline edited value can be truncated by MySQL
// without informing while saving
$column_name = $_POST['fields_name']['multi_edit'][0][0];
$this->insertEdit->verifyWhetherValueCanBeTruncatedAndAppendExtraData(
$db,
$table,
$column_name,
$extra_data
);
/**Get the total row count of the table*/
$_table = new Table($_POST['table'], $_POST['db']);
$extra_data['row_count'] = $_table->countRecords();
$extra_data['sql_query'] = Generator::getMessage($message, $GLOBALS['display_query']);
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON($extra_data);
return;
}
if (! empty($return_to_sql_query)) {
$disp_query = $GLOBALS['sql_query'];
$disp_message = $message;
unset($message);
$GLOBALS['sql_query'] = $return_to_sql_query;
}
$this->addScriptFiles(['vendor/jquery/additional-methods.js', 'table/change.js']);
$active_page = $goto_include;
/**
* If user asked for "and then Insert another new row" we have to remove
* WHERE clause information so that /table/change does not go back
* to the current record
*/
if (isset($_POST['after_insert']) && $_POST['after_insert'] === 'new_insert') {
unset($_POST['where_clause']);
}
if ($goto_include === '/sql') {
/** @var SqlController $controller */
$controller = $containerBuilder->get(SqlController::class);
$controller();
return;
}
if ($goto_include === '/database/sql') {
/** @var DatabaseSqlController $controller */
$controller = $containerBuilder->get(DatabaseSqlController::class);
$controller();
return;
}
if ($goto_include === '/table/change') {
/** @var ChangeController $controller */
$controller = $containerBuilder->get(ChangeController::class);
$controller();
return;
}
if ($goto_include === '/table/sql') {
/** @var TableSqlController $controller */
$controller = $containerBuilder->get(TableSqlController::class);
$controller();
return;
}
/**
* Load target page.
*/
/** @psalm-suppress UnresolvableInclude */
require ROOT_PATH . Core::securePath($goto_include);
}
}

View file

@ -0,0 +1,405 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Operations;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Table\Search;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\Gis;
use function in_array;
use function intval;
use function mb_strtolower;
use function md5;
use function preg_match;
use function preg_replace;
use function str_ireplace;
use function str_replace;
use function strncasecmp;
use function strtoupper;
/**
* Handles table search tab.
*
* Display table search form, create SQL query from form data
* and call Sql::executeQueryAndSendQueryResponse() to execute it.
*/
class SearchController extends AbstractController
{
/**
* Names of columns
*
* @var array
*/
private $columnNames;
/**
* Types of columns
*
* @var array
*/
private $columnTypes;
/**
* Types of columns without any replacement
*
* @var array
*/
private $originalColumnTypes;
/**
* Collations of columns
*
* @var array
*/
private $columnCollations;
/**
* Null Flags of columns
*
* @var array
*/
private $columnNullFlags;
/**
* Whether a geometry column is present
*
* @var bool
*/
private $geomColumnFlag;
/**
* Foreign Keys
*
* @var array
*/
private $foreigners;
/** @var Search */
private $search;
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Search $search,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->search = $search;
$this->relation = $relation;
$this->dbi = $dbi;
$this->columnNames = [];
$this->columnTypes = [];
$this->originalColumnTypes = [];
$this->columnCollations = [];
$this->columnNullFlags = [];
$this->geomColumnFlag = false;
$this->foreigners = [];
$this->loadTableInfo();
}
/**
* Gets all the columns of a table along with their types, collations
* and whether null or not.
*/
private function loadTableInfo(): void
{
// Gets the list and number of columns
$columns = $this->dbi->getColumns($this->db, $this->table, true);
// Get details about the geometry functions
$geom_types = Gis::getDataTypes();
foreach ($columns as $row) {
// set column name
$this->columnNames[] = $row['Field'];
$type = (string) $row['Type'];
// before any replacement
$this->originalColumnTypes[] = mb_strtolower($type);
// check whether table contains geometric columns
if (in_array($type, $geom_types)) {
$this->geomColumnFlag = true;
}
// reformat mysql query output
if (strncasecmp($type, 'set', 3) == 0 || strncasecmp($type, 'enum', 4) == 0) {
$type = str_replace(',', ', ', $type);
} else {
// strip the "BINARY" attribute, except if we find "BINARY(" because
// this would be a BINARY or VARBINARY column type
if (! preg_match('@BINARY[\(]@i', $type)) {
$type = str_ireplace('BINARY', '', $type);
}
$type = str_ireplace('ZEROFILL', '', $type);
$type = str_ireplace('UNSIGNED', '', $type);
$type = mb_strtolower($type);
}
if (empty($type)) {
$type = '&nbsp;';
}
$this->columnTypes[] = $type;
$this->columnNullFlags[] = $row['Null'];
$this->columnCollations[] = ! empty($row['Collation']) && $row['Collation'] !== 'NULL'
? $row['Collation']
: '';
}
// Retrieve foreign keys
$this->foreigners = $this->relation->getForeigners($this->db, $this->table);
}
/**
* Index action
*/
public function __invoke(): void
{
global $db, $table, $urlParams, $cfg, $errorUrl;
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$this->addScriptFiles([
'makegrid.js',
'sql.js',
'table/select.js',
'table/change.js',
'vendor/jquery/jquery.uitablefilter.js',
'gis_data_editor.js',
]);
if (isset($_POST['range_search'])) {
$this->rangeSearchAction();
return;
}
/**
* No selection criteria received -> display the selection form
*/
if (! isset($_POST['columnsToDisplay']) && ! isset($_POST['displayAllColumns'])) {
$this->displaySelectionFormAction();
} else {
$this->doSelectionAction();
}
}
/**
* Get data row action
*/
public function getDataRowAction(): void
{
if (! Core::checkSqlQuerySignature($_POST['where_clause'], $_POST['where_clause_sign'])) {
return;
}
$extra_data = [];
$row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.'
. Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
$result = $this->dbi->query($row_info_query . ';');
$fields_meta = $this->dbi->getFieldsMeta($result);
while ($row = $result->fetchAssoc()) {
// for bit fields we need to convert them to printable form
$i = 0;
foreach ($row as $col => $val) {
if (isset($fields_meta[$i]) && $fields_meta[$i]->isMappedTypeBit) {
$row[$col] = Util::printableBitValue((int) $val, (int) $fields_meta[$i]->length);
}
$i++;
}
$extra_data['row_info'] = $row;
}
$this->response->addJSON($extra_data);
}
/**
* Do selection action
*/
public function doSelectionAction(): void
{
/**
* Selection criteria have been submitted -> do the work
*/
$sql_query = $this->search->buildSqlQuery();
/**
* Add this to ensure following procedures included running correctly.
*/
$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
$this->db, // db
$this->table, // table
null, // find_real_end
null, // sql_query_for_bookmark
null, // extra_data
null, // message_to_show
null, // sql_data
$GLOBALS['goto'], // goto
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
));
}
/**
* Display selection form action
*/
public function displaySelectionFormAction(): void
{
global $goto, $cfg;
if (! isset($goto)) {
$goto = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
}
$this->render('table/search/index', [
'db' => $this->db,
'table' => $this->table,
'goto' => $goto,
'self' => $this,
'geom_column_flag' => $this->geomColumnFlag,
'column_names' => $this->columnNames,
'column_types' => $this->columnTypes,
'column_collations' => $this->columnCollations,
'default_sliders_state' => $cfg['InitialSlidersState'],
'max_rows' => intval($cfg['MaxRows']),
]);
}
/**
* Range search action
*/
public function rangeSearchAction(): void
{
$min_max = $this->getColumnMinMax($_POST['column']);
$this->response->addJSON('column_data', $min_max);
}
/**
* Finds minimum and maximum value of a given column.
*
* @param string $column Column name
*
* @return array|null
*/
public function getColumnMinMax($column): ?array
{
$sql_query = 'SELECT MIN(' . Util::backquote($column) . ') AS `min`, '
. 'MAX(' . Util::backquote($column) . ') AS `max` '
. 'FROM ' . Util::backquote($this->db) . '.'
. Util::backquote($this->table);
return $this->dbi->fetchSingleRow($sql_query);
}
/**
* Provides a column's type, collation, operators list, and criteria value
* to display in table search form
*
* @param int $search_index Row number in table search form
* @param int $column_index Column index in ColumnNames array
*
* @return array Array containing column's properties
*/
public function getColumnProperties($search_index, $column_index)
{
$selected_operator = ($_POST['criteriaColumnOperators'][$search_index] ?? '');
$entered_value = ($_POST['criteriaValues'] ?? '');
//Gets column's type and collation
$type = $this->columnTypes[$column_index];
$collation = $this->columnCollations[$column_index];
$cleanType = preg_replace('@\(.*@s', '', $type);
//Gets column's comparison operators depending on column type
$typeOperators = $this->dbi->types->getTypeOperatorsHtml(
$cleanType,
$this->columnNullFlags[$column_index],
$selected_operator
);
$func = $this->template->render('table/search/column_comparison_operators', [
'search_index' => $search_index,
'type_operators' => $typeOperators,
]);
//Gets link to browse foreign data(if any) and criteria inputbox
$foreignData = $this->relation->getForeignData(
$this->foreigners,
$this->columnNames[$column_index],
false,
'',
''
);
$htmlAttributes = '';
if (in_array($cleanType, $this->dbi->types->getIntegerTypes())) {
$extractedColumnspec = Util::extractColumnSpec($this->originalColumnTypes[$column_index]);
$is_unsigned = $extractedColumnspec['unsigned'];
$minMaxValues = $this->dbi->types->getIntegerRange($cleanType, ! $is_unsigned);
$htmlAttributes = 'data-min="' . $minMaxValues[0] . '" '
. 'data-max="' . $minMaxValues[1] . '"';
}
$htmlAttributes .= ' onfocus="return '
. 'verifyAfterSearchFieldChange(' . $search_index . ', \'#tbl_search_form\')"';
$value = $this->template->render('table/search/input_box', [
'str' => '',
'column_type' => (string) $type,
'column_data_type' => strtoupper($cleanType),
'html_attributes' => $htmlAttributes,
'column_id' => 'fieldID_',
'in_zoom_search_edit' => false,
'foreigners' => $this->foreigners,
'column_name' => $this->columnNames[$column_index],
'column_name_hash' => md5($this->columnNames[$column_index]),
'foreign_data' => $foreignData,
'table' => $this->table,
'column_index' => $search_index,
'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
'criteria_values' => $entered_value,
'db' => $this->db,
'in_fbs' => true,
]);
return [
'type' => $type,
'collation' => $collation,
'func' => $func,
'value' => $value,
];
}
}

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\SqlQueryForm;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function htmlspecialchars;
/**
* Table SQL executor
*/
final class SqlController extends AbstractController
{
/** @var SqlQueryForm */
private $sqlQueryForm;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
SqlQueryForm $sqlQueryForm
) {
parent::__construct($response, $template, $db, $table);
$this->sqlQueryForm = $sqlQueryForm;
}
public function __invoke(): void
{
global $errorUrl, $goto, $back, $db, $table, $cfg;
$this->addScriptFiles(['makegrid.js', 'vendor/jquery/jquery.uitablefilter.js', 'sql.js']);
$pageSettings = new PageSettings('Sql');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($url_params, '&');
DbTableExists::check();
/**
* After a syntax error, we return to this script
* with the typed query in the textarea.
*/
$goto = Url::getFromRoute('/table/sql');
$back = Url::getFromRoute('/table/sql');
$this->response->addHTML($this->sqlQueryForm->getHtml(
$db,
$table,
$_GET['sql_query'] ?? true,
false,
isset($_POST['delimiter'])
? htmlspecialchars($_POST['delimiter'])
: ';'
));
}
}

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
final class AddIndexController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $sql_query, $db, $table, $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$i = 1;
$selectedCount = count($selected);
$sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD INDEX(';
foreach ($selected as $field) {
$sql_query .= Util::backquote($field);
$sql_query .= $i++ === $selectedCount ? ');' : ', ';
}
$this->dbi->selectDb($db);
$result = $this->dbi->tryQuery($sql_query);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Sql\SqlController;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
final class AddKeyController extends AbstractController
{
/** @var SqlController */
private $sqlController;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
SqlController $sqlController,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->sqlController = $sqlController;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $reload;
($this->sqlController)();
$reload = true;
($this->structureController)();
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\ParseAnalyze;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function implode;
use function sprintf;
final class BrowseController extends AbstractController
{
/** @var Sql */
private $sql;
public function __construct(ResponseRenderer $response, Template $template, string $db, string $table, Sql $sql)
{
parent::__construct($response, $template, $db, $table);
$this->sql = $sql;
}
public function __invoke(): void
{
if (empty($_POST['selected_fld'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$this->displayTableBrowseForSelectedColumns($GLOBALS['goto']);
}
/**
* Function to display table browse for selected columns
*
* @param string $goto goto page url
*/
private function displayTableBrowseForSelectedColumns($goto): void
{
$GLOBALS['active_page'] = Url::getFromRoute('/sql');
$fields = [];
foreach ($_POST['selected_fld'] as $sval) {
$fields[] = Util::backquote($sval);
}
$sql_query = sprintf(
'SELECT %s FROM %s.%s',
implode(', ', $fields),
Util::backquote($this->db),
Util::backquote($this->table)
);
// Parse and analyze the query
[$analyzed_sql_results, $this->db] = ParseAnalyze::sqlQuery($sql_query, $this->db);
$this->response->addHTML(
$this->sql->executeQueryAndGetQueryResponse(
$analyzed_sql_results ?? '',
false, // is_gotofile
$this->db, // db
$this->table, // table
null, // find_real_end
null, // sql_query_for_bookmark
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
)
);
}
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\Database\CentralColumns;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use function __;
final class CentralColumnsAddController extends AbstractController
{
/** @var CentralColumns */
private $centralColumns;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
CentralColumns $centralColumns,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->centralColumns = $centralColumns;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$centralColsError = $this->centralColumns->syncUniqueColumns($selected, false);
if ($centralColsError instanceof Message) {
$message = $centralColsError;
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\Database\CentralColumns;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use function __;
final class CentralColumnsRemoveController extends AbstractController
{
/** @var CentralColumns */
private $centralColumns;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
CentralColumns $centralColumns,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->centralColumns = $centralColumns;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $db, $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$centralColsError = $this->centralColumns->deleteColumnsFromList($db, $selected, false);
if ($centralColsError instanceof Message) {
$message = $centralColsError;
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\ColumnsDefinition;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use function __;
use function count;
final class ChangeController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var Transformations */
private $transformations;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Relation $relation,
Transformations $transformations,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->relation = $relation;
$this->transformations = $transformations;
$this->dbi = $dbi;
}
public function __invoke(): void
{
if (isset($_GET['change_column'])) {
$this->displayHtmlForColumnChange(null);
return;
}
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$this->displayHtmlForColumnChange($selected);
}
/**
* Displays HTML for changing one or more columns
*
* @param array|null $selected the selected columns
*/
private function displayHtmlForColumnChange(?array $selected): void
{
global $action, $num_fields;
if (empty($selected)) {
$selected[] = $_REQUEST['field'];
$selected_cnt = 1;
} else { // from a multiple submit
$selected_cnt = count($selected);
}
/**
* @todo optimize in case of multiple fields to modify
*/
$fields_meta = [];
for ($i = 0; $i < $selected_cnt; $i++) {
$value = $this->dbi->getColumn($this->db, $this->table, $selected[$i], true);
if (count($value) === 0) {
$message = Message::error(
__('Failed to get description of column %s!')
);
$message->addParam($selected[$i]);
$this->response->addHTML($message->getDisplay());
} else {
$fields_meta[] = $value;
}
}
$num_fields = count($fields_meta);
$action = Url::getFromRoute('/table/structure/save');
/**
* Form for changing properties.
*/
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
$this->addScriptFiles(['vendor/jquery/jquery.uitablefilter.js', 'indexes.js']);
$templateData = ColumnsDefinition::displayForm(
$this->transformations,
$this->relation,
$this->dbi,
$action,
$num_fields,
null,
$selected,
$fields_meta
);
$this->render('columns_definitions/column_definitions_form', $templateData);
}
}

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
final class FulltextController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $sql_query, $db, $table, $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$i = 1;
$selectedCount = count($selected);
$sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD FULLTEXT(';
foreach ($selected as $field) {
$sql_query .= Util::backquote($field);
$sql_query .= $i++ === $selectedCount ? ');' : ', ';
}
$this->dbi->selectDb($db);
$result = $this->dbi->tryQuery($sql_query);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function array_keys;
use function array_splice;
use function count;
use function implode;
use function in_array;
use function is_array;
use function mb_strtoupper;
use function sprintf;
use function str_replace;
final class MoveColumnsController extends AbstractController
{
/** @var Table The table object */
private $tableObj;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->tableObj = $this->dbi->getTable($this->db, $this->table);
}
public function __invoke(): void
{
if (! isset($_POST['move_columns']) || ! is_array($_POST['move_columns']) || ! $this->response->isAjax()) {
return;
}
$this->dbi->selectDb($this->db);
/**
* load the definitions for all columns
*/
$columns = $this->dbi->getColumnsFull($this->db, $this->table);
$column_names = array_keys($columns);
$changes = [];
// @see https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-102/#information-schema
$usesLiteralNull = $this->dbi->isMariaDB() && $this->dbi->getVersion() >= 100200;
$defaultNullValue = $usesLiteralNull ? 'NULL' : null;
// move columns from first to last
for ($i = 0, $l = count($_POST['move_columns']); $i < $l; $i++) {
$column = $_POST['move_columns'][$i];
// is this column already correctly placed?
if ($column_names[$i] == $column) {
continue;
}
// it is not, let's move it to index $i
$data = $columns[$column];
$extracted_columnspec = Util::extractColumnSpec($data['Type']);
if (isset($data['Extra']) && $data['Extra'] === 'on update CURRENT_TIMESTAMP') {
$extracted_columnspec['attribute'] = $data['Extra'];
unset($data['Extra']);
}
$timeType = $data['Type'] === 'timestamp' || $data['Type'] === 'datetime';
$timeDefault = $data['Default'] === 'CURRENT_TIMESTAMP' || $data['Default'] === 'current_timestamp()';
$current_timestamp = $timeType && $timeDefault;
$uuidType = $data['Type'] === 'uuid';
$uuidDefault = $data['Default'] === 'UUID' || $data['Default'] === 'uuid()';
$uuid = $uuidType && $uuidDefault;
// @see https://mariadb.com/kb/en/library/information-schema-columns-table/#examples
if ($data['Null'] === 'YES' && in_array($data['Default'], [$defaultNullValue, null])) {
$default_type = 'NULL';
} elseif ($current_timestamp) {
$default_type = 'CURRENT_TIMESTAMP';
} elseif ($uuid) {
$default_type = 'UUID';
} elseif ($data['Default'] === null) {
$default_type = 'NONE';
} else {
$default_type = 'USER_DEFINED';
}
$virtual = [
'VIRTUAL',
'PERSISTENT',
'VIRTUAL GENERATED',
'STORED GENERATED',
];
$data['Virtuality'] = '';
$data['Expression'] = '';
if (isset($data['Extra']) && in_array($data['Extra'], $virtual)) {
$data['Virtuality'] = str_replace(' GENERATED', '', $data['Extra']);
$expressions = $this->tableObj->getColumnGenerationExpression($column);
$data['Expression'] = is_array($expressions) ? $expressions[$column] : null;
}
$changes[] = 'CHANGE ' . Table::generateAlter(
$column,
$column,
mb_strtoupper($extracted_columnspec['type']),
$extracted_columnspec['spec_in_brackets'],
$extracted_columnspec['attribute'],
$data['Collation'] ?? '',
$data['Null'] === 'YES' ? 'YES' : 'NO',
$default_type,
$current_timestamp ? '' : $data['Default'],
isset($data['Extra']) && $data['Extra'] !== '' ? $data['Extra']
: false,
isset($data['COLUMN_COMMENT']) && $data['COLUMN_COMMENT'] !== ''
? $data['COLUMN_COMMENT'] : false,
$data['Virtuality'],
$data['Expression'],
$i === 0 ? '-first' : $column_names[$i - 1]
);
// update current column_names array, first delete old position
for ($j = 0, $ll = count($column_names); $j < $ll; $j++) {
if ($column_names[$j] != $column) {
continue;
}
unset($column_names[$j]);
}
// insert moved column
array_splice($column_names, $i, 0, $column);
}
if (empty($changes) && ! isset($_REQUEST['preview_sql'])) { // should never happen
$this->response->setRequestStatus(false);
return;
}
// query for moving the columns
$sql_query = sprintf(
'ALTER TABLE %s %s',
Util::backquote($this->table),
implode(', ', $changes)
);
if (isset($_REQUEST['preview_sql'])) { // preview sql
$this->response->addJSON(
'sql_data',
$this->template->render('preview_sql', ['query_data' => $sql_query])
);
return;
}
$this->dbi->tryQuery($sql_query);
$tmp_error = $this->dbi->getError();
if ($tmp_error !== '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error($tmp_error));
return;
}
$message = Message::success(
__('The columns have been moved successfully.')
);
$this->response->addJSON('message', $message);
$this->response->addJSON('columns', $column_names);
}
}

View file

@ -0,0 +1,282 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\CreateAddField;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Partitioning\TablePartitionDefinition;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
use PhpMyAdmin\StorageEngine;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
use function strpos;
use function strrpos;
use function substr;
use function trim;
final class PartitioningController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var CreateAddField */
private $createAddField;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
CreateAddField $createAddField,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->createAddField = $createAddField;
$this->structureController = $structureController;
}
public function __invoke(): void
{
if (isset($_POST['save_partitioning'])) {
$this->dbi->selectDb($this->db);
$this->updatePartitioning();
($this->structureController)();
return;
}
$pageSettings = new PageSettings('TableStructure');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
$this->addScriptFiles(['table/structure.js', 'indexes.js']);
$partitionDetails = null;
if (! isset($_POST['partition_by'])) {
$partitionDetails = $this->extractPartitionDetails();
}
$storageEngines = StorageEngine::getArray();
$partitionDetails = TablePartitionDefinition::getDetails($partitionDetails);
$this->render('table/structure/partition_definition_form', [
'db' => $this->db,
'table' => $this->table,
'partition_details' => $partitionDetails,
'storage_engines' => $storageEngines,
]);
}
/**
* Extracts partition details from CREATE TABLE statement
*
* @return array<string, array<int, array<string, mixed>>|bool|int|string>|null array of partition details
*/
private function extractPartitionDetails(): ?array
{
$createTable = (new Table($this->table, $this->db))->showCreate();
if (! $createTable) {
return null;
}
$parser = new Parser($createTable);
/**
* @var CreateStatement $stmt
*/
$stmt = $parser->statements[0];
$partitionDetails = [];
$partitionDetails['partition_by'] = '';
$partitionDetails['partition_expr'] = '';
$partitionDetails['partition_count'] = 0;
if (! empty($stmt->partitionBy)) {
$openPos = strpos($stmt->partitionBy, '(');
$closePos = strrpos($stmt->partitionBy, ')');
if ($openPos !== false && $closePos !== false) {
$partitionDetails['partition_by'] = trim(substr($stmt->partitionBy, 0, $openPos));
$partitionDetails['partition_expr'] = trim(substr(
$stmt->partitionBy,
$openPos + 1,
$closePos - ($openPos + 1)
));
$count = $stmt->partitionsNum ?? count($stmt->partitions);
$partitionDetails['partition_count'] = $count;
}
}
$partitionDetails['subpartition_by'] = '';
$partitionDetails['subpartition_expr'] = '';
$partitionDetails['subpartition_count'] = 0;
if (! empty($stmt->subpartitionBy)) {
$openPos = strpos($stmt->subpartitionBy, '(');
$closePos = strrpos($stmt->subpartitionBy, ')');
if ($openPos !== false && $closePos !== false) {
$partitionDetails['subpartition_by'] = trim(substr($stmt->subpartitionBy, 0, $openPos));
$partitionDetails['subpartition_expr'] = trim(substr(
$stmt->subpartitionBy,
$openPos + 1,
$closePos - ($openPos + 1)
));
$count = $stmt->subpartitionsNum ?? count($stmt->partitions[0]->subpartitions);
$partitionDetails['subpartition_count'] = $count;
}
}
// Only LIST and RANGE type parameters allow subpartitioning
$partitionDetails['can_have_subpartitions'] = $partitionDetails['partition_count'] > 1
&& ($partitionDetails['partition_by'] === 'RANGE'
|| $partitionDetails['partition_by'] === 'RANGE COLUMNS'
|| $partitionDetails['partition_by'] === 'LIST'
|| $partitionDetails['partition_by'] === 'LIST COLUMNS');
// Values are specified only for LIST and RANGE type partitions
$partitionDetails['value_enabled'] = isset($partitionDetails['partition_by'])
&& ($partitionDetails['partition_by'] === 'RANGE'
|| $partitionDetails['partition_by'] === 'RANGE COLUMNS'
|| $partitionDetails['partition_by'] === 'LIST'
|| $partitionDetails['partition_by'] === 'LIST COLUMNS');
$partitionDetails['partitions'] = [];
for ($i = 0, $iMax = $partitionDetails['partition_count']; $i < $iMax; $i++) {
if (! isset($stmt->partitions[$i])) {
$partitionDetails['partitions'][$i] = [
'name' => 'p' . $i,
'value_type' => '',
'value' => '',
'engine' => '',
'comment' => '',
'data_directory' => '',
'index_directory' => '',
'max_rows' => '',
'min_rows' => '',
'tablespace' => '',
'node_group' => '',
];
} else {
$p = $stmt->partitions[$i];
$type = $p->type;
$expr = trim((string) $p->expr, '()');
if ($expr === 'MAXVALUE') {
$type .= ' MAXVALUE';
$expr = '';
}
$partitionDetails['partitions'][$i] = [
'name' => $p->name,
'value_type' => $type,
'value' => $expr,
'engine' => $p->options->has('ENGINE', true),
'comment' => trim((string) $p->options->has('COMMENT', true), "'"),
'data_directory' => trim((string) $p->options->has('DATA DIRECTORY', true), "'"),
'index_directory' => trim((string) $p->options->has('INDEX_DIRECTORY', true), "'"),
'max_rows' => $p->options->has('MAX_ROWS', true),
'min_rows' => $p->options->has('MIN_ROWS', true),
'tablespace' => $p->options->has('TABLESPACE', true),
'node_group' => $p->options->has('NODEGROUP', true),
];
}
$partition =& $partitionDetails['partitions'][$i];
$partition['prefix'] = 'partitions[' . $i . ']';
if ($partitionDetails['subpartition_count'] <= 1) {
continue;
}
$partition['subpartition_count'] = $partitionDetails['subpartition_count'];
$partition['subpartitions'] = [];
for ($j = 0, $jMax = $partitionDetails['subpartition_count']; $j < $jMax; $j++) {
if (! isset($stmt->partitions[$i]->subpartitions[$j])) {
$partition['subpartitions'][$j] = [
'name' => $partition['name'] . '_s' . $j,
'engine' => '',
'comment' => '',
'data_directory' => '',
'index_directory' => '',
'max_rows' => '',
'min_rows' => '',
'tablespace' => '',
'node_group' => '',
];
} else {
$sp = $stmt->partitions[$i]->subpartitions[$j];
$partition['subpartitions'][$j] = [
'name' => $sp->name,
'engine' => $sp->options->has('ENGINE', true),
'comment' => trim((string) $sp->options->has('COMMENT', true), "'"),
'data_directory' => trim((string) $sp->options->has('DATA DIRECTORY', true), "'"),
'index_directory' => trim((string) $sp->options->has('INDEX_DIRECTORY', true), "'"),
'max_rows' => $sp->options->has('MAX_ROWS', true),
'min_rows' => $sp->options->has('MIN_ROWS', true),
'tablespace' => $sp->options->has('TABLESPACE', true),
'node_group' => $sp->options->has('NODEGROUP', true),
];
}
$subpartition =& $partition['subpartitions'][$j];
$subpartition['prefix'] = 'partitions[' . $i . ']'
. '[subpartitions][' . $j . ']';
}
}
return $partitionDetails;
}
private function updatePartitioning(): void
{
$sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' '
. $this->createAddField->getPartitionsDefinition();
// Execute alter query
$result = $this->dbi->tryQuery($sql_query);
if ($result === false) {
$this->response->setRequestStatus(false);
$this->response->addJSON(
'message',
Message::rawError(
__('Query error') . ':<br>' . $this->dbi->getError()
)
);
return;
}
$message = Message::success(
__('Table %1$s has been altered successfully.')
);
$message->addParam($this->table);
$this->response->addHTML(
Generator::getMessage($message, $sql_query, 'success')
);
}
}

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function count;
final class PrimaryController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $db, $table, $message, $sql_query, $urlParams, $errorUrl, $cfg;
$selected = $_POST['selected'] ?? [];
$selected_fld = $_POST['selected_fld'] ?? [];
if (empty($selected) && empty($selected_fld)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$primary = $this->getKeyForTablePrimary();
if (empty($primary) && ! empty($selected_fld)) {
// no primary key, so we can safely create new
$mult_btn = __('Yes');
$selected = $selected_fld;
}
$mult_btn = $_POST['mult_btn'] ?? $mult_btn ?? '';
if (! empty($selected_fld) && ! empty($primary)) {
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$this->render('table/structure/primary', [
'db' => $db,
'table' => $table,
'selected' => $selected_fld,
]);
return;
}
if ($mult_btn === __('Yes')) {
$sql_query = 'ALTER TABLE ' . Util::backquote($table);
if (! empty($primary)) {
$sql_query .= ' DROP PRIMARY KEY,';
}
$sql_query .= ' ADD PRIMARY KEY(';
$i = 1;
$selectedCount = count($selected);
foreach ($selected as $field) {
$sql_query .= Util::backquote($field);
$sql_query .= $i++ === $selectedCount ? ');' : ', ';
}
$this->dbi->selectDb($db);
$result = $this->dbi->tryQuery($sql_query);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
/**
* Gets table primary key
*
* @return string
*/
private function getKeyForTablePrimary()
{
$this->dbi->selectDb($this->db);
$result = $this->dbi->query(
'SHOW KEYS FROM ' . Util::backquote($this->table) . ';'
);
$primary = '';
foreach ($result as $row) {
// Backups the list of primary keys
if ($row['Key_name'] !== 'PRIMARY') {
continue;
}
$primary .= $row['Column_name'] . ', ';
}
return $primary;
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\SqlParser\Context;
use function _ngettext;
use function count;
use function implode;
use function sprintf;
use function trim;
final class ReservedWordCheckController extends AbstractController
{
public function __invoke(): void
{
if ($GLOBALS['cfg']['ReservedWordDisableWarning'] !== false) {
$this->response->setRequestStatus(false);
return;
}
$columns_names = $_POST['field_name'];
$reserved_keywords_names = [];
foreach ($columns_names as $column) {
if (! Context::isKeyword(trim($column), true)) {
continue;
}
$reserved_keywords_names[] = trim($column);
}
if (Context::isKeyword(trim($this->table), true)) {
$reserved_keywords_names[] = trim($this->table);
}
if (count($reserved_keywords_names) === 0) {
$this->response->setRequestStatus(false);
}
$this->response->addJSON(
'message',
sprintf(
_ngettext(
'The name \'%s\' is a MySQL reserved keyword.',
'The names \'%s\' are MySQL reserved keywords.',
count($reserved_keywords_names)
),
implode(',', $reserved_keywords_names)
)
);
}
}

View file

@ -0,0 +1,402 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Index;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function count;
use function implode;
use function in_array;
use function is_array;
use function mb_strpos;
use function sprintf;
use function strlen;
final class SaveController extends AbstractController
{
/** @var Table The table object */
private $tableObj;
/** @var Relation */
private $relation;
/** @var Transformations */
private $transformations;
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Relation $relation,
Transformations $transformations,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->relation = $relation;
$this->transformations = $transformations;
$this->dbi = $dbi;
$this->structureController = $structureController;
$this->tableObj = $this->dbi->getTable($this->db, $this->table);
}
public function __invoke(): void
{
$regenerate = $this->updateColumns();
if (! $regenerate) {
// continue to show the table's structure
unset($_POST['selected']);
}
($this->structureController)();
}
/**
* Update the table's structure based on $_REQUEST
*
* @return bool true if error occurred
*/
private function updateColumns(): bool
{
$err_url = Url::getFromRoute('/table/structure', [
'db' => $this->db,
'table' => $this->table,
]);
$regenerate = false;
$field_cnt = count($_POST['field_name'] ?? []);
$changes = [];
$adjust_privileges = [];
$columns_with_index = $this->dbi
->getTable($this->db, $this->table)
->getColumnsWithIndex(Index::PRIMARY | Index::UNIQUE);
for ($i = 0; $i < $field_cnt; $i++) {
if (! $this->columnNeedsAlterTable($i)) {
continue;
}
$changes[] = 'CHANGE ' . Table::generateAlter(
Util::getValueByKey($_POST, 'field_orig.' . $i, ''),
$_POST['field_name'][$i],
$_POST['field_type'][$i],
$_POST['field_length'][$i],
$_POST['field_attribute'][$i],
Util::getValueByKey($_POST, 'field_collation.' . $i, ''),
Util::getValueByKey($_POST, 'field_null.' . $i, 'NO'),
$_POST['field_default_type'][$i],
$_POST['field_default_value'][$i],
Util::getValueByKey($_POST, 'field_extra.' . $i, false),
Util::getValueByKey($_POST, 'field_comments.' . $i, ''),
Util::getValueByKey($_POST, 'field_virtuality.' . $i, ''),
Util::getValueByKey($_POST, 'field_expression.' . $i, ''),
Util::getValueByKey($_POST, 'field_move_to.' . $i, ''),
$columns_with_index
);
// find the remembered sort expression
$sorted_col = $this->tableObj->getUiProp(Table::PROP_SORTED_COLUMN);
// if the old column name is part of the remembered sort expression
if (mb_strpos((string) $sorted_col, Util::backquote($_POST['field_orig'][$i])) !== false) {
// delete the whole remembered sort expression
$this->tableObj->removeUiProp(Table::PROP_SORTED_COLUMN);
}
if (
! isset($_POST['field_adjust_privileges'][$i])
|| empty($_POST['field_adjust_privileges'][$i])
|| $_POST['field_orig'][$i] == $_POST['field_name'][$i]
) {
continue;
}
$adjust_privileges[$_POST['field_orig'][$i]] = $_POST['field_name'][$i];
}
if (count($changes) > 0 || isset($_POST['preview_sql'])) {
// Builds the primary keys statements and updates the table
$key_query = '';
/**
* this is a little bit more complex
*
* @todo if someone selects A_I when altering a column we need to check:
* - no other column with A_I
* - the column has an index, if not create one
*/
// To allow replication, we first select the db to use
// and then run queries on this db.
if (! $this->dbi->selectDb($this->db)) {
Generator::mysqlDie(
$this->dbi->getError(),
'USE ' . Util::backquote($this->db) . ';',
false,
$err_url
);
}
$sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' ';
$sql_query .= implode(', ', $changes) . $key_query;
if (isset($_POST['online_transaction'])) {
$sql_query .= ', ALGORITHM=INPLACE, LOCK=NONE';
}
$sql_query .= ';';
// If there is a request for SQL previewing.
if (isset($_POST['preview_sql'])) {
Core::previewSQL(count($changes) > 0 ? $sql_query : '');
exit;
}
$columns_with_index = $this->dbi
->getTable($this->db, $this->table)
->getColumnsWithIndex(Index::PRIMARY | Index::UNIQUE | Index::INDEX | Index::SPATIAL | Index::FULLTEXT);
$changedToBlob = [];
// While changing the Column Collation
// First change to BLOB
for ($i = 0; $i < $field_cnt; $i++) {
if (
isset($_POST['field_collation'][$i], $_POST['field_collation_orig'][$i])
&& $_POST['field_collation'][$i] !== $_POST['field_collation_orig'][$i]
&& ! in_array($_POST['field_orig'][$i], $columns_with_index)
) {
$secondary_query = 'ALTER TABLE ' . Util::backquote($this->table)
. ' CHANGE ' . Util::backquote($_POST['field_orig'][$i])
. ' ' . Util::backquote($_POST['field_orig'][$i])
. ' BLOB';
if (isset($_POST['field_virtuality'][$i], $_POST['field_expression'][$i])) {
if ($_POST['field_virtuality'][$i]) {
$secondary_query .= ' AS (' . $_POST['field_expression'][$i] . ') '
. $_POST['field_virtuality'][$i];
}
}
$secondary_query .= ';';
$this->dbi->query($secondary_query);
$changedToBlob[$i] = true;
} else {
$changedToBlob[$i] = false;
}
}
// Then make the requested changes
$result = $this->dbi->tryQuery($sql_query);
if ($result !== false) {
$changed_privileges = $this->adjustColumnPrivileges($adjust_privileges);
if ($changed_privileges) {
$message = Message::success(
__(
'Table %1$s has been altered successfully. Privileges have been adjusted.'
)
);
} else {
$message = Message::success(
__('Table %1$s has been altered successfully.')
);
}
$message->addParam($this->table);
$this->response->addHTML(
Generator::getMessage($message, $sql_query, 'success')
);
} else {
// An error happened while inserting/updating a table definition
// Save the Original Error
$orig_error = $this->dbi->getError();
$changes_revert = [];
// Change back to Original Collation and data type
for ($i = 0; $i < $field_cnt; $i++) {
if (! $changedToBlob[$i]) {
continue;
}
$changes_revert[] = 'CHANGE ' . Table::generateAlter(
Util::getValueByKey($_POST, 'field_orig.' . $i, ''),
$_POST['field_name'][$i],
$_POST['field_type_orig'][$i],
$_POST['field_length_orig'][$i],
$_POST['field_attribute_orig'][$i],
Util::getValueByKey($_POST, 'field_collation_orig.' . $i, ''),
Util::getValueByKey($_POST, 'field_null_orig.' . $i, 'NO'),
$_POST['field_default_type_orig'][$i],
$_POST['field_default_value_orig'][$i],
Util::getValueByKey($_POST, 'field_extra_orig.' . $i, false),
Util::getValueByKey($_POST, 'field_comments_orig.' . $i, ''),
Util::getValueByKey($_POST, 'field_virtuality_orig.' . $i, ''),
Util::getValueByKey($_POST, 'field_expression_orig.' . $i, ''),
Util::getValueByKey($_POST, 'field_move_to_orig.' . $i, '')
);
}
$revert_query = 'ALTER TABLE ' . Util::backquote($this->table)
. ' ';
$revert_query .= implode(', ', $changes_revert) . '';
$revert_query .= ';';
// Column reverted back to original
$this->dbi->query($revert_query);
$this->response->setRequestStatus(false);
$message = Message::rawError(
__('Query error') . ':<br>' . $orig_error
);
$this->response->addHTML(
Generator::getMessage($message, $sql_query, 'error')
);
$regenerate = true;
}
}
// update field names in relation
if (isset($_POST['field_orig']) && is_array($_POST['field_orig'])) {
foreach ($_POST['field_orig'] as $fieldindex => $fieldcontent) {
if ($_POST['field_name'][$fieldindex] == $fieldcontent) {
continue;
}
$this->relation->renameField($this->db, $this->table, $fieldcontent, $_POST['field_name'][$fieldindex]);
}
}
// update mime types
if (isset($_POST['field_mimetype']) && is_array($_POST['field_mimetype']) && $GLOBALS['cfg']['BrowseMIME']) {
foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
if (! isset($_POST['field_name'][$fieldindex]) || strlen($_POST['field_name'][$fieldindex]) <= 0) {
continue;
}
$this->transformations->setMime(
$this->db,
$this->table,
$_POST['field_name'][$fieldindex],
$mimetype,
$_POST['field_transformation'][$fieldindex],
$_POST['field_transformation_options'][$fieldindex],
$_POST['field_input_transformation'][$fieldindex],
$_POST['field_input_transformation_options'][$fieldindex]
);
}
}
return $regenerate;
}
/**
* Verifies if some elements of a column have changed
*
* @param int $i column index in the request
*/
private function columnNeedsAlterTable($i): bool
{
// these two fields are checkboxes so might not be part of the
// request; therefore we define them to avoid notices below
if (! isset($_POST['field_null'][$i])) {
$_POST['field_null'][$i] = 'NO';
}
if (! isset($_POST['field_extra'][$i])) {
$_POST['field_extra'][$i] = '';
}
// field_name does not follow the convention (corresponds to field_orig)
if ($_POST['field_name'][$i] != $_POST['field_orig'][$i]) {
return true;
}
$fields = [
'field_attribute',
'field_collation',
'field_comments',
'field_default_value',
'field_default_type',
'field_extra',
'field_length',
'field_null',
'field_type',
];
foreach ($fields as $field) {
if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) {
return true;
}
}
return ! empty($_POST['field_move_to'][$i]);
}
/**
* Adjusts the Privileges for all the columns whose names have changed
*
* @param array $adjust_privileges assoc array of old col names mapped to new
* cols
*/
private function adjustColumnPrivileges(array $adjust_privileges): bool
{
$changed = false;
if (
Util::getValueByKey($GLOBALS, 'col_priv', false)
&& Util::getValueByKey($GLOBALS, 'is_reload_priv', false)
) {
$this->dbi->selectDb('mysql');
// For Column specific privileges
foreach ($adjust_privileges as $oldCol => $newCol) {
$this->dbi->query(
sprintf(
'UPDATE %s SET Column_name = "%s"
WHERE Db = "%s"
AND Table_name = "%s"
AND Column_name = "%s";',
Util::backquote('columns_priv'),
$newCol,
$this->db,
$this->table,
$oldCol
)
);
// i.e. if atleast one column privileges adjusted
$changed = true;
}
if ($changed) {
// Finally FLUSH the new privileges
$this->dbi->query('FLUSH PRIVILEGES;');
}
}
return $changed;
}
}

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
final class SpatialController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $sql_query, $db, $table, $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$i = 1;
$selectedCount = count($selected);
$sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD SPATIAL(';
foreach ($selected as $field) {
$sql_query .= Util::backquote($field);
$sql_query .= $i++ === $selectedCount ? ');' : ', ';
}
$this->dbi->selectDb($db);
$result = $this->dbi->tryQuery($sql_query);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\AbstractController;
use PhpMyAdmin\Controllers\Table\StructureController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
final class UniqueController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/** @var StructureController */
private $structureController;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi,
StructureController $structureController
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
$this->structureController = $structureController;
}
public function __invoke(): void
{
global $sql_query, $db, $table, $message;
$selected = $_POST['selected_fld'] ?? [];
if (empty($selected)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No column selected.'));
return;
}
$i = 1;
$selectedCount = count($selected);
$sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD UNIQUE(';
foreach ($selected as $field) {
$sql_query .= Util::backquote($field);
$sql_query .= $i++ === $selectedCount ? ');' : ', ';
}
$this->dbi->selectDb($db);
$result = $this->dbi->tryQuery($sql_query);
if (! $result) {
$message = Message::error($this->dbi->getError());
}
if (empty($message)) {
$message = Message::success();
}
($this->structureController)();
}
}

View file

@ -0,0 +1,404 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\ConfigStorage\RelationParameters;
use PhpMyAdmin\CreateAddField;
use PhpMyAdmin\Database\CentralColumns;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Engines\Innodb;
use PhpMyAdmin\FlashMessages;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Index;
use PhpMyAdmin\Partitioning\Partition;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\StorageEngine;
use PhpMyAdmin\Table;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tracker;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use stdClass;
use function __;
use function in_array;
use function is_string;
use function str_contains;
/**
* Displays table structure infos like columns, indexes, size, rows
* and allows manipulation of indexes and columns.
*/
class StructureController extends AbstractController
{
/** @var Table The table object */
protected $tableObj;
/** @var CreateAddField */
private $createAddField;
/** @var Relation */
private $relation;
/** @var Transformations */
private $transformations;
/** @var RelationCleanup */
private $relationCleanup;
/** @var DatabaseInterface */
private $dbi;
/** @var FlashMessages */
private $flash;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Relation $relation,
Transformations $transformations,
CreateAddField $createAddField,
RelationCleanup $relationCleanup,
DatabaseInterface $dbi,
FlashMessages $flash
) {
parent::__construct($response, $template, $db, $table);
$this->createAddField = $createAddField;
$this->relation = $relation;
$this->transformations = $transformations;
$this->relationCleanup = $relationCleanup;
$this->dbi = $dbi;
$this->flash = $flash;
$this->tableObj = $this->dbi->getTable($this->db, $this->table);
}
public function __invoke(): void
{
global $reread_info, $showtable, $db, $table, $cfg, $errorUrl;
global $tbl_is_view, $tbl_storage_engine, $tbl_collation, $table_info_num_rows;
$this->dbi->selectDb($this->db);
$reread_info = $this->tableObj->getStatusInfo(null, true);
$showtable = $this->tableObj->getStatusInfo(null, (isset($reread_info) && $reread_info));
if ($this->tableObj->isView()) {
$tbl_is_view = true;
$tbl_storage_engine = __('View');
} else {
$tbl_is_view = false;
$tbl_storage_engine = $this->tableObj->getStorageEngine();
}
$tbl_collation = $this->tableObj->getCollation();
$table_info_num_rows = $this->tableObj->getNumRows();
$pageSettings = new PageSettings('TableStructure');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
$this->addScriptFiles(['table/structure.js', 'indexes.js']);
$relationParameters = $this->relation->getRelationParameters();
Util::checkParameters(['db', 'table']);
$isSystemSchema = Utilities::isSystemSchema($db);
$url_params = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($url_params, '&');
DbTableExists::check();
$primary = Index::getPrimary($this->table, $this->db);
$columns_with_index = $this->dbi
->getTable($this->db, $this->table)
->getColumnsWithIndex(Index::UNIQUE | Index::INDEX | Index::SPATIAL | Index::FULLTEXT);
$columns_with_unique_index = $this->dbi
->getTable($this->db, $this->table)
->getColumnsWithIndex(Index::UNIQUE);
$fields = $this->dbi->getColumns($this->db, $this->table, true);
$this->response->addHTML($this->displayStructure(
$relationParameters,
$columns_with_unique_index,
$primary,
$fields,
$columns_with_index,
$isSystemSchema
));
}
/**
* Displays the table structure ('show table' works correct since 3.23.03)
*
* @param array $columns_with_unique_index Columns with unique index
* @param Index|false $primary_index primary index or false if no one exists
* @param array $fields Fields
* @param array $columns_with_index Columns with index
*
* @return string
*/
protected function displayStructure(
RelationParameters $relationParameters,
array $columns_with_unique_index,
$primary_index,
array $fields,
array $columns_with_index,
bool $isSystemSchema
) {
global $route, $tbl_is_view, $tbl_storage_engine;
// prepare comments
$comments_map = [];
$mime_map = [];
if ($GLOBALS['cfg']['ShowPropertyComments']) {
$comments_map = $this->relation->getComments($this->db, $this->table);
if ($relationParameters->browserTransformationFeature !== null && $GLOBALS['cfg']['BrowseMIME']) {
$mime_map = $this->transformations->getMime($this->db, $this->table, true);
}
}
$centralColumns = new CentralColumns($this->dbi);
$central_list = $centralColumns->getFromTable($this->db, $this->table);
/**
* Displays Space usage and row statistics
*/
// BEGIN - Calc Table Space
// Get valid statistics whatever is the table type
if ($GLOBALS['cfg']['ShowStats']) {
//get table stats in HTML format
$tablestats = $this->getTableStats($isSystemSchema);
//returning the response in JSON format to be used by Ajax
$this->response->addJSON('tableStat', $tablestats);
}
// END - Calc Table Space
// logic removed from Template
$rownum = 0;
$columns_list = [];
$attributes = [];
$displayed_fields = [];
$row_comments = [];
$extracted_columnspecs = [];
$collations = [];
foreach ($fields as &$field) {
++$rownum;
$columns_list[] = $field['Field'];
$extracted_columnspecs[$rownum] = Util::extractColumnSpec($field['Type']);
$attributes[$rownum] = $extracted_columnspecs[$rownum]['attribute'];
if (str_contains($field['Extra'], 'on update CURRENT_TIMESTAMP')) {
$attributes[$rownum] = 'on update CURRENT_TIMESTAMP';
}
$displayed_fields[$rownum] = new stdClass();
$displayed_fields[$rownum]->text = $field['Field'];
$displayed_fields[$rownum]->icon = '';
$row_comments[$rownum] = '';
if (isset($comments_map[$field['Field']])) {
$displayed_fields[$rownum]->comment = $comments_map[$field['Field']];
$row_comments[$rownum] = $comments_map[$field['Field']];
}
if ($primary_index && $primary_index->hasColumn($field['Field'])) {
$displayed_fields[$rownum]->icon .= Generator::getImage('b_primary', __('Primary'));
}
if (in_array($field['Field'], $columns_with_index)) {
$displayed_fields[$rownum]->icon .= Generator::getImage('bd_primary', __('Index'));
}
$collation = Charsets::findCollationByName(
$this->dbi,
$GLOBALS['cfg']['Server']['DisableIS'],
$field['Collation'] ?? ''
);
if ($collation === null) {
continue;
}
$collations[$collation->getName()] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
];
}
$engine = $this->tableObj->getStorageEngine();
return $this->template->render('table/structure/display_structure', [
'collations' => $collations,
'is_foreign_key_supported' => ForeignKey::isSupported($engine),
'indexes' => Index::getFromTable($this->table, $this->db),
'indexes_duplicates' => Index::findDuplicates($this->table, $this->db),
'relation_parameters' => $relationParameters,
'hide_structure_actions' => $GLOBALS['cfg']['HideStructureActions'] === true,
'db' => $this->db,
'table' => $this->table,
'db_is_system_schema' => $isSystemSchema,
'tbl_is_view' => $tbl_is_view,
'mime_map' => $mime_map,
'tbl_storage_engine' => $tbl_storage_engine,
'primary' => $primary_index,
'columns_with_unique_index' => $columns_with_unique_index,
'columns_list' => $columns_list,
'table_stats' => $tablestats ?? null,
'fields' => $fields,
'extracted_columnspecs' => $extracted_columnspecs,
'columns_with_index' => $columns_with_index,
'central_list' => $central_list,
'comments_map' => $comments_map,
'browse_mime' => $GLOBALS['cfg']['BrowseMIME'],
'show_column_comments' => $GLOBALS['cfg']['ShowColumnComments'],
'show_stats' => $GLOBALS['cfg']['ShowStats'],
'mysql_int_version' => $this->dbi->getVersion(),
'is_mariadb' => $this->dbi->isMariaDB(),
'text_dir' => $GLOBALS['text_dir'],
'is_active' => Tracker::isActive(),
'have_partitioning' => Partition::havePartitioning(),
'partitions' => Partition::getPartitions($this->db, $this->table),
'partition_names' => Partition::getPartitionNames($this->db, $this->table),
'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
'attributes' => $attributes,
'displayed_fields' => $displayed_fields,
'row_comments' => $row_comments,
'route' => $route,
]);
}
/**
* Get HTML snippet for display table statistics
*
* @return string
*/
protected function getTableStats(bool $isSystemSchema)
{
global $showtable, $tbl_is_view;
global $tbl_storage_engine, $table_info_num_rows, $tbl_collation;
if (empty($showtable)) {
$showtable = $this->dbi->getTable($this->db, $this->table)->getStatusInfo(null, true);
}
if (is_string($showtable)) {
$showtable = [];
}
if (empty($showtable['Data_length'])) {
$showtable['Data_length'] = 0;
}
if (empty($showtable['Index_length'])) {
$showtable['Index_length'] = 0;
}
$is_innodb = (isset($showtable['Type'])
&& $showtable['Type'] === 'InnoDB');
$mergetable = $this->tableObj->isMerge();
// this is to display for example 261.2 MiB instead of 268k KiB
$max_digits = 3;
$decimals = 1;
[$data_size, $data_unit] = Util::formatByteDown($showtable['Data_length'], $max_digits, $decimals);
if ($mergetable === false) {
[$index_size, $index_unit] = Util::formatByteDown($showtable['Index_length'], $max_digits, $decimals);
}
if (isset($showtable['Data_free'])) {
[$free_size, $free_unit] = Util::formatByteDown($showtable['Data_free'], $max_digits, $decimals);
[$effect_size, $effect_unit] = Util::formatByteDown(
$showtable['Data_length']
+ $showtable['Index_length']
- $showtable['Data_free'],
$max_digits,
$decimals
);
} else {
[$effect_size, $effect_unit] = Util::formatByteDown(
$showtable['Data_length']
+ $showtable['Index_length'],
$max_digits,
$decimals
);
}
[$tot_size, $tot_unit] = Util::formatByteDown(
$showtable['Data_length'] + $showtable['Index_length'],
$max_digits,
$decimals
);
$avg_size = '';
$avg_unit = '';
if ($table_info_num_rows > 0) {
[$avg_size, $avg_unit] = Util::formatByteDown(
($showtable['Data_length']
+ $showtable['Index_length'])
/ $showtable['Rows'],
6,
1
);
}
/** @var Innodb $innodbEnginePlugin */
$innodbEnginePlugin = StorageEngine::getEngine('Innodb');
$innodb_file_per_table = $innodbEnginePlugin->supportsFilePerTable();
$tableCollation = [];
$collation = Charsets::findCollationByName($this->dbi, $GLOBALS['cfg']['Server']['DisableIS'], $tbl_collation);
if ($collation !== null) {
$tableCollation = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
];
}
return $this->template->render('table/structure/display_table_stats', [
'db' => $GLOBALS['db'],
'table' => $GLOBALS['table'],
'showtable' => $showtable,
'table_info_num_rows' => $table_info_num_rows,
'tbl_is_view' => $tbl_is_view,
'db_is_system_schema' => $isSystemSchema,
'tbl_storage_engine' => $tbl_storage_engine,
'table_collation' => $tableCollation,
'is_innodb' => $is_innodb,
'mergetable' => $mergetable,
'avg_size' => $avg_size ?? null,
'avg_unit' => $avg_unit ?? null,
'data_size' => $data_size,
'data_unit' => $data_unit,
'index_size' => $index_size ?? null,
'index_unit' => $index_unit ?? null,
'innodb_file_per_table' => $innodb_file_per_table,
'free_size' => $free_size ?? null,
'free_unit' => $free_unit ?? null,
'effect_size' => $effect_size,
'effect_unit' => $effect_unit,
'tot_size' => $tot_size,
'tot_unit' => $tot_unit,
]);
}
}

View file

@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tracker;
use PhpMyAdmin\Tracking;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
use function array_map;
use function define;
use function explode;
use function htmlspecialchars;
use function sprintf;
use function strtotime;
final class TrackingController extends AbstractController
{
/** @var Tracking */
private $tracking;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Tracking $tracking
) {
parent::__construct($response, $template, $db, $table);
$this->tracking = $tracking;
}
public function __invoke(): void
{
global $text_dir, $urlParams, $msg, $errorUrl;
global $data, $entries, $filter_ts_from, $filter_ts_to, $filter_users, $selection_schema;
global $selection_data, $selection_both, $db, $table, $cfg;
$this->addScriptFiles(['vendor/jquery/jquery.tablesorter.js', 'table/tracking.js']);
define('TABLE_MAY_BE_ABSENT', true);
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$activeMessage = '';
if (
Tracker::isActive()
&& Tracker::isTracked($GLOBALS['db'], $GLOBALS['table'])
&& ! (isset($_POST['toggle_activation'])
&& $_POST['toggle_activation'] === 'deactivate_now')
&& ! (isset($_POST['report_export'])
&& $_POST['export_type'] === 'sqldumpfile')
) {
$msg = Message::notice(
sprintf(
__('Tracking of %s is activated.'),
htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table'])
)
);
$activeMessage = $msg->getDisplay();
}
$urlParams['goto'] = Url::getFromRoute('/table/tracking');
$urlParams['back'] = Url::getFromRoute('/table/tracking');
$data = [];
$entries = [];
$filter_ts_from = null;
$filter_ts_to = null;
$filter_users = [];
$selection_schema = false;
$selection_data = false;
$selection_both = false;
// Init vars for tracking report
if (isset($_POST['report']) || isset($_POST['report_export'])) {
$data = Tracker::getTrackedData($GLOBALS['db'], $GLOBALS['table'], $_POST['version']);
if (! isset($_POST['logtype'])) {
$_POST['logtype'] = 'schema_and_data';
}
if ($_POST['logtype'] === 'schema') {
$selection_schema = true;
} elseif ($_POST['logtype'] === 'data') {
$selection_data = true;
} else {
$selection_both = true;
}
if (! isset($_POST['date_from'])) {
$_POST['date_from'] = $data['date_from'];
}
if (! isset($_POST['date_to'])) {
$_POST['date_to'] = $data['date_to'];
}
if (! isset($_POST['users'])) {
$_POST['users'] = '*';
}
$filter_ts_from = strtotime($_POST['date_from']);
$filter_ts_to = strtotime($_POST['date_to']);
$filter_users = array_map('trim', explode(',', $_POST['users']));
}
// Prepare export
if (isset($_POST['report_export'])) {
$entries = $this->tracking->getEntries($data, (int) $filter_ts_from, (int) $filter_ts_to, $filter_users);
}
// Export as file download
if (isset($_POST['report_export']) && $_POST['export_type'] === 'sqldumpfile') {
$this->tracking->exportAsFileDownload($entries);
}
$actionMessage = '';
if (isset($_POST['submit_mult'])) {
if (! empty($_POST['selected_versions'])) {
if ($_POST['submit_mult'] === 'delete_version') {
foreach ($_POST['selected_versions'] as $version) {
$this->tracking->deleteTrackingVersion($db, $table, $version);
}
$actionMessage = Message::success(
__('Tracking versions deleted successfully.')
)->getDisplay();
}
} else {
$actionMessage = Message::notice(
__('No versions selected.')
)->getDisplay();
}
}
$deleteVersion = '';
if (isset($_POST['submit_delete_version'])) {
$deleteVersion = $this->tracking->deleteTrackingVersion($db, $table, $_POST['version']);
}
$createVersion = '';
if (isset($_POST['submit_create_version'])) {
$createVersion = $this->tracking->createTrackingVersion($db, $table);
}
$deactivateTracking = '';
if (isset($_POST['toggle_activation']) && $_POST['toggle_activation'] === 'deactivate_now') {
$deactivateTracking = $this->tracking->changeTracking($db, $table, 'deactivate');
}
$activateTracking = '';
if (isset($_POST['toggle_activation']) && $_POST['toggle_activation'] === 'activate_now') {
$activateTracking = $this->tracking->changeTracking($db, $table, 'activate');
}
// Export as SQL execution
$message = '';
if (isset($_POST['report_export']) && $_POST['export_type'] === 'execution') {
$this->tracking->exportAsSqlExecution($entries);
$msg = Message::success(__('SQL statements executed.'));
$message = $msg->getDisplay();
}
$sqlDump = '';
if (isset($_POST['report_export']) && $_POST['export_type'] === 'sqldump') {
$sqlDump = $this->tracking->exportAsSqlDump($db, $table, $entries);
}
$schemaSnapshot = '';
if (isset($_POST['snapshot'])) {
$schemaSnapshot = $this->tracking->getHtmlForSchemaSnapshot($urlParams);
}
$trackingReportRows = '';
if (isset($_POST['report']) && (isset($_POST['delete_ddlog']) || isset($_POST['delete_dmlog']))) {
$trackingReportRows = $this->tracking->deleteTrackingReportRows($db, $table, $data);
}
$trackingReport = '';
if (isset($_POST['report']) || isset($_POST['report_export'])) {
$trackingReport = $this->tracking->getHtmlForTrackingReport(
$data,
$urlParams,
$selection_schema,
$selection_data,
$selection_both,
(int) $filter_ts_to,
(int) $filter_ts_from,
$filter_users
);
}
$main = $this->tracking->getHtmlForMainPage($db, $table, $urlParams, $text_dir);
$this->render('table/tracking/index', [
'active_message' => $activeMessage,
'action_message' => $actionMessage,
'delete_version' => $deleteVersion,
'create_version' => $createVersion,
'deactivate_tracking' => $deactivateTracking,
'activate_tracking' => $activateTracking,
'message' => $message,
'sql_dump' => $sqlDump,
'schema_snapshot' => $schemaSnapshot,
'tracking_report_rows' => $trackingReportRows,
'tracking_report' => $trackingReport,
'main' => $main,
]);
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\Database\Triggers;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\ResponseRenderer;
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;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->dbi = $dbi;
}
public function __invoke(): void
{
global $db, $table, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
global $errors, $urlParams, $errorUrl, $cfg;
$this->addScriptFiles(['database/triggers.js']);
if (! $this->response->isAjax()) {
/**
* Displays the header and tabs
*/
if (! empty($table) && in_array($table, $this->dbi->getTables($db))) {
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
} else {
$table = '';
Util::checkParameters(['db']);
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$errorUrl .= 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,467 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Table\Search;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\Gis;
use function array_search;
use function array_values;
use function count;
use function htmlspecialchars;
use function in_array;
use function intval;
use function is_numeric;
use function json_encode;
use function mb_strtolower;
use function md5;
use function preg_match;
use function preg_replace;
use function str_ireplace;
use function str_replace;
use function strncasecmp;
use function strtoupper;
/**
* Handles table zoom search tab.
*
* Display table zoom search form, create SQL queries from form data.
*/
class ZoomSearchController extends AbstractController
{
/** @var Search */
private $search;
/** @var Relation */
private $relation;
/** @var array */
private $columnNames;
/** @var array */
private $columnTypes;
/** @var array */
private $originalColumnTypes;
/** @var array */
private $columnCollations;
/** @var array */
private $columnNullFlags;
/** @var bool Whether a geometry column is present */
private $geomColumnFlag;
/** @var array Foreign keys */
private $foreigners;
/** @var DatabaseInterface */
private $dbi;
public function __construct(
ResponseRenderer $response,
Template $template,
string $db,
string $table,
Search $search,
Relation $relation,
DatabaseInterface $dbi
) {
parent::__construct($response, $template, $db, $table);
$this->search = $search;
$this->relation = $relation;
$this->dbi = $dbi;
$this->columnNames = [];
$this->columnTypes = [];
$this->originalColumnTypes = [];
$this->columnCollations = [];
$this->columnNullFlags = [];
$this->geomColumnFlag = false;
$this->foreigners = [];
$this->loadTableInfo();
}
public function __invoke(): void
{
global $goto, $db, $table, $urlParams, $cfg, $errorUrl;
Util::checkParameters(['db', 'table']);
$urlParams = ['db' => $db, 'table' => $table];
$errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$errorUrl .= Url::getCommon($urlParams, '&');
DbTableExists::check();
$this->addScriptFiles([
'makegrid.js',
'sql.js',
'vendor/jqplot/jquery.jqplot.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',
'table/zoom_plot_jqplot.js',
'table/change.js',
]);
/**
* Handle AJAX request for data row on point select
*/
if (isset($_POST['get_data_row']) && $_POST['get_data_row'] == true) {
$this->getDataRowAction();
return;
}
/**
* Handle AJAX request for changing field information
* (value,collation,operators,field values) in input form
*/
if (isset($_POST['change_tbl_info']) && $_POST['change_tbl_info'] == true) {
$this->changeTableInfoAction();
return;
}
//Set default datalabel if not selected
if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') {
$dataLabel = $this->relation->getDisplayField($this->db, $this->table);
} else {
$dataLabel = $_POST['dataLabel'];
}
// Displays the zoom search form
$this->displaySelectionFormAction($dataLabel);
/**
* Handle the input criteria and generate the query result
* Form for displaying query results
*/
if (
! isset($_POST['zoom_submit'])
|| $_POST['criteriaColumnNames'][0] === 'pma_null'
|| $_POST['criteriaColumnNames'][1] === 'pma_null'
|| $_POST['criteriaColumnNames'][0] == $_POST['criteriaColumnNames'][1]
) {
return;
}
if (! isset($goto)) {
$goto = Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabTable'], 'table');
}
$this->zoomSubmitAction($dataLabel, $goto);
}
/**
* Gets all the columns of a table along with their types, collations
* and whether null or not.
*/
private function loadTableInfo(): void
{
// Gets the list and number of columns
$columns = $this->dbi->getColumns($this->db, $this->table, true);
// Get details about the geometry functions
$geom_types = Gis::getDataTypes();
foreach ($columns as $row) {
// set column name
$this->columnNames[] = $row['Field'];
$type = (string) $row['Type'];
// before any replacement
$this->originalColumnTypes[] = mb_strtolower($type);
// check whether table contains geometric columns
if (in_array($type, $geom_types)) {
$this->geomColumnFlag = true;
}
// reformat mysql query output
if (strncasecmp($type, 'set', 3) == 0 || strncasecmp($type, 'enum', 4) == 0) {
$type = str_replace(',', ', ', $type);
} else {
// strip the "BINARY" attribute, except if we find "BINARY(" because
// this would be a BINARY or VARBINARY column type
if (! preg_match('@BINARY[\(]@i', $type)) {
$type = str_ireplace('BINARY', '', $type);
}
$type = str_ireplace('ZEROFILL', '', $type);
$type = str_ireplace('UNSIGNED', '', $type);
$type = mb_strtolower($type);
}
if (empty($type)) {
$type = '&nbsp;';
}
$this->columnTypes[] = $type;
$this->columnNullFlags[] = $row['Null'];
$this->columnCollations[] = ! empty($row['Collation']) && $row['Collation'] !== 'NULL'
? $row['Collation']
: '';
}
// Retrieve foreign keys
$this->foreigners = $this->relation->getForeigners($this->db, $this->table);
}
/**
* Display selection form action
*
* @param string $dataLabel Data label
*/
public function displaySelectionFormAction($dataLabel = null): void
{
global $goto;
if (! isset($goto)) {
$goto = Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabTable'], 'table');
}
$column_names = $this->columnNames;
$criteria_column_names = $_POST['criteriaColumnNames'] ?? null;
$keys = [];
for ($i = 0; $i < 4; $i++) {
if (! isset($criteria_column_names[$i])) {
continue;
}
if ($criteria_column_names[$i] === 'pma_null') {
continue;
}
$keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names);
}
$this->render('table/zoom_search/index', [
'db' => $this->db,
'table' => $this->table,
'goto' => $goto,
'self' => $this,
'geom_column_flag' => $this->geomColumnFlag,
'column_names' => $column_names,
'data_label' => $dataLabel,
'keys' => $keys,
'criteria_column_names' => $criteria_column_names,
'criteria_column_types' => $_POST['criteriaColumnTypes'] ?? null,
'max_plot_limit' => ! empty($_POST['maxPlotLimit'])
? intval($_POST['maxPlotLimit'])
: intval($GLOBALS['cfg']['maxRowPlotLimit']),
]);
}
/**
* Get data row action
*/
public function getDataRowAction(): void
{
if (! Core::checkSqlQuerySignature($_POST['where_clause'], $_POST['where_clause_sign'])) {
return;
}
$extra_data = [];
$row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.'
. Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
$result = $this->dbi->query($row_info_query . ';');
$fields_meta = $this->dbi->getFieldsMeta($result);
while ($row = $result->fetchAssoc()) {
// for bit fields we need to convert them to printable form
$i = 0;
foreach ($row as $col => $val) {
if ($fields_meta[$i]->isMappedTypeBit) {
$row[$col] = Util::printableBitValue((int) $val, (int) $fields_meta[$i]->length);
}
$i++;
}
$extra_data['row_info'] = $row;
}
$this->response->addJSON($extra_data);
}
/**
* Change table info action
*/
public function changeTableInfoAction(): void
{
$field = $_POST['field'];
if ($field === 'pma_null') {
$this->response->addJSON('field_type', '');
$this->response->addJSON('field_collation', '');
$this->response->addJSON('field_operators', '');
$this->response->addJSON('field_value', '');
return;
}
$key = array_search($field, $this->columnNames);
$search_index = (isset($_POST['it']) && is_numeric($_POST['it'])
? intval($_POST['it']) : 0);
$properties = $this->getColumnProperties($search_index, $key);
$this->response->addJSON(
'field_type',
htmlspecialchars($properties['type'])
);
$this->response->addJSON('field_collation', $properties['collation']);
$this->response->addJSON('field_operators', $properties['func']);
$this->response->addJSON('field_value', $properties['value']);
}
/**
* Zoom submit action
*
* @param string $dataLabel Data label
* @param string $goto Goto
*/
public function zoomSubmitAction($dataLabel, $goto): void
{
//Query generation part
$sql_query = $this->search->buildSqlQuery();
$sql_query .= ' LIMIT ' . $_POST['maxPlotLimit'];
//Query execution part
$result = $this->dbi->query($sql_query . ';');
$fields_meta = $this->dbi->getFieldsMeta($result);
$data = [];
while ($row = $result->fetchAssoc()) {
//Need a row with indexes as 0,1,2 for the getUniqueCondition
// hence using a temporary array
$tmpRow = array_values($row);
//Get unique condition on each row (will be needed for row update)
$uniqueCondition = Util::getUniqueCondition(
count($this->columnNames),
$fields_meta,
$tmpRow,
true
);
//Append it to row array as where_clause
$row['where_clause'] = $uniqueCondition[0];
$row['where_clause_sign'] = Core::signSqlQuery($uniqueCondition[0]);
$tmpData = [
$_POST['criteriaColumnNames'][0] => $row[$_POST['criteriaColumnNames'][0]],
$_POST['criteriaColumnNames'][1] => $row[$_POST['criteriaColumnNames'][1]],
'where_clause' => $uniqueCondition[0],
'where_clause_sign' => Core::signSqlQuery($uniqueCondition[0]),
];
$tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : '';
$data[] = $tmpData;
}
unset($tmpData);
$column_names_hashes = [];
foreach ($this->columnNames as $columnName) {
$column_names_hashes[$columnName] = md5($columnName);
}
$this->render('table/zoom_search/result_form', [
'db' => $this->db,
'table' => $this->table,
'column_names' => $this->columnNames,
'column_names_hashes' => $column_names_hashes,
'foreigners' => $this->foreigners,
'column_null_flags' => $this->columnNullFlags,
'column_types' => $this->columnTypes,
'goto' => $goto,
'data' => $data,
'data_json' => json_encode($data),
'zoom_submit' => isset($_POST['zoom_submit']),
'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
]);
}
/**
* Provides a column's type, collation, operators list, and criteria value
* to display in table search form
*
* @param int $search_index Row number in table search form
* @param int $column_index Column index in ColumnNames array
*
* @return array Array containing column's properties
*/
public function getColumnProperties($search_index, $column_index)
{
$selected_operator = ($_POST['criteriaColumnOperators'][$search_index] ?? '');
$entered_value = ($_POST['criteriaValues'] ?? '');
//Gets column's type and collation
$type = $this->columnTypes[$column_index];
$collation = $this->columnCollations[$column_index];
$cleanType = preg_replace('@\(.*@s', '', $type);
//Gets column's comparison operators depending on column type
$typeOperators = $this->dbi->types->getTypeOperatorsHtml(
$cleanType,
$this->columnNullFlags[$column_index],
$selected_operator
);
$func = $this->template->render('table/search/column_comparison_operators', [
'search_index' => $search_index,
'type_operators' => $typeOperators,
]);
//Gets link to browse foreign data(if any) and criteria inputbox
$foreignData = $this->relation->getForeignData(
$this->foreigners,
$this->columnNames[$column_index],
false,
'',
''
);
$htmlAttributes = '';
if (in_array($cleanType, $this->dbi->types->getIntegerTypes())) {
$extractedColumnspec = Util::extractColumnSpec($this->originalColumnTypes[$column_index]);
$is_unsigned = $extractedColumnspec['unsigned'];
$minMaxValues = $this->dbi->types->getIntegerRange($cleanType, ! $is_unsigned);
$htmlAttributes = 'data-min="' . $minMaxValues[0] . '" '
. 'data-max="' . $minMaxValues[1] . '"';
}
$htmlAttributes .= ' onfocus="return '
. 'verifyAfterSearchFieldChange(' . $search_index . ', \'#zoom_search_form\')"';
$value = $this->template->render('table/search/input_box', [
'str' => '',
'column_type' => (string) $type,
'column_data_type' => strtoupper($cleanType),
'html_attributes' => $htmlAttributes,
'column_id' => 'fieldID_',
'in_zoom_search_edit' => false,
'foreigners' => $this->foreigners,
'column_name' => $this->columnNames[$column_index],
'column_name_hash' => md5($this->columnNames[$column_index]),
'foreign_data' => $foreignData,
'table' => $this->table,
'column_index' => $search_index,
'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
'criteria_values' => $entered_value,
'db' => $this->db,
'in_fbs' => true,
]);
return [
'type' => $type,
'collation' => $collation,
'func' => $func,
'value' => $value,
];
}
}