PageRenderTime 59ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/classes/Table.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 2713 lines | 1661 code | 344 blank | 708 comment | 303 complexity | 6ef938fb7bd36972c5f2f62e644b91f0 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Html\Generator;
  5. use PhpMyAdmin\Html\MySQLDocumentation;
  6. use PhpMyAdmin\Plugins\Export\ExportSql;
  7. use PhpMyAdmin\Query\Compatibility;
  8. use PhpMyAdmin\Query\Generator as QueryGenerator;
  9. use PhpMyAdmin\SqlParser\Components\Expression;
  10. use PhpMyAdmin\SqlParser\Components\OptionsArray;
  11. use PhpMyAdmin\SqlParser\Context;
  12. use PhpMyAdmin\SqlParser\Parser;
  13. use PhpMyAdmin\SqlParser\Statements\AlterStatement;
  14. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  15. use PhpMyAdmin\SqlParser\Statements\DropStatement;
  16. use PhpMyAdmin\SqlParser\Utils\Table as TableUtils;
  17. use Stringable;
  18. use function __;
  19. use function array_key_exists;
  20. use function array_map;
  21. use function count;
  22. use function end;
  23. use function explode;
  24. use function htmlspecialchars;
  25. use function implode;
  26. use function in_array;
  27. use function is_array;
  28. use function json_decode;
  29. use function json_encode;
  30. use function mb_stripos;
  31. use function mb_strlen;
  32. use function mb_substr;
  33. use function preg_match;
  34. use function preg_replace;
  35. use function rtrim;
  36. use function sprintf;
  37. use function str_contains;
  38. use function str_replace;
  39. use function stripos;
  40. use function strlen;
  41. use function strtolower;
  42. use function strtoupper;
  43. use function substr;
  44. use function substr_compare;
  45. use function trigger_error;
  46. use function trim;
  47. use const E_USER_WARNING;
  48. /**
  49. * Handles everything related to tables
  50. *
  51. * @todo make use of Message and Error
  52. */
  53. class Table implements Stringable
  54. {
  55. /**
  56. * UI preferences properties
  57. */
  58. public const PROP_SORTED_COLUMN = 'sorted_col';
  59. public const PROP_COLUMN_ORDER = 'col_order';
  60. public const PROP_COLUMN_VISIB = 'col_visib';
  61. /** @var string engine (innodb, myisam, bdb, ...) */
  62. public $engine = '';
  63. /** @var string type (view, base table, system view) */
  64. public $type = '';
  65. /** @var array UI preferences */
  66. public $uiprefs = [];
  67. /** @var array errors occurred */
  68. public $errors = [];
  69. /** @var array messages */
  70. public $messages = [];
  71. /** @var string table name */
  72. protected $name = '';
  73. /** @var string database name */
  74. protected $dbName = '';
  75. /** @var DatabaseInterface */
  76. protected $dbi;
  77. /** @var Relation */
  78. private $relation;
  79. /**
  80. * @param string $tableName table name
  81. * @param string $dbName database name
  82. * @param DatabaseInterface|null $dbi database interface for the table
  83. */
  84. public function __construct($tableName, $dbName, ?DatabaseInterface $dbi = null)
  85. {
  86. if (empty($dbi)) {
  87. $dbi = $GLOBALS['dbi'];
  88. }
  89. $this->dbi = $dbi;
  90. $this->name = $tableName;
  91. $this->dbName = $dbName;
  92. $this->relation = new Relation($this->dbi);
  93. }
  94. /**
  95. * returns table name
  96. *
  97. * @see Table::getName()
  98. */
  99. public function __toString(): string
  100. {
  101. return $this->getName();
  102. }
  103. /**
  104. * Table getter
  105. *
  106. * @param string $tableName table name
  107. * @param string $dbName database name
  108. * @param DatabaseInterface|null $dbi database interface for the table
  109. *
  110. * @return Table
  111. */
  112. public static function get($tableName, $dbName, ?DatabaseInterface $dbi = null)
  113. {
  114. return new Table($tableName, $dbName, $dbi);
  115. }
  116. /**
  117. * return the last error
  118. *
  119. * @return string the last error
  120. */
  121. public function getLastError()
  122. {
  123. return end($this->errors);
  124. }
  125. /**
  126. * return the last message
  127. *
  128. * @return string the last message
  129. */
  130. public function getLastMessage()
  131. {
  132. return end($this->messages);
  133. }
  134. /**
  135. * returns table name
  136. *
  137. * @param bool $backquoted whether to quote name with backticks ``
  138. *
  139. * @return string table name
  140. */
  141. public function getName($backquoted = false)
  142. {
  143. if ($backquoted) {
  144. return Util::backquote($this->name);
  145. }
  146. return $this->name;
  147. }
  148. /**
  149. * returns database name for this table
  150. *
  151. * @param bool $backquoted whether to quote name with backticks ``
  152. *
  153. * @return string database name for this table
  154. */
  155. public function getDbName($backquoted = false)
  156. {
  157. if ($backquoted) {
  158. return Util::backquote($this->dbName);
  159. }
  160. return $this->dbName;
  161. }
  162. /**
  163. * returns full name for table, including database name
  164. *
  165. * @param bool $backquoted whether to quote name with backticks ``
  166. *
  167. * @return string
  168. */
  169. public function getFullName($backquoted = false)
  170. {
  171. return $this->getDbName($backquoted) . '.'
  172. . $this->getName($backquoted);
  173. }
  174. /**
  175. * Checks the storage engine used to create table
  176. *
  177. * @param array|string $engine Checks the table engine against an
  178. * array of engine strings or a single string, should be uppercase
  179. */
  180. public function isEngine($engine): bool
  181. {
  182. $tableStorageEngine = $this->getStorageEngine();
  183. if (is_array($engine)) {
  184. foreach ($engine as $e) {
  185. if ($e == $tableStorageEngine) {
  186. return true;
  187. }
  188. }
  189. return false;
  190. }
  191. return $tableStorageEngine == $engine;
  192. }
  193. /**
  194. * returns whether the table is actually a view
  195. */
  196. public function isView(): bool
  197. {
  198. $db = $this->dbName;
  199. $table = $this->name;
  200. if (empty($db) || empty($table)) {
  201. return false;
  202. }
  203. // use cached data or load information with SHOW command
  204. if (
  205. $this->dbi->getCache()->getCachedTableContent([$db, $table]) != null
  206. || $GLOBALS['cfg']['Server']['DisableIS']
  207. ) {
  208. $type = $this->getStatusInfo('TABLE_TYPE');
  209. return $type === 'VIEW' || $type === 'SYSTEM VIEW';
  210. }
  211. // information_schema tables are 'SYSTEM VIEW's
  212. if ($db === 'information_schema') {
  213. return true;
  214. }
  215. // query information_schema
  216. $result = $this->dbi->fetchResult(
  217. 'SELECT TABLE_NAME'
  218. . ' FROM information_schema.VIEWS'
  219. . ' WHERE TABLE_SCHEMA = \'' . $this->dbi->escapeString((string) $db) . '\''
  220. . ' AND TABLE_NAME = \'' . $this->dbi->escapeString((string) $table) . '\''
  221. );
  222. return (bool) $result;
  223. }
  224. /**
  225. * Returns whether the table is actually an updatable view
  226. */
  227. public function isUpdatableView(): bool
  228. {
  229. if (empty($this->dbName) || empty($this->name)) {
  230. return false;
  231. }
  232. $result = $this->dbi->fetchResult(
  233. 'SELECT TABLE_NAME'
  234. . ' FROM information_schema.VIEWS'
  235. . ' WHERE TABLE_SCHEMA = \'' . $this->dbi->escapeString($this->dbName) . '\''
  236. . ' AND TABLE_NAME = \'' . $this->dbi->escapeString($this->name) . '\''
  237. . ' AND IS_UPDATABLE = \'YES\''
  238. );
  239. return (bool) $result;
  240. }
  241. /**
  242. * Checks if this is a merge table
  243. *
  244. * If the ENGINE of the table is MERGE or MRG_MYISAM (alias),
  245. * this is a merge table.
  246. */
  247. public function isMerge(): bool
  248. {
  249. return $this->isEngine(['MERGE', 'MRG_MYISAM']);
  250. }
  251. /**
  252. * Returns full table status info, or specific if $info provided
  253. * this info is collected from information_schema
  254. *
  255. * @param string $info specific information to be fetched
  256. * @param bool $forceRead read new rather than serving from cache
  257. * @param bool $disableError if true, disables error message
  258. *
  259. * @return mixed
  260. *
  261. * @todo DatabaseInterface::getTablesFull needs to be merged
  262. * somehow into this class or at least better documented
  263. */
  264. public function getStatusInfo(
  265. $info = null,
  266. $forceRead = false,
  267. $disableError = false
  268. ) {
  269. $db = $this->dbName;
  270. $table = $this->name;
  271. if (! empty($_SESSION['is_multi_query'])) {
  272. $disableError = true;
  273. }
  274. $cachedResult = $this->dbi->getCache()->getCachedTableContent([$db, $table]);
  275. // sometimes there is only one entry (ExactRows) so
  276. // we have to get the table's details
  277. if ($cachedResult === null || $forceRead || count($cachedResult) === 1) {
  278. $this->dbi->getTablesFull($db, $table);
  279. $cachedResult = $this->dbi->getCache()->getCachedTableContent([$db, $table]);
  280. }
  281. if ($cachedResult === null) {
  282. // happens when we enter the table creation dialog
  283. // or when we really did not get any status info, for example
  284. // when $table === 'TABLE_NAMES' after the user tried SHOW TABLES
  285. return '';
  286. }
  287. if ($info === null) {
  288. return $cachedResult;
  289. }
  290. // array_key_exists allows for null values
  291. if (! array_key_exists($info, $cachedResult)) {
  292. if (! $disableError) {
  293. trigger_error(
  294. __('Unknown table status:') . ' ' . $info,
  295. E_USER_WARNING
  296. );
  297. }
  298. return false;
  299. }
  300. return $this->dbi->getCache()->getCachedTableContent([$db, $table, $info]);
  301. }
  302. /**
  303. * Returns the Table storage Engine for current table.
  304. *
  305. * @return string Return storage engine info if it is set for
  306. * the selected table else return blank.
  307. */
  308. public function getStorageEngine(): string
  309. {
  310. $tableStorageEngine = $this->getStatusInfo('ENGINE', false, true);
  311. if ($tableStorageEngine === false) {
  312. return '';
  313. }
  314. return strtoupper((string) $tableStorageEngine);
  315. }
  316. /**
  317. * Returns the comments for current table.
  318. *
  319. * @return string Return comment info if it is set for the selected table or return blank.
  320. */
  321. public function getComment()
  322. {
  323. $tableComment = $this->getStatusInfo('TABLE_COMMENT', false, true);
  324. if ($tableComment === false) {
  325. return '';
  326. }
  327. return $tableComment;
  328. }
  329. /**
  330. * Returns the collation for current table.
  331. *
  332. * @return string Return blank if collation is empty else return the collation info from table info.
  333. */
  334. public function getCollation()
  335. {
  336. $tableCollation = $this->getStatusInfo('TABLE_COLLATION', false, true);
  337. if ($tableCollation === false) {
  338. return '';
  339. }
  340. return $tableCollation;
  341. }
  342. /**
  343. * Returns the info about no of rows for current table.
  344. *
  345. * @return int Return no of rows info if it is not null for the selected table or return 0.
  346. */
  347. public function getNumRows()
  348. {
  349. $tableNumRowInfo = $this->getStatusInfo('TABLE_ROWS', false, true);
  350. if ($tableNumRowInfo === false) {
  351. $tableNumRowInfo = $this->dbi->getTable($this->dbName, $GLOBALS['showtable']['Name'])
  352. ->countRecords(true);
  353. }
  354. return $tableNumRowInfo ?: 0;
  355. }
  356. /**
  357. * Returns the Row format for current table.
  358. *
  359. * @return string Return table row format info if it is set for the selected table or return blank.
  360. */
  361. public function getRowFormat()
  362. {
  363. $tableRowFormat = $this->getStatusInfo('ROW_FORMAT', false, true);
  364. if ($tableRowFormat === false) {
  365. return '';
  366. }
  367. return $tableRowFormat;
  368. }
  369. /**
  370. * Returns the auto increment option for current table.
  371. *
  372. * @return int Return auto increment info if it is set for the selected table or return blank.
  373. */
  374. public function getAutoIncrement()
  375. {
  376. $tableAutoIncrement = $this->getStatusInfo('AUTO_INCREMENT', false, true);
  377. return $tableAutoIncrement ?? '';
  378. }
  379. /**
  380. * Returns the array for CREATE statement for current table.
  381. *
  382. * @return array Return options array info if it is set for the selected table or return blank.
  383. */
  384. public function getCreateOptions()
  385. {
  386. $tableOptions = $this->getStatusInfo('CREATE_OPTIONS', false, true);
  387. $createOptionsTmp = empty($tableOptions) ? [] : explode(' ', $tableOptions);
  388. $createOptions = [];
  389. // export create options by its name as variables into global namespace
  390. // f.e. pack_keys=1 becomes available as $pack_keys with value of '1'
  391. // unset($pack_keys);
  392. foreach ($createOptionsTmp as $eachCreateOption) {
  393. $eachCreateOption = explode('=', $eachCreateOption);
  394. if (! isset($eachCreateOption[1])) {
  395. continue;
  396. }
  397. // ensure there is no ambiguity for PHP 5 and 7
  398. $createOptions[$eachCreateOption[0]] = $eachCreateOption[1];
  399. }
  400. // we need explicit DEFAULT value here (different from '0')
  401. $hasPackKeys = isset($createOptions['pack_keys']) && strlen($createOptions['pack_keys']) > 0;
  402. $createOptions['pack_keys'] = $hasPackKeys ? $createOptions['pack_keys'] : 'DEFAULT';
  403. return $createOptions;
  404. }
  405. /**
  406. * generates column specification for ALTER or CREATE TABLE syntax
  407. *
  408. * @param string $name name
  409. * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
  410. * @param string $length length ('2', '5,2', '', ...)
  411. * @param string $attribute attribute
  412. * @param string $collation collation
  413. * @param bool|string $null with 'NULL' or 'NOT NULL'
  414. * @param string $defaultType whether default is CURRENT_TIMESTAMP,
  415. * NULL, NONE, USER_DEFINED
  416. * @param string $defaultValue default value for USER_DEFINED
  417. * default type
  418. * @param string $extra 'AUTO_INCREMENT'
  419. * @param string $comment field comment
  420. * @param string $virtuality virtuality of the column
  421. * @param string $expression expression for the virtual column
  422. * @param string $moveTo new position for column
  423. * @param array $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
  424. * @param string $oldColumnName Old column name
  425. *
  426. * @return string field specification
  427. *
  428. * @todo move into class PMA_Column
  429. * @todo on the interface, some js to clear the default value when the
  430. * default current_timestamp is checked
  431. */
  432. public static function generateFieldSpec(
  433. $name,
  434. string $type,
  435. string $length = '',
  436. $attribute = '',
  437. $collation = '',
  438. $null = false,
  439. $defaultType = 'USER_DEFINED',
  440. $defaultValue = '',
  441. $extra = '',
  442. $comment = '',
  443. $virtuality = '',
  444. $expression = '',
  445. $moveTo = '',
  446. $columnsWithIndex = null,
  447. $oldColumnName = null
  448. ) {
  449. global $dbi;
  450. $strLength = strlen($length);
  451. $isTimestamp = mb_stripos($type, 'TIMESTAMP') !== false;
  452. $query = Util::backquote($name) . ' ' . $type;
  453. // allow the possibility of a length for TIME, DATETIME and TIMESTAMP
  454. // (will work on MySQL >= 5.6.4)
  455. //
  456. // MySQL permits a non-standard syntax for FLOAT and DOUBLE,
  457. // see https://dev.mysql.com/doc/refman/5.5/en/floating-point-types.html
  458. $pattern = '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|'
  459. . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN|UUID|JSON)$@i';
  460. if (
  461. $strLength !== 0
  462. && ! preg_match($pattern, $type)
  463. && Compatibility::isIntegersSupportLength($type, $length, $dbi)
  464. ) {
  465. // Note: The variable $length here can contain several other things
  466. // besides length - ENUM/SET value or length of DECIMAL (eg. 12,3)
  467. // so we can't just convert it to integer
  468. $query .= '(' . $length . ')';
  469. }
  470. if ($attribute != '') {
  471. $query .= ' ' . $attribute;
  472. if ($isTimestamp && stripos($attribute, 'TIMESTAMP') !== false && $strLength !== 0) {
  473. $query .= '(' . $length . ')';
  474. }
  475. }
  476. // if column is virtual, check if server type is Mysql as only Mysql server
  477. // supports extra column properties
  478. $isVirtualColMysql = $virtuality && Compatibility::isMySqlOrPerconaDb();
  479. // if column is virtual, check if server type is MariaDB as MariaDB server
  480. // supports no extra virtual column properties except CHARACTER SET for text column types
  481. $isVirtualColMariaDB = $virtuality && Compatibility::isMariaDb();
  482. $matches = preg_match('@^(TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|VARCHAR|CHAR|ENUM|SET)$@i', $type);
  483. if (! empty($collation) && $collation !== 'NULL' && $matches) {
  484. $query .= Util::getCharsetQueryPart(
  485. $isVirtualColMariaDB ? (string) preg_replace('~_.+~s', '', $collation) : $collation,
  486. true
  487. );
  488. }
  489. if ($virtuality) {
  490. $query .= ' AS (' . $expression . ') ' . $virtuality;
  491. }
  492. if (! $virtuality || $isVirtualColMysql) {
  493. if ($null !== false) {
  494. if ($null === 'YES') {
  495. $query .= ' NULL';
  496. } else {
  497. $query .= ' NOT NULL';
  498. }
  499. }
  500. if (! $virtuality) {
  501. switch ($defaultType) {
  502. case 'USER_DEFINED':
  503. if ($isTimestamp && $defaultValue === '0') {
  504. // a TIMESTAMP does not accept DEFAULT '0'
  505. // but DEFAULT 0 works
  506. $query .= ' DEFAULT 0';
  507. } elseif (
  508. $isTimestamp
  509. && preg_match(
  510. '/^\'\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})?\'$/',
  511. (string) $defaultValue
  512. )
  513. ) {
  514. $query .= ' DEFAULT ' . (string) $defaultValue;
  515. } elseif ($type === 'BIT') {
  516. $query .= ' DEFAULT b\''
  517. . preg_replace('/[^01]/', '0', (string) $defaultValue)
  518. . '\'';
  519. } elseif ($type === 'BOOLEAN') {
  520. if (preg_match('/^1|T|TRUE|YES$/i', (string) $defaultValue)) {
  521. $query .= ' DEFAULT TRUE';
  522. } elseif (preg_match('/^0|F|FALSE|NO$/i', $defaultValue)) {
  523. $query .= ' DEFAULT FALSE';
  524. } else {
  525. // Invalid BOOLEAN value
  526. $query .= ' DEFAULT \''
  527. . $dbi->escapeString($defaultValue) . '\'';
  528. }
  529. } elseif ($type === 'BINARY' || $type === 'VARBINARY') {
  530. $query .= ' DEFAULT 0x' . $defaultValue;
  531. } else {
  532. $query .= ' DEFAULT \''
  533. . $dbi->escapeString((string) $defaultValue) . '\'';
  534. }
  535. break;
  536. /** @noinspection PhpMissingBreakStatementInspection */
  537. case 'NULL':
  538. // If user uncheck null checkbox and not change default value null,
  539. // default value will be ignored.
  540. if ($null !== false && $null !== 'YES') {
  541. break;
  542. }
  543. // else fall-through intended, no break here
  544. case 'CURRENT_TIMESTAMP':
  545. case 'current_timestamp()':
  546. $query .= ' DEFAULT ' . $defaultType;
  547. if (
  548. $strLength !== 0
  549. && $isTimestamp
  550. && $defaultType !== 'NULL' // Not to be added in case of NULL
  551. ) {
  552. $query .= '(' . $length . ')';
  553. }
  554. break;
  555. case 'NONE':
  556. default:
  557. break;
  558. }
  559. }
  560. if (! empty($extra)) {
  561. if ($virtuality) {
  562. $extra = trim((string) preg_replace('~^\s*AUTO_INCREMENT\s*~is', ' ', $extra));
  563. }
  564. $query .= ' ' . $extra;
  565. }
  566. }
  567. if (! empty($comment)) {
  568. $query .= " COMMENT '" . $dbi->escapeString($comment) . "'";
  569. }
  570. // move column
  571. if ($moveTo === '-first') { // dash can't appear as part of column name
  572. $query .= ' FIRST';
  573. } elseif ($moveTo != '') {
  574. $query .= ' AFTER ' . Util::backquote($moveTo);
  575. }
  576. if (! $virtuality && ! empty($extra)) {
  577. if ($oldColumnName === null) {
  578. if (is_array($columnsWithIndex) && ! in_array($name, $columnsWithIndex)) {
  579. $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
  580. }
  581. } else {
  582. if (is_array($columnsWithIndex) && ! in_array($oldColumnName, $columnsWithIndex)) {
  583. $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
  584. }
  585. }
  586. }
  587. return $query;
  588. }
  589. /**
  590. * Checks if the number of records in a table is at least equal to
  591. * $min_records
  592. *
  593. * @param int $minRecords Number of records to check for in a table
  594. */
  595. public function checkIfMinRecordsExist($minRecords = 0): bool
  596. {
  597. $checkQuery = 'SELECT ';
  598. $uniqueFields = $this->getUniqueColumns(true, false);
  599. if (count($uniqueFields) > 0) {
  600. $fieldsToSelect = implode(', ', $uniqueFields);
  601. } else {
  602. $indexedCols = $this->getIndexedColumns(true, false);
  603. if (count($indexedCols) > 0) {
  604. $fieldsToSelect = implode(', ', $indexedCols);
  605. } else {
  606. $fieldsToSelect = '*';
  607. }
  608. }
  609. $checkQuery .= $fieldsToSelect
  610. . ' FROM ' . $this->getFullName(true)
  611. . ' LIMIT ' . $minRecords;
  612. $res = $this->dbi->tryQuery($checkQuery);
  613. if ($res !== false) {
  614. $numRecords = $this->dbi->numRows($res);
  615. if ($numRecords >= $minRecords) {
  616. return true;
  617. }
  618. }
  619. return false;
  620. }
  621. /**
  622. * Counts and returns (or displays) the number of records in a table
  623. *
  624. * @param bool $forceExact whether to force an exact count
  625. *
  626. * @return mixed the number of records if "retain" param is true,
  627. * otherwise true
  628. */
  629. public function countRecords($forceExact = false)
  630. {
  631. $isView = $this->isView();
  632. $db = $this->dbName;
  633. $table = $this->name;
  634. if ($this->dbi->getCache()->getCachedTableContent([$db, $table, 'ExactRows']) != null) {
  635. return $this->dbi->getCache()->getCachedTableContent(
  636. [
  637. $db,
  638. $table,
  639. 'ExactRows',
  640. ]
  641. );
  642. }
  643. $rowCount = false;
  644. if (! $forceExact) {
  645. if (($this->dbi->getCache()->getCachedTableContent([$db, $table, 'Rows']) == null) && ! $isView) {
  646. $tmpTables = $this->dbi->getTablesFull($db, $table);
  647. if (isset($tmpTables[$table])) {
  648. $this->dbi->getCache()->cacheTableContent(
  649. [
  650. $db,
  651. $table,
  652. ],
  653. $tmpTables[$table]
  654. );
  655. }
  656. }
  657. if ($this->dbi->getCache()->getCachedTableContent([$db, $table, 'Rows']) != null) {
  658. $rowCount = $this->dbi->getCache()->getCachedTableContent(
  659. [
  660. $db,
  661. $table,
  662. 'Rows',
  663. ]
  664. );
  665. } else {
  666. $rowCount = false;
  667. }
  668. }
  669. // for a VIEW, $row_count is always false at this point
  670. if ($rowCount !== false && $rowCount >= $GLOBALS['cfg']['MaxExactCount']) {
  671. return $rowCount;
  672. }
  673. if (! $isView) {
  674. $rowCount = $this->dbi->fetchValue(
  675. 'SELECT COUNT(*) FROM ' . Util::backquote($db) . '.'
  676. . Util::backquote($table)
  677. );
  678. } else {
  679. // For complex views, even trying to get a partial record
  680. // count could bring down a server, so we offer an
  681. // alternative: setting MaxExactCountViews to 0 will bypass
  682. // completely the record counting for views
  683. if ($GLOBALS['cfg']['MaxExactCountViews'] == 0) {
  684. $rowCount = false;
  685. } else {
  686. // Counting all rows of a VIEW could be too long,
  687. // so use a LIMIT clause.
  688. // Use try_query because it can fail (when a VIEW is
  689. // based on a table that no longer exists)
  690. $result = $this->dbi->tryQuery(
  691. 'SELECT 1 FROM ' . Util::backquote($db) . '.'
  692. . Util::backquote($table) . ' LIMIT '
  693. . $GLOBALS['cfg']['MaxExactCountViews'],
  694. DatabaseInterface::CONNECT_USER,
  695. DatabaseInterface::QUERY_STORE
  696. );
  697. if (! $this->dbi->getError()) {
  698. $rowCount = $this->dbi->numRows($result);
  699. $this->dbi->freeResult($result);
  700. }
  701. }
  702. }
  703. if ($rowCount) {
  704. $this->dbi->getCache()->cacheTableContent([$db, $table, 'ExactRows'], $rowCount);
  705. }
  706. return $rowCount;
  707. }
  708. /**
  709. * Generates column specification for ALTER syntax
  710. *
  711. * @see Table::generateFieldSpec()
  712. *
  713. * @param string $oldcol old column name
  714. * @param string $newcol new column name
  715. * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
  716. * @param string $length length ('2', '5,2', '', ...)
  717. * @param string $attribute attribute
  718. * @param string $collation collation
  719. * @param bool|string $null with 'NULL' or 'NOT NULL'
  720. * @param string $defaultType whether default is CURRENT_TIMESTAMP,
  721. * NULL, NONE, USER_DEFINED
  722. * @param string $defaultValue default value for USER_DEFINED default
  723. * type
  724. * @param string $extra 'AUTO_INCREMENT'
  725. * @param string $comment field comment
  726. * @param string $virtuality virtuality of the column
  727. * @param string $expression expression for the virtual column
  728. * @param string $moveTo new position for column
  729. * @param array $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
  730. *
  731. * @return string field specification
  732. */
  733. public static function generateAlter(
  734. $oldcol,
  735. $newcol,
  736. $type,
  737. $length,
  738. $attribute,
  739. $collation,
  740. $null,
  741. $defaultType,
  742. $defaultValue,
  743. $extra,
  744. $comment,
  745. $virtuality,
  746. $expression,
  747. $moveTo,
  748. $columnsWithIndex = null
  749. ) {
  750. return Util::backquote($oldcol) . ' '
  751. . self::generateFieldSpec(
  752. $newcol,
  753. $type,
  754. $length,
  755. $attribute,
  756. $collation,
  757. $null,
  758. $defaultType,
  759. $defaultValue,
  760. $extra,
  761. $comment,
  762. $virtuality,
  763. $expression,
  764. $moveTo,
  765. $columnsWithIndex,
  766. $oldcol
  767. );
  768. }
  769. /**
  770. * Inserts existing entries in a PMA_* table by reading a value from an old
  771. * entry
  772. *
  773. * @param string $work The array index, which Relation feature to check ('relwork', 'commwork', ...)
  774. * @param string $table The array index, which PMA-table to update ('bookmark', 'relation', ...)
  775. * @param array $getFields Which fields will be SELECT'ed from the old entry
  776. * @param array $whereFields Which fields will be used for the WHERE query (array('FIELDNAME' => 'FIELDVALUE'))
  777. * @param array $newFields Which fields will be used as new VALUES. These are the important keys which differ
  778. * from the old entry (array('FIELDNAME' => 'NEW FIELDVALUE'))
  779. *
  780. * @return int|bool
  781. */
  782. public static function duplicateInfo(
  783. $work,
  784. $table,
  785. array $getFields,
  786. array $whereFields,
  787. array $newFields
  788. ) {
  789. global $dbi;
  790. $relation = new Relation($dbi);
  791. $lastId = -1;
  792. if (! isset($GLOBALS['cfgRelation']) || ! $GLOBALS['cfgRelation'][$work]) {
  793. return true;
  794. }
  795. $selectParts = [];
  796. $rowFields = [];
  797. foreach ($getFields as $getField) {
  798. $selectParts[] = Util::backquote($getField);
  799. $rowFields[$getField] = 'cc';
  800. }
  801. $whereParts = [];
  802. foreach ($whereFields as $where => $value) {
  803. $whereParts[] = Util::backquote($where) . ' = \''
  804. . $dbi->escapeString((string) $value) . '\'';
  805. }
  806. $newParts = [];
  807. $newValueParts = [];
  808. foreach ($newFields as $where => $value) {
  809. $newParts[] = Util::backquote($where);
  810. $newValueParts[] = $dbi->escapeString((string) $value);
  811. }
  812. $tableCopyQuery = '
  813. SELECT ' . implode(', ', $selectParts) . '
  814. FROM ' . Util::backquote($GLOBALS['cfgRelation']['db']) . '.'
  815. . Util::backquote($GLOBALS['cfgRelation'][$table]) . '
  816. WHERE ' . implode(' AND ', $whereParts);
  817. // must use DatabaseInterface::QUERY_STORE here, since we execute
  818. // another query inside the loop
  819. $tableCopyRs = $relation->queryAsControlUser($tableCopyQuery, true, DatabaseInterface::QUERY_STORE);
  820. while ($tableCopyRow = @$dbi->fetchAssoc($tableCopyRs)) {
  821. $valueParts = [];
  822. foreach ($tableCopyRow as $key => $val) {
  823. if (! isset($rowFields[$key]) || $rowFields[$key] != 'cc') {
  824. continue;
  825. }
  826. $valueParts[] = $dbi->escapeString($val);
  827. }
  828. $newTableQuery = 'INSERT IGNORE INTO '
  829. . Util::backquote($GLOBALS['cfgRelation']['db'])
  830. . '.' . Util::backquote($GLOBALS['cfgRelation'][$table])
  831. . ' (' . implode(', ', $selectParts) . ', '
  832. . implode(', ', $newParts) . ') VALUES (\''
  833. . implode('\', \'', $valueParts) . '\', \''
  834. . implode('\', \'', $newValueParts) . '\')';
  835. $relation->queryAsControlUser($newTableQuery);
  836. $lastId = $dbi->insertId();
  837. }
  838. $dbi->freeResult($tableCopyRs);
  839. return $lastId;
  840. }
  841. /**
  842. * Copies or renames table
  843. *
  844. * @param string $sourceDb source database
  845. * @param string $sourceTable source table
  846. * @param string|null $targetDb target database
  847. * @param string $targetTable target table
  848. * @param string $what what to be moved or copied (data, dataonly)
  849. * @param bool $move whether to move
  850. * @param string $mode mode
  851. */
  852. public static function moveCopy(
  853. $sourceDb,
  854. $sourceTable,
  855. ?string $targetDb,
  856. $targetTable,
  857. $what,
  858. $move,
  859. $mode,
  860. bool $addDropIfExists
  861. ): bool {
  862. global $errorUrl, $dbi;
  863. $relation = new Relation($dbi);
  864. // Try moving the tables directly, using native `RENAME` statement.
  865. if ($move && $what === 'data') {
  866. $tbl = new Table($sourceTable, $sourceDb);
  867. if ($tbl->rename($targetTable, $targetDb)) {
  868. $GLOBALS['message'] = $tbl->getLastMessage();
  869. return true;
  870. }
  871. }
  872. // Setting required export settings.
  873. $GLOBALS['sql_backquotes'] = 1;
  874. $GLOBALS['asfile'] = 1;
  875. // Ensuring the target database is valid.
  876. if (! $GLOBALS['dblist']->databases->exists($sourceDb, $targetDb)) {
  877. if (! $GLOBALS['dblist']->databases->exists($sourceDb)) {
  878. $GLOBALS['message'] = Message::rawError(
  879. sprintf(
  880. __('Source database `%s` was not found!'),
  881. htmlspecialchars($sourceDb)
  882. )
  883. );
  884. }
  885. if (! $GLOBALS['dblist']->databases->exists($targetDb)) {
  886. $GLOBALS['message'] = Message::rawError(
  887. sprintf(
  888. __('Target database `%s` was not found!'),
  889. htmlspecialchars((string) $targetDb)
  890. )
  891. );
  892. }
  893. return false;
  894. }
  895. /**
  896. * The full name of source table, quoted.
  897. *
  898. * @var string $source
  899. */
  900. $source = Util::backquote($sourceDb)
  901. . '.' . Util::backquote($sourceTable);
  902. // If the target database is not specified, the operation is taking
  903. // place in the same database.
  904. if (! isset($targetDb) || strlen($targetDb) === 0) {
  905. $targetDb = $sourceDb;
  906. }
  907. // Selecting the database could avoid some problems with replicated
  908. // databases, when moving table from replicated one to not replicated one.
  909. $dbi->selectDb($targetDb);
  910. /**
  911. * The full name of target table, quoted.
  912. *
  913. * @var string $target
  914. */
  915. $target = Util::backquote($targetDb)
  916. . '.' . Util::backquote($targetTable);
  917. // No table is created when this is a data-only operation.
  918. if ($what !== 'dataonly') {
  919. /**
  920. * Instance used for exporting the current structure of the table.
  921. *
  922. * @var ExportSql $exportSqlPlugin
  923. */
  924. $exportSqlPlugin = Plugins::getPlugin('export', 'sql', [
  925. 'export_type' => 'table',
  926. 'single_table' => false,
  927. ]);
  928. $noConstraintsComments = true;
  929. $GLOBALS['sql_constraints_query'] = '';
  930. // set the value of global sql_auto_increment variable
  931. if (isset($_POST['sql_auto_increment'])) {
  932. $GLOBALS['sql_auto_increment'] = $_POST['sql_auto_increment'];
  933. }
  934. /**
  935. * The old structure of the table..
  936. *
  937. * @var string $sqlStructure
  938. */
  939. $sqlStructure = $exportSqlPlugin->getTableDef($sourceDb, $sourceTable, "\n", $errorUrl, false, false);
  940. unset($noConstraintsComments);
  941. // -----------------------------------------------------------------
  942. // Phase 0: Preparing structures used.
  943. /**
  944. * The destination where the table is moved or copied to.
  945. *
  946. * @var Expression
  947. */
  948. $destination = new Expression($targetDb, $targetTable, '');
  949. // Find server's SQL mode so the builder can generate correct
  950. // queries.
  951. // One of the options that alters the behaviour is `ANSI_QUOTES`.
  952. Context::setMode((string) $dbi->fetchValue('SELECT @@sql_mode'));
  953. // -----------------------------------------------------------------
  954. // Phase 1: Dropping existent element of the same name (if exists
  955. // and required).
  956. if ($addDropIfExists) {
  957. /**
  958. * Drop statement used for building the query.
  959. *
  960. * @var DropStatement $statement
  961. */
  962. $statement = new DropStatement();
  963. $tbl = new Table($targetDb, $targetTable);
  964. $statement->options = new OptionsArray(
  965. [
  966. $tbl->isView() ? 'VIEW' : 'TABLE',
  967. 'IF EXISTS',
  968. ]
  969. );
  970. $statement->fields = [$destination];
  971. // Building the query.
  972. $dropQuery = $statement->build() . ';';
  973. // Executing it.
  974. $dbi->query($dropQuery);
  975. $GLOBALS['sql_query'] .= "\n" . $dropQuery;
  976. // If an existing table gets deleted, maintain any entries for
  977. // the PMA_* tables.
  978. $maintainRelations = true;
  979. }
  980. // -----------------------------------------------------------------
  981. // Phase 2: Generating the new query of this structure.
  982. /**
  983. * The parser responsible for parsing the old queries.
  984. *
  985. * @var Parser $parser
  986. */
  987. $parser = new Parser($sqlStructure);
  988. if (! empty($parser->statements[0])) {
  989. /**
  990. * The CREATE statement of this structure.
  991. *
  992. * @var CreateStatement $statement
  993. */
  994. $statement = $parser->statements[0];
  995. // Changing the destination.
  996. $statement->name = $destination;
  997. // Building back the query.
  998. $sqlStructure = $statement->build() . ';';
  999. // This is to avoid some issues when renaming databases with views
  1000. // See: https://github.com/phpmyadmin/phpmyadmin/issues/16422
  1001. if ($move) {
  1002. $dbi->selectDb($targetDb);
  1003. }
  1004. // Executing it
  1005. $dbi->query($sqlStructure);
  1006. $GLOBALS['sql_query'] .= "\n" . $sqlStructure;
  1007. }
  1008. // -----------------------------------------------------------------
  1009. // Phase 3: Adding constraints.
  1010. // All constraint names are removed because they must be unique.
  1011. if (($move || isset($GLOBALS['add_constraints'])) && ! empty($GLOBALS['sql_constraints_query'])) {
  1012. $parser = new Parser($GLOBALS['sql_constraints_query']);
  1013. /**
  1014. * The ALTER statement that generates the constraints.
  1015. *
  1016. * @var AlterStatement $statement
  1017. */
  1018. $statement = $parser->statements[0];
  1019. // Changing the altered table to the destination.
  1020. $statement->table = $destination;
  1021. // Removing the name of the constraints.
  1022. foreach ($statement->altered as $idx => $altered) {
  1023. // All constraint names are removed because they must be unique.
  1024. if (! $altered->options->has('CONSTRAINT')) {
  1025. continue;
  1026. }
  1027. $altered->field = null;
  1028. }
  1029. // Building back the query.
  1030. $GLOBALS['sql_constraints_query'] = $statement->build() . ';';
  1031. // Executing it.
  1032. if ($mode === 'one_table') {
  1033. $dbi->query($GLOBALS['sql_constraints_query']);
  1034. }
  1035. $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_constraints_query'];
  1036. if ($mode === 'one_table') {
  1037. unset($GLOBALS['sql_constraints_query']);
  1038. }
  1039. }
  1040. // -----------------------------------------------------------------
  1041. // Phase 4: Adding indexes.
  1042. // View phase 3.
  1043. if (! empty($GLOBALS['sql_indexes'])) {
  1044. $parser = new Parser($GLOBALS['sql_indexes']);
  1045. $GLOBALS['sql_indexes'] = '';
  1046. /**
  1047. * The ALTER statement that generates the indexes.
  1048. *
  1049. * @var AlterStatement $statement
  1050. */
  1051. foreach ($parser->statements as $statement) {
  1052. // Changing the altered table to the destination.
  1053. $statement->table = $destination;
  1054. // Removing the name of the constraints.
  1055. foreach ($statement->altered as $idx => $altered) {
  1056. // All constraint names are removed because they must be unique.
  1057. if (! $altered->options->has('CONSTRAINT')) {
  1058. continue;
  1059. }
  1060. $altered->field = null;
  1061. }
  1062. // Building back the query.
  1063. $sqlIndex = $statement->build() . ';';
  1064. // Executing it.
  1065. if ($mode === 'one_table' || $mode === 'db_copy') {
  1066. $dbi->query($sqlIndex);
  1067. }
  1068. $GLOBALS['sql_indexes'] .= $sqlIndex;
  1069. }
  1070. $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_indexes'];
  1071. if ($mode === 'one_table' || $mode === 'db_copy') {
  1072. unset($GLOBALS['sql_indexes']);
  1073. }
  1074. }
  1075. // -----------------------------------------------------------------
  1076. // Phase 5: Adding AUTO_INCREMENT.
  1077. if (! empty($GLOBALS['sql_auto_increments'])) {
  1078. if ($mode === 'one_table' || $mode === 'db_copy') {
  1079. $parser = new Parser($GLOBALS['sql_auto_increments']);
  1080. /**
  1081. * The ALTER statement that alters the AUTO_INCREMENT value.
  1082. *
  1083. * @var AlterStatement $statement
  1084. */
  1085. $statement = $parser->statements[0];
  1086. // Changing the altered table to the destination.
  1087. $statement->table = $destination;
  1088. // Building back the query.
  1089. $GLOBALS['sql_auto_increments'] = $statement->build() . ';';
  1090. // Executing it.
  1091. $dbi->query($GLOBALS['sql_auto_increments']);
  1092. $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_auto_increments'];
  1093. unset($GLOBALS['sql_auto_increments']);
  1094. }
  1095. }
  1096. } else {
  1097. $GLOBALS['sql_query'] = '';
  1098. }
  1099. $table = new Table($targetTable, $targetDb);
  1100. // Copy the data unless this is a VIEW
  1101. if (($what === 'data' || $what === 'dataonly') && ! $table->isView()) {
  1102. $sqlSetMode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
  1103. $dbi->query($sqlSetMode);
  1104. $GLOBALS['sql_query'] .= "\n\n" . $sqlSetMode . ';';
  1105. $oldTable = new Table($sourceTable, $sourceDb);
  1106. $nonGeneratedCols = $oldTable->getNonGeneratedColumns(true);
  1107. if (count($nonGeneratedCols) > 0) {
  1108. $sqlInsertData = 'INSERT INTO ' . $target . '('
  1109. . implode(', ', $nonGeneratedCols)
  1110. . ') SELECT ' . implode(', ', $nonGeneratedCols)
  1111. . ' FROM ' . $source;
  1112. $dbi->query($sqlInsertData);
  1113. $GLOBALS['sql_query'] .= "\n\n" . $sqlInsertData . ';';
  1114. }
  1115. }
  1116. $relation->getRelationsParam();
  1117. // Drops old table if the user has requested to move it
  1118. if ($move) {
  1119. // This could avoid some problems with replicated databases, when
  1120. // moving table from replicated one to not replicated one
  1121. $dbi->selectDb($sourceDb);
  1122. $sourceTableObj = new Table($sourceTable, $sourceDb);
  1123. if ($sourceTableObj->isView()) {
  1124. $sqlDropQuery = 'DROP VIEW';
  1125. } else {
  1126. $sqlDropQuery = 'DROP TABLE';
  1127. }
  1128. $sqlDropQuery .= ' ' . $source;
  1129. $dbi->query($sqlDropQuery);
  1130. // Rename table in configuration storage
  1131. $relation->renameTable($sourceDb, $targetDb, $sourceTable, $targetTable);
  1132. $GLOBALS['sql_query'] .= "\n\n" . $sqlDropQuery . ';';
  1133. return true;
  1134. }
  1135. // we are copying
  1136. // Create new entries as duplicates from old PMA DBs
  1137. if ($what === 'dataonly' || isset($maintainRelations)) {
  1138. return true;
  1139. }
  1140. if ($GLOBALS['cfgRelation']['commwork']) {
  1141. // Get all comments and MIME-Types for current table
  1142. $commentsCopyRs = $relation->queryAsControlUser(
  1143. 'SELECT column_name, comment'
  1144. . ($GLOBALS['cfgRelation']['mimework']
  1145. ? ', mimetype, transformation, transformation_options'
  1146. : '')
  1147. . ' FROM '
  1148. . Util::backquote($GLOBALS['cfgRelation']['db'])
  1149. . '.'
  1150. . Util::backquote($GLOBALS['cfgRelation']['column_info'])
  1151. . ' WHERE '
  1152. . ' db_name = \''
  1153. . $dbi->escapeString($sourceDb) . '\''
  1154. . ' AND '
  1155. . ' table_name = \''
  1156. . $dbi->escapeString((string) $sourceTable) . '\''
  1157. );
  1158. // Write every comment as new copied entry. [MIME]
  1159. while ($commentsCopyRow = $dbi->fetchAssoc($commentsCopyRs)) {
  1160. $newCommentQuery = 'REPLACE INTO '
  1161. . Util::backquote($GLOBALS['cfgRelation']['db'])
  1162. . '.' . Util::backquote($GLOBALS['cfgRelation']['column_info'])
  1163. . ' (db_name, table_name, column_name, comment'
  1164. . ($GLOBALS['cfgRelation']['mimework']
  1165. ? ', mimetype, transformation, transformation_options'
  1166. : '')
  1167. . ') VALUES(\'' . $dbi->escapeString($targetDb)
  1168. . '\',\'' . $dbi->escapeString($targetTable) . '\',\''
  1169. . $dbi->escapeString($commentsCopyRow['column_name'])
  1170. . '\',\''
  1171. . $dbi->escapeString($commentsCopyRow['comment'])
  1172. . '\''
  1173. . ($GLOBALS['cfgRelation']['mimework']
  1174. ? ',\'' . $dbi->escapeString($commentsCopyRow['mimetype'])
  1175. . '\',\'' . $dbi->escapeString($commentsCopyRow['transformation'])
  1176. . '\',\'' . $dbi->escapeString($commentsCopyRow['transformation_options'])
  1177. . '\''
  1178. : '')
  1179. . ')';
  1180. $relation->queryAsControlUser($newCommentQuery);
  1181. }
  1182. $dbi->freeResult($commentsCopyRs);
  1183. unset($commentsCopyRs);
  1184. }
  1185. // duplicating the bookmarks must not be done here, but
  1186. // just once per db
  1187. $getFields = ['display_field'];
  1188. $whereFields = [
  1189. 'db_name' => $sourceDb,
  1190. 'table_name' => $sourceTable,
  1191. ];
  1192. $newFields = [
  1193. 'db_name' => $targetDb,
  1194. 'table_name' => $targetTable,
  1195. ];
  1196. self::duplicateInfo('displaywork', 'table_info', $getFields, $whereFields, $newFields);
  1197. /**
  1198. * @todo revise this code when we support cross-db relations
  1199. */
  1200. $getFields = [
  1201. 'master_field',
  1202. 'foreign_table',
  1203. 'foreign_field',
  1204. ];
  1205. $whereFields = [
  1206. 'master_db' => $sourceDb,
  1207. 'master_table' => $sourceTable,
  1208. ];
  1209. $newFields = [
  1210. 'master_db' => $targetDb,
  1211. 'foreign_db' => $targetDb,
  1212. 'master_table' => $targetTable,
  1213. ];
  1214. self::duplicateInfo('relwork', 'relation', $getFields, $whereFields, $newFields);
  1215. $getFields = [
  1216. 'foreign_field',
  1217. 'master_table',
  1218. 'master_field',
  1219. ];
  1220. $whereFields = [
  1221. 'foreign_db' => $sourceDb,
  1222. 'foreign_table' => $sourceTable,
  1223. ];
  1224. $newFields = [
  1225. 'master_db' => $targetDb,
  1226. 'foreign_db' => $targetDb,
  1227. 'foreign_table' => $targetTable,
  1228. ];
  1229. self::duplicateInfo('relwork', 'relation', $getFields, $whereFields, $newFields);
  1230. /**
  1231. * @todo Can't get duplicating PDFs the right way. The
  1232. * page numbers always get screwed up independently from
  1233. * duplication because the numbers do not seem to be stored on a
  1234. * per-database basis. Would the author of pdf support please
  1235. * have a look at it?
  1236. *
  1237. $get_fields = array('page_descr');
  1238. $where_fields = array('db_name' => $source_db);
  1239. $new_fields = array('db_name' => $target_db);
  1240. $last_id = self::duplicateInfo(
  1241. 'pdfwork',
  1242. 'pdf_pages',
  1243. $get_fields,
  1244. $where_fields,
  1245. $new_fields
  1246. );
  1247. if (isset($last_id) && $last_id >= 0) {
  1248. $get_fields = array('x', 'y');
  1249. $where_fields = array(
  1250. 'db_name' => $source_db,
  1251. 'table_name' => $source_table
  1252. );
  1253. $new_fields = array(
  1254. 'db_name' => $target_db,
  1255. 'table_name' => $target_table,
  1256. 'pdf_page_number' => $last_id
  1257. );
  1258. self::duplicateInfo(
  1259. 'pdfwork',
  1260. 'table_coords',
  1261. $get_fields,
  1262. $where_fields,
  1263. $new_fields
  1264. );
  1265. }
  1266. */
  1267. return true;
  1268. }
  1269. /**
  1270. * checks if given name is a valid table name,
  1271. * currently if not empty, trailing spaces, '.', '/' and '\'
  1272. *
  1273. * @see https://dev.mysql.com/doc/refman/5.0/en/legal-names.html
  1274. *
  1275. * @param string $tableName name to check
  1276. * @param bool $isBackquoted whether this name is used inside backquotes or not
  1277. *
  1278. * @todo add check for valid chars in filename on current system/os
  1279. */
  1280. public static function isValidName($tableName, $isBackquoted = false): bool
  1281. {
  1282. if ($tableName !== rtrim((string) $tableName)) {
  1283. // trailing spaces not allowed even in backquotes
  1284. return false;
  1285. }
  1286. if (strlen($tableName) === 0) {
  1287. // zero length
  1288. return false;
  1289. }
  1290. if (! $isBackquoted && $tableName !== trim($tableName)) {
  1291. // spaces at the start or in between only allowed inside backquotes
  1292. return false;
  1293. }
  1294. if (! $isBackquoted && preg_match('/^[a-zA-Z0-9_$]+$/', $tableName)) {
  1295. // only allow the above regex in unquoted identifiers
  1296. // see : https://dev.mysql.com/doc/refman/5.7/en/identifiers.html
  1297. return true;
  1298. }
  1299. // If backquoted, all characters should be allowed (except w/ trailing spaces).
  1300. return $isBackquoted;
  1301. }
  1302. /**
  1303. * renames table
  1304. *
  1305. * @param string $newName new table name
  1306. * @param string $newDb new database name
  1307. */
  1308. public function rename($newName, $newDb = null): bool
  1309. {
  1310. if ($this->dbi->getLowerCaseNames() === '1') {
  1311. $newName = strtolower($newName);
  1312. }
  1313. if ($newDb !== null && $newDb !== $this->getDbName()) {
  1314. // Ensure the target is valid
  1315. if (! $GLOBALS['dblist']->databases->exists($newDb)) {
  1316. $this->errors[] = __('Invalid database:') . ' ' . $newDb;
  1317. return false;
  1318. }
  1319. } else {
  1320. $newDb = $this->getDbName();
  1321. }
  1322. $newTable = new Table($newName, $newDb);
  1323. if ($this->getFullName() === $newTable->getFullName()) {
  1324. return true;
  1325. }
  1326. // Allow whitespaces (not trailing) in $new_name,
  1327. // since we are using $backquoted in getting the fullName of table
  1328. // below to be used in the query
  1329. if (! self::isValidName($newName, true)) {
  1330. $this->errors[] = __('Invalid table name:') . ' '
  1331. . $newTable->getFullName();
  1332. return false;
  1333. }
  1334. // If the table is moved to a different database drop its triggers first
  1335. $triggers = $this->dbi->getTriggers(
  1336. $this->getDbName(),
  1337. $this->getName(),
  1338. ''
  1339. );
  1340. $handleTriggers = $this->getDbName() != $newDb && $triggers;
  1341. if ($handleTriggers) {
  1342. foreach ($triggers as $trigger) {
  1343. $sql = 'DROP TRIGGER IF EXISTS '
  1344. . Util::backquote($this->getDbName())
  1345. . '.' . Util::backquote($trigger['name']) . ';';
  1346. $this->dbi->query($sql);
  1347. }
  1348. }
  1349. /*
  1350. * tested also for a view, in MySQL 5.0.92, 5.1.55 and 5.5.13
  1351. */
  1352. $GLOBALS['sql_query'] = '
  1353. RENAME TABLE ' . $this->getFullName(true) . '
  1354. TO ' . $newTable->getFullName(true) . ';';
  1355. // I don't think a specific error message for views is necessary
  1356. if (! $this->dbi->query($GLOBALS['sql_query'])) {
  1357. // Restore triggers in the old database
  1358. if ($handleTriggers) {
  1359. $this->dbi->selectDb($this->getDbName());
  1360. foreach ($triggers as $trigger) {
  1361. $this->dbi->query($trigger['create']);
  1362. }
  1363. }
  1364. $this->errors[] = sprintf(
  1365. __('Failed to rename table %1$s to %2$s!'),
  1366. $this->getFullName(),
  1367. $newTable->getFullName()
  1368. );
  1369. return false;
  1370. }
  1371. $oldName = $this->getName();
  1372. $oldDb = $this->getDbName();
  1373. $this->name = $newName;
  1374. $this->dbName = $newDb;
  1375. // Rename table in configuration storage
  1376. $this->relation->renameTable($oldDb, $newDb, $oldName, $newName);
  1377. $this->messages[] = sprintf(
  1378. __('Table %1$s has been renamed to %2$s.'),
  1379. htmlspecialchars($oldName),
  1380. htmlspecialchars($newName)
  1381. );
  1382. return true;
  1383. }
  1384. /**
  1385. * Get all unique columns
  1386. *
  1387. * returns an array with all columns with unique content, in fact these are
  1388. * all columns being single indexed in PRIMARY or UNIQUE
  1389. *
  1390. * e.g.
  1391. * - PRIMARY(id) // id
  1392. * - UNIQUE(name) // name
  1393. * - PRIMARY(fk_id1, fk_id2) // NONE
  1394. * - UNIQUE(x,y) // NONE
  1395. *
  1396. * @param bool $backquoted whether to quote name with backticks ``
  1397. * @param bool $fullName whether to include full name of the table as a prefix
  1398. *
  1399. * @return array
  1400. */
  1401. public function getUniqueColumns($backquoted = true, $fullName = true)
  1402. {
  1403. $sql = QueryGenerator::getTableIndexesSql(
  1404. $this->getDbName(),
  1405. $this->getName(),
  1406. 'Non_unique = 0'
  1407. );
  1408. $uniques = $this->dbi->fetchResult(
  1409. $sql,
  1410. [
  1411. 'Key_name',
  1412. null,
  1413. ],
  1414. 'Column_name'
  1415. );
  1416. $return = [];
  1417. foreach ($uniques as $index) {
  1418. if (count($index) > 1) {
  1419. continue;
  1420. }
  1421. if ($fullName) {
  1422. $possibleColumn = $this->getFullName($backquoted) . '.';
  1423. } else {
  1424. $possibleColumn = '';
  1425. }
  1426. if ($backquoted) {
  1427. $possibleColumn .= Util::backquote($index[0]);
  1428. } else {
  1429. $possibleColumn .= $index[0];
  1430. }
  1431. // a column might have a primary and an unique index on it
  1432. if (in_array($possibleColumn, $return)) {
  1433. continue;
  1434. }
  1435. $return[] = $possibleColumn;
  1436. }
  1437. return $return;
  1438. }
  1439. /**
  1440. * Formats lists of columns
  1441. *
  1442. * returns an array with all columns that make use of an index
  1443. *
  1444. * e.g. index(col1, col2) would return col1, col2
  1445. *
  1446. * @param array $indexed column data
  1447. * @param bool $backquoted whether to quote name with backticks ``
  1448. * @param bool $fullName whether to include full name of the table as a prefix
  1449. *
  1450. * @return array
  1451. */
  1452. private function formatColumns(array $indexed, $backquoted, $fullName)
  1453. {
  1454. $return = [];
  1455. foreach ($indexed as $column) {
  1456. $return[] = ($fullName ? $this->getFullName($backquoted) . '.' : '')
  1457. . ($backquoted ? Util::backquote($column) : $column);
  1458. }
  1459. return $return;
  1460. }
  1461. /**
  1462. * Get all indexed columns
  1463. *
  1464. * returns an array with all columns that make use of an index
  1465. *
  1466. * e.g. index(col1, col2) would return col1, col2
  1467. *
  1468. * @param bool $backquoted whether to quote name with backticks ``
  1469. * @param bool $fullName whether to include full name of the table as a prefix
  1470. *
  1471. * @return array
  1472. */
  1473. public function getIndexedColumns($backquoted = true, $fullName = true)
  1474. {
  1475. $sql = QueryGenerator::getTableIndexesSql(
  1476. $this->getDbName(),
  1477. $this->getName(),
  1478. ''
  1479. );
  1480. $indexed = $this->dbi->fetchResult($sql, 'Column_name', 'Column_name');
  1481. return $this->formatColumns($indexed, $backquoted, $fullName);
  1482. }
  1483. /**
  1484. * Get all columns
  1485. *
  1486. * returns an array with all columns
  1487. *
  1488. * @param bool $backquoted whether to quote name with backticks ``
  1489. * @param bool $fullName whether to include full name of the table as a prefix
  1490. *
  1491. * @return array
  1492. */
  1493. public function getColumns($backquoted = true, $fullName = true)
  1494. {
  1495. $sql = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
  1496. $indexed = $this->dbi->fetchResult($sql, 'Field', 'Field');
  1497. return $this->formatColumns($indexed, $backquoted, $fullName);
  1498. }
  1499. /**
  1500. * Get meta info for fields in table
  1501. *
  1502. * @return mixed
  1503. */
  1504. public function getColumnsMeta()
  1505. {
  1506. $moveColumnsSqlQuery = sprintf(
  1507. 'SELECT * FROM %s.%s LIMIT 1',
  1508. Util::backquote($this->dbName),
  1509. Util::backquote($this->name)
  1510. );
  1511. $moveColumnsSqlResult = $this->dbi->tryQuery($moveColumnsSqlQuery);
  1512. if ($moveColumnsSqlResult !== false) {
  1513. return $this->dbi->getFieldsMeta($moveColumnsSqlResult);
  1514. }
  1515. // unsure how to reproduce but it was seen on the reporting server
  1516. return [];
  1517. }
  1518. /**
  1519. * Get non-generated columns in table
  1520. *
  1521. * @param bool $backquoted whether to quote name with backticks ``
  1522. *
  1523. * @return array
  1524. */
  1525. public function getNonGeneratedColumns($backquoted = true)
  1526. {
  1527. $columnsMetaQuery = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
  1528. $ret = [];
  1529. $columnsMetaQueryResult = $this->dbi->fetchResult($columnsMetaQuery);
  1530. if ($columnsMetaQueryResult && $columnsMetaQueryResult !== false) {
  1531. foreach ($columnsMetaQueryResult as $column) {
  1532. $value = $column['Field'];
  1533. if ($backquoted === true) {
  1534. $value = Util::backquote($value);
  1535. }
  1536. // If contains GENERATED or VIRTUAL and does not contain DEFAULT_GENERATED
  1537. if (
  1538. (
  1539. str_contains($column['Extra'], 'GENERATED')
  1540. || str_contains($column['Extra'], 'VIRTUAL')
  1541. ) && ! str_contains($column['Extra'], 'DEFAULT_GENERATED')
  1542. ) {
  1543. continue;
  1544. }
  1545. $ret[] = $value;
  1546. }
  1547. }
  1548. return $ret;
  1549. }
  1550. /**
  1551. * Return UI preferences for this table from phpMyAdmin database.
  1552. *
  1553. * @return array
  1554. */
  1555. protected function getUiPrefsFromDb()
  1556. {
  1557. $cfgRelation = $this->relation->getRelationsParam();
  1558. $table = Util::backquote($cfgRelation['db']) . '.'
  1559. . Util::backquote($cfgRelation['table_uiprefs']);
  1560. // Read from phpMyAdmin database
  1561. $sqlQuery = ' SELECT `prefs` FROM ' . $table
  1562. . " WHERE `username` = '" . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'"
  1563. . " AND `db_name` = '" . $this->dbi->escapeString($this->dbName) . "'"
  1564. . " AND `table_name` = '" . $this->dbi->escapeString($this->name) . "'";
  1565. $row = $this->dbi->fetchArray($this->relation->queryAsControlUser($sqlQuery));
  1566. if (isset($row[0])) {
  1567. return json_decode($row[0], true);
  1568. }
  1569. return [];
  1570. }
  1571. /**
  1572. * Save this table's UI preferences into phpMyAdmin database.
  1573. *
  1574. * @return true|Message
  1575. */
  1576. protected function saveUiPrefsToDb()
  1577. {
  1578. $cfgRelation = $this->relation->getRelationsParam();
  1579. $table = Util::backquote($cfgRelation['db']) . '.'
  1580. . Util::backquote($cfgRelation['table_uiprefs']);
  1581. $secureDbName = $this->dbi->escapeString($this->dbName);
  1582. $username = $GLOBALS['cfg']['Server']['user'];
  1583. $sqlQuery = ' REPLACE INTO ' . $table
  1584. . " (username, db_name, table_name, prefs) VALUES ('"
  1585. . $this->dbi->escapeString($username) . "', '" . $secureDbName
  1586. . "', '" . $this->dbi->escapeString($this->name) . "', '"
  1587. . $this->dbi->escapeString((string) json_encode($this->uiprefs)) . "')";
  1588. $success = $this->dbi->tryQuery($sqlQuery, DatabaseInterface::CONNECT_CONTROL);
  1589. if (! $success) {
  1590. $message = Message::error(
  1591. __('Could not save table UI preferences!')
  1592. );
  1593. $message->addMessage(
  1594. Message::rawError(
  1595. (string) $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL)
  1596. ),
  1597. '<br><br>'
  1598. );
  1599. return $message;
  1600. }
  1601. // Remove some old rows in table_uiprefs if it exceeds the configured
  1602. // maximum rows
  1603. $sqlQuery = 'SELECT COUNT(*) FROM ' . $table;
  1604. $rowsCount = (int) $this->dbi->fetchValue($sqlQuery);
  1605. $maxRows = (int) $GLOBALS['cfg']['Server']['MaxTableUiprefs'];
  1606. if ($rowsCount > $maxRows) {
  1607. $numRowsToDelete = $rowsCount - $maxRows;
  1608. $sqlQuery = ' DELETE FROM ' . $table .
  1609. ' ORDER BY last_update ASC' .
  1610. ' LIMIT ' . $numRowsToDelete;
  1611. $success = $this->dbi->tryQuery($sqlQuery, DatabaseInterface::CONNECT_CONTROL);
  1612. if (! $success) {
  1613. $message = Message::error(
  1614. sprintf(
  1615. __(
  1616. 'Failed to cleanup table UI preferences (see $cfg[\'Servers\'][$i][\'MaxTableUiprefs\'] %s)'
  1617. ),
  1618. MySQLDocumentation::showDocumentation('config', 'cfg_Servers_MaxTableUiprefs')
  1619. )
  1620. );
  1621. $message->addMessage(
  1622. Message::rawError(
  1623. (string) $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL)
  1624. ),
  1625. '<br><br>'
  1626. );
  1627. return $message;
  1628. }
  1629. }
  1630. return true;
  1631. }
  1632. /**
  1633. * Loads the UI preferences for this table.
  1634. * If pmadb and table_uiprefs is set, it will load the UI preferences from
  1635. * phpMyAdmin database.
  1636. */
  1637. protected function loadUiPrefs(): void
  1638. {
  1639. $cfgRelation = $this->relation->getRelationsParam();
  1640. $serverId = $GLOBALS['server'];
  1641. // set session variable if it's still undefined
  1642. if (! isset($_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name])) {
  1643. // check whether we can get from pmadb
  1644. $uiPrefs = $cfgRelation['uiprefswork'] ? $this->getUiPrefsFromDb() : [];
  1645. $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name] = $uiPrefs;
  1646. }
  1647. $this->uiprefs =& $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name];
  1648. }
  1649. /**
  1650. * Get a property from UI preferences.
  1651. * Return false if the property is not found.
  1652. * Available property:
  1653. * - PROP_SORTED_COLUMN
  1654. * - PROP_COLUMN_ORDER
  1655. * - PROP_COLUMN_VISIB
  1656. *
  1657. * @param string $property property
  1658. *
  1659. * @return mixed
  1660. */
  1661. public function getUiProp($property)
  1662. {
  1663. if (! isset($this->uiprefs)) {
  1664. $this->loadUiPrefs();
  1665. }
  1666. // do checking based on property
  1667. if ($property == self::PROP_SORTED_COLUMN) {
  1668. if (! isset($this->uiprefs[$property])) {
  1669. return false;
  1670. }
  1671. if (! isset($_POST['discard_remembered_sort'])) {
  1672. // check if the column name exists in this table
  1673. $tmp = explode(' ', $this->uiprefs[$property]);
  1674. $colname = $tmp[0];
  1675. //remove backquoting from colname
  1676. $colname = str_replace('`', '', $colname);
  1677. //get the available column name without backquoting
  1678. $availColumns = $this->getColumns(false);
  1679. foreach ($availColumns as $eachCol) {
  1680. // check if $each_col ends with $colname
  1681. if (substr_compare($eachCol, $colname, mb_strlen($eachCol) - mb_strlen($colname)) === 0) {
  1682. return $this->uiprefs[$property];
  1683. }
  1684. }
  1685. }
  1686. // remove the property, since it no longer exists in database
  1687. $this->removeUiProp($property);
  1688. return false;
  1689. }
  1690. if ($property == self::PROP_COLUMN_ORDER || $property == self::PROP_COLUMN_VISIB) {
  1691. if ($this->isView() || ! isset($this->uiprefs[$property])) {
  1692. return false;
  1693. }
  1694. // check if the table has not been modified
  1695. if ($this->getStatusInfo('Create_time') == $this->uiprefs['CREATE_TIME']) {
  1696. return array_map('intval', $this->uiprefs[$property]);
  1697. }
  1698. // remove the property, since the table has been modified
  1699. $this->removeUiProp($property);
  1700. return false;
  1701. }
  1702. // default behaviour for other property:
  1703. return $this->uiprefs[$property] ?? false;
  1704. }
  1705. /**
  1706. * Set a property from UI preferences.
  1707. * If pmadb and table_uiprefs is set, it will save the UI preferences to
  1708. * phpMyAdmin database.
  1709. * Available property:
  1710. * - PROP_SORTED_COLUMN
  1711. * - PROP_COLUMN_ORDER
  1712. * - PROP_COLUMN_VISIB
  1713. *
  1714. * @param string $property Property
  1715. * @param mixed $value Value for the property
  1716. * @param string $tableCreateTime Needed for PROP_COLUMN_ORDER and PROP_COLUMN_VISIB
  1717. *
  1718. * @return bool|Message
  1719. */
  1720. public function setUiProp($property, $value, $tableCreateTime = null)
  1721. {
  1722. if (! isset($this->uiprefs)) {
  1723. $this->loadUiPrefs();
  1724. }
  1725. // we want to save the create time if the property is PROP_COLUMN_ORDER
  1726. if (! $this->isView() && ($property == self::PROP_COLUMN_ORDER || $property == self::PROP_COLUMN_VISIB)) {
  1727. $currCreateTime = $this->getStatusInfo('CREATE_TIME');
  1728. if (! isset($tableCreateTime) || $tableCreateTime != $currCreateTime) {
  1729. // there is no $table_create_time, or
  1730. // supplied $table_create_time is older than current create time,
  1731. // so don't save
  1732. return Message::error(
  1733. sprintf(
  1734. __(
  1735. 'Cannot save UI property "%s". The changes made will ' .
  1736. 'not be persistent after you refresh this page. ' .
  1737. 'Please check if the table structure has been changed.'
  1738. ),
  1739. $property
  1740. )
  1741. );
  1742. }
  1743. $this->uiprefs['CREATE_TIME'] = $currCreateTime;
  1744. }
  1745. // save the value
  1746. $this->uiprefs[$property] = $value;
  1747. // check if pmadb is set
  1748. $cfgRelation = $this->relation->getRelationsParam();
  1749. if ($cfgRelation['uiprefswork']) {
  1750. return $this->saveUiPrefsToDb();
  1751. }
  1752. return true;
  1753. }
  1754. /**
  1755. * Remove a property from UI preferences.
  1756. *
  1757. * @param string $property the property
  1758. *
  1759. * @return true|Message
  1760. */
  1761. public function removeUiProp($property)
  1762. {
  1763. if (! isset($this->uiprefs)) {
  1764. $this->loadUiPrefs();
  1765. }
  1766. if (isset($this->uiprefs[$property])) {
  1767. unset($this->uiprefs[$property]);
  1768. // check if pmadb is set
  1769. $cfgRelation = $this->relation->getRelationsParam();
  1770. if ($cfgRelation['uiprefswork']) {
  1771. return $this->saveUiPrefsToDb();
  1772. }
  1773. }
  1774. return true;
  1775. }
  1776. /**
  1777. * Get all column names which are MySQL reserved words
  1778. *
  1779. * @return array
  1780. *
  1781. * @access public
  1782. */
  1783. public function getReservedColumnNames()
  1784. {
  1785. $columns = $this->getColumns(false);
  1786. $return = [];
  1787. foreach ($columns as $column) {
  1788. $temp = explode('.', $column);
  1789. $columnName = $temp[2];
  1790. if (! Context::isKeyword($columnName, true)) {
  1791. continue;
  1792. }
  1793. $return[] = $columnName;
  1794. }
  1795. return $return;
  1796. }
  1797. /**
  1798. * Function to get the name and type of the columns of a table
  1799. *
  1800. * @return array
  1801. */
  1802. public function getNameAndTypeOfTheColumns()
  1803. {
  1804. $columns = [];
  1805. foreach (
  1806. $this->dbi->getColumnsFull($this->dbName, $this->name) as $row
  1807. ) {
  1808. if (preg_match('@^(set|enum)\((.+)\)$@i', $row['Type'], $tmp)) {
  1809. $tmp[2] = mb_substr(
  1810. (string) preg_replace('@([^,])\'\'@', '\\1\\\'', ',' . $tmp[2]),
  1811. 1
  1812. );
  1813. $columns[$row['Field']] = $tmp[1] . '('
  1814. . str_replace(',', ', ', $tmp[2]) . ')';
  1815. } else {
  1816. $columns[$row['Field']] = $row['Type'];
  1817. }
  1818. }
  1819. return $columns;
  1820. }
  1821. /**
  1822. * Get index with index name
  1823. *
  1824. * @param string $index Index name
  1825. *
  1826. * @return Index
  1827. */
  1828. public function getIndex($index)
  1829. {
  1830. return Index::singleton($this->dbName, $this->name, $index);
  1831. }
  1832. /**
  1833. * Function to get the sql query for index creation or edit
  1834. *
  1835. * @param Index $index current index
  1836. * @param bool $error whether error occurred or not
  1837. *
  1838. * @return string
  1839. */
  1840. public function getSqlQueryForIndexCreateOrEdit($index, &$error)
  1841. {
  1842. // $sql_query is the one displayed in the query box
  1843. $sqlQuery = sprintf(
  1844. 'ALTER TABLE %s.%s',
  1845. Util::backquote($this->dbName),
  1846. Util::backquote($this->name)
  1847. );
  1848. // Drops the old index
  1849. if (! empty($_POST['old_index'])) {
  1850. $oldIndex = is_array($_POST['old_index']) ? $_POST['old_index']['Key_name'] : $_POST['old_index'];
  1851. if ($oldIndex === 'PRIMARY') {
  1852. $sqlQuery .= ' DROP PRIMARY KEY,';
  1853. } else {
  1854. $sqlQuery .= sprintf(
  1855. ' DROP INDEX %s,',
  1856. Util::backquote($oldIndex)
  1857. );
  1858. }
  1859. }
  1860. // Builds the new one
  1861. switch ($index->getChoice()) {
  1862. case 'PRIMARY':
  1863. if ($index->getName() == '') {
  1864. $index->setName('PRIMARY');
  1865. } elseif ($index->getName() !== 'PRIMARY') {
  1866. $error = Message::error(
  1867. __('The name of the primary key must be "PRIMARY"!')
  1868. );
  1869. }
  1870. $sqlQuery .= ' ADD PRIMARY KEY';
  1871. break;
  1872. case 'FULLTEXT':
  1873. case 'UNIQUE':
  1874. case 'INDEX':
  1875. case 'SPATIAL':
  1876. if ($index->getName() === 'PRIMARY') {
  1877. $error = Message::error(
  1878. __('Can\'t rename index to PRIMARY!')
  1879. );
  1880. }
  1881. $sqlQuery .= sprintf(
  1882. ' ADD %s ',
  1883. $index->getChoice()
  1884. );
  1885. if ($index->getName()) {
  1886. $sqlQuery .= Util::backquote($index->getName());
  1887. }
  1888. break;
  1889. }
  1890. $indexFields = [];
  1891. foreach ($index->getColumns() as $key => $column) {
  1892. $indexFields[$key] = Util::backquote($column->getName());
  1893. if (! $column->getSubPart()) {
  1894. continue;
  1895. }
  1896. $indexFields[$key] .= '(' . $column->getSubPart() . ')';
  1897. }
  1898. if (empty($indexFields)) {
  1899. $error = Message::error(__('No index parts defined!'));
  1900. } else {
  1901. $sqlQuery .= ' (' . implode(', ', $indexFields) . ')';
  1902. }
  1903. $keyBlockSizes = $index->getKeyBlockSize();
  1904. if (! empty($keyBlockSizes)) {
  1905. $sqlQuery .= sprintf(
  1906. ' KEY_BLOCK_SIZE = %s',
  1907. $this->dbi->escapeString((string) $keyBlockSizes)
  1908. );
  1909. }
  1910. // specifying index type is allowed only for primary, unique and index only
  1911. // TokuDB is using Fractal Tree, Using Type is not useless
  1912. // Ref: https://mariadb.com/kb/en/mariadb/storage-engine-index-types/
  1913. $type = $index->getType();
  1914. if (
  1915. $index->getChoice() !== 'SPATIAL'
  1916. && $index->getChoice() !== 'FULLTEXT'
  1917. && in_array($type, Index::getIndexTypes())
  1918. && ! $this->isEngine(['TOKUDB'])
  1919. ) {
  1920. $sqlQuery .= ' USING ' . $type;
  1921. }
  1922. $parser = $index->getParser();
  1923. if ($index->getChoice() === 'FULLTEXT' && ! empty($parser)) {
  1924. $sqlQuery .= ' WITH PARSER ' . $this->dbi->escapeString($parser);
  1925. }
  1926. $comment = $index->getComment();
  1927. if (! empty($comment)) {
  1928. $sqlQuery .= sprintf(
  1929. " COMMENT '%s'",
  1930. $this->dbi->escapeString($comment)
  1931. );
  1932. }
  1933. $sqlQuery .= ';';
  1934. return $sqlQuery;
  1935. }
  1936. /**
  1937. * Function to handle update for display field
  1938. *
  1939. * @param string $displayField display field
  1940. * @param array $cfgRelation configuration relation
  1941. */
  1942. public function updateDisplayField($displayField, array $cfgRelation): bool
  1943. {
  1944. if ($displayField == '') {
  1945. $updQuery = 'DELETE FROM '
  1946. . Util::backquote($GLOBALS['cfgRelation']['db'])
  1947. . '.' . Util::backquote($cfgRelation['table_info'])
  1948. . ' WHERE db_name = \''
  1949. . $this->dbi->escapeString($this->dbName) . '\''
  1950. . ' AND table_name = \''
  1951. . $this->dbi->escapeString($this->name) . '\'';
  1952. } else {
  1953. $updQuery = 'REPLACE INTO '
  1954. . Util::backquote($GLOBALS['cfgRelation']['db'])
  1955. . '.' . Util::backquote($cfgRelation['table_info'])
  1956. . '(db_name, table_name, display_field) VALUES('
  1957. . '\'' . $this->dbi->escapeString($this->dbName) . '\','
  1958. . '\'' . $this->dbi->escapeString($this->name) . '\','
  1959. . '\'' . $this->dbi->escapeString($displayField) . '\')';
  1960. }
  1961. return $this->dbi->query($updQuery, DatabaseInterface::CONNECT_CONTROL, 0, false) === true;
  1962. }
  1963. /**
  1964. * Function to get update query for updating internal relations
  1965. *
  1966. * @param array $multiEditColumnsName multi edit column names
  1967. * @param array $destinationDb destination tables
  1968. * @param array $destinationTable destination tables
  1969. * @param array $destinationColumn destination columns
  1970. * @param array $cfgRelation configuration relation
  1971. * @param array|null $existrel db, table, column
  1972. */
  1973. public function updateInternalRelations(
  1974. array $multiEditColumnsName,
  1975. array $destinationDb,
  1976. array $destinationTable,
  1977. array $destinationColumn,
  1978. array $cfgRelation,
  1979. $existrel
  1980. ): bool {
  1981. $updated = false;
  1982. foreach ($destinationDb as $masterFieldMd5 => $foreignDb) {
  1983. $updQuery = null;
  1984. // Map the fieldname's md5 back to its real name
  1985. $masterField = $multiEditColumnsName[$masterFieldMd5];
  1986. $foreignTable = $destinationTable[$masterFieldMd5];
  1987. $foreignField = $destinationColumn[$masterFieldMd5];
  1988. if (! empty($foreignDb) && ! empty($foreignTable) && ! empty($foreignField)) {
  1989. if (! isset($existrel[$masterField])) {
  1990. $updQuery = 'INSERT INTO '
  1991. . Util::backquote($GLOBALS['cfgRelation']['db'])
  1992. . '.' . Util::backquote($cfgRelation['relation'])
  1993. . '(master_db, master_table, master_field, foreign_db,'
  1994. . ' foreign_table, foreign_field)'
  1995. . ' values('
  1996. . '\'' . $this->dbi->escapeString($this->dbName) . '\', '
  1997. . '\'' . $this->dbi->escapeString($this->name) . '\', '
  1998. . '\'' . $this->dbi->escapeString($masterField) . '\', '
  1999. . '\'' . $this->dbi->escapeString($foreignDb) . '\', '
  2000. . '\'' . $this->dbi->escapeString($foreignTable) . '\','
  2001. . '\'' . $this->dbi->escapeString($foreignField) . '\')';
  2002. } elseif (
  2003. $existrel[$masterField]['foreign_db'] != $foreignDb
  2004. || $existrel[$masterField]['foreign_table'] != $foreignTable
  2005. || $existrel[$masterField]['foreign_field'] != $foreignField
  2006. ) {
  2007. $updQuery = 'UPDATE '
  2008. . Util::backquote($GLOBALS['cfgRelation']['db'])
  2009. . '.' . Util::backquote($cfgRelation['relation'])
  2010. . ' SET foreign_db = \''
  2011. . $this->dbi->escapeString($foreignDb) . '\', '
  2012. . ' foreign_table = \''
  2013. . $this->dbi->escapeString($foreignTable) . '\', '
  2014. . ' foreign_field = \''
  2015. . $this->dbi->escapeString($foreignField) . '\' '
  2016. . ' WHERE master_db = \''
  2017. . $this->dbi->escapeString($this->dbName) . '\''
  2018. . ' AND master_table = \''
  2019. . $this->dbi->escapeString($this->name) . '\''
  2020. . ' AND master_field = \''
  2021. . $this->dbi->escapeString($masterField) . '\'';
  2022. }
  2023. } elseif (isset($existrel[$masterField])) {
  2024. $updQuery = 'DELETE FROM '
  2025. . Util::backquote($GLOBALS['cfgRelation']['db'])
  2026. . '.' . Util::backquote($cfgRelation['relation'])
  2027. . ' WHERE master_db = \''
  2028. . $this->dbi->escapeString($this->dbName) . '\''
  2029. . ' AND master_table = \''
  2030. . $this->dbi->escapeString($this->name) . '\''
  2031. . ' AND master_field = \''
  2032. . $this->dbi->escapeString($masterField) . '\'';
  2033. }
  2034. if (! isset($updQuery)) {
  2035. continue;
  2036. }
  2037. $this->dbi->query($updQuery, DatabaseInterface::CONNECT_CONTROL, 0, false);
  2038. $updated = true;
  2039. }
  2040. return $updated;
  2041. }
  2042. /**
  2043. * Function to handle foreign key updates
  2044. *
  2045. * @param array $destinationForeignDb destination foreign database
  2046. * @param array $multiEditColumnsName multi edit column names
  2047. * @param array $destinationForeignTable destination foreign table
  2048. * @param array $destinationForeignColumn destination foreign column
  2049. * @param array $optionsArray options array
  2050. * @param string $table current table
  2051. * @param array $existrelForeign db, table, column
  2052. *
  2053. * @return array
  2054. */
  2055. public function updateForeignKeys(
  2056. array $destinationForeignDb,
  2057. array $multiEditColumnsName,
  2058. array $destinationForeignTable,
  2059. array $destinationForeignColumn,
  2060. array $optionsArray,
  2061. $table,
  2062. array $existrelForeign
  2063. ) {
  2064. $htmlOutput = '';
  2065. $previewSqlData = '';
  2066. $displayQuery = '';
  2067. $seenError = false;
  2068. foreach ($destinationForeignDb as $masterFieldMd5 => $foreignDb) {
  2069. $create = false;
  2070. $drop = false;
  2071. // Map the fieldname's md5 back to its real name
  2072. $masterField = $multiEditColumnsName[$masterFieldMd5];
  2073. $foreignTable = $destinationForeignTable[$masterFieldMd5];
  2074. $foreignField = $destinationForeignColumn[$masterFieldMd5];
  2075. if (isset($existrelForeign[$masterFieldMd5]['ref_db_name'])) {
  2076. $refDbName = $existrelForeign[$masterFieldMd5]['ref_db_name'];
  2077. } else {
  2078. $refDbName = $GLOBALS['db'];
  2079. }
  2080. $emptyFields = false;
  2081. foreach ($masterField as $key => $oneField) {
  2082. if (
  2083. (! empty($oneField) && empty($foreignField[$key]))
  2084. || (empty($oneField) && ! empty($foreignField[$key]))
  2085. ) {
  2086. $emptyFields = true;
  2087. }
  2088. if (! empty($oneField) || ! empty($foreignField[$key])) {
  2089. continue;
  2090. }
  2091. unset($masterField[$key], $foreignField[$key]);
  2092. }
  2093. if (! empty($foreignDb) && ! empty($foreignTable) && ! $emptyFields) {
  2094. if (isset($existrelForeign[$masterFieldMd5])) {
  2095. $constraintName = $existrelForeign[$masterFieldMd5]['constraint'];
  2096. $onDelete = ! empty(
  2097. $existrelForeign[$masterFieldMd5]['on_delete']
  2098. )
  2099. ? $existrelForeign[$masterFieldMd5]['on_delete']
  2100. : 'RESTRICT';
  2101. $onUpdate = ! empty(
  2102. $existrelForeign[$masterFieldMd5]['on_update']
  2103. )
  2104. ? $existrelForeign[$masterFieldMd5]['on_update']
  2105. : 'RESTRICT';
  2106. if (
  2107. $refDbName != $foreignDb
  2108. || $existrelForeign[$masterFieldMd5]['ref_table_name'] != $foreignTable
  2109. || $existrelForeign[$masterFieldMd5]['ref_index_list'] != $foreignField
  2110. || $existrelForeign[$masterFieldMd5]['index_list'] != $masterField
  2111. || $_POST['constraint_name'][$masterFieldMd5] != $constraintName
  2112. || ($_POST['on_delete'][$masterFieldMd5] != $onDelete)
  2113. || ($_POST['on_update'][$masterFieldMd5] != $onUpdate)
  2114. ) {
  2115. // another foreign key is already defined for this field
  2116. // or an option has been changed for ON DELETE or ON UPDATE
  2117. $drop = true;
  2118. $create = true;
  2119. }
  2120. } else {
  2121. // no key defined for this field(s)
  2122. $create = true;
  2123. }
  2124. } elseif (isset($existrelForeign[$masterFieldMd5])) {
  2125. $drop = true;
  2126. }
  2127. $tmpErrorDrop = false;
  2128. if ($drop) {
  2129. $dropQuery = 'ALTER TABLE ' . Util::backquote($table)
  2130. . ' DROP FOREIGN KEY '
  2131. . Util::backquote($existrelForeign[$masterFieldMd5]['constraint'])
  2132. . ';';
  2133. if (! isset($_POST['preview_sql'])) {
  2134. $displayQuery .= $dropQuery . "\n";
  2135. $this->dbi->tryQuery($dropQuery);
  2136. $tmpErrorDrop = $this->dbi->getError();
  2137. if (! empty($tmpErrorDrop)) {
  2138. $seenError = true;
  2139. $htmlOutput .= Generator::mysqlDie($tmpErrorDrop, $dropQuery, false, '', false);
  2140. continue;
  2141. }
  2142. } else {
  2143. $previewSqlData .= $dropQuery . "\n";
  2144. }
  2145. }
  2146. $tmpErrorCreate = false;
  2147. if (! $create) {
  2148. continue;
  2149. }
  2150. $createQuery = $this->getSQLToCreateForeignKey(
  2151. $table,
  2152. $masterField,
  2153. $foreignDb,
  2154. $foreignTable,
  2155. $foreignField,
  2156. $_POST['constraint_name'][$masterFieldMd5],
  2157. $optionsArray[$_POST['on_delete'][$masterFieldMd5]],
  2158. $optionsArray[$_POST['on_update'][$masterFieldMd5]]
  2159. );
  2160. if (! isset($_POST['preview_sql'])) {
  2161. $displayQuery .= $createQuery . "\n";
  2162. $this->dbi->tryQuery($createQuery);
  2163. $tmpErrorCreate = (string) $this->dbi->getError();
  2164. if (! empty($tmpErrorCreate)) {
  2165. $seenError = true;
  2166. if (substr($tmpErrorCreate, 1, 4) == '1005') {
  2167. $message = Message::error(
  2168. __(
  2169. 'Error creating foreign key on %1$s (check data types)'
  2170. )
  2171. );
  2172. $message->addParam(implode(', ', $masterField));
  2173. $htmlOutput .= $message->getDisplay();
  2174. } else {
  2175. $htmlOutput .= Generator::mysqlDie($tmpErrorCreate, $createQuery, false, '', false);
  2176. }
  2177. $htmlOutput .= MySQLDocumentation::show('create-table-foreign-keys') . "\n";
  2178. }
  2179. } else {
  2180. $previewSqlData .= $createQuery . "\n";
  2181. }
  2182. // this is an alteration and the old constraint has been dropped
  2183. // without creation of a new one
  2184. if (! $drop || ! empty($tmpErrorDrop) || empty($tmpErrorCreate)) {
  2185. continue;
  2186. }
  2187. // a rollback may be better here
  2188. $sqlQueryRecreate = '# Restoring the dropped constraint...' . "\n";
  2189. $sqlQueryRecreate .= $this->getSQLToCreateForeignKey(
  2190. $table,
  2191. $masterField,
  2192. $existrelForeign[$masterFieldMd5]['ref_db_name'],
  2193. $existrelForeign[$masterFieldMd5]['ref_table_name'],
  2194. $existrelForeign[$masterFieldMd5]['ref_index_list'],
  2195. $existrelForeign[$masterFieldMd5]['constraint'],
  2196. $optionsArray[$existrelForeign[$masterFieldMd5]['on_delete'] ?? ''] ?? null,
  2197. $optionsArray[$existrelForeign[$masterFieldMd5]['on_update'] ?? ''] ?? null
  2198. );
  2199. if (! isset($_POST['preview_sql'])) {
  2200. $displayQuery .= $sqlQueryRecreate . "\n";
  2201. $this->dbi->tryQuery($sqlQueryRecreate);
  2202. } else {
  2203. $previewSqlData .= $sqlQueryRecreate;
  2204. }
  2205. }
  2206. return [
  2207. $htmlOutput,
  2208. $previewSqlData,
  2209. $displayQuery,
  2210. $seenError,
  2211. ];
  2212. }
  2213. /**
  2214. * Returns the SQL query for foreign key constraint creation
  2215. *
  2216. * @param string $table table name
  2217. * @param array $field field names
  2218. * @param string $foreignDb foreign database name
  2219. * @param string $foreignTable foreign table name
  2220. * @param array $foreignField foreign field names
  2221. * @param string $name name of the constraint
  2222. * @param string $onDelete on delete action
  2223. * @param string $onUpdate on update action
  2224. *
  2225. * @return string SQL query for foreign key constraint creation
  2226. */
  2227. private function getSQLToCreateForeignKey(
  2228. $table,
  2229. array $field,
  2230. $foreignDb,
  2231. $foreignTable,
  2232. array $foreignField,
  2233. $name = null,
  2234. $onDelete = null,
  2235. $onUpdate = null
  2236. ) {
  2237. $sqlQuery = 'ALTER TABLE ' . Util::backquote($table) . ' ADD ';
  2238. // if user entered a constraint name
  2239. if (! empty($name)) {
  2240. $sqlQuery .= ' CONSTRAINT ' . Util::backquote($name);
  2241. }
  2242. foreach ($field as $key => $oneField) {
  2243. $field[$key] = Util::backquote($oneField);
  2244. }
  2245. foreach ($foreignField as $key => $oneField) {
  2246. $foreignField[$key] = Util::backquote($oneField);
  2247. }
  2248. $sqlQuery .= ' FOREIGN KEY (' . implode(', ', $field) . ') REFERENCES '
  2249. . ($this->dbName != $foreignDb
  2250. ? Util::backquote($foreignDb) . '.' : '')
  2251. . Util::backquote($foreignTable)
  2252. . '(' . implode(', ', $foreignField) . ')';
  2253. if (! empty($onDelete)) {
  2254. $sqlQuery .= ' ON DELETE ' . $onDelete;
  2255. }
  2256. if (! empty($onUpdate)) {
  2257. $sqlQuery .= ' ON UPDATE ' . $onUpdate;
  2258. }
  2259. $sqlQuery .= ';';
  2260. return $sqlQuery;
  2261. }
  2262. /**
  2263. * Returns the generation expression for virtual columns
  2264. *
  2265. * @param string $column name of the column
  2266. *
  2267. * @return array|bool associative array of column name and their expressions
  2268. * or false on failure
  2269. */
  2270. public function getColumnGenerationExpression($column = null)
  2271. {
  2272. if (
  2273. Compatibility::isMySqlOrPerconaDb()
  2274. && $this->dbi->getVersion() > 50705
  2275. && ! $GLOBALS['cfg']['Server']['DisableIS']
  2276. ) {
  2277. $sql = "SELECT
  2278. `COLUMN_NAME` AS `Field`,
  2279. `GENERATION_EXPRESSION` AS `Expression`
  2280. FROM
  2281. `information_schema`.`COLUMNS`
  2282. WHERE
  2283. `TABLE_SCHEMA` = '" . $this->dbi->escapeString($this->dbName) . "'
  2284. AND `TABLE_NAME` = '" . $this->dbi->escapeString($this->name) . "'";
  2285. if ($column != null) {
  2286. $sql .= " AND `COLUMN_NAME` = '" . $this->dbi->escapeString($column)
  2287. . "'";
  2288. }
  2289. return $this->dbi->fetchResult($sql, 'Field', 'Expression');
  2290. }
  2291. $createTable = $this->showCreate();
  2292. if (! $createTable) {
  2293. return false;
  2294. }
  2295. $parser = new Parser($createTable);
  2296. /**
  2297. * @var CreateStatement $stmt
  2298. */
  2299. $stmt = $parser->statements[0];
  2300. $fields = TableUtils::getFields($stmt);
  2301. if ($column != null) {
  2302. $expression = isset($fields[$column]['expr']) ?
  2303. substr($fields[$column]['expr'], 1, -1) : '';
  2304. return [$column => $expression];
  2305. }
  2306. $ret = [];
  2307. foreach ($fields as $field => $options) {
  2308. if (! isset($options['expr'])) {
  2309. continue;
  2310. }
  2311. $ret[$field] = substr($options['expr'], 1, -1);
  2312. }
  2313. return $ret;
  2314. }
  2315. /**
  2316. * Returns the CREATE statement for this table
  2317. *
  2318. * @return mixed
  2319. */
  2320. public function showCreate()
  2321. {
  2322. return $this->dbi->fetchValue(
  2323. 'SHOW CREATE TABLE ' . Util::backquote($this->dbName) . '.'
  2324. . Util::backquote($this->name),
  2325. 0,
  2326. 1
  2327. );
  2328. }
  2329. /**
  2330. * Returns the real row count for a table
  2331. */
  2332. public function getRealRowCountTable(): ?int
  2333. {
  2334. // SQL query to get row count for a table.
  2335. $result = $this->dbi->fetchSingleRow(
  2336. sprintf(
  2337. 'SELECT COUNT(*) AS %s FROM %s.%s',
  2338. Util::backquote('row_count'),
  2339. Util::backquote($this->dbName),
  2340. Util::backquote($this->name)
  2341. )
  2342. );
  2343. if (! is_array($result)) {
  2344. return null;
  2345. }
  2346. return (int) $result['row_count'];
  2347. }
  2348. /**
  2349. * Get columns with indexes
  2350. *
  2351. * @param int $types types bitmask
  2352. *
  2353. * @return array an array of columns
  2354. */
  2355. public function getColumnsWithIndex($types)
  2356. {
  2357. $columnsWithIndex = [];
  2358. foreach (
  2359. Index::getFromTableByChoice($this->name, $this->dbName, $types) as $index
  2360. ) {
  2361. $columns = $index->getColumns();
  2362. foreach ($columns as $columnName => $dummy) {
  2363. $columnsWithIndex[] = $columnName;
  2364. }
  2365. }
  2366. return $columnsWithIndex;
  2367. }
  2368. }