Update website
This commit is contained in:
parent
41ce1aa076
commit
ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions
1003
admin/phpMyAdmin/libraries/classes/Database/CentralColumns.php
Normal file
1003
admin/phpMyAdmin/libraries/classes/Database/CentralColumns.php
Normal file
File diff suppressed because it is too large
Load diff
48
admin/phpMyAdmin/libraries/classes/Database/DatabaseList.php
Normal file
48
admin/phpMyAdmin/libraries/classes/Database/DatabaseList.php
Normal 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;
|
||||
}
|
||||
}
|
412
admin/phpMyAdmin/libraries/classes/Database/Designer.php
Normal file
412
admin/phpMyAdmin/libraries/classes/Database/Designer.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
816
admin/phpMyAdmin/libraries/classes/Database/Designer/Common.php
Normal file
816
admin/phpMyAdmin/libraries/classes/Database/Designer/Common.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
604
admin/phpMyAdmin/libraries/classes/Database/Events.php
Normal file
604
admin/phpMyAdmin/libraries/classes/Database/Events.php
Normal 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());
|
||||
}
|
||||
}
|
143
admin/phpMyAdmin/libraries/classes/Database/MultiTableQuery.php
Normal file
143
admin/phpMyAdmin/libraries/classes/Database/MultiTableQuery.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
1796
admin/phpMyAdmin/libraries/classes/Database/Qbe.php
Normal file
1796
admin/phpMyAdmin/libraries/classes/Database/Qbe.php
Normal file
File diff suppressed because it is too large
Load diff
1606
admin/phpMyAdmin/libraries/classes/Database/Routines.php
Normal file
1606
admin/phpMyAdmin/libraries/classes/Database/Routines.php
Normal file
File diff suppressed because it is too large
Load diff
316
admin/phpMyAdmin/libraries/classes/Database/Search.php
Normal file
316
admin/phpMyAdmin/libraries/classes/Database/Search.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
535
admin/phpMyAdmin/libraries/classes/Database/Triggers.php
Normal file
535
admin/phpMyAdmin/libraries/classes/Database/Triggers.php
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue