Update website

This commit is contained in:
Guilhem Lavaux 2024-11-23 20:45:29 +01:00
parent 41ce1aa076
commit ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\ListDatabase;
class DatabaseList
{
/**
* Holds database list
*
* @var ListDatabase
*/
protected $databases = null;
/**
* magic access to protected/inaccessible members/properties
*
* @see https://www.php.net/language.oop5.overloading
*
* @param string $param parameter name
*
* @return mixed
*/
public function __get($param)
{
switch ($param) {
case 'databases':
return $this->getDatabaseList();
}
return null;
}
/**
* Accessor to PMA::$databases
*/
public function getDatabaseList(): ListDatabase
{
if ($this->databases === null) {
$this->databases = new ListDatabase();
}
return $this->databases;
}
}

View file

@ -0,0 +1,412 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Database\Designer\DesignerTable;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use stdClass;
use function __;
use function count;
use function intval;
use function is_array;
use function json_decode;
use function json_encode;
use function str_contains;
/**
* Set of functions related to database designer
*/
class Designer
{
/** @var DatabaseInterface */
private $dbi;
/** @var Relation */
private $relation;
/** @var Template */
public $template;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param Relation $relation Relation instance
* @param Template $template Template instance
*/
public function __construct(DatabaseInterface $dbi, Relation $relation, Template $template)
{
$this->dbi = $dbi;
$this->relation = $relation;
$this->template = $template;
}
/**
* Function to get html for displaying the page edit/delete form
*
* @param string $db database name
* @param string $operation 'edit' or 'delete' depending on the operation
*
* @return string html content
*/
public function getHtmlForEditOrDeletePages($db, $operation)
{
$relationParameters = $this->relation->getRelationParameters();
return $this->template->render('database/designer/edit_delete_pages', [
'db' => $db,
'operation' => $operation,
'pdfwork' => $relationParameters->pdfFeature !== null,
'pages' => $this->getPageIdsAndNames($db),
]);
}
/**
* Function to get html for displaying the page save as form
*
* @param string $db database name
*
* @return string html content
*/
public function getHtmlForPageSaveAs($db)
{
$relationParameters = $this->relation->getRelationParameters();
return $this->template->render('database/designer/page_save_as', [
'db' => $db,
'pdfwork' => $relationParameters->pdfFeature !== null,
'pages' => $this->getPageIdsAndNames($db),
]);
}
/**
* Retrieve IDs and names of schema pages
*
* @param string $db database name
*
* @return array array of schema page id and names
*/
private function getPageIdsAndNames($db)
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return [];
}
$page_query = 'SELECT `page_nr`, `page_descr` FROM '
. Util::backquote($pdfFeature->database) . '.'
. Util::backquote($pdfFeature->pdfPages)
. " WHERE db_name = '" . $this->dbi->escapeString($db) . "'"
. ' ORDER BY `page_descr`';
$page_rs = $this->dbi->tryQueryAsControlUser($page_query);
if (! $page_rs) {
return [];
}
$result = [];
while ($curr_page = $page_rs->fetchAssoc()) {
$result[intval($curr_page['page_nr'])] = $curr_page['page_descr'];
}
return $result;
}
/**
* Function to get html for displaying the schema export
*
* @param string $db database name
* @param int $page the page to be exported
*
* @return string
*/
public function getHtmlForSchemaExport($db, $page)
{
$export_list = Plugins::getSchema();
/* Fail if we didn't find any schema plugin */
if (empty($export_list)) {
return Message::error(
__('Could not load schema plugins, please check your installation!')
)->getDisplay();
}
$default = isset($_GET['export_type'])
? (string) $_GET['export_type']
: Plugins::getDefault('Schema', 'format');
$choice = Plugins::getChoice($export_list, $default);
$options = Plugins::getOptions('Schema', $export_list);
return $this->template->render('database/designer/schema_export', [
'db' => $db,
'page' => $page,
'plugins_choice' => $choice,
'options' => $options,
]);
}
/**
* Returns array of stored values of Designer Settings
*
* @return array stored values
*/
private function getSideMenuParamsArray()
{
global $dbi;
$params = [];
$databaseDesignerSettingsFeature = $this->relation->getRelationParameters()->databaseDesignerSettingsFeature;
if ($databaseDesignerSettingsFeature !== null) {
$query = 'SELECT `settings_data` FROM '
. Util::backquote($databaseDesignerSettingsFeature->database) . '.'
. Util::backquote($databaseDesignerSettingsFeature->designerSettings)
. ' WHERE ' . Util::backquote('username') . ' = "'
. $dbi->escapeString($GLOBALS['cfg']['Server']['user'])
. '";';
$result = $this->dbi->fetchSingleRow($query);
if (is_array($result)) {
$params = json_decode((string) $result['settings_data'], true);
}
}
return $params;
}
/**
* Returns class names for various buttons on Designer Side Menu
*
* @return array class names of various buttons
*/
public function returnClassNamesFromMenuButtons()
{
$classes_array = [];
$params_array = $this->getSideMenuParamsArray();
if (isset($params_array['angular_direct']) && $params_array['angular_direct'] === 'angular') {
$classes_array['angular_direct'] = 'M_butt_Selected_down';
} else {
$classes_array['angular_direct'] = 'M_butt';
}
if (isset($params_array['snap_to_grid']) && $params_array['snap_to_grid'] === 'on') {
$classes_array['snap_to_grid'] = 'M_butt_Selected_down';
} else {
$classes_array['snap_to_grid'] = 'M_butt';
}
if (isset($params_array['pin_text']) && $params_array['pin_text'] === 'true') {
$classes_array['pin_text'] = 'M_butt_Selected_down';
} else {
$classes_array['pin_text'] = 'M_butt';
}
if (isset($params_array['relation_lines']) && $params_array['relation_lines'] === 'false') {
$classes_array['relation_lines'] = 'M_butt_Selected_down';
} else {
$classes_array['relation_lines'] = 'M_butt';
}
if (isset($params_array['small_big_all']) && $params_array['small_big_all'] === 'v') {
$classes_array['small_big_all'] = 'M_butt_Selected_down';
} else {
$classes_array['small_big_all'] = 'M_butt';
}
if (isset($params_array['side_menu']) && $params_array['side_menu'] === 'true') {
$classes_array['side_menu'] = 'M_butt_Selected_down';
} else {
$classes_array['side_menu'] = 'M_butt';
}
return $classes_array;
}
/**
* Get HTML to display tables on designer page
*
* @param string $db The database name from the request
* @param DesignerTable[] $designerTables The designer tables
* @param array $tab_pos tables positions
* @param int $display_page page number of the selected page
* @param array $tab_column table column info
* @param array $tables_all_keys all indices
* @param array $tables_pk_or_unique_keys unique or primary indices
*
* @return string html
*/
public function getDatabaseTables(
string $db,
array $designerTables,
array $tab_pos,
$display_page,
array $tab_column,
array $tables_all_keys,
array $tables_pk_or_unique_keys
) {
global $text_dir;
$columns_type = [];
foreach ($designerTables as $designerTable) {
$table_name = $designerTable->getDbTableString();
$limit = count($tab_column[$table_name]['COLUMN_ID']);
for ($j = 0; $j < $limit; $j++) {
$table_column_name = $table_name . '.' . $tab_column[$table_name]['COLUMN_NAME'][$j];
if (isset($tables_pk_or_unique_keys[$table_column_name])) {
$columns_type[$table_column_name] = 'designer/FieldKey_small';
} else {
$columns_type[$table_column_name] = 'designer/Field_small';
if (
str_contains($tab_column[$table_name]['TYPE'][$j], 'char')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'text')
) {
$columns_type[$table_column_name] .= '_char';
} elseif (
str_contains($tab_column[$table_name]['TYPE'][$j], 'int')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'float')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'double')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'decimal')
) {
$columns_type[$table_column_name] .= '_int';
} elseif (
str_contains($tab_column[$table_name]['TYPE'][$j], 'date')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'time')
|| str_contains($tab_column[$table_name]['TYPE'][$j], 'year')
) {
$columns_type[$table_column_name] .= '_date';
}
}
}
}
return $this->template->render('database/designer/database_tables', [
'db' => $GLOBALS['db'],
'text_dir' => $text_dir,
'get_db' => $db,
'has_query' => isset($_REQUEST['query']),
'tab_pos' => $tab_pos,
'display_page' => $display_page,
'tab_column' => $tab_column,
'tables_all_keys' => $tables_all_keys,
'tables_pk_or_unique_keys' => $tables_pk_or_unique_keys,
'tables' => $designerTables,
'columns_type' => $columns_type,
]);
}
/**
* Returns HTML for Designer page
*
* @param string $db database in use
* @param string $getDb database in url
* @param DesignerTable[] $designerTables The designer tables
* @param array $scriptTables array on foreign key support for each table
* @param array $scriptContr initialization data array
* @param DesignerTable[] $scriptDisplayField displayed tables in designer with their display fields
* @param int $displayPage page number of the selected page
* @param bool $visualBuilderMode whether this is visual query builder
* @param string $selectedPage name of the selected page
* @param array $paramsArray array with class name for various buttons on side menu
* @param array|null $tabPos table positions
* @param array $tabColumn table column info
* @param array $tablesAllKeys all indices
* @param array $tablesPkOrUniqueKeys unique or primary indices
*
* @return string html
*/
public function getHtmlForMain(
string $db,
string $getDb,
array $designerTables,
array $scriptTables,
array $scriptContr,
array $scriptDisplayField,
$displayPage,
bool $visualBuilderMode,
$selectedPage,
array $paramsArray,
?array $tabPos,
array $tabColumn,
array $tablesAllKeys,
array $tablesPkOrUniqueKeys
): string {
global $text_dir;
$relationParameters = $this->relation->getRelationParameters();
$columnsType = [];
foreach ($designerTables as $designerTable) {
$tableName = $designerTable->getDbTableString();
$limit = count($tabColumn[$tableName]['COLUMN_ID']);
for ($j = 0; $j < $limit; $j++) {
$tableColumnName = $tableName . '.' . $tabColumn[$tableName]['COLUMN_NAME'][$j];
if (isset($tablesPkOrUniqueKeys[$tableColumnName])) {
$columnsType[$tableColumnName] = 'designer/FieldKey_small';
} else {
$columnsType[$tableColumnName] = 'designer/Field_small';
if (
str_contains($tabColumn[$tableName]['TYPE'][$j], 'char')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'text')
) {
$columnsType[$tableColumnName] .= '_char';
} elseif (
str_contains($tabColumn[$tableName]['TYPE'][$j], 'int')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'float')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'double')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'decimal')
) {
$columnsType[$tableColumnName] .= '_int';
} elseif (
str_contains($tabColumn[$tableName]['TYPE'][$j], 'date')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'time')
|| str_contains($tabColumn[$tableName]['TYPE'][$j], 'year')
) {
$columnsType[$tableColumnName] .= '_date';
}
}
}
}
$displayedFields = [];
foreach ($scriptDisplayField as $designerTable) {
if ($designerTable->getDisplayField() === null) {
continue;
}
$displayedFields[$designerTable->getTableName()] = $designerTable->getDisplayField();
}
$designerConfig = new stdClass();
$designerConfig->db = $db;
$designerConfig->scriptTables = $scriptTables;
$designerConfig->scriptContr = $scriptContr;
$designerConfig->server = $GLOBALS['server'];
$designerConfig->scriptDisplayField = $displayedFields;
$designerConfig->displayPage = (int) $displayPage;
$designerConfig->tablesEnabled = $relationParameters->pdfFeature !== null;
return $this->template->render('database/designer/main', [
'db' => $db,
'text_dir' => $text_dir,
'get_db' => $getDb,
'designer_config' => json_encode($designerConfig),
'display_page' => (int) $displayPage,
'has_query' => $visualBuilderMode,
'visual_builder' => $visualBuilderMode,
'selected_page' => $selectedPage,
'params_array' => $paramsArray,
'tab_pos' => $tabPos,
'tab_column' => $tabColumn,
'tables_all_keys' => $tablesAllKeys,
'tables_pk_or_unique_keys' => $tablesPkOrUniqueKeys,
'designerTables' => $designerTables,
'columns_type' => $columnsType,
]);
}
}

View file

@ -0,0 +1,816 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database\Designer;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Index;
use PhpMyAdmin\Query\Generator as QueryGenerator;
use PhpMyAdmin\Table;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\ForeignKey;
use function __;
use function _pgettext;
use function array_keys;
use function count;
use function explode;
use function in_array;
use function intval;
use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use function mb_strtoupper;
use function rawurlencode;
/**
* Common functions for Designer
*/
class Common
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param Relation $relation Relation instance
*/
public function __construct(DatabaseInterface $dbi, Relation $relation)
{
$this->dbi = $dbi;
$this->relation = $relation;
}
/**
* Retrieves table info and returns it
*
* @param string $db (optional) Filter only a DB ($table is required if you use $db)
* @param string $table (optional) Filter only a table ($db is now required)
*
* @return DesignerTable[] with table info
*/
public function getTablesInfo(?string $db = null, ?string $table = null): array
{
$designerTables = [];
$db = $db ?? $GLOBALS['db'];
// seems to be needed later
$this->dbi->selectDb($db);
if ($table === null) {
$tables = $this->dbi->getTablesFull($db);
} else {
$tables = $this->dbi->getTablesFull($db, $table);
}
foreach ($tables as $one_table) {
$DF = $this->relation->getDisplayField($db, $one_table['TABLE_NAME']);
$DF = is_string($DF) ? $DF : '';
$DF = $DF !== '' ? $DF : null;
$designerTables[] = new DesignerTable(
$db,
$one_table['TABLE_NAME'],
is_string($one_table['ENGINE']) ? $one_table['ENGINE'] : '',
$DF
);
}
return $designerTables;
}
/**
* Retrieves table column info
*
* @param DesignerTable[] $designerTables The designer tables
*
* @return array table column nfo
*/
public function getColumnsInfo(array $designerTables): array
{
//$this->dbi->selectDb($GLOBALS['db']);
$tabColumn = [];
foreach ($designerTables as $designerTable) {
$fieldsRs = $this->dbi->query(
QueryGenerator::getColumnsSql(
$designerTable->getDatabaseName(),
$designerTable->getTableName()
)
);
$j = 0;
while ($row = $fieldsRs->fetchAssoc()) {
if (! isset($tabColumn[$designerTable->getDbTableString()])) {
$tabColumn[$designerTable->getDbTableString()] = [];
}
$tabColumn[$designerTable->getDbTableString()]['COLUMN_ID'][$j] = $j;
$tabColumn[$designerTable->getDbTableString()]['COLUMN_NAME'][$j] = $row['Field'];
$tabColumn[$designerTable->getDbTableString()]['TYPE'][$j] = $row['Type'];
$tabColumn[$designerTable->getDbTableString()]['NULLABLE'][$j] = $row['Null'];
$j++;
}
}
return $tabColumn;
}
/**
* Returns JavaScript code for initializing vars
*
* @param DesignerTable[] $designerTables The designer tables
*
* @return array JavaScript code
*/
public function getScriptContr(array $designerTables): array
{
$this->dbi->selectDb($GLOBALS['db']);
$con = [];
$con['C_NAME'] = [];
$i = 0;
$alltab_rs = $this->dbi->query('SHOW TABLES FROM ' . Util::backquote($GLOBALS['db']));
while ($val = $alltab_rs->fetchRow()) {
$val = (string) $val[0];
$row = $this->relation->getForeigners($GLOBALS['db'], $val, '', 'internal');
foreach ($row as $field => $value) {
$con['C_NAME'][$i] = '';
$con['DTN'][$i] = rawurlencode($GLOBALS['db'] . '.' . $val);
$con['DCN'][$i] = rawurlencode((string) $field);
$con['STN'][$i] = rawurlencode($value['foreign_db'] . '.' . $value['foreign_table']);
$con['SCN'][$i] = rawurlencode($value['foreign_field']);
$i++;
}
$row = $this->relation->getForeigners($GLOBALS['db'], $val, '', 'foreign');
// We do not have access to the foreign keys if the user has partial access to the columns
if (! isset($row['foreign_keys_data'])) {
continue;
}
foreach ($row['foreign_keys_data'] as $one_key) {
foreach ($one_key['index_list'] as $index => $one_field) {
$con['C_NAME'][$i] = rawurlencode($one_key['constraint']);
$con['DTN'][$i] = rawurlencode($GLOBALS['db'] . '.' . $val);
$con['DCN'][$i] = rawurlencode($one_field);
$con['STN'][$i] = rawurlencode(
($one_key['ref_db_name'] ?? $GLOBALS['db'])
. '.' . $one_key['ref_table_name']
);
$con['SCN'][$i] = rawurlencode($one_key['ref_index_list'][$index]);
$i++;
}
}
}
$tableDbNames = [];
foreach ($designerTables as $designerTable) {
$tableDbNames[] = rawurlencode($designerTable->getDbTableString());
}
$ti = 0;
$retval = [];
for ($i = 0, $cnt = count($con['C_NAME']); $i < $cnt; $i++) {
$c_name_i = $con['C_NAME'][$i];
$dtn_i = $con['DTN'][$i];
$retval[$ti] = [];
$retval[$ti][$c_name_i] = [];
if (in_array($dtn_i, $tableDbNames) && in_array($con['STN'][$i], $tableDbNames)) {
$retval[$ti][$c_name_i][$dtn_i] = [];
$retval[$ti][$c_name_i][$dtn_i][$con['DCN'][$i]] = [
0 => $con['STN'][$i],
1 => $con['SCN'][$i],
];
}
$ti++;
}
return $retval;
}
/**
* Returns UNIQUE and PRIMARY indices
*
* @param DesignerTable[] $designerTables The designer tables
*
* @return array unique or primary indices
*/
public function getPkOrUniqueKeys(array $designerTables): array
{
return $this->getAllKeys($designerTables, true);
}
/**
* Returns all indices
*
* @param DesignerTable[] $designerTables The designer tables
* @param bool $unique_only whether to include only unique ones
*
* @return array indices
*/
public function getAllKeys(array $designerTables, bool $unique_only = false): array
{
$keys = [];
foreach ($designerTables as $designerTable) {
$schema = $designerTable->getDatabaseName();
// for now, take into account only the first index segment
foreach (Index::getFromTable($designerTable->getTableName(), $schema) as $index) {
if ($unique_only && ! $index->isUnique()) {
continue;
}
$columns = $index->getColumns();
foreach (array_keys($columns) as $column_name) {
$keys[$schema . '.' . $designerTable->getTableName() . '.' . $column_name] = 1;
}
}
}
return $keys;
}
/**
* Return j_tab and h_tab arrays
*
* @param DesignerTable[] $designerTables The designer tables
*
* @return array
*/
public function getScriptTabs(array $designerTables): array
{
$retval = [
'j_tabs' => [],
'h_tabs' => [],
];
foreach ($designerTables as $designerTable) {
$key = rawurlencode($designerTable->getDbTableString());
$retval['j_tabs'][$key] = $designerTable->supportsForeignkeys() ? 1 : 0;
$retval['h_tabs'][$key] = 1;
}
return $retval;
}
/**
* Returns table positions of a given pdf page
*
* @param int $pg pdf page id
*
* @return array|null of table positions
*/
public function getTablePositions($pg): ?array
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return [];
}
$query = "
SELECT CONCAT_WS('.', `db_name`, `table_name`) AS `name`,
`db_name` as `dbName`, `table_name` as `tableName`,
`x` AS `X`,
`y` AS `Y`,
1 AS `V`,
1 AS `H`
FROM " . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->tableCoords) . '
WHERE pdf_page_number = ' . intval($pg);
return $this->dbi->fetchResult(
$query,
'name',
null,
DatabaseInterface::CONNECT_CONTROL
);
}
/**
* Returns page name of a given pdf page
*
* @param int $pg pdf page id
*
* @return string|null table name
*/
public function getPageName($pg)
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return null;
}
$query = 'SELECT `page_descr`'
. ' FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->pdfPages)
. ' WHERE ' . Util::backquote('page_nr') . ' = ' . intval($pg);
$page_name = $this->dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL
);
return $page_name[0] ?? null;
}
/**
* Deletes a given pdf page and its corresponding coordinates
*
* @param int $pg page id
*/
public function deletePage($pg): bool
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return false;
}
$query = 'DELETE FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->tableCoords)
. ' WHERE ' . Util::backquote('pdf_page_number') . ' = ' . intval($pg);
$this->dbi->queryAsControlUser($query);
$query = 'DELETE FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->pdfPages)
. ' WHERE ' . Util::backquote('page_nr') . ' = ' . intval($pg);
$this->dbi->queryAsControlUser($query);
return true;
}
/**
* Returns the id of the default pdf page of the database.
* Default page is the one which has the same name as the database.
*
* @param string $db database
*
* @return int|null id of the default pdf page for the database
*/
public function getDefaultPage($db): ?int
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return -1;
}
$query = 'SELECT `page_nr`'
. ' FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->pdfPages)
. " WHERE `db_name` = '" . $this->dbi->escapeString($db) . "'"
. " AND `page_descr` = '" . $this->dbi->escapeString($db) . "'";
$default_page_no = $this->dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL
);
if (isset($default_page_no[0])) {
return intval($default_page_no[0]);
}
return -1;
}
/**
* Get the status if the page already exists
* If no such exists, returns negative index.
*
* @param string $pg name
*/
public function getPageExists(string $pg): bool
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return false;
}
$query = 'SELECT `page_nr`'
. ' FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->pdfPages)
. " WHERE `page_descr` = '" . $this->dbi->escapeString($pg) . "'";
$pageNos = $this->dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL
);
return count($pageNos) > 0;
}
/**
* Get the id of the page to load. If a default page exists it will be returned.
* If no such exists, returns the id of the first page of the database.
*
* @param string $db database
*
* @return int id of the page to load
*/
public function getLoadingPage($db)
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return -1;
}
$default_page_no = $this->getDefaultPage($db);
if ($default_page_no != -1) {
return intval($default_page_no);
}
$query = 'SELECT MIN(`page_nr`)'
. ' FROM ' . Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->pdfPages)
. " WHERE `db_name` = '" . $this->dbi->escapeString($db) . "'";
$min_page_no = $this->dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL
);
$page_no = $min_page_no[0] ?? -1;
return intval($page_no);
}
/**
* Creates a new page and returns its auto-incrementing id
*
* @param string $pageName name of the page
* @param string $db name of the database
*
* @return int|null
*/
public function createNewPage($pageName, $db)
{
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return null;
}
return $this->relation->createPage($pageName, $pdfFeature, $db);
}
/**
* Saves positions of table(s) of a given pdf page
*
* @param int $pg pdf page id
*/
public function saveTablePositions($pg): bool
{
$pageId = $this->dbi->escapeString((string) $pg);
$pdfFeature = $this->relation->getRelationParameters()->pdfFeature;
if ($pdfFeature === null) {
return false;
}
$query = 'DELETE FROM '
. Util::backquote($pdfFeature->database)
. '.' . Util::backquote($pdfFeature->tableCoords)
. " WHERE `pdf_page_number` = '" . $pageId . "'";
$this->dbi->queryAsControlUser($query);
foreach ($_POST['t_h'] as $key => $value) {
$DB = $_POST['t_db'][$key];
$TAB = $_POST['t_tbl'][$key];
if (! $value) {
continue;
}
$query = 'INSERT INTO '
. Util::backquote($pdfFeature->database) . '.'
. Util::backquote($pdfFeature->tableCoords)
. ' (`db_name`, `table_name`, `pdf_page_number`, `x`, `y`)'
. ' VALUES ('
. "'" . $this->dbi->escapeString($DB) . "', "
. "'" . $this->dbi->escapeString($TAB) . "', "
. "'" . $pageId . "', "
. "'" . $this->dbi->escapeString($_POST['t_x'][$key]) . "', "
. "'" . $this->dbi->escapeString($_POST['t_y'][$key]) . "')";
$this->dbi->queryAsControlUser($query);
}
return true;
}
/**
* Saves the display field for a table.
*
* @param string $db database name
* @param string $table table name
* @param string $field display field name
*
* @return array<int,string|bool|null>
* @psalm-return array{0: bool, 1: string|null}
*/
public function saveDisplayField($db, $table, $field): array
{
$displayFeature = $this->relation->getRelationParameters()->displayFeature;
if ($displayFeature === null) {
return [
false,
_pgettext(
'phpMyAdmin configuration storage is not configured for'
. ' "Display Features" on designer when user tries to set a display field.',
'phpMyAdmin configuration storage is not configured for "Display Features".'
),
];
}
$upd_query = new Table($table, $db, $this->dbi);
$upd_query->updateDisplayField($field, $displayFeature);
return [
true,
null,
];
}
/**
* Adds a new foreign relation
*
* @param string $db database name
* @param string $T1 foreign table
* @param string $F1 foreign field
* @param string $T2 master table
* @param string $F2 master field
* @param string $on_delete on delete action
* @param string $on_update on update action
* @param string $DB1 database
* @param string $DB2 database
*
* @return array<int,string|bool> array of success/failure and message
* @psalm-return array{0: bool, 1: string}
*/
public function addNewRelation($db, $T1, $F1, $T2, $F2, $on_delete, $on_update, $DB1, $DB2): array
{
$tables = $this->dbi->getTablesFull($DB1, $T1);
$type_T1 = mb_strtoupper($tables[$T1]['ENGINE'] ?? '');
$tables = $this->dbi->getTablesFull($DB2, $T2);
$type_T2 = mb_strtoupper($tables[$T2]['ENGINE'] ?? '');
// native foreign key
if (ForeignKey::isSupported($type_T1) && ForeignKey::isSupported($type_T2) && $type_T1 == $type_T2) {
// relation exists?
$existrel_foreign = $this->relation->getForeigners($DB2, $T2, '', 'foreign');
$foreigner = $this->relation->searchColumnInForeigners($existrel_foreign, $F2);
if ($foreigner && isset($foreigner['constraint'])) {
return [
false,
__('Error: relationship already exists.'),
];
}
// note: in InnoDB, the index does not requires to be on a PRIMARY
// or UNIQUE key
// improve: check all other requirements for InnoDB relations
$result = $this->dbi->query(
'SHOW INDEX FROM ' . Util::backquote($DB1)
. '.' . Util::backquote($T1) . ';'
);
// will be use to emphasis prim. keys in the table view
$index_array1 = [];
while ($row = $result->fetchAssoc()) {
$index_array1[$row['Column_name']] = 1;
}
$result = $this->dbi->query(
'SHOW INDEX FROM ' . Util::backquote($DB2)
. '.' . Util::backquote($T2) . ';'
);
// will be used to emphasis prim. keys in the table view
$index_array2 = [];
while ($row = $result->fetchAssoc()) {
$index_array2[$row['Column_name']] = 1;
}
unset($result);
if (! empty($index_array1[$F1]) && ! empty($index_array2[$F2])) {
$upd_query = 'ALTER TABLE ' . Util::backquote($DB2)
. '.' . Util::backquote($T2)
. ' ADD FOREIGN KEY ('
. Util::backquote($F2) . ')'
. ' REFERENCES '
. Util::backquote($DB1) . '.'
. Util::backquote($T1) . '('
. Util::backquote($F1) . ')';
if ($on_delete !== 'nix') {
$upd_query .= ' ON DELETE ' . $on_delete;
}
if ($on_update !== 'nix') {
$upd_query .= ' ON UPDATE ' . $on_update;
}
$upd_query .= ';';
if ($this->dbi->tryQuery($upd_query)) {
return [
true,
__('FOREIGN KEY relationship has been added.'),
];
}
$error = $this->dbi->getError();
return [
false,
__('Error: FOREIGN KEY relationship could not be added!')
. '<br>' . $error,
];
}
return [
false,
__('Error: Missing index on column(s).'),
];
}
$relationFeature = $this->relation->getRelationParameters()->relationFeature;
if ($relationFeature === null) {
return [
false,
__('Error: Relational features are disabled!'),
];
}
// no need to recheck if the keys are primary or unique at this point,
// this was checked on the interface part
$q = 'INSERT INTO '
. Util::backquote($relationFeature->database)
. '.'
. Util::backquote($relationFeature->relation)
. '(master_db, master_table, master_field, '
. 'foreign_db, foreign_table, foreign_field)'
. ' values('
. "'" . $this->dbi->escapeString($DB2) . "', "
. "'" . $this->dbi->escapeString($T2) . "', "
. "'" . $this->dbi->escapeString($F2) . "', "
. "'" . $this->dbi->escapeString($DB1) . "', "
. "'" . $this->dbi->escapeString($T1) . "', "
. "'" . $this->dbi->escapeString($F1) . "')";
if ($this->dbi->tryQueryAsControlUser($q)) {
return [
true,
__('Internal relationship has been added.'),
];
}
$error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL);
return [
false,
__('Error: Internal relationship could not be added!')
. '<br>' . $error,
];
}
/**
* Removes a foreign relation
*
* @param string $T1 foreign db.table
* @param string $F1 foreign field
* @param string $T2 master db.table
* @param string $F2 master field
*
* @return array array of success/failure and message
*/
public function removeRelation($T1, $F1, $T2, $F2)
{
[$DB1, $T1] = explode('.', $T1);
[$DB2, $T2] = explode('.', $T2);
$tables = $this->dbi->getTablesFull($DB1, $T1);
$type_T1 = mb_strtoupper($tables[$T1]['ENGINE']);
$tables = $this->dbi->getTablesFull($DB2, $T2);
$type_T2 = mb_strtoupper($tables[$T2]['ENGINE']);
if (ForeignKey::isSupported($type_T1) && ForeignKey::isSupported($type_T2) && $type_T1 == $type_T2) {
// InnoDB
$existrel_foreign = $this->relation->getForeigners($DB2, $T2, '', 'foreign');
$foreigner = $this->relation->searchColumnInForeigners($existrel_foreign, $F2);
if (is_array($foreigner) && isset($foreigner['constraint'])) {
$upd_query = 'ALTER TABLE ' . Util::backquote($DB2)
. '.' . Util::backquote($T2) . ' DROP FOREIGN KEY '
. Util::backquote($foreigner['constraint']) . ';';
$this->dbi->query($upd_query);
return [
true,
__('FOREIGN KEY relationship has been removed.'),
];
}
}
$relationFeature = $this->relation->getRelationParameters()->relationFeature;
if ($relationFeature === null) {
return [
false,
__('Error: Relational features are disabled!'),
];
}
// internal relations
$delete_query = 'DELETE FROM '
. Util::backquote($relationFeature->database) . '.'
. Util::backquote($relationFeature->relation) . ' WHERE '
. "master_db = '" . $this->dbi->escapeString($DB2) . "'"
. " AND master_table = '" . $this->dbi->escapeString($T2) . "'"
. " AND master_field = '" . $this->dbi->escapeString($F2) . "'"
. " AND foreign_db = '" . $this->dbi->escapeString($DB1) . "'"
. " AND foreign_table = '" . $this->dbi->escapeString($T1) . "'"
. " AND foreign_field = '" . $this->dbi->escapeString($F1) . "'";
$result = $this->dbi->tryQueryAsControlUser($delete_query);
if (! $result) {
$error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL);
return [
false,
__('Error: Internal relationship could not be removed!') . '<br>' . $error,
];
}
return [
true,
__('Internal relationship has been removed.'),
];
}
/**
* Save value for a designer setting
*
* @param string $index setting
* @param string $value value
*/
public function saveSetting($index, $value): bool
{
$databaseDesignerSettingsFeature = $this->relation->getRelationParameters()->databaseDesignerSettingsFeature;
if ($databaseDesignerSettingsFeature !== null) {
$cfgDesigner = [
'user' => $GLOBALS['cfg']['Server']['user'],
'db' => $databaseDesignerSettingsFeature->database->getName(),
'table' => $databaseDesignerSettingsFeature->designerSettings->getName(),
];
$orig_data_query = 'SELECT settings_data'
. ' FROM ' . Util::backquote($cfgDesigner['db'])
. '.' . Util::backquote($cfgDesigner['table'])
. " WHERE username = '"
. $this->dbi->escapeString($cfgDesigner['user']) . "';";
$orig_data = $this->dbi->fetchSingleRow(
$orig_data_query,
DatabaseInterface::FETCH_ASSOC,
DatabaseInterface::CONNECT_CONTROL
);
if (! empty($orig_data)) {
$orig_data = json_decode($orig_data['settings_data'], true);
$orig_data[$index] = $value;
$orig_data = json_encode($orig_data);
$save_query = 'UPDATE '
. Util::backquote($cfgDesigner['db'])
. '.' . Util::backquote($cfgDesigner['table'])
. " SET settings_data = '" . $orig_data . "'"
. " WHERE username = '"
. $this->dbi->escapeString($cfgDesigner['user']) . "';";
$this->dbi->queryAsControlUser($save_query);
} else {
$save_data = [$index => $value];
$query = 'INSERT INTO '
. Util::backquote($cfgDesigner['db'])
. '.' . Util::backquote($cfgDesigner['table'])
. ' (username, settings_data)'
. " VALUES('" . $this->dbi->escapeString($cfgDesigner['user'])
. "', '" . json_encode($save_data) . "');";
$this->dbi->queryAsControlUser($query);
}
}
return true;
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database\Designer;
use PhpMyAdmin\Utils\ForeignKey;
/**
* Common functions for Designer
*/
class DesignerTable
{
/** @var string */
private $tableName;
/** @var string */
private $databaseName;
/** @var string */
private $tableEngine;
/** @var string|null */
private $displayField;
/**
* Create a new DesignerTable
*
* @param string $databaseName The database name
* @param string $tableName The table name
* @param string $tableEngine The table engine
* @param string|null $displayField The display field if available
*/
public function __construct(
string $databaseName,
string $tableName,
string $tableEngine,
?string $displayField
) {
$this->databaseName = $databaseName;
$this->tableName = $tableName;
$this->tableEngine = $tableEngine;
$this->displayField = $displayField;
}
/**
* The table engine supports or not foreign keys
*/
public function supportsForeignkeys(): bool
{
return ForeignKey::isSupported($this->tableEngine);
}
/**
* Get the database name
*/
public function getDatabaseName(): string
{
return $this->databaseName;
}
/**
* Get the table name
*/
public function getTableName(): string
{
return $this->tableName;
}
/**
* Get the table engine
*/
public function getTableEngine(): string
{
return $this->tableEngine;
}
/**
* Get the displayed field
*/
public function getDisplayField(): ?string
{
return $this->displayField;
}
/**
* Get the db and table separated with a dot
*/
public function getDbTableString(): string
{
return $this->databaseName . '.' . $this->tableName;
}
}

View file

@ -0,0 +1,604 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
use function explode;
use function htmlspecialchars;
use function in_array;
use function intval;
use function mb_strtoupper;
use function sprintf;
use function str_contains;
use function strtoupper;
use function trim;
/**
* Functions for event management.
*/
class Events
{
/** @var array<string, array<int, string>> */
private $status = [
'query' => ['ENABLE', 'DISABLE', 'DISABLE ON SLAVE'],
'display' => ['ENABLED', 'DISABLED', 'SLAVESIDE_DISABLED'],
];
/** @var array<int, string> */
private $type = ['RECURRING', 'ONE TIME'];
/** @var array<int, string> */
private $interval = [
'YEAR',
'QUARTER',
'MONTH',
'DAY',
'HOUR',
'MINUTE',
'WEEK',
'SECOND',
'YEAR_MONTH',
'DAY_HOUR',
'DAY_MINUTE',
'DAY_SECOND',
'HOUR_MINUTE',
'HOUR_SECOND',
'MINUTE_SECOND',
];
/** @var DatabaseInterface */
private $dbi;
/** @var Template */
private $template;
/** @var ResponseRenderer */
private $response;
/**
* @param DatabaseInterface $dbi DatabaseInterface instance.
* @param Template $template Template instance.
* @param ResponseRenderer $response Response instance.
*/
public function __construct(DatabaseInterface $dbi, Template $template, $response)
{
$this->dbi = $dbi;
$this->template = $template;
$this->response = $response;
}
/**
* Handles editor requests for adding or editing an item
*/
public function handleEditor(): void
{
global $db, $table, $errors, $message;
if (! empty($_POST['editor_process_add']) || ! empty($_POST['editor_process_edit'])) {
$sql_query = '';
$item_query = $this->getQueryFromRequest();
// set by getQueryFromRequest()
if (! count($errors)) {
// Execute the created query
if (! empty($_POST['editor_process_edit'])) {
// Backup the old trigger, in case something goes wrong
$create_item = $this->dbi->getDefinition($db, 'EVENT', $_POST['item_original_name']);
$drop_item = 'DROP EVENT IF EXISTS '
. Util::backquote($_POST['item_original_name'])
. ";\n";
$result = $this->dbi->tryQuery($drop_item);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($drop_item)
)
. '<br>'
. __('MySQL said: ') . $this->dbi->getError();
} else {
$result = $this->dbi->tryQuery($item_query);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($item_query)
)
. '<br>'
. __('MySQL said: ') . $this->dbi->getError();
// We dropped the old item, but were unable to create
// the new one. Try to restore the backup query
$result = $this->dbi->tryQuery($create_item);
if (! $result) {
$errors = $this->checkResult($create_item, $errors);
}
} else {
$message = Message::success(
__('Event %1$s has been modified.')
);
$message->addParam(
Util::backquote($_POST['item_name'])
);
$sql_query = $drop_item . $item_query;
}
}
} else {
// 'Add a new item' mode
$result = $this->dbi->tryQuery($item_query);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($item_query)
)
. '<br><br>'
. __('MySQL said: ') . $this->dbi->getError();
} else {
$message = Message::success(
__('Event %1$s has been created.')
);
$message->addParam(
Util::backquote($_POST['item_name'])
);
$sql_query = $item_query;
}
}
}
if (count($errors)) {
$message = Message::error(
'<b>'
. __(
'One or more errors have occurred while processing your request:'
)
. '</b>'
);
$message->addHtml('<ul>');
foreach ($errors as $string) {
$message->addHtml('<li>' . $string . '</li>');
}
$message->addHtml('</ul>');
}
$output = Generator::getMessage($message, $sql_query);
if ($this->response->isAjax()) {
if ($message->isSuccess()) {
$events = $this->dbi->getEvents($db, $_POST['item_name']);
$event = $events[0];
$this->response->addJSON(
'name',
htmlspecialchars(
mb_strtoupper($_POST['item_name'])
)
);
if (! empty($event)) {
$sqlDrop = sprintf(
'DROP EVENT IF EXISTS %s',
Util::backquote($event['name'])
);
$this->response->addJSON(
'new_row',
$this->template->render('database/events/row', [
'db' => $db,
'table' => $table,
'event' => $event,
'has_privilege' => Util::currentUserHasPrivilege('EVENT', $db),
'sql_drop' => $sqlDrop,
'row_class' => '',
])
);
}
$this->response->addJSON('insert', ! empty($event));
$this->response->addJSON('message', $output);
} else {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
}
$this->response->addJSON('tableType', 'events');
exit;
}
}
/**
* Display a form used to add/edit a trigger, if necessary
*/
if (
! count($errors)
&& (! empty($_POST['editor_process_add'])
|| ! empty($_POST['editor_process_edit'])
|| (empty($_REQUEST['add_item'])
&& empty($_REQUEST['edit_item'])
&& empty($_POST['item_changetype'])))
) {
return;
}
// FIXME: this must be simpler than that
$operation = '';
$title = '';
$item = null;
$mode = '';
if (! empty($_POST['item_changetype'])) {
$operation = 'change';
}
// Get the data for the form (if any)
if (! empty($_REQUEST['add_item'])) {
$title = __('Add event');
$item = $this->getDataFromRequest();
$mode = 'add';
} elseif (! empty($_REQUEST['edit_item'])) {
$title = __('Edit event');
if (
! empty($_REQUEST['item_name'])
&& empty($_POST['editor_process_edit'])
&& empty($_POST['item_changetype'])
) {
$item = $this->getDataFromName($_REQUEST['item_name']);
if ($item !== null) {
$item['item_original_name'] = $item['item_name'];
}
} else {
$item = $this->getDataFromRequest();
}
$mode = 'edit';
}
$this->sendEditor($mode, $item, $title, $db, $operation);
}
/**
* This function will generate the values that are required to for the editor
*
* @return array Data necessary to create the editor.
*/
public function getDataFromRequest()
{
$retval = [];
$indices = [
'item_name',
'item_original_name',
'item_status',
'item_execute_at',
'item_interval_value',
'item_interval_field',
'item_starts',
'item_ends',
'item_definition',
'item_preserve',
'item_comment',
'item_definer',
];
foreach ($indices as $index) {
$retval[$index] = $_POST[$index] ?? '';
}
$retval['item_type'] = 'ONE TIME';
$retval['item_type_toggle'] = 'RECURRING';
if (isset($_POST['item_type']) && $_POST['item_type'] === 'RECURRING') {
$retval['item_type'] = 'RECURRING';
$retval['item_type_toggle'] = 'ONE TIME';
}
return $retval;
}
/**
* This function will generate the values that are required to complete
* the "Edit event" form given the name of a event.
*
* @param string $name The name of the event.
*
* @return array|null Data necessary to create the editor.
*/
public function getDataFromName($name): ?array
{
global $db;
$retval = [];
$columns = '`EVENT_NAME`, `STATUS`, `EVENT_TYPE`, `EXECUTE_AT`, '
. '`INTERVAL_VALUE`, `INTERVAL_FIELD`, `STARTS`, `ENDS`, '
. '`EVENT_DEFINITION`, `ON_COMPLETION`, `DEFINER`, `EVENT_COMMENT`';
$where = 'EVENT_SCHEMA ' . Util::getCollateForIS() . '='
. "'" . $this->dbi->escapeString($db) . "' "
. "AND EVENT_NAME='" . $this->dbi->escapeString($name) . "'";
$query = 'SELECT ' . $columns . ' FROM `INFORMATION_SCHEMA`.`EVENTS` WHERE ' . $where . ';';
$item = $this->dbi->fetchSingleRow($query);
if (! $item) {
return null;
}
$retval['item_name'] = $item['EVENT_NAME'];
$retval['item_status'] = $item['STATUS'];
$retval['item_type'] = $item['EVENT_TYPE'];
if ($retval['item_type'] === 'RECURRING') {
$retval['item_type_toggle'] = 'ONE TIME';
} else {
$retval['item_type_toggle'] = 'RECURRING';
}
$retval['item_execute_at'] = $item['EXECUTE_AT'];
$retval['item_interval_value'] = $item['INTERVAL_VALUE'];
$retval['item_interval_field'] = $item['INTERVAL_FIELD'];
$retval['item_starts'] = $item['STARTS'];
$retval['item_ends'] = $item['ENDS'];
$retval['item_preserve'] = '';
if ($item['ON_COMPLETION'] === 'PRESERVE') {
$retval['item_preserve'] = " checked='checked'";
}
$retval['item_definition'] = $item['EVENT_DEFINITION'];
$retval['item_definer'] = $item['DEFINER'];
$retval['item_comment'] = $item['EVENT_COMMENT'];
return $retval;
}
/**
* Displays a form used to add/edit an event
*
* @param string $mode If the editor will be used to edit an event
* or add a new one: 'edit' or 'add'.
* @param string $operation If the editor was previously invoked with
* JS turned off, this will hold the name of
* the current operation
* @param array $item Data for the event returned by
* getDataFromRequest() or getDataFromName()
*
* @return string HTML code for the editor.
*/
public function getEditorForm($mode, $operation, array $item)
{
global $db;
if ($operation === 'change') {
if ($item['item_type'] === 'RECURRING') {
$item['item_type'] = 'ONE TIME';
$item['item_type_toggle'] = 'RECURRING';
} else {
$item['item_type'] = 'RECURRING';
$item['item_type_toggle'] = 'ONE TIME';
}
}
return $this->template->render('database/events/editor_form', [
'db' => $db,
'event' => $item,
'mode' => $mode,
'is_ajax' => $this->response->isAjax(),
'status_display' => $this->status['display'],
'event_type' => $this->type,
'event_interval' => $this->interval,
]);
}
/**
* Composes the query necessary to create an event from an HTTP request.
*
* @return string The CREATE EVENT query.
*/
public function getQueryFromRequest()
{
global $errors;
$query = 'CREATE ';
if (! empty($_POST['item_definer'])) {
if (str_contains($_POST['item_definer'], '@')) {
$arr = explode('@', $_POST['item_definer']);
$query .= 'DEFINER=' . Util::backquote($arr[0]);
$query .= '@' . Util::backquote($arr[1]) . ' ';
} else {
$errors[] = __('The definer must be in the "username@hostname" format!');
}
}
$query .= 'EVENT ';
if (! empty($_POST['item_name'])) {
$query .= Util::backquote($_POST['item_name']) . ' ';
} else {
$errors[] = __('You must provide an event name!');
}
$query .= 'ON SCHEDULE ';
if (! empty($_POST['item_type']) && in_array($_POST['item_type'], $this->type)) {
if ($_POST['item_type'] === 'RECURRING') {
if (
! empty($_POST['item_interval_value'])
&& ! empty($_POST['item_interval_field'])
&& in_array($_POST['item_interval_field'], $this->interval)
) {
$query .= 'EVERY ' . intval($_POST['item_interval_value']) . ' ';
$query .= $_POST['item_interval_field'] . ' ';
} else {
$errors[] = __('You must provide a valid interval value for the event.');
}
if (! empty($_POST['item_starts'])) {
$query .= "STARTS '"
. $this->dbi->escapeString($_POST['item_starts'])
. "' ";
}
if (! empty($_POST['item_ends'])) {
$query .= "ENDS '"
. $this->dbi->escapeString($_POST['item_ends'])
. "' ";
}
} else {
if (! empty($_POST['item_execute_at'])) {
$query .= "AT '"
. $this->dbi->escapeString($_POST['item_execute_at'])
. "' ";
} else {
$errors[] = __('You must provide a valid execution time for the event.');
}
}
} else {
$errors[] = __('You must provide a valid type for the event.');
}
$query .= 'ON COMPLETION ';
if (empty($_POST['item_preserve'])) {
$query .= 'NOT ';
}
$query .= 'PRESERVE ';
if (! empty($_POST['item_status'])) {
foreach ($this->status['display'] as $key => $value) {
if ($value == $_POST['item_status']) {
$query .= $this->status['query'][$key] . ' ';
break;
}
}
}
if (! empty($_POST['item_comment'])) {
$query .= "COMMENT '" . $this->dbi->escapeString($_POST['item_comment']) . "' ";
}
$query .= 'DO ';
if (! empty($_POST['item_definition'])) {
$query .= $_POST['item_definition'];
} else {
$errors[] = __('You must provide an event definition.');
}
return $query;
}
public function getEventSchedulerStatus(): bool
{
$state = (string) $this->dbi->fetchValue('SHOW GLOBAL VARIABLES LIKE \'event_scheduler\'', 1);
return strtoupper($state) === 'ON' || $state === '1';
}
/**
* @param string|null $createStatement Query
* @param array $errors Errors
*
* @return array
*/
private function checkResult($createStatement, array $errors)
{
// OMG, this is really bad! We dropped the query,
// failed to create a new one
// and now even the backup query does not execute!
// This should not happen, but we better handle
// this just in case.
$errors[] = __('Sorry, we failed to restore the dropped event.') . '<br>'
. __('The backed up query was:')
. '"' . htmlspecialchars((string) $createStatement) . '"<br>'
. __('MySQL said: ') . $this->dbi->getError();
return $errors;
}
/**
* Send editor via ajax or by echoing.
*
* @param string $mode Editor mode 'add' or 'edit'
* @param array|null $item Data necessary to create the editor
* @param string $title Title of the editor
* @param string $db Database
* @param string $operation Operation 'change' or ''
*/
private function sendEditor($mode, ?array $item, $title, $db, $operation): void
{
if ($item !== null) {
$editor = $this->getEditorForm($mode, $operation, $item);
if ($this->response->isAjax()) {
$this->response->addJSON('message', $editor);
$this->response->addJSON('title', $title);
} else {
echo "\n\n<h2>" . $title . "</h2>\n\n" . $editor;
unset($_POST);
}
exit;
}
$message = __('Error in processing request:') . ' ';
$message .= sprintf(
__('No event with name %1$s found in database %2$s.'),
htmlspecialchars(Util::backquote($_REQUEST['item_name'])),
htmlspecialchars(Util::backquote($db))
);
$message = Message::error($message);
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
exit;
}
echo $message->getDisplay();
}
public function export(): void
{
global $db;
if (empty($_GET['export_item']) || empty($_GET['item_name'])) {
return;
}
$itemName = $_GET['item_name'];
$exportData = $this->dbi->getDefinition($db, 'EVENT', $itemName);
if (! $exportData) {
$exportData = false;
}
$itemName = htmlspecialchars(Util::backquote($itemName));
if ($exportData !== false) {
$exportData = htmlspecialchars(trim($exportData));
$title = sprintf(__('Export of event %s'), $itemName);
if ($this->response->isAjax()) {
$this->response->addJSON('message', $exportData);
$this->response->addJSON('title', $title);
exit;
}
$output = '<div class="container">';
$output .= '<h2>' . $title . '</h2>';
$output .= '<div class="card"><div class="card-body">';
$output .= '<textarea rows="15" class="form-control">' . $exportData . '</textarea>';
$output .= '</div></div></div>';
$this->response->addHTML($output);
return;
}
$message = sprintf(
__('Error in processing request: No event with name %1$s found in database %2$s.'),
$itemName,
htmlspecialchars(Util::backquote($db))
);
$message = Message::error($message);
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
exit;
}
$this->response->addHTML($message->getDisplay());
}
}

