RelationController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table;
  4. use PhpMyAdmin\Core;
  5. use PhpMyAdmin\DatabaseInterface;
  6. use PhpMyAdmin\Html\Generator;
  7. use PhpMyAdmin\Index;
  8. use PhpMyAdmin\Relation;
  9. use PhpMyAdmin\Response;
  10. use PhpMyAdmin\Table;
  11. use PhpMyAdmin\Template;
  12. use PhpMyAdmin\Util;
  13. use function array_key_exists;
  14. use function array_keys;
  15. use function array_values;
  16. use function htmlspecialchars;
  17. use function mb_strtoupper;
  18. use function md5;
  19. use function strtoupper;
  20. use function uksort;
  21. use function usort;
  22. /**
  23. * Display table relations for viewing and editing.
  24. *
  25. * Includes phpMyAdmin relations and InnoDB relations.
  26. */
  27. final class RelationController extends AbstractController
  28. {
  29. /** @var Relation */
  30. private $relation;
  31. /** @var DatabaseInterface */
  32. private $dbi;
  33. /**
  34. * @param Response $response
  35. * @param string $db Database name
  36. * @param string $table Table name
  37. * @param DatabaseInterface $dbi
  38. */
  39. public function __construct(
  40. $response,
  41. Template $template,
  42. $db,
  43. $table,
  44. Relation $relation,
  45. $dbi
  46. ) {
  47. parent::__construct($response, $template, $db, $table);
  48. $this->relation = $relation;
  49. $this->dbi = $dbi;
  50. }
  51. /**
  52. * Index
  53. */
  54. public function index(): void
  55. {
  56. global $route;
  57. $options = [
  58. 'CASCADE' => 'CASCADE',
  59. 'SET_NULL' => 'SET NULL',
  60. 'NO_ACTION' => 'NO ACTION',
  61. 'RESTRICT' => 'RESTRICT',
  62. ];
  63. $table = $this->dbi->getTable($this->db, $this->table);
  64. $storageEngine = mb_strtoupper((string) $table->getStatusInfo('Engine'));
  65. $cfgRelation = $this->relation->getRelationsParam();
  66. $relations = [];
  67. if ($cfgRelation['relwork']) {
  68. $relations = $this->relation->getForeigners(
  69. $this->db,
  70. $this->table,
  71. '',
  72. 'internal'
  73. );
  74. }
  75. $relationsForeign = [];
  76. if (Util::isForeignKeySupported($storageEngine)) {
  77. $relationsForeign = $this->relation->getForeigners(
  78. $this->db,
  79. $this->table,
  80. '',
  81. 'foreign'
  82. );
  83. }
  84. // Send table of column names to populate corresponding dropdowns depending
  85. // on the current selection
  86. if (isset($_POST['getDropdownValues'])
  87. && $_POST['getDropdownValues'] === 'true'
  88. ) {
  89. // if both db and table are selected
  90. if (isset($_POST['foreignTable'])) {
  91. $this->getDropdownValueForTable();
  92. } else { // if only the db is selected
  93. $this->getDropdownValueForDatabase($storageEngine);
  94. }
  95. return;
  96. }
  97. $this->addScriptFiles(['table/relation.js', 'indexes.js']);
  98. // Set the database
  99. $this->dbi->selectDb($this->db);
  100. // updates for Internal relations
  101. if (isset($_POST['destination_db']) && $cfgRelation['relwork']) {
  102. $this->updateForInternalRelation($table, $cfgRelation, $relations);
  103. }
  104. // updates for foreign keys
  105. $this->updateForForeignKeys($table, $options, $relationsForeign);
  106. // Updates for display field
  107. if ($cfgRelation['displaywork'] && isset($_POST['display_field'])) {
  108. $this->updateForDisplayField($table, $cfgRelation);
  109. }
  110. // If we did an update, refresh our data
  111. if (isset($_POST['destination_db']) && $cfgRelation['relwork']) {
  112. $relations = $this->relation->getForeigners(
  113. $this->db,
  114. $this->table,
  115. '',
  116. 'internal'
  117. );
  118. }
  119. if (isset($_POST['destination_foreign_db'])
  120. && Util::isForeignKeySupported($storageEngine)
  121. ) {
  122. $relationsForeign = $this->relation->getForeigners(
  123. $this->db,
  124. $this->table,
  125. '',
  126. 'foreign'
  127. );
  128. }
  129. /**
  130. * Dialog
  131. */
  132. // Now find out the columns of our $table
  133. // need to use DatabaseInterface::QUERY_STORE with $this->dbi->numRows()
  134. // in mysqli
  135. $columns = $this->dbi->getColumns($this->db, $this->table);
  136. $column_array = [];
  137. $column_hash_array = [];
  138. $column_array[''] = '';
  139. foreach ($columns as $column) {
  140. if (strtoupper($storageEngine) !== 'INNODB'
  141. && empty($column['Key'])
  142. ) {
  143. continue;
  144. }
  145. $column_array[$column['Field']] = $column['Field'];
  146. $column_hash_array[$column['Field']] = md5($column['Field']);
  147. }
  148. if ($GLOBALS['cfg']['NaturalOrder']) {
  149. uksort($column_array, 'strnatcasecmp');
  150. }
  151. // common form
  152. $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine();
  153. $this->render('table/relation/common_form', [
  154. 'is_foreign_key_supported' => Util::isForeignKeySupported($engine),
  155. 'db' => $this->db,
  156. 'table' => $this->table,
  157. 'cfg_relation' => $cfgRelation,
  158. 'tbl_storage_engine' => $storageEngine,
  159. 'existrel' => $relations,
  160. 'existrel_foreign' => array_key_exists('foreign_keys_data', $relationsForeign)
  161. ? $relationsForeign['foreign_keys_data']
  162. : [],
  163. 'options_array' => $options,
  164. 'column_array' => $column_array,
  165. 'column_hash_array' => $column_hash_array,
  166. 'save_row' => array_values($columns),
  167. 'url_params' => $GLOBALS['url_params'],
  168. 'databases' => $GLOBALS['dblist']->databases,
  169. 'dbi' => $this->dbi,
  170. 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
  171. 'route' => $route,
  172. ]);
  173. }
  174. /**
  175. * Update for display field
  176. *
  177. * @param Table $table table
  178. * @param array $cfgRelation relation parameters
  179. */
  180. private function updateForDisplayField(Table $table, array $cfgRelation): void
  181. {
  182. if (! $table->updateDisplayField(
  183. $_POST['display_field'],
  184. $cfgRelation
  185. )) {
  186. return;
  187. }
  188. $this->response->addHTML(
  189. Generator::getMessage(
  190. __('Display column was successfully updated.'),
  191. '',
  192. 'success'
  193. )
  194. );
  195. }
  196. /**
  197. * Update for FK
  198. *
  199. * @param Table $table Table
  200. * @param array $options Options
  201. * @param array $relationsForeign External relations
  202. */
  203. private function updateForForeignKeys(Table $table, array $options, array $relationsForeign): void
  204. {
  205. $multi_edit_columns_name = $_POST['foreign_key_fields_name'] ?? null;
  206. $preview_sql_data = '';
  207. $seen_error = false;
  208. // (for now, one index name only; we keep the definitions if the
  209. // foreign db is not the same)
  210. if (isset($_POST['destination_foreign_db'], $_POST['destination_foreign_table'])
  211. && isset($_POST['destination_foreign_column'])) {
  212. [
  213. $html,
  214. $preview_sql_data,
  215. $display_query,
  216. $seen_error,
  217. ] = $table->updateForeignKeys(
  218. $_POST['destination_foreign_db'],
  219. $multi_edit_columns_name,
  220. $_POST['destination_foreign_table'],
  221. $_POST['destination_foreign_column'],
  222. $options,
  223. $this->table,
  224. array_key_exists('foreign_keys_data', $relationsForeign)
  225. ? $relationsForeign['foreign_keys_data']
  226. : []
  227. );
  228. $this->response->addHTML($html);
  229. }
  230. // If there is a request for SQL previewing.
  231. if (isset($_POST['preview_sql'])) {
  232. Core::previewSQL($preview_sql_data);
  233. exit;
  234. }
  235. if (empty($display_query) || $seen_error) {
  236. return;
  237. }
  238. $GLOBALS['display_query'] = $display_query;
  239. $this->response->addHTML(
  240. Generator::getMessage(
  241. __('Your SQL query has been executed successfully.'),
  242. null,
  243. 'success'
  244. )
  245. );
  246. }
  247. /**
  248. * Update for internal relation
  249. *
  250. * @param Table $table Table
  251. * @param array $cfgRelation Relation parameters
  252. * @param array $relations Relations
  253. */
  254. private function updateForInternalRelation(Table $table, array $cfgRelation, array $relations): void
  255. {
  256. $multi_edit_columns_name = $_POST['fields_name'] ?? null;
  257. if (! $table->updateInternalRelations(
  258. $multi_edit_columns_name,
  259. $_POST['destination_db'],
  260. $_POST['destination_table'],
  261. $_POST['destination_column'],
  262. $cfgRelation,
  263. $relations
  264. )) {
  265. return;
  266. }
  267. $this->response->addHTML(
  268. Generator::getMessage(
  269. __('Internal relationships were successfully updated.'),
  270. '',
  271. 'success'
  272. )
  273. );
  274. }
  275. /**
  276. * Send table columns for foreign table dropdown
  277. */
  278. public function getDropdownValueForTable(): void
  279. {
  280. $foreignTable = $_POST['foreignTable'];
  281. $table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable);
  282. // Since views do not have keys defined on them provide the full list of
  283. // columns
  284. if ($table_obj->isView()) {
  285. $columnList = $table_obj->getColumns(false, false);
  286. } else {
  287. $columnList = $table_obj->getIndexedColumns(false, false);
  288. }
  289. $columns = [];
  290. foreach ($columnList as $column) {
  291. $columns[] = htmlspecialchars($column);
  292. }
  293. if ($GLOBALS['cfg']['NaturalOrder']) {
  294. usort($columns, 'strnatcasecmp');
  295. }
  296. $this->response->addJSON('columns', $columns);
  297. // @todo should be: $server->db($db)->table($table)->primary()
  298. $primary = Index::getPrimary($foreignTable, $_POST['foreignDb']);
  299. if ($primary === false) {
  300. return;
  301. }
  302. $this->response->addJSON('primary', array_keys($primary->getColumns()));
  303. }
  304. /**
  305. * Send database selection values for dropdown
  306. *
  307. * @param string $storageEngine Storage engine.
  308. */
  309. public function getDropdownValueForDatabase(string $storageEngine): void
  310. {
  311. $tables = [];
  312. $foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true';
  313. if ($foreign) {
  314. $query = 'SHOW TABLE STATUS FROM '
  315. . Util::backquote($_POST['foreignDb']);
  316. $tables_rs = $this->dbi->query(
  317. $query,
  318. DatabaseInterface::CONNECT_USER,
  319. DatabaseInterface::QUERY_STORE
  320. );
  321. while ($row = $this->dbi->fetchArray($tables_rs)) {
  322. if (! isset($row['Engine'])
  323. || mb_strtoupper($row['Engine']) != $storageEngine
  324. ) {
  325. continue;
  326. }
  327. $tables[] = htmlspecialchars($row['Name']);
  328. }
  329. } else {
  330. $query = 'SHOW TABLES FROM '
  331. . Util::backquote($_POST['foreignDb']);
  332. $tables_rs = $this->dbi->query(
  333. $query,
  334. DatabaseInterface::CONNECT_USER,
  335. DatabaseInterface::QUERY_STORE
  336. );
  337. while ($row = $this->dbi->fetchArray($tables_rs)) {
  338. $tables[] = htmlspecialchars($row[0]);
  339. }
  340. }
  341. if ($GLOBALS['cfg']['NaturalOrder']) {
  342. usort($tables, 'strnatcasecmp');
  343. }
  344. $this->response->addJSON('tables', $tables);
  345. }
  346. }