Add AbstractController::render method
[phpmyadmin.git] / libraries / classes / Controllers / Table / RelationController.php
blobc0a1e99c08b13d5696601b6bfa461b35951940f7
1 <?php
2 declare(strict_types=1);
4 namespace PhpMyAdmin\Controllers\Table;
6 use PhpMyAdmin\Core;
7 use PhpMyAdmin\DatabaseInterface;
8 use PhpMyAdmin\Html\Generator;
9 use PhpMyAdmin\Index;
10 use PhpMyAdmin\Relation;
11 use PhpMyAdmin\Response;
12 use PhpMyAdmin\Table;
13 use PhpMyAdmin\Template;
14 use PhpMyAdmin\Util;
15 use function array_key_exists;
16 use function array_keys;
17 use function array_values;
18 use function htmlspecialchars;
19 use function mb_strtoupper;
20 use function md5;
21 use function strtoupper;
22 use function uksort;
23 use function usort;
25 /**
26 * Display table relations for viewing and editing.
28 * Includes phpMyAdmin relations and InnoDB relations.
30 final class RelationController extends AbstractController
32 /** @var Relation */
33 private $relation;
35 /**
36 * @param Response $response Response object
37 * @param DatabaseInterface $dbi DatabaseInterface object
38 * @param Template $template Template object
39 * @param string $db Database name
40 * @param string $table Table name
41 * @param Relation $relation Relation instance
43 public function __construct(
44 $response,
45 $dbi,
46 Template $template,
47 $db,
48 $table,
49 Relation $relation
50 ) {
51 parent::__construct($response, $dbi, $template, $db, $table);
52 $this->relation = $relation;
55 /**
56 * Index
58 public function index(): void
60 global $route;
62 $options = [
63 'CASCADE' => 'CASCADE',
64 'SET_NULL' => 'SET NULL',
65 'NO_ACTION' => 'NO ACTION',
66 'RESTRICT' => 'RESTRICT',
69 $table = $this->dbi->getTable($this->db, $this->table);
70 $storageEngine = mb_strtoupper((string) $table->getStatusInfo('Engine'));
72 $cfgRelation = $this->relation->getRelationsParam();
74 $relations = [];
75 if ($cfgRelation['relwork']) {
76 $relations = $this->relation->getForeigners(
77 $this->db,
78 $this->table,
79 '',
80 'internal'
84 $relationsForeign = [];
85 if (Util::isForeignKeySupported($storageEngine)) {
86 $relationsForeign = $this->relation->getForeigners(
87 $this->db,
88 $this->table,
89 '',
90 'foreign'
94 // Send table of column names to populate corresponding dropdowns depending
95 // on the current selection
96 if (isset($_POST['getDropdownValues'])
97 && $_POST['getDropdownValues'] === 'true'
98 ) {
99 // if both db and table are selected
100 if (isset($_POST['foreignTable'])) {
101 $this->getDropdownValueForTable();
102 } else { // if only the db is selected
103 $this->getDropdownValueForDatabase($storageEngine);
105 return;
108 $this->response->getHeader()->getScripts()->addFiles(
110 'table/relation.js',
111 'indexes.js',
115 // Set the database
116 $this->dbi->selectDb($this->db);
118 // updates for Internal relations
119 if (isset($_POST['destination_db']) && $cfgRelation['relwork']) {
120 $this->updateForInternalRelation($table, $cfgRelation, $relations);
123 // updates for foreign keys
124 $this->updateForForeignKeys($table, $options, $relationsForeign);
126 // Updates for display field
127 if ($cfgRelation['displaywork'] && isset($_POST['display_field'])) {
128 $this->updateForDisplayField($table, $cfgRelation);
131 // If we did an update, refresh our data
132 if (isset($_POST['destination_db']) && $cfgRelation['relwork']) {
133 $relations = $this->relation->getForeigners(
134 $this->db,
135 $this->table,
137 'internal'
140 if (isset($_POST['destination_foreign_db'])
141 && Util::isForeignKeySupported($storageEngine)
143 $relationsForeign = $this->relation->getForeigners(
144 $this->db,
145 $this->table,
147 'foreign'
152 * Dialog
154 // Now find out the columns of our $table
155 // need to use DatabaseInterface::QUERY_STORE with $this->dbi->numRows()
156 // in mysqli
157 $columns = $this->dbi->getColumns($this->db, $this->table);
159 $column_array = [];
160 $column_hash_array = [];
161 $column_array[''] = '';
162 foreach ($columns as $column) {
163 if (strtoupper($storageEngine) == 'INNODB'
164 || ! empty($column['Key'])
166 $column_array[$column['Field']] = $column['Field'];
167 $column_hash_array[$column['Field']] = md5($column['Field']);
170 if ($GLOBALS['cfg']['NaturalOrder']) {
171 uksort($column_array, 'strnatcasecmp');
174 // common form
175 $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine();
176 $foreignKeySupported = Util::isForeignKeySupported($storageEngine);
177 $this->render('table/relation/common_form', [
178 'is_foreign_key_supported' => Util::isForeignKeySupported($engine),
179 'db' => $this->db,
180 'table' => $this->table,
181 'cfg_relation' => $cfgRelation,
182 'tbl_storage_engine' => $storageEngine,
183 'existrel' => $relations,
184 'existrel_foreign' => array_key_exists('foreign_keys_data', $relationsForeign)
185 ? $relationsForeign['foreign_keys_data']
186 : [],
187 'options_array' => $options,
188 'column_array' => $column_array,
189 'column_hash_array' => $column_hash_array,
190 'save_row' => array_values($columns),
191 'url_params' => $GLOBALS['url_params'],
192 'databases' => $GLOBALS['dblist']->databases,
193 'dbi' => $this->dbi,
194 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
195 'foreignKeySupported' => $foreignKeySupported,
196 'indexes' => $foreignKeySupported ? Index::getFromTable($this->table, $this->db) : null,
197 'indexes_duplicates' => $foreignKeySupported ? Index::findDuplicates($this->table, $this->db) : null,
198 'route' => $route,
203 * Update for display field
205 * @param Table $table table
206 * @param array $cfgRelation relation parameters
208 private function updateForDisplayField(Table $table, array $cfgRelation): void
210 if ($table->updateDisplayField(
211 $_POST['display_field'],
212 $cfgRelation
213 )) {
214 $this->response->addHTML(
215 Generator::getMessage(
216 __('Display column was successfully updated.'),
218 'success'
225 * Update for FK
227 * @param Table $table Table
228 * @param array $options Options
229 * @param array $relationsForeign External relations
231 private function updateForForeignKeys(Table $table, array $options, array $relationsForeign): void
233 $multi_edit_columns_name = $_POST['foreign_key_fields_name'] ?? null;
234 $preview_sql_data = '';
235 $seen_error = false;
237 // (for now, one index name only; we keep the definitions if the
238 // foreign db is not the same)
239 if (isset($_POST['destination_foreign_db'], $_POST['destination_foreign_table'])
240 && isset($_POST['destination_foreign_column'])) {
242 $html,
243 $preview_sql_data,
244 $display_query,
245 $seen_error,
246 ] = $table->updateForeignKeys(
247 $_POST['destination_foreign_db'],
248 $multi_edit_columns_name,
249 $_POST['destination_foreign_table'],
250 $_POST['destination_foreign_column'],
251 $options,
252 $this->table,
253 array_key_exists('foreign_keys_data', $relationsForeign)
254 ? $relationsForeign['foreign_keys_data']
255 : []
257 $this->response->addHTML($html);
260 // If there is a request for SQL previewing.
261 if (isset($_POST['preview_sql'])) {
262 Core::previewSQL($preview_sql_data);
265 if (! empty($display_query) && ! $seen_error) {
266 $GLOBALS['display_query'] = $display_query;
267 $this->response->addHTML(
268 Generator::getMessage(
269 __('Your SQL query has been executed successfully.'),
270 null,
271 'success'
278 * Update for internal relation
280 * @param Table $table Table
281 * @param array $cfgRelation Relation parameters
282 * @param array $relations Relations
284 private function updateForInternalRelation(Table $table, array $cfgRelation, array $relations): void
286 $multi_edit_columns_name = $_POST['fields_name'] ?? null;
288 if ($table->updateInternalRelations(
289 $multi_edit_columns_name,
290 $_POST['destination_db'],
291 $_POST['destination_table'],
292 $_POST['destination_column'],
293 $cfgRelation,
294 $relations
295 )) {
296 $this->response->addHTML(
297 Generator::getMessage(
298 __('Internal relationships were successfully updated.'),
300 'success'
307 * Send table columns for foreign table dropdown
309 public function getDropdownValueForTable(): void
311 $foreignTable = $_POST['foreignTable'];
312 $table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable);
313 // Since views do not have keys defined on them provide the full list of
314 // columns
315 if ($table_obj->isView()) {
316 $columnList = $table_obj->getColumns(false, false);
317 } else {
318 $columnList = $table_obj->getIndexedColumns(false, false);
320 $columns = [];
321 foreach ($columnList as $column) {
322 $columns[] = htmlspecialchars($column);
324 if ($GLOBALS['cfg']['NaturalOrder']) {
325 usort($columns, 'strnatcasecmp');
327 $this->response->addJSON('columns', $columns);
329 // @todo should be: $server->db($db)->table($table)->primary()
330 $primary = Index::getPrimary($foreignTable, $_POST['foreignDb']);
331 if ($primary === false) {
332 return;
335 $this->response->addJSON('primary', array_keys($primary->getColumns()));
339 * Send database selection values for dropdown
341 * @param string $storageEngine Storage engine.
343 public function getDropdownValueForDatabase(string $storageEngine): void
345 $tables = [];
346 $foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true';
348 if ($foreign) {
349 $query = 'SHOW TABLE STATUS FROM '
350 . Util::backquote($_POST['foreignDb']);
351 $tables_rs = $this->dbi->query(
352 $query,
353 DatabaseInterface::CONNECT_USER,
354 DatabaseInterface::QUERY_STORE
357 while ($row = $this->dbi->fetchArray($tables_rs)) {
358 if (isset($row['Engine'])
359 && mb_strtoupper($row['Engine']) == $storageEngine
361 $tables[] = htmlspecialchars($row['Name']);
364 } else {
365 $query = 'SHOW TABLES FROM '
366 . Util::backquote($_POST['foreignDb']);
367 $tables_rs = $this->dbi->query(
368 $query,
369 DatabaseInterface::CONNECT_USER,
370 DatabaseInterface::QUERY_STORE
372 while ($row = $this->dbi->fetchArray($tables_rs)) {
373 $tables[] = htmlspecialchars($row[0]);
376 if ($GLOBALS['cfg']['NaturalOrder']) {
377 usort($tables, 'strnatcasecmp');
379 $this->response->addJSON('tables', $tables);