View file

@ -0,0 +1,143 @@
<?php
/**
* Handles DB Multi-table query
*/
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Operations;
use PhpMyAdmin\ParseAnalyze;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use function array_keys;
use function md5;
/**
* Class to handle database Multi-table querying
*/
class MultiTableQuery
{
/**
* DatabaseInterface instance
*
* @var DatabaseInterface
*/
private $dbi;
/**
* Database name
*
* @var string
*/
private $db;
/**
* Default number of columns
*
* @var int
*/
private $defaultNoOfColumns;
/**
* Table names
*
* @var array
*/
private $tables;
/** @var Template */
public $template;
/**
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param Template $template Template instance
* @param string $dbName Database name
* @param int $defaultNoOfColumns Default number of columns
*/
public function __construct(
DatabaseInterface $dbi,
Template $template,
$dbName,
$defaultNoOfColumns = 3
) {
$this->dbi = $dbi;
$this->db = $dbName;
$this->defaultNoOfColumns = $defaultNoOfColumns;
$this->template = $template;
$this->tables = $this->dbi->getTables($this->db);
}
/**
* Get Multi-Table query page HTML
*
* @return string Multi-Table query page HTML
*/
public function getFormHtml()
{
$tables = [];
foreach ($this->tables as $table) {
$tables[$table]['hash'] = md5($table);
$tables[$table]['columns'] = array_keys(
$this->dbi->getColumns($this->db, $table)
);
}
return $this->template->render('database/multi_table_query/form', [
'db' => $this->db,
'tables' => $tables,
'default_no_of_columns' => $this->defaultNoOfColumns,
]);
}
/**
* Displays multi-table query results
*
* @param string $sqlQuery The query to parse
* @param string $db The current database
*/
public static function displayResults($sqlQuery, $db): string
{
global $dbi;
[, $db] = ParseAnalyze::sqlQuery($sqlQuery, $db);
$goto = Url::getFromRoute('/database/multi-table-query');
$relation = new Relation($dbi);
$sql = new Sql(
$dbi,
$relation,
new RelationCleanup($dbi, $relation),
new Operations($dbi, $relation),
new Transformations(),
new Template()
);
return $sql->executeQueryAndSendQueryResponse(
null, // analyzed_sql_results
false, // is_gotofile
$db, // db
null, // table
null, // find_real_end
null, // sql_query_for_bookmark - see below
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
null, // disp_query
null, // disp_message
$sqlQuery, // sql_query
null // complete_query
);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,316 @@
<?php
/**
* Handles Database Search
*/
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function array_intersect;
use function array_key_exists;
use function count;
use function explode;
use function htmlspecialchars;
use function implode;
use function intval;
use function is_array;
use function is_string;
use function strlen;
/**
* Class to handle database search
*/
class Search
{
/**
* Database name
*
* @var string
*/
private $db;
/**
* Table Names
*
* @var array
*/
private $tablesNamesOnly;
/**
* Type of search
*
* @var array
*/
private $searchTypes;
/**
* Already set search type
*
* @var int
*/
private $criteriaSearchType;
/**
* Already set search type's description
*
* @var string
*/
private $searchTypeDescription;
/**
* Search string/regexp
*
* @var string
*/
private $criteriaSearchString;
/**
* Criteria Tables to search in
*
* @var array
*/
private $criteriaTables;
/**
* Restrict the search to this column
*
* @var string
*/
private $criteriaColumnName;
/** @var DatabaseInterface */
private $dbi;
/** @var Template */
public $template;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $db Database name
* @param Template $template Template object
*/
public function __construct(DatabaseInterface $dbi, $db, Template $template)
{
$this->db = $db;
$this->dbi = $dbi;
$this->searchTypes = [
'1' => __('at least one of the words'),
'2' => __('all of the words'),
'3' => __('the exact phrase as substring'),
'4' => __('the exact phrase as whole field'),
'5' => __('as regular expression'),
];
$this->template = $template;
// Sets criteria parameters
$this->setSearchParams();
}
/**
* Sets search parameters
*/
private function setSearchParams(): void
{
$this->tablesNamesOnly = $this->dbi->getTables($this->db);
if (
empty($_POST['criteriaSearchType'])
|| ! is_string($_POST['criteriaSearchType'])
|| ! array_key_exists($_POST['criteriaSearchType'], $this->searchTypes)
) {
$this->criteriaSearchType = 1;
unset($_POST['submit_search']);
} else {
$this->criteriaSearchType = (int) $_POST['criteriaSearchType'];
$this->searchTypeDescription = $this->searchTypes[$_POST['criteriaSearchType']];
}
if (empty($_POST['criteriaSearchString']) || ! is_string($_POST['criteriaSearchString'])) {
$this->criteriaSearchString = '';
unset($_POST['submit_search']);
} else {
$this->criteriaSearchString = $_POST['criteriaSearchString'];
}
$this->criteriaTables = [];
if (empty($_POST['criteriaTables']) || ! is_array($_POST['criteriaTables'])) {
unset($_POST['submit_search']);
} else {
$this->criteriaTables = array_intersect($_POST['criteriaTables'], $this->tablesNamesOnly);
}
if (empty($_POST['criteriaColumnName']) || ! is_string($_POST['criteriaColumnName'])) {
unset($this->criteriaColumnName);
} else {
$this->criteriaColumnName = $this->dbi->escapeString($_POST['criteriaColumnName']);
}
}
/**
* Builds the SQL search query
*
* @param string $table The table name
*
* @return array 3 SQL queries (for count, display and delete results)
*
* @todo can we make use of fulltextsearch IN BOOLEAN MODE for this?
* PMA_backquote
* DatabaseInterface::fetchAssoc
* $GLOBALS['db']
* explode
* count
* strlen
*/
private function getSearchSqls($table)
{
// Statement types
$sqlstr_select = 'SELECT';
$sqlstr_delete = 'DELETE';
// Table to use
$sqlstr_from = ' FROM '
. Util::backquote($GLOBALS['db']) . '.'
. Util::backquote($table);
// Gets where clause for the query
$where_clause = $this->getWhereClause($table);
// Builds complete queries
$sql = [];
$sql['select_columns'] = $sqlstr_select . ' * ' . $sqlstr_from
. $where_clause;
// here, I think we need to still use the COUNT clause, even for
// VIEWs, anyway we have a WHERE clause that should limit results
$sql['select_count'] = $sqlstr_select . ' COUNT(*) AS `count`'
. $sqlstr_from . $where_clause;
$sql['delete'] = $sqlstr_delete . $sqlstr_from . $where_clause;
return $sql;
}
/**
* Provides where clause for building SQL query
*
* @param string $table The table name
*
* @return string The generated where clause
*/
private function getWhereClause($table)
{
// Columns to select
$allColumns = $this->dbi->getColumns($GLOBALS['db'], $table);
$likeClauses = [];
// Based on search type, decide like/regex & '%'/''
$like_or_regex = ($this->criteriaSearchType == 5 ? 'REGEXP' : 'LIKE');
$automatic_wildcard = ($this->criteriaSearchType < 4 ? '%' : '');
// For "as regular expression" (search option 5), LIKE won't be used
// Usage example: If user is searching for a literal $ in a regexp search,
// they should enter \$ as the value.
$criteriaSearchStringEscaped = $this->dbi->escapeString($this->criteriaSearchString);
// Extract search words or pattern
$search_words = $this->criteriaSearchType > 2
? [$criteriaSearchStringEscaped]
: explode(' ', $criteriaSearchStringEscaped);
foreach ($search_words as $search_word) {
// Eliminates empty values
if (strlen($search_word) === 0) {
continue;
}
$likeClausesPerColumn = [];
// for each column in the table
foreach ($allColumns as $column) {
if (
isset($this->criteriaColumnName)
&& strlen($this->criteriaColumnName) !== 0
&& $column['Field'] != $this->criteriaColumnName
) {
continue;
}
$column = 'CONVERT(' . Util::backquote($column['Field'])
. ' USING utf8)';
$likeClausesPerColumn[] = $column . ' ' . $like_or_regex . ' '
. "'"
. $automatic_wildcard . $search_word . $automatic_wildcard
. "'";
}
if (count($likeClausesPerColumn) <= 0) {
continue;
}
$likeClauses[] = implode(' OR ', $likeClausesPerColumn);
}
// Use 'OR' if 'at least one word' is to be searched, else use 'AND'
$implode_str = ($this->criteriaSearchType == 1 ? ' OR ' : ' AND ');
if (empty($likeClauses)) {
// this could happen when the "inside column" does not exist
// in any selected tables
$where_clause = ' WHERE FALSE';
} else {
$where_clause = ' WHERE ('
. implode(') ' . $implode_str . ' (', $likeClauses)
. ')';
}
return $where_clause;
}
/**
* Displays database search results
*
* @return string HTML for search results
*/
public function getSearchResults()
{
$resultTotal = 0;
$rows = [];
// For each table selected as search criteria
foreach ($this->criteriaTables as $eachTable) {
// Gets the SQL statements
$newSearchSqls = $this->getSearchSqls($eachTable);
// Executes the "COUNT" statement
$resultCount = intval($this->dbi->fetchValue(
$newSearchSqls['select_count']
));
$resultTotal += $resultCount;
// Gets the result row's HTML for a table
$rows[] = [
'table' => htmlspecialchars($eachTable),
'new_search_sqls' => $newSearchSqls,
'result_count' => $resultCount,
];
}
return $this->template->render('database/search/results', [
'db' => $this->db,
'rows' => $rows,
'result_total' => $resultTotal,
'criteria_tables' => $this->criteriaTables,
'criteria_search_string' => htmlspecialchars($this->criteriaSearchString),
'search_type_description' => $this->searchTypeDescription,
]);
}
/**
* Provides the main search form's html
*
* @return string HTML for selection form
*/
public function getMainHtml()
{
return $this->template->render('database/search/main', [
'db' => $this->db,
'criteria_search_string' => $this->criteriaSearchString,
'criteria_search_type' => $this->criteriaSearchType,
'criteria_tables' => $this->criteriaTables,
'tables_names_only' => $this->tablesNamesOnly,
'criteria_column_name' => $this->criteriaColumnName ?? null,
]);
}
}

View file

@ -0,0 +1,535 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
use function __;
use function count;
use function explode;
use function htmlspecialchars;
use function in_array;
use function mb_strtoupper;
use function sprintf;
use function str_contains;
use function trim;
/**
* Functions for trigger management.
*/
class Triggers
{
/** @var array<int, string> */
private $time = ['BEFORE', 'AFTER'];
/** @var array<int, string> */
private $event = ['INSERT', 'UPDATE', 'DELETE'];
/** @var DatabaseInterface */
private $dbi;
/** @var Template */
private $template;
/** @var ResponseRenderer */
private $response;
/**
* @param DatabaseInterface $dbi DatabaseInterface instance.
* @param Template $template Template instance.
* @param ResponseRenderer $response Response instance.
*/
public function __construct(DatabaseInterface $dbi, Template $template, $response)
{
$this->dbi = $dbi;
$this->template = $template;
$this->response = $response;
}
/**
* Main function for the triggers functionality
*/
public function main(): void
{
global $db, $table;
/**
* Process all requests
*/
$this->handleEditor();
$this->export();
$items = $this->dbi->getTriggers($db, $table);
$hasTriggerPrivilege = Util::currentUserHasPrivilege('TRIGGER', $db, $table);
$isAjax = $this->response->isAjax() && empty($_REQUEST['ajax_page_request']);
$rows = '';
foreach ($items as $item) {
$rows .= $this->template->render('database/triggers/row', [
'db' => $db,
'table' => $table,
'trigger' => $item,
'has_drop_privilege' => $hasTriggerPrivilege,
'has_edit_privilege' => $hasTriggerPrivilege,
'row_class' => $isAjax ? 'ajaxInsert hide' : '',
]);
}
echo $this->template->render('database/triggers/list', [
'db' => $db,
'table' => $table,
'items' => $items,
'rows' => $rows,
'has_privilege' => $hasTriggerPrivilege,
]);
}
/**
* Handles editor requests for adding or editing an item
*/
public function handleEditor(): void
{
global $db, $errors, $message, $table;
if (! empty($_POST['editor_process_add']) || ! empty($_POST['editor_process_edit'])) {
$sql_query = '';
$item_query = $this->getQueryFromRequest();
// set by getQueryFromRequest()
if (! count($errors)) {
// Execute the created query
if (! empty($_POST['editor_process_edit'])) {
// Backup the old trigger, in case something goes wrong
$trigger = $this->getDataFromName($_POST['item_original_name']);
$create_item = $trigger['create'];
$drop_item = $trigger['drop'] . ';';
$result = $this->dbi->tryQuery($drop_item);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($drop_item)
)
. '<br>'
. __('MySQL said: ') . $this->dbi->getError();
} else {
$result = $this->dbi->tryQuery($item_query);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($item_query)
)
. '<br>'
. __('MySQL said: ') . $this->dbi->getError();
// We dropped the old item, but were unable to create the
// new one. Try to restore the backup query.
$result = $this->dbi->tryQuery($create_item);
if (! $result) {
$errors = $this->checkResult($create_item, $errors);
}
} else {
$message = Message::success(
__('Trigger %1$s has been modified.')
);
$message->addParam(
Util::backquote($_POST['item_name'])
);
$sql_query = $drop_item . $item_query;
}
}
} else {
// 'Add a new item' mode
$result = $this->dbi->tryQuery($item_query);
if (! $result) {
$errors[] = sprintf(
__('The following query has failed: "%s"'),
htmlspecialchars($item_query)
)
. '<br><br>'
. __('MySQL said: ') . $this->dbi->getError();
} else {
$message = Message::success(
__('Trigger %1$s has been created.')
);
$message->addParam(
Util::backquote($_POST['item_name'])
);
$sql_query = $item_query;
}
}
}
if (count($errors)) {
$message = Message::error(
'<b>'
. __(
'One or more errors have occurred while processing your request:'
)
. '</b>'
);
$message->addHtml('<ul>');
foreach ($errors as $string) {
$message->addHtml('<li>' . $string . '</li>');
}
$message->addHtml('</ul>');
}
$output = Generator::getMessage($message, $sql_query);
if ($this->response->isAjax()) {
if ($message->isSuccess()) {
$items = $this->dbi->getTriggers($db, $table, '');
$trigger = false;
foreach ($items as $value) {
if ($value['name'] != $_POST['item_name']) {
continue;
}
$trigger = $value;
}
$insert = false;
if (empty($table) || ($trigger !== false && $table == $trigger['table'])) {
$insert = true;
$hasTriggerPrivilege = Util::currentUserHasPrivilege('TRIGGER', $db, $table);
$this->response->addJSON(
'new_row',
$this->template->render('database/triggers/row', [
'db' => $db,
'table' => $table,
'trigger' => $trigger,
'has_drop_privilege' => $hasTriggerPrivilege,
'has_edit_privilege' => $hasTriggerPrivilege,
'row_class' => '',
])
);
$this->response->addJSON(
'name',
htmlspecialchars(
mb_strtoupper(
$_POST['item_name']
)
)
);
}
$this->response->addJSON('insert', $insert);
$this->response->addJSON('message', $output);
} else {
$this->response->addJSON('message', $message);
$this->response->setRequestStatus(false);
}
$this->response->addJSON('tableType', 'triggers');
exit;
}
}
/**
* Display a form used to add/edit a trigger, if necessary
*/
if (
! count($errors)
&& (! empty($_POST['editor_process_add'])
|| ! empty($_POST['editor_process_edit'])
|| (empty($_REQUEST['add_item'])
&& empty($_REQUEST['edit_item']))) // FIXME: this must be simpler than that
) {
return;
}
$mode = '';
$item = null;
$title = '';
// Get the data for the form (if any)
if (! empty($_REQUEST['add_item'])) {
$title = __('Add trigger');
$item = $this->getDataFromRequest();
$mode = 'add';
} elseif (! empty($_REQUEST['edit_item'])) {
$title = __('Edit trigger');
if (! empty($_REQUEST['item_name']) && empty($_POST['editor_process_edit'])) {
$item = $this->getDataFromName($_REQUEST['item_name']);
if ($item !== null) {
$item['item_original_name'] = $item['item_name'];
}
} else {
$item = $this->getDataFromRequest();
}
$mode = 'edit';
}
$this->sendEditor($mode, $item, $title, $db, $table);
}
/**
* This function will generate the values that are required to for the editor
*
* @return array Data necessary to create the editor.
*/
public function getDataFromRequest()
{
$retval = [];
$indices = [
'item_name',
'item_table',
'item_original_name',
'item_action_timing',
'item_event_manipulation',
'item_definition',
'item_definer',
];
foreach ($indices as $index) {
$retval[$index] = $_POST[$index] ?? '';
}
return $retval;
}
/**
* This function will generate the values that are required to complete
* the "Edit trigger" form given the name of a trigger.
*
* @param string $name The name of the trigger.
*
* @return array|null Data necessary to create the editor.
*/
public function getDataFromName($name): ?array
{
global $db, $table;
$temp = [];
$items = $this->dbi->getTriggers($db, $table, '');
foreach ($items as $value) {
if ($value['name'] != $name) {
continue;
}
$temp = $value;
}
if (empty($temp)) {
return null;
}
$retval = [];
$retval['create'] = $temp['create'];
$retval['drop'] = $temp['drop'];
$retval['item_name'] = $temp['name'];
$retval['item_table'] = $temp['table'];
$retval['item_action_timing'] = $temp['action_timing'];
$retval['item_event_manipulation'] = $temp['event_manipulation'];
$retval['item_definition'] = $temp['definition'];
$retval['item_definer'] = $temp['definer'];
return $retval;
}
/**
* Displays a form used to add/edit a trigger
*
* @param string $db
* @param string $table
* @param string $mode If the editor will be used to edit a trigger or add a new one: 'edit' or 'add'.
* @param array $item Data for the trigger returned by getDataFromRequest() or getDataFromName()
*/
public function getEditorForm($db, $table, $mode, array $item): string
{
$query = 'SELECT `TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` ';
$query .= 'WHERE `TABLE_SCHEMA`=\'' . $this->dbi->escapeString($db) . '\' ';
$query .= 'AND `TABLE_TYPE` IN (\'BASE TABLE\', \'SYSTEM VERSIONED\')';
$tables = $this->dbi->fetchResult($query);
return $this->template->render('database/triggers/editor_form', [
'db' => $db,
'table' => $table,
'is_edit' => $mode === 'edit',
'item' => $item,
'tables' => $tables,
'time' => $this->time,
'events' => $this->event,
'is_ajax' => $this->response->isAjax(),
]);
}
/**
* Composes the query necessary to create a trigger from an HTTP request.
*
* @return string The CREATE TRIGGER query.
*/
public function getQueryFromRequest()
{
global $db, $errors;
$query = 'CREATE ';
if (! empty($_POST['item_definer'])) {
if (str_contains($_POST['item_definer'], '@')) {
$arr = explode('@', $_POST['item_definer']);
$query .= 'DEFINER=' . Util::backquote($arr[0]);
$query .= '@' . Util::backquote($arr[1]) . ' ';
} else {
$errors[] = __('The definer must be in the "username@hostname" format!');
}
}
$query .= 'TRIGGER ';
if (! empty($_POST['item_name'])) {
$query .= Util::backquote($_POST['item_name']) . ' ';
} else {
$errors[] = __('You must provide a trigger name!');
}
if (! empty($_POST['item_timing']) && in_array($_POST['item_timing'], $this->time)) {
$query .= $_POST['item_timing'] . ' ';
} else {
$errors[] = __('You must provide a valid timing for the trigger!');
}
if (! empty($_POST['item_event']) && in_array($_POST['item_event'], $this->event)) {
$query .= $_POST['item_event'] . ' ';
} else {
$errors[] = __('You must provide a valid event for the trigger!');
}
$query .= 'ON ';
if (! empty($_POST['item_table']) && in_array($_POST['item_table'], $this->dbi->getTables($db))) {
$query .= Util::backquote($_POST['item_table']);
} else {
$errors[] = __('You must provide a valid table name!');
}
$query .= ' FOR EACH ROW ';
if (! empty($_POST['item_definition'])) {
$query .= $_POST['item_definition'];
} else {
$errors[] = __('You must provide a trigger definition.');
}
return $query;
}
/**
* @param string $createStatement Query
* @param array $errors Errors
*
* @return array
*/
private function checkResult($createStatement, array $errors)
{
// OMG, this is really bad! We dropped the query,
// failed to create a new one
// and now even the backup query does not execute!
// This should not happen, but we better handle
// this just in case.
$errors[] = __('Sorry, we failed to restore the dropped trigger.') . '<br>'
. __('The backed up query was:')
. '"' . htmlspecialchars($createStatement) . '"<br>'
. __('MySQL said: ') . $this->dbi->getError();
return $errors;
}
/**
* Send editor via ajax or by echoing.
*
* @param string $mode Editor mode 'add' or 'edit'
* @param array|null $item Data necessary to create the editor
* @param string $title Title of the editor
* @param string $db Database
* @param string $table Table
*/
private function sendEditor($mode, ?array $item, $title, $db, $table): void
{
if ($item !== null) {
$editor = $this->getEditorForm($db, $table, $mode, $item);
if ($this->response->isAjax()) {
$this->response->addJSON('message', $editor);
$this->response->addJSON('title', $title);
} else {
echo "\n\n<h2>" . $title . "</h2>\n\n" . $editor;
unset($_POST);
}
exit;
}
$message = __('Error in processing request:') . ' ';
$message .= sprintf(
__('No trigger with name %1$s found in database %2$s.'),
htmlspecialchars(Util::backquote($_REQUEST['item_name'])),
htmlspecialchars(Util::backquote($db))
);
$message = Message::error($message);
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
exit;
}
echo $message->getDisplay();
}
private function export(): void
{
global $db, $table;
if (empty($_GET['export_item']) || empty($_GET['item_name'])) {
return;
}
$itemName = $_GET['item_name'];
$triggers = $this->dbi->getTriggers($db, $table, '');
$exportData = false;
foreach ($triggers as $trigger) {
if ($trigger['name'] === $itemName) {
$exportData = $trigger['create'];
break;
}
}
if ($exportData !== false) {
$title = sprintf(__('Export of trigger %s'), htmlspecialchars(Util::backquote($itemName)));
if ($this->response->isAjax()) {
$this->response->addJSON('message', htmlspecialchars(trim($exportData)));
$this->response->addJSON('title', $title);
exit;
}
$this->response->addHTML($this->template->render('database/triggers/export', [
'data' => $exportData,
'item_name' => $itemName,
]));
return;
}
$message = sprintf(
__('Error in processing request: No trigger with name %1$s found in database %2$s.'),
htmlspecialchars(Util::backquote($itemName)),
htmlspecialchars(Util::backquote($db))
);
$message = Message::error($message);
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
exit;
}
$this->response->addHTML($message->getDisplay());
}
}